Merge remote-tracking branch 'origin/master' into jetty-9.1
diff --git a/VERSION.txt b/VERSION.txt
index 2073456..8a973b7 100644
--- a/VERSION.txt
+++ b/VERSION.txt
@@ -1,4 +1,4 @@
-jetty-9.0.5-SNAPSHOT
+jetty-9.1.0-SNAPSHOT
jetty-9.0.4.v20130625 - 25 June 2013
+ 396706 CGI support parameters
diff --git a/aggregates/jetty-all/pom.xml b/aggregates/jetty-all/pom.xml
index b7a23e9..f7049bb 100644
--- a/aggregates/jetty-all/pom.xml
+++ b/aggregates/jetty-all/pom.xml
@@ -2,7 +2,7 @@
<parent>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-project</artifactId>
- <version>9.0.5-SNAPSHOT</version>
+ <version>9.1.0-SNAPSHOT</version>
<relativePath>../../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
@@ -127,10 +127,19 @@
<scope>provided</scope>
</dependency>
<dependency>
+ <groupId>javax.servlet</groupId>
+ <artifactId>javax.servlet-api</artifactId>
+ <version>3.1-b01</version>
+ <scope>compile</scope>
+ </dependency>
+
+<!--
+ <dependency>
<groupId>org.eclipse.jetty.orbit</groupId>
<artifactId>javax.servlet</artifactId>
<scope>compile</scope>
</dependency>
+-->
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-jmx</artifactId>
diff --git a/examples/async-rest/async-rest-jar/pom.xml b/examples/async-rest/async-rest-jar/pom.xml
index 9a6c6e8..aeb3267 100644
--- a/examples/async-rest/async-rest-jar/pom.xml
+++ b/examples/async-rest/async-rest-jar/pom.xml
@@ -2,7 +2,7 @@
<parent>
<groupId>org.eclipse.jetty</groupId>
<artifactId>example-async-rest</artifactId>
- <version>9.0.5-SNAPSHOT</version>
+ <version>9.1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<groupId>org.eclipse.jetty.example-async-rest</groupId>
@@ -22,8 +22,9 @@
<version>${project.version}</version>
</dependency>
<dependency>
- <groupId>org.eclipse.jetty.orbit</groupId>
- <artifactId>javax.servlet</artifactId>
+ <groupId>javax.servlet</groupId>
+ <artifactId>javax.servlet-api</artifactId>
+ <version>3.1-b08</version>
<scope>provided</scope>
</dependency>
</dependencies>
diff --git a/examples/async-rest/async-rest-webapp/pom.xml b/examples/async-rest/async-rest-webapp/pom.xml
index 3e3d48d..5f48d59 100644
--- a/examples/async-rest/async-rest-webapp/pom.xml
+++ b/examples/async-rest/async-rest-webapp/pom.xml
@@ -2,7 +2,7 @@
<parent>
<groupId>org.eclipse.jetty</groupId>
<artifactId>example-async-rest</artifactId>
- <version>9.0.5-SNAPSHOT</version>
+ <version>9.1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<groupId>org.eclipse.jetty.example-async-rest</groupId>
@@ -25,8 +25,9 @@
<scope>test</scope>
</dependency>
<dependency>
- <groupId>org.eclipse.jetty.orbit</groupId>
- <artifactId>javax.servlet</artifactId>
+ <groupId>javax.servlet</groupId>
+ <artifactId>javax.servlet-api</artifactId>
+ <version>3.1-b08</version>
<scope>provided</scope>
</dependency>
</dependencies>
diff --git a/examples/async-rest/pom.xml b/examples/async-rest/pom.xml
index 25f20fc..a4c16ff 100644
--- a/examples/async-rest/pom.xml
+++ b/examples/async-rest/pom.xml
@@ -2,7 +2,7 @@
<parent>
<groupId>org.eclipse.jetty.examples</groupId>
<artifactId>examples-parent</artifactId>
- <version>9.0.5-SNAPSHOT</version>
+ <version>9.1.0-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
diff --git a/examples/embedded/pom.xml b/examples/embedded/pom.xml
index 80d1656..f1f5bee 100644
--- a/examples/embedded/pom.xml
+++ b/examples/embedded/pom.xml
@@ -2,7 +2,7 @@
<parent>
<groupId>org.eclipse.jetty.examples</groupId>
<artifactId>examples-parent</artifactId>
- <version>9.0.5-SNAPSHOT</version>
+ <version>9.1.0-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
@@ -68,6 +68,10 @@
<version>${project.version}</version>
</dependency>
<dependency>
+ <groupId>javax.transaction</groupId>
+ <artifactId>javax.transaction-api</artifactId>
+ </dependency>
+ <dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-proxy</artifactId>
<version>${project.version}</version>
diff --git a/examples/embedded/src/main/java/org/eclipse/jetty/embedded/SecuredHelloHandler.java b/examples/embedded/src/main/java/org/eclipse/jetty/embedded/SecuredHelloHandler.java
index 043a608..aa1a4d6 100644
--- a/examples/embedded/src/main/java/org/eclipse/jetty/embedded/SecuredHelloHandler.java
+++ b/examples/embedded/src/main/java/org/eclipse/jetty/embedded/SecuredHelloHandler.java
@@ -54,7 +54,6 @@
security.setConstraintMappings(Collections.singletonList(mapping));
security.setAuthenticator(new BasicAuthenticator());
security.setLoginService(loginService);
- security.setStrict(false);
HelloHandler hh = new HelloHandler();
diff --git a/examples/pom.xml b/examples/pom.xml
index e311e4d..4e4535c 100644
--- a/examples/pom.xml
+++ b/examples/pom.xml
@@ -21,7 +21,7 @@
<parent>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-project</artifactId>
- <version>9.0.5-SNAPSHOT</version>
+ <version>9.1.0-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<groupId>org.eclipse.jetty.examples</groupId>
diff --git a/jetty-annotations/pom.xml b/jetty-annotations/pom.xml
index 122641b..cf1576f 100644
--- a/jetty-annotations/pom.xml
+++ b/jetty-annotations/pom.xml
@@ -2,7 +2,7 @@
<parent>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-project</artifactId>
- <version>9.0.5-SNAPSHOT</version>
+ <version>9.1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>jetty-annotations</artifactId>
@@ -43,7 +43,7 @@
</goals>
<configuration>
<instructions>
- <Import-Package>javax.servlet.*;version="2.6.0",*</Import-Package>
+ <Import-Package>javax.servlet.*;version="[2.6.0,3.2)",*</Import-Package>
</instructions>
</configuration>
</execution>
@@ -93,8 +93,8 @@
<version>${project.version}</version>
</dependency>
<dependency>
- <groupId>org.eclipse.jetty.orbit</groupId>
- <artifactId>javax.annotation</artifactId>
+ <groupId>javax.annotation</groupId>
+ <artifactId>javax.annotation-api</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.jetty.orbit</groupId>
diff --git a/jetty-annotations/src/main/java/org/eclipse/jetty/annotations/AnnotationConfiguration.java b/jetty-annotations/src/main/java/org/eclipse/jetty/annotations/AnnotationConfiguration.java
index 3672db4..84c7b34 100644
--- a/jetty-annotations/src/main/java/org/eclipse/jetty/annotations/AnnotationConfiguration.java
+++ b/jetty-annotations/src/main/java/org/eclipse/jetty/annotations/AnnotationConfiguration.java
@@ -65,6 +65,11 @@
}
+ public void addDiscoverableAnnotationHandler(DiscoverableAnnotationHandler handler)
+ {
+ _discoverableAnnotationHandlers.add(handler);
+ }
+
@Override
public void deconfigure(WebAppContext context) throws Exception
{
@@ -135,7 +140,7 @@
@Override
public void postConfigure(WebAppContext context) throws Exception
{
- MultiMap map = (MultiMap)context.getAttribute(CLASS_INHERITANCE_MAP);
+ MultiMap<String> map = (MultiMap<String>)context.getAttribute(CLASS_INHERITANCE_MAP);
if (map != null)
map.clear();
@@ -211,7 +216,7 @@
//process the whole class hierarchy to satisfy the ServletContainerInitializer
if (context.getAttribute(CLASS_INHERITANCE_MAP) == null)
{
- MultiMap map = new MultiMap();
+ MultiMap<String> map = new MultiMap<>();
context.setAttribute(CLASS_INHERITANCE_MAP, map);
_classInheritanceHandler = new ClassInheritanceHandler(map);
}
diff --git a/jetty-annotations/src/main/java/org/eclipse/jetty/annotations/AnnotationDecorator.java b/jetty-annotations/src/main/java/org/eclipse/jetty/annotations/AnnotationDecorator.java
index d0fd954..b50fb45 100644
--- a/jetty-annotations/src/main/java/org/eclipse/jetty/annotations/AnnotationDecorator.java
+++ b/jetty-annotations/src/main/java/org/eclipse/jetty/annotations/AnnotationDecorator.java
@@ -18,19 +18,11 @@
package org.eclipse.jetty.annotations;
-import java.util.EventListener;
-
-import javax.servlet.Filter;
-import javax.servlet.Servlet;
-import javax.servlet.ServletException;
-
-import org.eclipse.jetty.servlet.FilterHolder;
import org.eclipse.jetty.servlet.ServletContextHandler.Decorator;
-import org.eclipse.jetty.servlet.ServletHolder;
import org.eclipse.jetty.webapp.WebAppContext;
/**
- * WebAppDecoratorWrapper
+ * AnnotationDecorator
*
*
*/
@@ -53,99 +45,6 @@
_introspector.registerHandler(new ServletSecurityAnnotationHandler(context));
}
- /* ------------------------------------------------------------ */
- /**
- * @param filter
- * @throws ServletException
- * @see org.eclipse.jetty.servlet.ServletContextHandler.Decorator#decorateFilterHolder(org.eclipse.jetty.servlet.FilterHolder)
- */
- public void decorateFilterHolder(FilterHolder filter) throws ServletException
- {
- }
-
- /* ------------------------------------------------------------ */
- /**
- * @param <T>
- * @param filter
- * @return the decorated filter
- * @throws ServletException
- * @see org.eclipse.jetty.servlet.ServletContextHandler.Decorator#decorateFilterInstance(javax.servlet.Filter)
- */
- public <T extends Filter> T decorateFilterInstance(T filter) throws ServletException
- {
- introspect(filter);
- return filter;
- }
-
- /* ------------------------------------------------------------ */
- /**
- * @param <T>
- * @param listener
- * @return the decorated event listener instance
- * @throws ServletException
- * @see org.eclipse.jetty.servlet.ServletContextHandler.Decorator#decorateListenerInstance(java.util.EventListener)
- */
- public <T extends EventListener> T decorateListenerInstance(T listener) throws ServletException
- {
- introspect(listener);
- return listener;
- }
-
- /* ------------------------------------------------------------ */
- /**
- * @param servlet
- * @throws ServletException
- * @see org.eclipse.jetty.servlet.ServletContextHandler.Decorator#decorateServletHolder(org.eclipse.jetty.servlet.ServletHolder)
- */
- public void decorateServletHolder(ServletHolder servlet) throws ServletException
- {
- }
-
- /* ------------------------------------------------------------ */
- /**
- * @param <T>
- * @param servlet
- * @return the decorated servlet instance
- * @throws ServletException
- * @see org.eclipse.jetty.servlet.ServletContextHandler.Decorator#decorateServletInstance(javax.servlet.Servlet)
- */
- public <T extends Servlet> T decorateServletInstance(T servlet) throws ServletException
- {
- introspect(servlet);
- return servlet;
- }
-
- /* ------------------------------------------------------------ */
- /**
- * @param f
- * @see org.eclipse.jetty.servlet.ServletContextHandler.Decorator#destroyFilterInstance(javax.servlet.Filter)
- */
- public void destroyFilterInstance(Filter f)
- {
- }
-
- /* ------------------------------------------------------------ */
- /**
- * @param s
- * @see org.eclipse.jetty.servlet.ServletContextHandler.Decorator#destroyServletInstance(javax.servlet.Servlet)
- */
- public void destroyServletInstance(Servlet s)
- {
- }
-
-
-
-
-
- /* ------------------------------------------------------------ */
- /**
- * @param f
- * @see org.eclipse.jetty.servlet.ServletContextHandler.Decorator#destroyListenerInstance(java.util.EventListener)
- */
- public void destroyListenerInstance(EventListener f)
- {
- }
-
/**
* Look for annotations that can be discovered with introspection:
* <ul>
@@ -161,4 +60,17 @@
{
_introspector.introspect(o.getClass());
}
+
+ @Override
+ public Object decorate(Object o)
+ {
+ introspect(o);
+ return o;
+ }
+
+ @Override
+ public void destroy(Object o)
+ {
+
+ }
}
diff --git a/jetty-annotations/src/main/java/org/eclipse/jetty/annotations/ClassInheritanceHandler.java b/jetty-annotations/src/main/java/org/eclipse/jetty/annotations/ClassInheritanceHandler.java
index ca6c61b..50892a8 100644
--- a/jetty-annotations/src/main/java/org/eclipse/jetty/annotations/ClassInheritanceHandler.java
+++ b/jetty-annotations/src/main/java/org/eclipse/jetty/annotations/ClassInheritanceHandler.java
@@ -33,16 +33,15 @@
public class ClassInheritanceHandler implements ClassHandler
{
private static final Logger LOG = Log.getLogger(ClassInheritanceHandler.class);
-
- MultiMap _inheritanceMap;
+ MultiMap<String> _inheritanceMap;
public ClassInheritanceHandler()
{
- _inheritanceMap = new MultiMap();
+ _inheritanceMap = new MultiMap<>();
}
- public ClassInheritanceHandler(MultiMap map)
+ public ClassInheritanceHandler(MultiMap<String> map)
{
_inheritanceMap = map;
}
@@ -65,12 +64,12 @@
}
}
- public List getClassNamesExtendingOrImplementing (String className)
+ public List<String> getClassNamesExtendingOrImplementing (String className)
{
return _inheritanceMap.getValues(className);
}
- public MultiMap getMap ()
+ public MultiMap<String> getMap ()
{
return _inheritanceMap;
}
diff --git a/jetty-annotations/src/main/java/org/eclipse/jetty/annotations/PostConstructAnnotationHandler.java b/jetty-annotations/src/main/java/org/eclipse/jetty/annotations/PostConstructAnnotationHandler.java
index 2b962a5..5d5daea 100644
--- a/jetty-annotations/src/main/java/org/eclipse/jetty/annotations/PostConstructAnnotationHandler.java
+++ b/jetty-annotations/src/main/java/org/eclipse/jetty/annotations/PostConstructAnnotationHandler.java
@@ -44,7 +44,7 @@
public void doHandle(Class clazz)
{
//Check that the PostConstruct is on a class that we're interested in
- if (Util.isServletType(clazz))
+ if (Util.supportsPostConstructPreDestroy(clazz))
{
Method[] methods = clazz.getDeclaredMethods();
for (int i=0; i<methods.length; i++)
diff --git a/jetty-annotations/src/main/java/org/eclipse/jetty/annotations/PreDestroyAnnotationHandler.java b/jetty-annotations/src/main/java/org/eclipse/jetty/annotations/PreDestroyAnnotationHandler.java
index 5450542..a536885 100644
--- a/jetty-annotations/src/main/java/org/eclipse/jetty/annotations/PreDestroyAnnotationHandler.java
+++ b/jetty-annotations/src/main/java/org/eclipse/jetty/annotations/PreDestroyAnnotationHandler.java
@@ -43,7 +43,7 @@
public void doHandle(Class clazz)
{
//Check that the PreDestroy is on a class that we're interested in
- if (Util.isServletType(clazz))
+ if (Util.supportsPostConstructPreDestroy(clazz))
{
Method[] methods = clazz.getDeclaredMethods();
for (int i=0; i<methods.length; i++)
diff --git a/jetty-annotations/src/main/java/org/eclipse/jetty/annotations/ResourceAnnotationHandler.java b/jetty-annotations/src/main/java/org/eclipse/jetty/annotations/ResourceAnnotationHandler.java
index 875de2d..ce8d9a7 100644
--- a/jetty-annotations/src/main/java/org/eclipse/jetty/annotations/ResourceAnnotationHandler.java
+++ b/jetty-annotations/src/main/java/org/eclipse/jetty/annotations/ResourceAnnotationHandler.java
@@ -57,7 +57,7 @@
*/
public void doHandle(Class<?> clazz)
{
- if (Util.isServletType(clazz))
+ if (Util.supportsResourceInjection(clazz))
{
handleClass(clazz);
diff --git a/jetty-annotations/src/main/java/org/eclipse/jetty/annotations/ServletSecurityAnnotationHandler.java b/jetty-annotations/src/main/java/org/eclipse/jetty/annotations/ServletSecurityAnnotationHandler.java
index 41c654c..20f3f7d 100644
--- a/jetty-annotations/src/main/java/org/eclipse/jetty/annotations/ServletSecurityAnnotationHandler.java
+++ b/jetty-annotations/src/main/java/org/eclipse/jetty/annotations/ServletSecurityAnnotationHandler.java
@@ -112,6 +112,9 @@
for (ConstraintMapping m:constraintMappings)
securityHandler.addConstraintMapping(m);
+
+ //Servlet Spec 3.1 requires paths with uncovered http methods to be reported
+ securityHandler.checkPathsWithUncoveredHttpMethods();
}
@@ -128,12 +131,6 @@
protected Constraint makeConstraint (Class servlet, String[] rolesAllowed, EmptyRoleSemantic permitOrDeny, TransportGuarantee transport)
{
return ConstraintSecurityHandler.createConstraint(servlet.getName(), rolesAllowed, permitOrDeny, transport);
-
-
-
-
-
-
}
diff --git a/jetty-annotations/src/main/java/org/eclipse/jetty/annotations/Util.java b/jetty-annotations/src/main/java/org/eclipse/jetty/annotations/Util.java
index 37a22a3..08527c3 100644
--- a/jetty-annotations/src/main/java/org/eclipse/jetty/annotations/Util.java
+++ b/jetty-annotations/src/main/java/org/eclipse/jetty/annotations/Util.java
@@ -68,6 +68,42 @@
return isServlet;
}
+
+
+ public static boolean supportsResourceInjection (Class c)
+ {
+ if (javax.servlet.Servlet.class.isAssignableFrom(c) ||
+ javax.servlet.Filter.class.isAssignableFrom(c) ||
+ javax.servlet.ServletContextListener.class.isAssignableFrom(c) ||
+ javax.servlet.ServletContextAttributeListener.class.isAssignableFrom(c) ||
+ javax.servlet.ServletRequestListener.class.isAssignableFrom(c) ||
+ javax.servlet.ServletRequestAttributeListener.class.isAssignableFrom(c) ||
+ javax.servlet.http.HttpSessionListener.class.isAssignableFrom(c) ||
+ javax.servlet.http.HttpSessionAttributeListener.class.isAssignableFrom(c) ||
+ javax.servlet.AsyncListener.class.isAssignableFrom(c) ||
+ javax.servlet.http.HttpUpgradeHandler.class.isAssignableFrom(c))
+ return true;
+
+ return false;
+ }
+
+
+ public static boolean supportsPostConstructPreDestroy (Class c)
+ {
+ if (javax.servlet.Servlet.class.isAssignableFrom(c) ||
+ javax.servlet.Filter.class.isAssignableFrom(c) ||
+ javax.servlet.ServletContextListener.class.isAssignableFrom(c) ||
+ javax.servlet.ServletContextAttributeListener.class.isAssignableFrom(c) ||
+ javax.servlet.ServletRequestListener.class.isAssignableFrom(c) ||
+ javax.servlet.ServletRequestAttributeListener.class.isAssignableFrom(c) ||
+ javax.servlet.http.HttpSessionListener.class.isAssignableFrom(c) ||
+ javax.servlet.http.HttpSessionAttributeListener.class.isAssignableFrom(c) ||
+ javax.servlet.AsyncListener.class.isAssignableFrom(c) ||
+ javax.servlet.http.HttpUpgradeHandler.class.isAssignableFrom(c))
+ return true;
+
+ return false;
+ }
public static boolean isEnvEntryType (Class type)
{
diff --git a/jetty-annotations/src/main/java/org/eclipse/jetty/annotations/WebListenerAnnotation.java b/jetty-annotations/src/main/java/org/eclipse/jetty/annotations/WebListenerAnnotation.java
index b82fd11..c55bca2 100644
--- a/jetty-annotations/src/main/java/org/eclipse/jetty/annotations/WebListenerAnnotation.java
+++ b/jetty-annotations/src/main/java/org/eclipse/jetty/annotations/WebListenerAnnotation.java
@@ -23,6 +23,7 @@
import javax.servlet.ServletRequestAttributeListener;
import javax.servlet.ServletRequestListener;
import javax.servlet.http.HttpSessionAttributeListener;
+import javax.servlet.http.HttpSessionIdListener;
import javax.servlet.http.HttpSessionListener;
import org.eclipse.jetty.util.log.Log;
@@ -78,7 +79,8 @@
ServletRequestListener.class.isAssignableFrom(clazz) ||
ServletRequestAttributeListener.class.isAssignableFrom(clazz) ||
HttpSessionListener.class.isAssignableFrom(clazz) ||
- HttpSessionAttributeListener.class.isAssignableFrom(clazz))
+ HttpSessionAttributeListener.class.isAssignableFrom(clazz) ||
+ HttpSessionIdListener.class.isAssignableFrom(clazz))
{
java.util.EventListener listener = (java.util.EventListener)clazz.newInstance();
MetaData metaData = _context.getMetaData();
diff --git a/jetty-annotations/src/test/java/org/eclipse/jetty/annotations/TestSecurityAnnotationConversions.java b/jetty-annotations/src/test/java/org/eclipse/jetty/annotations/TestSecurityAnnotationConversions.java
index 4ef220f..d0c07aa 100644
--- a/jetty-annotations/src/test/java/org/eclipse/jetty/annotations/TestSecurityAnnotationConversions.java
+++ b/jetty-annotations/src/test/java/org/eclipse/jetty/annotations/TestSecurityAnnotationConversions.java
@@ -115,21 +115,12 @@
introspector.registerHandler(annotationHandler);
- //set up the expected outcomes:
+ //set up the expected outcomes - no constraints at all as per Servlet Spec 3.1 pg 129
//1 ConstraintMapping per ServletMapping pathSpec
- Constraint expectedConstraint = new Constraint();
- expectedConstraint.setAuthenticate(false);
- expectedConstraint.setDataConstraint(Constraint.DC_NONE);
+
- ConstraintMapping[] expectedMappings = new ConstraintMapping[2];
- expectedMappings[0] = new ConstraintMapping();
- expectedMappings[0].setConstraint(expectedConstraint);
- expectedMappings[0].setPathSpec("/foo/*");
-
- expectedMappings[1] = new ConstraintMapping();
- expectedMappings[1].setConstraint(expectedConstraint);
- expectedMappings[1].setPathSpec("*.foo");
-
+ ConstraintMapping[] expectedMappings = new ConstraintMapping[]{};
+
introspector.introspect(PermitServlet.class);
compareResults (expectedMappings, ((ConstraintAware)wac.getSecurityHandler()).getConstraintMappings());
diff --git a/jetty-ant/pom.xml b/jetty-ant/pom.xml
index d756041..9f10f17 100755
--- a/jetty-ant/pom.xml
+++ b/jetty-ant/pom.xml
@@ -2,7 +2,7 @@
<parent>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-project</artifactId>
- <version>9.0.5-SNAPSHOT</version>
+ <version>9.1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>jetty-ant</artifactId>
diff --git a/jetty-client/pom.xml b/jetty-client/pom.xml
index cfa10eb..a7cd736 100644
--- a/jetty-client/pom.xml
+++ b/jetty-client/pom.xml
@@ -2,7 +2,7 @@
<parent>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-project</artifactId>
- <version>9.0.5-SNAPSHOT</version>
+ <version>9.1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
@@ -92,6 +92,12 @@
<version>${project.version}</version>
</dependency>
<dependency>
+ <groupId>org.eclipse.jetty.spdy</groupId>
+ <artifactId>spdy-client</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+
+ <dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-server</artifactId>
<version>${project.version}</version>
diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/HttpChannel.java b/jetty-client/src/main/java/org/eclipse/jetty/client/HttpChannel.java
new file mode 100644
index 0000000..fab738d
--- /dev/null
+++ b/jetty-client/src/main/java/org/eclipse/jetty/client/HttpChannel.java
@@ -0,0 +1,76 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.client;
+
+import java.util.concurrent.atomic.AtomicReference;
+
+import org.eclipse.jetty.client.api.Result;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+
+public abstract class HttpChannel
+{
+ protected static final Logger LOG = Log.getLogger(new Object(){}.getClass().getEnclosingClass());
+
+ private final AtomicReference<HttpExchange> exchange = new AtomicReference<>();
+ private final HttpDestination destination;
+
+ protected HttpChannel(HttpDestination destination)
+ {
+ this.destination = destination;
+ }
+
+ public HttpDestination getHttpDestination()
+ {
+ return destination;
+ }
+
+ public void associate(HttpExchange exchange)
+ {
+ if (!this.exchange.compareAndSet(null, exchange))
+ throw new UnsupportedOperationException("Pipelined requests not supported");
+ exchange.associate(this);
+ LOG.debug("{} associated to {}", exchange, this);
+ }
+
+ public HttpExchange disassociate()
+ {
+ HttpExchange exchange = this.exchange.getAndSet(null);
+ if (exchange != null)
+ exchange.disassociate(this);
+ LOG.debug("{} disassociated from {}", exchange, this);
+ return exchange;
+ }
+
+ public HttpExchange getHttpExchange()
+ {
+ return exchange.get();
+ }
+
+ public abstract void send();
+
+ public abstract void proceed(HttpExchange exchange, boolean proceed);
+
+ public abstract boolean abort(Throwable cause);
+
+ public void exchangeTerminated(Result result)
+ {
+ disassociate();
+ }
+}
diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/HttpClient.java b/jetty-client/src/main/java/org/eclipse/jetty/client/HttpClient.java
index 3fe4028..8a2b4c7 100644
--- a/jetty-client/src/main/java/org/eclipse/jetty/client/HttpClient.java
+++ b/jetty-client/src/main/java/org/eclipse/jetty/client/HttpClient.java
@@ -19,14 +19,11 @@
package org.eclipse.jetty.client;
import java.io.IOException;
-import java.net.ConnectException;
import java.net.CookieManager;
import java.net.CookiePolicy;
import java.net.CookieStore;
import java.net.SocketAddress;
-import java.net.SocketException;
import java.net.URI;
-import java.nio.channels.SelectionKey;
import java.nio.channels.SocketChannel;
import java.util.ArrayList;
import java.util.Collection;
@@ -41,7 +38,6 @@
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executor;
import java.util.concurrent.TimeoutException;
-import javax.net.ssl.SSLEngine;
import org.eclipse.jetty.client.api.AuthenticationStore;
import org.eclipse.jetty.client.api.Connection;
@@ -50,16 +46,13 @@
import org.eclipse.jetty.client.api.ProxyConfiguration;
import org.eclipse.jetty.client.api.Request;
import org.eclipse.jetty.client.api.Response;
+import org.eclipse.jetty.client.http.HttpClientTransportOverHTTP;
import org.eclipse.jetty.http.HttpField;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpMethod;
import org.eclipse.jetty.http.HttpScheme;
import org.eclipse.jetty.io.ByteBufferPool;
-import org.eclipse.jetty.io.EndPoint;
import org.eclipse.jetty.io.MappedByteBufferPool;
-import org.eclipse.jetty.io.SelectChannelEndPoint;
-import org.eclipse.jetty.io.SelectorManager;
-import org.eclipse.jetty.io.ssl.SslConnection;
import org.eclipse.jetty.util.Jetty;
import org.eclipse.jetty.util.Promise;
import org.eclipse.jetty.util.SocketAddressResolver;
@@ -116,6 +109,7 @@
private final List<Request.Listener> requestListeners = new ArrayList<>();
private final AuthenticationStore authenticationStore = new HttpAuthenticationStore();
private final Set<ContentDecoder.Factory> decoderFactories = new ContentDecoderFactorySet();
+ private final HttpClientTransport transport;
private final SslContextFactory sslContextFactory;
private volatile CookieManager cookieManager;
private volatile CookieStore cookieStore;
@@ -123,13 +117,12 @@
private volatile ByteBufferPool byteBufferPool;
private volatile Scheduler scheduler;
private volatile SocketAddressResolver resolver;
- private volatile SelectorManager selectorManager;
private volatile HttpField agentField = new HttpField(HttpHeader.USER_AGENT, "Jetty/" + Jetty.VERSION);
private volatile boolean followRedirects = true;
private volatile int maxConnectionsPerDestination = 64;
private volatile int maxRequestsQueuedPerDestination = 1024;
private volatile int requestBufferSize = 4096;
- private volatile int responseBufferSize = 4096;
+ private volatile int responseBufferSize = 16384;
private volatile int maxRedirects = 8;
private volatile SocketAddress bindAddress;
private volatile long connectTimeout = 15000;
@@ -137,6 +130,7 @@
private volatile long idleTimeout;
private volatile boolean tcpNoDelay = true;
private volatile boolean dispatchIO = true;
+ private volatile boolean strictEventOrdering = false;
private volatile ProxyConfiguration proxyConfig;
private volatile HttpField encodingField;
@@ -160,9 +154,20 @@
*/
public HttpClient(SslContextFactory sslContextFactory)
{
+ this(new HttpClientTransportOverHTTP(), sslContextFactory);
+ }
+
+ public HttpClient(HttpClientTransport transport, SslContextFactory sslContextFactory)
+ {
+ this.transport = transport;
this.sslContextFactory = sslContextFactory;
}
+ public HttpClientTransport getTransport()
+ {
+ return transport;
+ }
+
/**
* @return the {@link SslContextFactory} that manages TLS encryption
* @see #HttpClient(SslContextFactory)
@@ -196,11 +201,10 @@
scheduler = new ScheduledExecutorScheduler(name + "-scheduler", false);
addBean(scheduler);
- resolver = new SocketAddressResolver(executor, scheduler, getAddressResolutionTimeout());
+ addBean(transport);
+ transport.setHttpClient(this);
- selectorManager = newSelectorManager();
- selectorManager.setConnectTimeout(getConnectTimeout());
- addBean(selectorManager);
+ resolver = new SocketAddressResolver(executor, scheduler, getAddressResolutionTimeout());
handlers.add(new ContinueProtocolHandler(this));
handlers.add(new RedirectProtocolHandler(this));
@@ -215,11 +219,6 @@
super.doStart();
}
- protected SelectorManager newSelectorManager()
- {
- return new ClientSelectorManager(getExecutor(), getScheduler());
- }
-
private CookieManager newCookieManager()
{
return new CookieManager(getCookieStore(), CookiePolicy.ACCEPT_ALL);
@@ -387,7 +386,7 @@
protected Request copyRequest(Request oldRequest, URI newURI)
{
Request newRequest = new HttpRequest(this, oldRequest.getConversationID(), newURI);
- newRequest.method(oldRequest.method())
+ newRequest.method(oldRequest.getMethod())
.version(oldRequest.getVersion())
.content(oldRequest.getContent());
for (HttpField header : oldRequest.getHeaders())
@@ -414,7 +413,7 @@
return newRequest;
}
- protected String address(String scheme, String host, int port)
+ public String address(String scheme, String host, int port)
{
StringBuilder result = new StringBuilder();
URIUtil.appendSchemeHostPort(result, scheme, host, port);
@@ -447,7 +446,7 @@
HttpDestination destination = destinations.get(address);
if (destination == null)
{
- destination = new HttpDestination(this, scheme, host, port);
+ destination = transport.newHttpDestination(this, scheme, host, port);
if (isRunning())
{
HttpDestination existing = destinations.putIfAbsent(address, destination);
@@ -489,28 +488,7 @@
@Override
public void succeeded(SocketAddress socketAddress)
{
- SocketChannel channel = null;
- try
- {
- channel = SocketChannel.open();
- SocketAddress bindAddress = getBindAddress();
- if (bindAddress != null)
- channel.bind(bindAddress);
- configure(channel);
- channel.configureBlocking(false);
- channel.connect(socketAddress);
-
- ConnectionCallback callback = new ConnectionCallback(destination, promise);
- selectorManager.connect(channel, callback);
- }
- // Must catch all exceptions, since some like
- // UnresolvedAddressException are not IOExceptions.
- catch (Throwable x)
- {
- if (channel != null)
- close(channel);
- promise.failed(x);
- }
+ transport.connect(destination, socketAddress, promise);
}
@Override
@@ -521,23 +499,6 @@
});
}
- protected void configure(SocketChannel channel) throws SocketException
- {
- channel.socket().setTcpNoDelay(isTCPNoDelay());
- }
-
- private void close(SocketChannel channel)
- {
- try
- {
- channel.close();
- }
- catch (IOException x)
- {
- LOG.ignore(x);
- }
- }
-
protected HttpConversation getConversation(long id, boolean create)
{
HttpConversation conversation = conversations.get(id);
@@ -729,11 +690,6 @@
this.scheduler = scheduler;
}
- protected SelectorManager getSelectorManager()
- {
- return selectorManager;
- }
-
/**
* @return the max number of connections that this {@link HttpClient} opens to {@link Destination}s
*/
@@ -879,6 +835,41 @@
}
/**
+ * @return whether request events must be strictly ordered
+ */
+ public boolean isStrictEventOrdering()
+ {
+ return strictEventOrdering;
+ }
+
+ /**
+ * Whether request events must be strictly ordered.
+ * <p />
+ * {@link Response.CompleteListener}s may send a second request.
+ * If the second request is for the same destination, there is an inherent race
+ * condition for the use of the connection: the first request may still be associated with the
+ * connection, so the second request cannot use that connection and is forced to open another one.
+ * <p />
+ * From the point of view of connection usage, the connection is reusable just before the "complete"
+ * event, so it would be possible to reuse that connection from {@link Response.CompleteListener}s;
+ * but in this case the second request's events will fire before the "complete" events of the first
+ * request.
+ * <p />
+ * This setting enforces strict event ordering so that a "begin" event of a second request can never
+ * fire before the "complete" event of a first request, but at the expense of an increased usage
+ * of connections.
+ * <p />
+ * When not enforced, a "begin" event of a second request may happen before the "complete" event of
+ * a first request and allow for better usage of connections.
+ *
+ * @param strictEventOrdering whether request events must be strictly ordered
+ */
+ public void setStrictEventOrdering(boolean strictEventOrdering)
+ {
+ this.strictEventOrdering = strictEventOrdering;
+ }
+
+ /**
* @return the forward proxy configuration
*/
public ProxyConfiguration getProxyConfiguration()
@@ -916,16 +907,6 @@
return HttpScheme.HTTPS.is(scheme) ? port == 443 : port == 80;
}
- protected HttpConnection newHttpConnection(HttpClient httpClient, EndPoint endPoint, HttpDestination destination)
- {
- return new HttpConnection(httpClient, endPoint, destination);
- }
-
- protected SslConnection newSslConnection(HttpClient httpClient, EndPoint endPoint, SSLEngine engine)
- {
- return new SslConnection(httpClient.getByteBufferPool(), httpClient.getExecutor(), endPoint, engine);
- }
-
@Override
public void dump(Appendable out, String indent) throws IOException
{
@@ -933,113 +914,6 @@
dump(out, indent, getBeans(), destinations.values());
}
- protected Connection tunnel(Connection connection)
- {
- HttpConnection httpConnection = (HttpConnection)connection;
- HttpDestination destination = httpConnection.getDestination();
- SslConnection sslConnection = createSslConnection(destination, httpConnection.getEndPoint());
- Connection result = (Connection)sslConnection.getDecryptedEndPoint().getConnection();
- selectorManager.connectionClosed(httpConnection);
- selectorManager.connectionOpened(sslConnection);
- LOG.debug("Tunnelled {} over {}", connection, result);
- return result;
- }
-
- private SslConnection createSslConnection(HttpDestination destination, EndPoint endPoint)
- {
- SSLEngine engine = sslContextFactory.newSSLEngine(destination.getHost(), destination.getPort());
- engine.setUseClientMode(true);
-
- SslConnection sslConnection = newSslConnection(HttpClient.this, endPoint, engine);
- sslConnection.setRenegotiationAllowed(sslContextFactory.isRenegotiationAllowed());
- endPoint.setConnection(sslConnection);
- EndPoint appEndPoint = sslConnection.getDecryptedEndPoint();
- HttpConnection connection = newHttpConnection(this, appEndPoint, destination);
- appEndPoint.setConnection(connection);
-
- return sslConnection;
- }
-
- protected class ClientSelectorManager extends SelectorManager
- {
- public ClientSelectorManager(Executor executor, Scheduler scheduler)
- {
- this(executor, scheduler, 1);
- }
-
- public ClientSelectorManager(Executor executor, Scheduler scheduler, int selectors)
- {
- super(executor, scheduler, selectors);
- }
-
- @Override
- protected EndPoint newEndPoint(SocketChannel channel, ManagedSelector selector, SelectionKey key)
- {
- return new SelectChannelEndPoint(channel, selector, key, getScheduler(), getIdleTimeout());
- }
-
- @Override
- public org.eclipse.jetty.io.Connection newConnection(SocketChannel channel, EndPoint endPoint, Object attachment) throws IOException
- {
- ConnectionCallback callback = (ConnectionCallback)attachment;
- HttpDestination destination = callback.destination;
-
- SslContextFactory sslContextFactory = getSslContextFactory();
- if (!destination.isProxied() && HttpScheme.HTTPS.is(destination.getScheme()))
- {
- if (sslContextFactory == null)
- {
- IOException failure = new ConnectException("Missing " + SslContextFactory.class.getSimpleName() + " for " + destination.getScheme() + " requests");
- callback.failed(failure);
- throw failure;
- }
- else
- {
- SslConnection sslConnection = createSslConnection(destination, endPoint);
- callback.succeeded((Connection)sslConnection.getDecryptedEndPoint().getConnection());
- return sslConnection;
- }
- }
- else
- {
- HttpConnection connection = newHttpConnection(HttpClient.this, endPoint, destination);
- callback.succeeded(connection);
- return connection;
- }
- }
-
- @Override
- protected void connectionFailed(SocketChannel channel, Throwable ex, Object attachment)
- {
- ConnectionCallback callback = (ConnectionCallback)attachment;
- callback.failed(ex);
- }
- }
-
- private class ConnectionCallback implements Promise<Connection>
- {
- private final HttpDestination destination;
- private final Promise<Connection> promise;
-
- private ConnectionCallback(HttpDestination destination, Promise<Connection> promise)
- {
- this.destination = destination;
- this.promise = promise;
- }
-
- @Override
- public void succeeded(Connection result)
- {
- promise.succeeded(result);
- }
-
- @Override
- public void failed(Throwable x)
- {
- promise.failed(x);
- }
- }
-
private class ContentDecoderFactorySet implements Set<ContentDecoder.Factory>
{
private final Set<ContentDecoder.Factory> set = new HashSet<>();
diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/HttpClientTransport.java b/jetty-client/src/main/java/org/eclipse/jetty/client/HttpClientTransport.java
new file mode 100644
index 0000000..80d284c
--- /dev/null
+++ b/jetty-client/src/main/java/org/eclipse/jetty/client/HttpClientTransport.java
@@ -0,0 +1,35 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.client;
+
+import java.net.SocketAddress;
+
+import org.eclipse.jetty.client.api.Connection;
+import org.eclipse.jetty.util.Promise;
+
+public interface HttpClientTransport
+{
+ void setHttpClient(HttpClient client);
+
+ HttpDestination newHttpDestination(HttpClient httpClient, String scheme, String host, int port);
+
+ void connect(HttpDestination destination, SocketAddress address, Promise<Connection> promise);
+
+ Connection tunnel(Connection connection);
+}
diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/HttpConnection.java b/jetty-client/src/main/java/org/eclipse/jetty/client/HttpConnection.java
index d578f1c..1559fc9 100644
--- a/jetty-client/src/main/java/org/eclipse/jetty/client/HttpConnection.java
+++ b/jetty-client/src/main/java/org/eclipse/jetty/client/HttpConnection.java
@@ -21,10 +21,7 @@
import java.net.HttpCookie;
import java.net.URI;
import java.util.ArrayList;
-import java.util.Enumeration;
import java.util.List;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.atomic.AtomicReference;
import org.eclipse.jetty.client.api.Authentication;
import org.eclipse.jetty.client.api.Connection;
@@ -37,31 +34,18 @@
import org.eclipse.jetty.http.HttpHeaderValue;
import org.eclipse.jetty.http.HttpMethod;
import org.eclipse.jetty.http.HttpVersion;
-import org.eclipse.jetty.io.AbstractConnection;
-import org.eclipse.jetty.io.EndPoint;
-import org.eclipse.jetty.util.log.Log;
-import org.eclipse.jetty.util.log.Logger;
-public class HttpConnection extends AbstractConnection implements Connection
+public abstract class HttpConnection implements Connection
{
- private static final Logger LOG = Log.getLogger(HttpConnection.class);
private static final HttpField CHUNKED_FIELD = new HttpField(HttpHeader.TRANSFER_ENCODING, HttpHeaderValue.CHUNKED);
- private final AtomicReference<HttpExchange> exchange = new AtomicReference<>();
private final HttpClient client;
private final HttpDestination destination;
- private final HttpSender sender;
- private final HttpReceiver receiver;
- private long idleTimeout;
- private volatile boolean closed;
- public HttpConnection(HttpClient client, EndPoint endPoint, HttpDestination destination)
+ protected HttpConnection(HttpClient client, HttpDestination destination)
{
- super(endPoint, client.getExecutor(), client.isDispatchIO());
this.client = client;
this.destination = destination;
- this.sender = new HttpSender(this);
- this.receiver = new HttpReceiver(this);
}
public HttpClient getHttpClient()
@@ -69,56 +53,12 @@
return client;
}
- public HttpDestination getDestination()
+ public HttpDestination getHttpDestination()
{
return destination;
}
@Override
- public void onOpen()
- {
- super.onOpen();
- fillInterested();
- }
-
- @Override
- public void onClose()
- {
- closed = true;
- super.onClose();
- }
-
- @Override
- public void fillInterested()
- {
- // This is necessary when "upgrading" the connection for example after proxied
- // CONNECT requests, because the old connection will read the CONNECT response
- // and then set the read interest, while the new connection attached to the same
- // EndPoint also will set the read interest, causing a ReadPendingException.
- if (!closed)
- super.fillInterested();
- }
-
- @Override
- protected boolean onReadTimeout()
- {
- LOG.debug("{} idle timeout", this);
-
- HttpExchange exchange = getExchange();
- if (exchange != null)
- idleTimeout();
- else
- destination.remove(this);
-
- return true;
- }
-
- protected void idleTimeout()
- {
- receiver.idleTimeout();
- }
-
- @Override
public void send(Request request, Response.CompleteListener listener)
{
ArrayList<Response.ResponseListener> listeners = new ArrayList<>(2);
@@ -132,45 +72,20 @@
listeners.add(listener);
HttpConversation conversation = client.getConversation(request.getConversationID(), true);
- HttpExchange exchange = new HttpExchange(conversation, getDestination(), request, listeners);
+ HttpExchange exchange = new HttpExchange(conversation, getHttpDestination(), request, listeners);
+
send(exchange);
}
- public void send(HttpExchange exchange)
+ protected abstract void send(HttpExchange exchange);
+
+ protected void normalizeRequest(Request request)
{
- Request request = exchange.getRequest();
- normalizeRequest(request);
-
- // Save the old idle timeout to restore it
- EndPoint endPoint = getEndPoint();
- idleTimeout = endPoint.getIdleTimeout();
- endPoint.setIdleTimeout(request.getIdleTimeout());
-
- // Associate the exchange to the connection
- associate(exchange);
-
- sender.send(exchange);
- }
-
- private void normalizeRequest(Request request)
- {
- if (request.method() == null)
- request.method(HttpMethod.GET);
-
- if (request.getVersion() == null)
- request.version(HttpVersion.HTTP_1_1);
-
- if (request.getIdleTimeout() <= 0)
- request.idleTimeout(client.getIdleTimeout(), TimeUnit.MILLISECONDS);
-
- String method = request.method();
+ String method = request.getMethod();
HttpVersion version = request.getVersion();
HttpFields headers = request.getHeaders();
ContentProvider content = request.getContent();
- if (request.getAgent() == null)
- headers.put(client.getUserAgentField());
-
// Make sure the path is there
String path = request.getPath();
if (path.trim().length() == 0)
@@ -188,7 +103,7 @@
if (version.getVersion() > 10)
{
if (!headers.containsKey(HttpHeader.HOST.asString()))
- headers.put(getDestination().getHostField());
+ headers.put(getHttpDestination().getHostField());
}
// Add content headers
@@ -227,137 +142,5 @@
Authentication.Result authnResult = client.getAuthenticationStore().findAuthenticationResult(authenticationURI);
if (authnResult != null)
authnResult.apply(request);
-
- if (!headers.containsKey(HttpHeader.ACCEPT_ENCODING.asString()))
- {
- HttpField acceptEncodingField = client.getAcceptEncodingField();
- if (acceptEncodingField != null)
- headers.put(acceptEncodingField);
- }
- }
-
- public HttpExchange getExchange()
- {
- return exchange.get();
- }
-
- protected void associate(HttpExchange exchange)
- {
- if (!this.exchange.compareAndSet(null, exchange))
- throw new UnsupportedOperationException("Pipelined requests not supported");
- exchange.setConnection(this);
- LOG.debug("{} associated to {}", exchange, this);
- }
-
- protected HttpExchange disassociate()
- {
- HttpExchange exchange = this.exchange.getAndSet(null);
- if (exchange != null)
- exchange.setConnection(null);
- LOG.debug("{} disassociated from {}", exchange, this);
- return exchange;
- }
-
- @Override
- public void onFillable()
- {
- HttpExchange exchange = getExchange();
- if (exchange != null)
- {
- receive();
- }
- else
- {
- // If there is no exchange, then could be either a remote close,
- // or garbage bytes; in both cases we close the connection
- close();
- }
- }
-
- protected void receive()
- {
- receiver.receive();
- }
-
- public void complete(HttpExchange exchange, boolean success)
- {
- HttpExchange existing = disassociate();
- if (existing == exchange)
- {
- exchange.awaitTermination();
-
- // Restore idle timeout
- getEndPoint().setIdleTimeout(idleTimeout);
-
- LOG.debug("{} disassociated from {}", exchange, this);
- if (success)
- {
- HttpFields responseHeaders = exchange.getResponse().getHeaders();
- Enumeration<String> values = responseHeaders.getValues(HttpHeader.CONNECTION.asString(), ",");
- if (values != null)
- {
- while (values.hasMoreElements())
- {
- if ("close".equalsIgnoreCase(values.nextElement()))
- {
- close();
- return;
- }
- }
- }
- destination.release(this);
- }
- else
- {
- close();
- }
- }
- else if (existing == null)
- {
- // It is possible that the exchange has already been disassociated,
- // for example if the connection idle timeouts: this will fail
- // the response, but the request may still be under processing.
- // Eventually the request will also fail as the connection is closed
- // and will arrive here without an exchange being present.
- // We just ignore this fact, as the exchange has already been processed
- }
- else
- {
- throw new IllegalStateException();
- }
- }
-
- public boolean abort(Throwable cause)
- {
- // We want the return value to be that of the response
- // because if the response has already successfully
- // arrived then we failed to abort the exchange
- sender.abort(cause);
- return receiver.abort(cause);
- }
-
- public void proceed(boolean proceed)
- {
- sender.proceed(proceed);
- }
-
- @Override
- public void close()
- {
- destination.remove(this);
- getEndPoint().shutdownOutput();
- LOG.debug("{} oshut", this);
- getEndPoint().close();
- LOG.debug("{} closed", this);
- }
-
- @Override
- public String toString()
- {
- return String.format("%s@%x(l:%s <-> r:%s)",
- HttpConnection.class.getSimpleName(),
- hashCode(),
- getEndPoint().getLocalAddress(),
- getEndPoint().getRemoteAddress());
}
}
diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/HttpContent.java b/jetty-client/src/main/java/org/eclipse/jetty/client/HttpContent.java
new file mode 100644
index 0000000..989e7f2
--- /dev/null
+++ b/jetty-client/src/main/java/org/eclipse/jetty/client/HttpContent.java
@@ -0,0 +1,156 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.client;
+
+import java.nio.ByteBuffer;
+import java.util.Collections;
+import java.util.Iterator;
+
+import org.eclipse.jetty.client.api.ContentProvider;
+import org.eclipse.jetty.client.util.DeferredContentProvider;
+import org.eclipse.jetty.util.BufferUtil;
+
+/**
+ * {@link HttpContent} is a stateful, linear representation of the request content provided
+ * by a {@link ContentProvider} that can be traversed one-way to obtain content buffers to
+ * send to a HTTP server.
+ * <p />
+ * {@link HttpContent} offers the notion of a one-way cursor to traverse the content.
+ * The cursor starts in a virtual "before" position and can be advanced using {@link #advance()}
+ * until it reaches a virtual "after" position where the content is fully consumed.
+ * <pre>
+ * +---+ +---+ +---+ +---+ +---+
+ * | | | | | | | | | |
+ * +---+ +---+ +---+ +---+ +---+
+ * ^ ^ ^ ^
+ * | | --> advance() | |
+ * | | last |
+ * | | |
+ * before | after
+ * |
+ * current
+ * </pre>
+ * At each valid (non-before and non-after) cursor position, {@link HttpContent} provides the following state:
+ * <ul>
+ * <li>the buffer containing the content to send, via {@link #getByteBuffer()}</li>
+ * <li>a copy of the content buffer that can be used for notifications, via {@link #getContent()}</li>
+ * <li>whether the buffer to write is the last one, via {@link #isLast()}</li>
+ * </ul>
+ * {@link HttpContent} may not have content, if the related {@link ContentProvider} is {@code null}, and this
+ * is reflected by {@link #hasContent()}.
+ * <p />
+ * {@link HttpContent} may have {@link DeferredContentProvider deferred content}, in which case {@link #advance()}
+ * moves the cursor to a position that provides {@code null} {@link #getByteBuffer() buffer} and
+ * {@link #getContent() content}. When the deferred content is available, a further call to {@link #advance()}
+ * will move the cursor to a position that provides non {@code null} buffer and content.
+ */
+public class HttpContent
+{
+ private static final ByteBuffer AFTER = ByteBuffer.allocate(0);
+
+ private final ContentProvider provider;
+ private final Iterator<ByteBuffer> iterator;
+ private ByteBuffer buffer;
+ private volatile ByteBuffer content;
+
+ public HttpContent(ContentProvider provider)
+ {
+ this.provider = provider;
+ this.iterator = provider == null ? Collections.<ByteBuffer>emptyIterator() : provider.iterator();
+ }
+
+ /**
+ * @return whether there is any content at all
+ */
+ public boolean hasContent()
+ {
+ return provider != null;
+ }
+
+ /**
+ * @return whether the cursor points to the last content
+ */
+ public boolean isLast()
+ {
+ return !iterator.hasNext();
+ }
+
+ /**
+ * @return the {@link ByteBuffer} containing the content at the cursor's position
+ */
+ public ByteBuffer getByteBuffer()
+ {
+ return buffer;
+ }
+
+ /**
+ * @return a {@link ByteBuffer#slice()} of {@link #getByteBuffer()} at the cursor's position
+ */
+ public ByteBuffer getContent()
+ {
+ return content;
+ }
+
+ /**
+ * Advances the cursor to the next block of content.
+ * <p />
+ * The next block of content may be valid (which yields a non-null buffer
+ * returned by {@link #getByteBuffer()}), but may also be deferred
+ * (which yields a null buffer returned by {@link #getByteBuffer()}).
+ * <p />
+ * If the block of content pointed by the new cursor position is valid, this method returns true.
+ *
+ * @return true if there is content at the new cursor's position, false otherwise.
+ */
+ public boolean advance()
+ {
+ if (isLast())
+ {
+ if (content != AFTER)
+ content = buffer = AFTER;
+ return false;
+ }
+ else
+ {
+ ByteBuffer buffer = this.buffer = iterator.next();
+ content = buffer == null ? null : buffer.slice();
+ return buffer != null;
+ }
+ }
+
+ /**
+ * @return whether the cursor has been advanced past the {@link #isLast() last} position.
+ */
+ public boolean isConsumed()
+ {
+ return content == AFTER;
+ }
+
+ @Override
+ public String toString()
+ {
+ return String.format("%s@%x - has=%b,last=%b,consumed=%b,buffer=%s",
+ getClass().getSimpleName(),
+ hashCode(),
+ hasContent(),
+ isLast(),
+ isConsumed(),
+ BufferUtil.toDetailString(getContent()));
+ }
+}
diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/HttpDestination.java b/jetty-client/src/main/java/org/eclipse/jetty/client/HttpDestination.java
index 94744d0..7531cdd 100644
--- a/jetty-client/src/main/java/org/eclipse/jetty/client/HttpDestination.java
+++ b/jetty-client/src/main/java/org/eclipse/jetty/client/HttpDestination.java
@@ -22,13 +22,10 @@
import java.io.IOException;
import java.net.URI;
import java.nio.channels.AsynchronousCloseException;
-import java.util.ArrayList;
import java.util.List;
import java.util.Queue;
-import java.util.concurrent.BlockingQueue;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.TimeUnit;
-import java.util.concurrent.atomic.AtomicInteger;
import org.eclipse.jetty.client.api.Connection;
import org.eclipse.jetty.client.api.Destination;
@@ -48,18 +45,15 @@
import org.eclipse.jetty.util.log.Logger;
import org.eclipse.jetty.util.ssl.SslContextFactory;
-public class HttpDestination implements Destination, Closeable, Dumpable
+public abstract class HttpDestination implements Destination, Closeable, Dumpable
{
- private static final Logger LOG = Log.getLogger(HttpDestination.class);
+ protected static final Logger LOG = Log.getLogger(new Object(){}.getClass().getEnclosingClass());
- private final AtomicInteger connectionCount = new AtomicInteger();
private final HttpClient client;
private final String scheme;
private final String host;
private final Address address;
private final Queue<HttpExchange> exchanges;
- private final BlockingQueue<Connection> idleConnections;
- private final BlockingQueue<Connection> activeConnections;
private final RequestNotifier requestNotifier;
private final ResponseNotifier responseNotifier;
private final Address proxyAddress;
@@ -72,14 +66,7 @@
this.host = host;
this.address = new Address(host, port);
- int maxRequestsQueued = client.getMaxRequestsQueuedPerDestination();
- int capacity = Math.min(32, maxRequestsQueued);
- this.exchanges = new BlockingArrayQueue<>(capacity, capacity, maxRequestsQueued);
-
- int maxConnections = client.getMaxConnectionsPerDestination();
- capacity = Math.min(8, maxConnections);
- this.idleConnections = new BlockingArrayQueue<>(capacity, capacity, maxConnections);
- this.activeConnections = new BlockingArrayQueue<>(capacity, capacity, maxConnections);
+ this.exchanges = new BlockingArrayQueue<>(client.getMaxRequestsQueuedPerDestination());
this.requestNotifier = new RequestNotifier(client);
this.responseNotifier = new ResponseNotifier(client);
@@ -93,14 +80,14 @@
hostField = new HttpField(HttpHeader.HOST, host);
}
- protected BlockingQueue<Connection> getIdleConnections()
+ public HttpClient getHttpClient()
{
- return idleConnections;
+ return client;
}
- protected BlockingQueue<Connection> getActiveConnections()
+ public Queue<HttpExchange> getHttpExchanges()
{
- return activeConnections;
+ return exchanges;
}
public RequestNotifier getRequestNotifier()
@@ -157,7 +144,7 @@
return hostField;
}
- public void send(Request request, List<Response.ResponseListener> listeners)
+ protected void send(Request request, List<Response.ResponseListener> listeners)
{
if (!scheme.equals(request.getScheme()))
throw new IllegalArgumentException("Invalid request scheme " + request.getScheme() + " for destination " + this);
@@ -182,9 +169,7 @@
{
LOG.debug("Queued {}", request);
requestNotifier.notifyQueued(request);
- Connection connection = acquire();
- if (connection != null)
- process(connection, false);
+ send();
}
}
else
@@ -199,6 +184,8 @@
}
}
+ protected abstract void send();
+
public void newConnection(Promise<Connection> promise)
{
createConnection(new ProxyPromise(promise));
@@ -209,80 +196,24 @@
client.newConnection(this, promise);
}
- protected Connection acquire()
+ public boolean remove(HttpExchange exchange)
{
- Connection result = idleConnections.poll();
- if (result != null)
- return result;
-
- final int maxConnections = client.getMaxConnectionsPerDestination();
- while (true)
- {
- int current = connectionCount.get();
- final int next = current + 1;
-
- if (next > maxConnections)
- {
- LOG.debug("Max connections per destination {} exceeded for {}", current, this);
- // Try again the idle connections
- return idleConnections.poll();
- }
-
- if (connectionCount.compareAndSet(current, next))
- {
- LOG.debug("Creating connection {}/{} for {}", next, maxConnections, this);
-
- // This is the promise that is being called when a connection (eventually proxied) succeeds or fails.
- Promise<Connection> promise = new Promise<Connection>()
- {
- @Override
- public void succeeded(Connection connection)
- {
- process(connection, true);
- }
-
- @Override
- public void failed(final Throwable x)
- {
- client.getExecutor().execute(new Runnable()
- {
- @Override
- public void run()
- {
- abort(x);
- }
- });
- }
- };
-
- // Create a new connection, and pass a ProxyPromise to establish a proxy tunnel, if needed.
- // Differently from the case where the connection is created explicitly by applications, here
- // we need to do a bit more logging and keep track of the connection count in case of failures.
- createConnection(new ProxyPromise(promise)
- {
- @Override
- public void succeeded(Connection connection)
- {
- LOG.debug("Created connection {}/{} {} for {}", next, maxConnections, connection, HttpDestination.this);
- super.succeeded(connection);
- }
-
- @Override
- public void failed(Throwable x)
- {
- LOG.debug("Connection failed {} for {}", x, HttpDestination.this);
- connectionCount.decrementAndGet();
- super.failed(x);
- }
- });
-
- // Try again the idle connections
- return idleConnections.poll();
- }
- }
+ return exchanges.remove(exchange);
}
- private void abort(Throwable cause)
+ public void close()
+ {
+ abort(new AsynchronousCloseException());
+ LOG.debug("Closed {}", this);
+ }
+
+ /**
+ * Aborts all the {@link HttpExchange}s queued in this destination.
+ *
+ * @param cause the abort cause
+ * @see #abort(HttpExchange, Throwable)
+ */
+ public void abort(Throwable cause)
{
HttpExchange exchange;
while ((exchange = exchanges.poll()) != null)
@@ -290,134 +221,11 @@
}
/**
- * <p>Processes a new connection making it idle or active depending on whether requests are waiting to be sent.</p>
- * <p>A new connection is created when a request needs to be executed; it is possible that the request that
- * triggered the request creation is executed by another connection that was just released, so the new connection
- * may become idle.</p>
- * <p>If a request is waiting to be executed, it will be dequeued and executed by the new connection.</p>
+ * Aborts the given {@code exchange}, notifies listeners of the failure, and completes the exchange.
*
- * @param connection the new connection
- * @param dispatch whether to dispatch the processing to another thread
+ * @param exchange the {@link HttpExchange} to abort
+ * @param cause the abort cause
*/
- protected void process(Connection connection, boolean dispatch)
- {
- // Ugly cast, but lack of generic reification forces it
- final HttpConnection httpConnection = (HttpConnection)connection;
-
- final HttpExchange exchange = exchanges.poll();
- if (exchange == null)
- {
- LOG.debug("{} idle", httpConnection);
- if (!idleConnections.offer(httpConnection))
- {
- LOG.debug("{} idle overflow");
- httpConnection.close();
- }
- if (!client.isRunning())
- {
- LOG.debug("{} is stopping", client);
- remove(httpConnection);
- httpConnection.close();
- }
- }
- else
- {
- final Request request = exchange.getRequest();
- Throwable cause = request.getAbortCause();
- if (cause != null)
- {
- abort(exchange, cause);
- LOG.debug("Aborted before processing {}: {}", exchange, cause);
- }
- else
- {
- LOG.debug("{} active", httpConnection);
- if (!activeConnections.offer(httpConnection))
- {
- LOG.warn("{} active overflow");
- }
- if (dispatch)
- {
- client.getExecutor().execute(new Runnable()
- {
- @Override
- public void run()
- {
- httpConnection.send(exchange);
- }
- });
- }
- else
- {
- httpConnection.send(exchange);
- }
- }
- }
- }
-
- public void release(Connection connection)
- {
- LOG.debug("{} released", connection);
- if (client.isRunning())
- {
- boolean removed = activeConnections.remove(connection);
- if (removed)
- process(connection, false);
- else
- LOG.debug("{} explicit", connection);
- }
- else
- {
- LOG.debug("{} is stopped", client);
- remove(connection);
- connection.close();
- }
- }
-
- public void remove(Connection connection)
- {
- boolean removed = activeConnections.remove(connection);
- removed |= idleConnections.remove(connection);
- if (removed)
- {
- int open = connectionCount.decrementAndGet();
- LOG.debug("Removed connection {} for {} - open: {}", connection, this, open);
- }
-
- // We need to execute queued requests even if this connection failed.
- // We may create a connection that is not needed, but it will eventually
- // idle timeout, so no worries
- if (!exchanges.isEmpty())
- {
- connection = acquire();
- if (connection != null)
- process(connection, false);
- }
- }
-
- public void close()
- {
- for (Connection connection : idleConnections)
- connection.close();
- idleConnections.clear();
-
- // A bit drastic, but we cannot wait for all requests to complete
- for (Connection connection : activeConnections)
- connection.close();
- activeConnections.clear();
-
- abort(new AsynchronousCloseException());
-
- connectionCount.set(0);
-
- LOG.debug("Closed {}", this);
- }
-
- public boolean remove(HttpExchange exchange)
- {
- return exchanges.remove(exchange);
- }
-
protected void abort(HttpExchange exchange, Throwable cause)
{
Request request = exchange.getRequest();
@@ -438,22 +246,19 @@
public void dump(Appendable out, String indent) throws IOException
{
ContainerLifeCycle.dumpObject(out, this + " - requests queued: " + exchanges.size());
- List<String> connections = new ArrayList<>();
- for (Connection connection : idleConnections)
- connections.add(connection + " - IDLE");
- for (Connection connection : activeConnections)
- connections.add(connection + " - ACTIVE");
- ContainerLifeCycle.dump(out, indent, connections);
+ }
+
+ public String asString()
+ {
+ return client.address(getScheme(), getHost(), getPort());
}
@Override
public String toString()
{
- return String.format("%s(%s://%s:%d)%s",
+ return String.format("%s(%s)%s",
HttpDestination.class.getSimpleName(),
- getScheme(),
- getHost(),
- getPort(),
+ asString(),
proxyAddress == null ? "" : " via " + proxyAddress.getHost() + ":" + proxyAddress.getPort());
}
@@ -525,7 +330,7 @@
if (response.getStatus() == 200)
{
// Wrap the connection with TLS
- Connection tunnel = client.tunnel(connection);
+ Connection tunnel = client.getTransport().tunnel(connection);
delegate.succeeded(tunnel);
}
else
diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/HttpExchange.java b/jetty-client/src/main/java/org/eclipse/jetty/client/HttpExchange.java
index de04377..b370524 100644
--- a/jetty-client/src/main/java/org/eclipse/jetty/client/HttpExchange.java
+++ b/jetty-client/src/main/java/org/eclipse/jetty/client/HttpExchange.java
@@ -19,9 +19,9 @@
package org.eclipse.jetty.client;
import java.util.List;
-import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
-import java.util.concurrent.atomic.AtomicMarkableReference;
+import java.util.concurrent.atomic.AtomicReference;
import org.eclipse.jetty.client.api.Request;
import org.eclipse.jetty.client.api.Response;
@@ -33,14 +33,15 @@
{
private static final Logger LOG = Log.getLogger(HttpExchange.class);
+ private final AtomicBoolean requestComplete = new AtomicBoolean();
+ private final AtomicBoolean responseComplete = new AtomicBoolean();
private final AtomicInteger complete = new AtomicInteger();
- private final CountDownLatch terminate = new CountDownLatch(2);
+ private final AtomicReference<HttpChannel> channel = new AtomicReference<>();
private final HttpConversation conversation;
private final HttpDestination destination;
private final Request request;
private final List<Response.ResponseListener> listeners;
private final HttpResponse response;
- private volatile HttpConnection connection;
private volatile Throwable requestFailure;
private volatile Throwable responseFailure;
@@ -86,30 +87,47 @@
return responseFailure;
}
- public void setConnection(HttpConnection connection)
+ public void associate(HttpChannel channel)
{
- this.connection = connection;
+ if (!this.channel.compareAndSet(null, channel))
+ throw new IllegalStateException();
}
- public AtomicMarkableReference<Result> requestComplete(Throwable failure)
+ public void disassociate(HttpChannel channel)
+ {
+ if (!this.channel.compareAndSet(channel, null))
+ throw new IllegalStateException();
+ }
+
+ public boolean requestComplete()
+ {
+ return requestComplete.compareAndSet(false, true);
+ }
+
+ public boolean responseComplete()
+ {
+ return responseComplete.compareAndSet(false, true);
+ }
+
+ public Result terminateRequest(Throwable failure)
{
int requestSuccess = 0b0011;
int requestFailure = 0b0001;
- return complete(failure == null ? requestSuccess : requestFailure, failure);
+ return terminate(failure == null ? requestSuccess : requestFailure, failure);
}
- public AtomicMarkableReference<Result> responseComplete(Throwable failure)
+ public Result terminateResponse(Throwable failure)
{
if (failure == null)
{
int responseSuccess = 0b1100;
- return complete(responseSuccess, failure);
+ return terminate(responseSuccess, failure);
}
else
{
proceed(false);
int responseFailure = 0b0100;
- return complete(responseFailure, failure);
+ return terminate(responseFailure, failure);
}
}
@@ -126,16 +144,10 @@
* By using {@link AtomicInteger} to atomically sum these codes we can know
* whether the exchange is completed and whether is successful.
*
- * @param code the bits representing the status code for either the request or the response
- * @param failure the failure - if any - associated with the status code for either the request or the response
- * @return an AtomicMarkableReference holding whether the operation modified the
- * completion status and the {@link Result} - if any - associated with the status
+ * @return the {@link Result} - if any - associated with the status
*/
- private AtomicMarkableReference<Result> complete(int code, Throwable failure)
+ private Result terminate(int code, Throwable failure)
{
- Result result = null;
- boolean modified = false;
-
int current;
while (true)
{
@@ -147,7 +159,6 @@
if (!complete.compareAndSet(current, candidate))
continue;
current = candidate;
- modified = true;
if ((code & 0b01) == 0b01)
requestFailure = failure;
else
@@ -157,19 +168,16 @@
break;
}
- int completed = 0b0101;
- if ((current & completed) == completed)
+ int terminated = 0b0101;
+ if ((current & terminated) == terminated)
{
- if (modified)
- {
- // Request and response completed
- LOG.debug("{} complete", this);
- conversation.complete();
- }
- result = new Result(getRequest(), getRequestFailure(), getResponse(), getResponseFailure());
+ // Request and response terminated
+ LOG.debug("{} terminated", this);
+ conversation.complete();
+ return new Result(getRequest(), getRequestFailure(), getResponse(), getResponseFailure());
}
- return new AtomicMarkableReference<>(result, modified);
+ return null;
}
public boolean abort(Throwable cause)
@@ -182,12 +190,12 @@
}
else
{
- HttpConnection connection = this.connection;
- // If there is no connection, this exchange is already completed
- if (connection == null)
+ HttpChannel channel = this.channel.get();
+ // If there is no channel, this exchange is already completed
+ if (channel == null)
return false;
- boolean aborted = connection.abort(cause);
+ boolean aborted = channel.abort(cause);
LOG.debug("Aborted while active ({}) {}: {}", aborted, this, cause);
return aborted;
}
@@ -195,6 +203,7 @@
public void resetResponse(boolean success)
{
+ responseComplete.set(false);
int responseSuccess = 0b1100;
int responseFailure = 0b0100;
int code = success ? responseSuccess : responseFailure;
@@ -203,42 +212,25 @@
public void proceed(boolean proceed)
{
- HttpConnection connection = this.connection;
- if (connection != null)
- connection.proceed(proceed);
+ HttpChannel channel = this.channel.get();
+ if (channel != null)
+ channel.proceed(this, proceed);
}
- public void terminateRequest()
- {
- terminate.countDown();
- }
-
- public void terminateResponse()
- {
- terminate.countDown();
- }
-
- public void awaitTermination()
- {
- try
- {
- terminate.await();
- }
- catch (InterruptedException x)
- {
- LOG.ignore(x);
- }
- }
-
- @Override
- public String toString()
+ private String toString(int code)
{
String padding = "0000";
- String status = Integer.toBinaryString(complete.get());
+ String status = Integer.toBinaryString(code);
return String.format("%s@%x status=%s%s",
HttpExchange.class.getSimpleName(),
hashCode(),
padding.substring(status.length()),
status);
}
+
+ @Override
+ public String toString()
+ {
+ return toString(complete.get());
+ }
}
diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/HttpReceiver.java b/jetty-client/src/main/java/org/eclipse/jetty/client/HttpReceiver.java
index b3f8d51..597fed4 100644
--- a/jetty-client/src/main/java/org/eclipse/jetty/client/HttpReceiver.java
+++ b/jetty-client/src/main/java/org/eclipse/jetty/client/HttpReceiver.java
@@ -18,7 +18,6 @@
package org.eclipse.jetty.client;
-import java.io.EOFException;
import java.io.IOException;
import java.net.URI;
import java.nio.ByteBuffer;
@@ -28,188 +27,169 @@
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeoutException;
-import java.util.concurrent.atomic.AtomicMarkableReference;
import java.util.concurrent.atomic.AtomicReference;
import org.eclipse.jetty.client.api.Response;
import org.eclipse.jetty.client.api.Result;
import org.eclipse.jetty.http.HttpField;
import org.eclipse.jetty.http.HttpHeader;
-import org.eclipse.jetty.http.HttpMethod;
-import org.eclipse.jetty.http.HttpParser;
-import org.eclipse.jetty.http.HttpVersion;
-import org.eclipse.jetty.io.ByteBufferPool;
-import org.eclipse.jetty.io.EndPoint;
-import org.eclipse.jetty.io.EofException;
+import org.eclipse.jetty.util.BufferUtil;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;
-public class HttpReceiver implements HttpParser.ResponseHandler<ByteBuffer>
+/**
+ * {@link HttpReceiver} provides the abstract code to implement the various steps of the receive of HTTP responses.
+ * <p />
+ * {@link HttpReceiver} maintains a state machine that is updated when the steps of receiving a response are executed.
+ * <p />
+ * Subclasses must handle the transport-specific details, for example how to read from the raw socket and how to parse
+ * the bytes read from the socket. Then they have to call the methods defined in this class in the following order:
+ * <ol>
+ * <li>{@link #responseBegin(HttpExchange)}, when the HTTP response data containing the HTTP status code
+ * is available</li>
+ * <li>{@link #responseHeader(HttpExchange, HttpField)}, when a HTTP field is available</li>
+ * <li>{@link #responseHeaders(HttpExchange)}, when all HTTP headers are available</li>
+ * <li>{@link #responseContent(HttpExchange, ByteBuffer)}, when HTTP content is available; this is the only method
+ * that may be invoked multiple times with different buffers containing different content</li>
+ * <li>{@link #responseSuccess(HttpExchange)}, when the response is complete</li>
+ * </ol>
+ * At any time, subclasses may invoke {@link #responseFailure(Throwable)} to indicate that the response has failed
+ * (for example, because of I/O exceptions).
+ * At any time, user threads may abort the response which will cause {@link #responseFailure(Throwable)} to be
+ * invoked.
+ * <p />
+ * The state machine maintained by this class ensures that the response steps are not executed by an I/O thread
+ * if the response has already been failed.
+ *
+ * @see HttpSender
+ */
+public abstract class HttpReceiver
{
- private static final Logger LOG = Log.getLogger(HttpReceiver.class);
+ protected static final Logger LOG = Log.getLogger(new Object(){}.getClass().getEnclosingClass());
- private final AtomicReference<State> state = new AtomicReference<>(State.IDLE);
- private final HttpParser parser = new HttpParser(this);
- private final HttpConnection connection;
- private ContentDecoder decoder;
+ private final AtomicReference<ResponseState> responseState = new AtomicReference<>(ResponseState.IDLE);
+ private final HttpChannel channel;
+ private volatile ContentDecoder decoder;
- public HttpReceiver(HttpConnection connection)
+ protected HttpReceiver(HttpChannel channel)
{
- this.connection = connection;
+ this.channel = channel;
}
- public void receive()
+ protected HttpChannel getHttpChannel()
{
- EndPoint endPoint = connection.getEndPoint();
- HttpClient client = connection.getHttpClient();
- ByteBufferPool bufferPool = client.getByteBufferPool();
- ByteBuffer buffer = bufferPool.acquire(client.getResponseBufferSize(), true);
- try
+ return channel;
+ }
+
+ protected HttpExchange getHttpExchange()
+ {
+ return channel.getHttpExchange();
+ }
+
+ protected HttpDestination getHttpDestination()
+ {
+ return channel.getHttpDestination();
+ }
+
+ /**
+ * Method to be invoked when the response status code is available.
+ * <p />
+ * Subclasses must have set the response status code on the {@link Response} object of the {@link HttpExchange}
+ * prior invoking this method.
+ * <p />
+ * This method takes case of notifying {@link Response.BeginListener}s.
+ *
+ * @param exchange the HTTP exchange
+ * @return whether the processing should continue
+ */
+ protected boolean responseBegin(HttpExchange exchange)
+ {
+ if (!updateResponseState(ResponseState.IDLE, ResponseState.BEGIN))
+ return false;
+
+ HttpConversation conversation = exchange.getConversation();
+ HttpResponse response = exchange.getResponse();
+ // Probe the protocol handlers
+ HttpDestination destination = getHttpDestination();
+ HttpClient client = destination.getHttpClient();
+ ProtocolHandler protocolHandler = client.findProtocolHandler(exchange.getRequest(), response);
+ Response.Listener handlerListener = null;
+ if (protocolHandler != null)
{
- while (true)
+ handlerListener = protocolHandler.getResponseListener();
+ LOG.debug("Found protocol handler {}", protocolHandler);
+ }
+ exchange.getConversation().updateResponseListeners(handlerListener);
+
+ LOG.debug("Response begin {}", response);
+ ResponseNotifier notifier = destination.getResponseNotifier();
+ notifier.notifyBegin(conversation.getResponseListeners(), response);
+
+ return true;
+ }
+
+ /**
+ * Method to be invoked when a response HTTP header is available.
+ * <p />
+ * Subclasses must not have added the header to the {@link Response} object of the {@link HttpExchange}
+ * prior invoking this method.
+ * <p />
+ * This method takes case of notifying {@link Response.HeaderListener}s and storing cookies.
+ *
+ * @param exchange the HTTP exchange
+ * @param field the response HTTP field
+ * @return whether the processing should continue
+ */
+ protected boolean responseHeader(HttpExchange exchange, HttpField field)
+ {
+ out: while (true)
+ {
+ ResponseState current = responseState.get();
+ switch (current)
{
- int read = endPoint.fill(buffer);
- LOG.debug("Read {} bytes from {}", read, connection);
- if (read > 0)
+ case BEGIN:
+ case HEADER:
{
- parse(buffer);
- }
- else if (read == 0)
- {
- fillInterested();
+ if (updateResponseState(current, ResponseState.HEADER))
+ break out;
break;
}
- else
+ default:
{
- shutdown();
- break;
+ return false;
}
}
}
- catch (EofException x)
+
+ HttpResponse response = exchange.getResponse();
+ ResponseNotifier notifier = getHttpDestination().getResponseNotifier();
+ boolean process = notifier.notifyHeader(exchange.getConversation().getResponseListeners(), response, field);
+ if (process)
{
- LOG.ignore(x);
- failAndClose(x);
- }
- catch (Exception x)
- {
- LOG.debug(x);
- failAndClose(x);
- }
- finally
- {
- bufferPool.release(buffer);
- }
- }
-
- private void parse(ByteBuffer buffer)
- {
- while (buffer.hasRemaining())
- parser.parseNext(buffer);
- }
-
- private void fillInterested()
- {
- State state = this.state.get();
- if (state == State.IDLE || state == State.RECEIVE)
- connection.fillInterested();
- }
-
- private void shutdown()
- {
- // Shutting down the parser may invoke messageComplete() or fail()
- parser.shutdownInput();
- State state = this.state.get();
- if (state == State.IDLE || state == State.RECEIVE)
- {
- if (!fail(new EOFException()))
- connection.close();
- }
- }
-
-
- @Override
- public int getHeaderCacheSize()
- {
- // TODO get from configuration
- return 256;
- }
-
- @Override
- public boolean startResponse(HttpVersion version, int status, String reason)
- {
- if (updateState(State.IDLE, State.RECEIVE))
- {
- HttpExchange exchange = connection.getExchange();
- // The exchange may be null if it failed concurrently
- if (exchange != null)
+ response.getHeaders().add(field);
+ HttpHeader fieldHeader = field.getHeader();
+ if (fieldHeader != null)
{
- HttpConversation conversation = exchange.getConversation();
- HttpResponse response = exchange.getResponse();
-
- String method = exchange.getRequest().method();
- parser.setHeadResponse(HttpMethod.HEAD.is(method) || HttpMethod.CONNECT.is(method));
- response.version(version).status(status).reason(reason);
-
- // Probe the protocol handlers
- HttpClient client = connection.getHttpClient();
- ProtocolHandler protocolHandler = client.findProtocolHandler(exchange.getRequest(), response);
- Response.Listener handlerListener = null;
- if (protocolHandler != null)
+ switch (fieldHeader)
{
- handlerListener = protocolHandler.getResponseListener();
- LOG.debug("Found protocol handler {}", protocolHandler);
- }
- exchange.getConversation().updateResponseListeners(handlerListener);
-
- LOG.debug("Receiving {}", response);
- ResponseNotifier notifier = connection.getDestination().getResponseNotifier();
- notifier.notifyBegin(conversation.getResponseListeners(), response);
- }
- }
- return false;
- }
-
- @Override
- public boolean parsedHeader(HttpField field)
- {
- if (updateState(State.RECEIVE, State.RECEIVE))
- {
- HttpExchange exchange = connection.getExchange();
- // The exchange may be null if it failed concurrently
- if (exchange != null)
- {
- HttpConversation conversation = exchange.getConversation();
- HttpResponse response = exchange.getResponse();
- ResponseNotifier notifier = connection.getDestination().getResponseNotifier();
- boolean process = notifier.notifyHeader(conversation.getResponseListeners(), response, field);
- if (process)
- {
- response.getHeaders().add(field);
- HttpHeader fieldHeader = field.getHeader();
- if (fieldHeader != null)
+ case SET_COOKIE:
+ case SET_COOKIE2:
{
- switch (fieldHeader)
- {
- case SET_COOKIE:
- case SET_COOKIE2:
- {
- storeCookie(exchange.getRequest().getURI(), field);
- break;
- }
- default:
- {
- break;
- }
- }
+ storeCookie(exchange.getRequest().getURI(), field);
+ break;
+ }
+ default:
+ {
+ break;
}
}
}
}
- return false;
+
+ return true;
}
- private void storeCookie(URI uri, HttpField field)
+ protected void storeCookie(URI uri, HttpField field)
{
try
{
@@ -218,7 +198,7 @@
{
Map<String, List<String>> header = new HashMap<>(1);
header.put(field.getHeader().asString(), Collections.singletonList(value));
- connection.getHttpClient().getCookieManager().put(uri, header);
+ getHttpDestination().getHttpClient().getCookieManager().put(uri, header);
}
}
catch (IOException x)
@@ -227,113 +207,166 @@
}
}
- @Override
- public boolean headerComplete()
+ /**
+ * Method to be invoked after all response HTTP headers are available.
+ * <p />
+ * This method takes case of notifying {@link Response.HeadersListener}s.
+ *
+ * @param exchange the HTTP exchange
+ * @return whether the processing should continue
+ */
+ protected boolean responseHeaders(HttpExchange exchange)
{
- if (updateState(State.RECEIVE, State.RECEIVE))
+ out: while (true)
{
- HttpExchange exchange = connection.getExchange();
- // The exchange may be null if it failed concurrently
- if (exchange != null)
+ ResponseState current = responseState.get();
+ switch (current)
{
- HttpConversation conversation = exchange.getConversation();
- HttpResponse response = exchange.getResponse();
- LOG.debug("Headers {}", response);
- ResponseNotifier notifier = connection.getDestination().getResponseNotifier();
- notifier.notifyHeaders(conversation.getResponseListeners(), response);
-
- Enumeration<String> contentEncodings = response.getHeaders().getValues(HttpHeader.CONTENT_ENCODING.asString(), ",");
- if (contentEncodings != null)
+ case BEGIN:
+ case HEADER:
{
- for (ContentDecoder.Factory factory : connection.getHttpClient().getContentDecoderFactories())
+ if (updateResponseState(current, ResponseState.HEADERS))
+ break out;
+ break;
+ }
+ default:
+ {
+ return false;
+ }
+ }
+ }
+
+ HttpResponse response = exchange.getResponse();
+ if (LOG.isDebugEnabled())
+ LOG.debug("Response headers {}{}{}", response, System.getProperty("line.separator"), response.getHeaders().toString().trim());
+ ResponseNotifier notifier = getHttpDestination().getResponseNotifier();
+ notifier.notifyHeaders(exchange.getConversation().getResponseListeners(), response);
+
+ Enumeration<String> contentEncodings = response.getHeaders().getValues(HttpHeader.CONTENT_ENCODING.asString(), ",");
+ if (contentEncodings != null)
+ {
+ for (ContentDecoder.Factory factory : getHttpDestination().getHttpClient().getContentDecoderFactories())
+ {
+ while (contentEncodings.hasMoreElements())
+ {
+ if (factory.getEncoding().equalsIgnoreCase(contentEncodings.nextElement()))
{
- while (contentEncodings.hasMoreElements())
- {
- if (factory.getEncoding().equalsIgnoreCase(contentEncodings.nextElement()))
- {
- this.decoder = factory.newContentDecoder();
- break;
- }
- }
+ this.decoder = factory.newContentDecoder();
+ break;
}
}
}
}
- return false;
+
+ return true;
}
- @Override
- public boolean content(ByteBuffer buffer)
+ /**
+ * Method to be invoked when response HTTP content is available.
+ * <p />
+ * This method takes case of decoding the content, if necessary, and notifying {@link Response.ContentListener}s.
+ *
+ * @param exchange the HTTP exchange
+ * @param buffer the response HTTP content buffer
+ * @return whether the processing should continue
+ */
+ protected boolean responseContent(HttpExchange exchange, ByteBuffer buffer)
{
- if (updateState(State.RECEIVE, State.RECEIVE))
+ out: while (true)
{
- HttpExchange exchange = connection.getExchange();
- // The exchange may be null if it failed concurrently
- if (exchange != null)
+ ResponseState current = responseState.get();
+ switch (current)
{
- HttpConversation conversation = exchange.getConversation();
- HttpResponse response = exchange.getResponse();
- LOG.debug("Content {}: {} bytes", response, buffer.remaining());
-
- ContentDecoder decoder = this.decoder;
- if (decoder != null)
+ case HEADERS:
+ case CONTENT:
{
- buffer = decoder.decode(buffer);
- LOG.debug("{} {}: {} bytes", decoder, response, buffer.remaining());
+ if (updateResponseState(current, ResponseState.CONTENT))
+ break out;
+ break;
}
-
- ResponseNotifier notifier = connection.getDestination().getResponseNotifier();
- notifier.notifyContent(conversation.getResponseListeners(), response, buffer);
+ default:
+ {
+ return false;
+ }
}
}
- return false;
- }
- @Override
- public boolean messageComplete()
- {
- if (updateState(State.RECEIVE, State.RECEIVE))
- success();
+ HttpResponse response = exchange.getResponse();
+ if (LOG.isDebugEnabled())
+ LOG.debug("Response content {}{}{}", response, System.getProperty("line.separator"), BufferUtil.toDetailString(buffer));
+
+ ContentDecoder decoder = this.decoder;
+ if (decoder != null)
+ {
+ buffer = decoder.decode(buffer);
+ if (LOG.isDebugEnabled())
+ LOG.debug("Response content decoded ({}) {}{}{}", decoder, response, System.getProperty("line.separator"), BufferUtil.toDetailString(buffer));
+ }
+
+ ResponseNotifier notifier = getHttpDestination().getResponseNotifier();
+ notifier.notifyContent(exchange.getConversation().getResponseListeners(), response, buffer);
+
return true;
}
- protected boolean success()
+ /**
+ * Method to be invoked when the response is successful.
+ * <p />
+ * This method takes case of notifying {@link Response.SuccessListener}s and possibly
+ * {@link Response.CompleteListener}s (if the exchange is completed).
+ *
+ * @param exchange the HTTP exchange
+ * @return whether the response was processed as successful
+ */
+ protected boolean responseSuccess(HttpExchange exchange)
{
- HttpExchange exchange = connection.getExchange();
- if (exchange == null)
+ // Mark atomically the response as completed, with respect
+ // to concurrency between response success and response failure.
+ boolean completed = exchange.responseComplete();
+ if (!completed)
return false;
- AtomicMarkableReference<Result> completion = exchange.responseComplete(null);
- if (!completion.isMarked())
- return false;
+ // Reset to be ready for another response
+ reset();
- parser.reset();
- decoder = null;
-
- if (!updateState(State.RECEIVE, State.IDLE))
- throw new IllegalStateException();
-
- exchange.terminateResponse();
+ // Mark atomically the response as terminated and succeeded,
+ // with respect to concurrency between request and response.
+ // If there is a non-null result, then both sender and
+ // receiver are reset and ready to be reused, and the
+ // connection closed/pooled (depending on the transport).
+ Result result = exchange.terminateResponse(null);
HttpResponse response = exchange.getResponse();
+ LOG.debug("Response success {}", response);
List<Response.ResponseListener> listeners = exchange.getConversation().getResponseListeners();
- ResponseNotifier notifier = connection.getDestination().getResponseNotifier();
+ ResponseNotifier notifier = getHttpDestination().getResponseNotifier();
notifier.notifySuccess(listeners, response);
- LOG.debug("Received {}", response);
- Result result = completion.getReference();
if (result != null)
{
- connection.complete(exchange, !result.isFailed());
+ boolean ordered = getHttpDestination().getHttpClient().isStrictEventOrdering();
+ if (!ordered)
+ channel.exchangeTerminated(result);
+ LOG.debug("Request/Response complete {}", response);
notifier.notifyComplete(listeners, result);
+ if (ordered)
+ channel.exchangeTerminated(result);
}
return true;
}
- protected boolean fail(Throwable failure)
+ /**
+ * Method to be invoked when the response is failed.
+ * <p />
+ * This method takes case of notifying {@link Response.FailureListener}s.
+ *
+ * @param failure the response failure
+ * @return whether the response was processed as failed
+ */
+ protected boolean responseFailure(Throwable failure)
{
- HttpExchange exchange = connection.getExchange();
+ HttpExchange exchange = getHttpExchange();
// In case of a response error, the failure has already been notified
// and it is possible that a further attempt to read in the receive
// loop throws an exception that reenters here but without exchange;
@@ -341,82 +374,112 @@
if (exchange == null)
return false;
- AtomicMarkableReference<Result> completion = exchange.responseComplete(failure);
- if (!completion.isMarked())
+ // Mark atomically the response as completed, with respect
+ // to concurrency between response success and response failure.
+ boolean completed = exchange.responseComplete();
+ if (!completed)
return false;
- parser.close();
- decoder = null;
+ // Dispose to avoid further responses
+ dispose();
- while (true)
- {
- State current = state.get();
- if (updateState(current, State.FAILURE))
- break;
- }
-
- exchange.terminateResponse();
+ // Mark atomically the response as terminated and failed,
+ // with respect to concurrency between request and response.
+ Result result = exchange.terminateResponse(failure);
HttpResponse response = exchange.getResponse();
- HttpConversation conversation = exchange.getConversation();
- ResponseNotifier notifier = connection.getDestination().getResponseNotifier();
- notifier.notifyFailure(conversation.getResponseListeners(), response, failure);
- LOG.debug("Failed {} {}", response, failure);
+ LOG.debug("Response failure {} {}", response, failure);
+ List<Response.ResponseListener> listeners = exchange.getConversation().getResponseListeners();
+ ResponseNotifier notifier = getHttpDestination().getResponseNotifier();
+ notifier.notifyFailure(listeners, response, failure);
- Result result = completion.getReference();
if (result != null)
{
- connection.complete(exchange, false);
-
- notifier.notifyComplete(conversation.getResponseListeners(), result);
+ boolean ordered = getHttpDestination().getHttpClient().isStrictEventOrdering();
+ if (!ordered)
+ channel.exchangeTerminated(result);
+ notifier.notifyComplete(listeners, result);
+ if (ordered)
+ channel.exchangeTerminated(result);
}
return true;
}
- @Override
- public void earlyEOF()
+ /**
+ * Resets this {@link HttpReceiver} state.
+ * <p />
+ * Subclasses should override (but remember to call {@code super}) to reset their own state.
+ * <p />
+ * Either this method or {@link #dispose()} is called.
+ */
+ protected void reset()
{
- failAndClose(new EOFException());
+ decoder = null;
+ responseState.set(ResponseState.IDLE);
}
- private void failAndClose(Throwable failure)
+ /**
+ * Disposes this {@link HttpReceiver} state.
+ * <p />
+ * Subclasses should override (but remember to call {@code super}) to dispose their own state.
+ * <p />
+ * Either this method or {@link #reset()} is called.
+ */
+ protected void dispose()
{
- fail(failure);
- connection.close();
- }
-
- @Override
- public void badMessage(int status, String reason)
- {
- HttpExchange exchange = connection.getExchange();
- HttpResponse response = exchange.getResponse();
- response.status(status).reason(reason);
- failAndClose(new HttpResponseException("HTTP protocol violation: bad response", response));
+ decoder = null;
+ responseState.set(ResponseState.FAILURE);
}
public void idleTimeout()
{
// If we cannot fail, it means a response arrived
// just when we were timeout idling, so we don't close
- fail(new TimeoutException());
+ responseFailure(new TimeoutException());
}
public boolean abort(Throwable cause)
{
- return fail(cause);
+ return responseFailure(cause);
}
- private boolean updateState(State from, State to)
+ private boolean updateResponseState(ResponseState from, ResponseState to)
{
- boolean updated = state.compareAndSet(from, to);
+ boolean updated = responseState.compareAndSet(from, to);
if (!updated)
- LOG.debug("State update failed: {} -> {}: {}", from, to, state.get());
+ LOG.debug("State update failed: {} -> {}: {}", from, to, responseState.get());
return updated;
}
- private enum State
+ /**
+ * The request states {@link HttpReceiver} goes through when receiving a response.
+ */
+ private enum ResponseState
{
- IDLE, RECEIVE, FAILURE
+ /**
+ * The response is not yet received, the initial state
+ */
+ IDLE,
+ /**
+ * The response status code has been received
+ */
+ BEGIN,
+ /**
+ * The response headers are being received
+ */
+ HEADER,
+ /**
+ * All the response headers have been received
+ */
+ HEADERS,
+ /**
+ * The response content is being received
+ */
+ CONTENT,
+ /**
+ * The response is failed
+ */
+ FAILURE
}
}
diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/HttpRequest.java b/jetty-client/src/main/java/org/eclipse/jetty/client/HttpRequest.java
index 2e4cbec..aa83877 100644
--- a/jetty-client/src/main/java/org/eclipse/jetty/client/HttpRequest.java
+++ b/jetty-client/src/main/java/org/eclipse/jetty/client/HttpRequest.java
@@ -43,6 +43,7 @@
import org.eclipse.jetty.client.api.Response;
import org.eclipse.jetty.client.util.FutureResponseListener;
import org.eclipse.jetty.client.util.PathContentProvider;
+import org.eclipse.jetty.http.HttpField;
import org.eclipse.jetty.http.HttpFields;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpMethod;
@@ -66,8 +67,8 @@
private String scheme;
private String path;
private String query;
- private String method;
- private HttpVersion version;
+ private String method = HttpMethod.GET.asString();
+ private HttpVersion version = HttpVersion.HTTP_1_1;
private long idleTimeout;
private long timeout;
private ContentProvider content;
@@ -91,6 +92,11 @@
extractParams(query);
this.uri = buildURI(true);
followRedirects(client.isFollowRedirects());
+ idleTimeout = client.getIdleTimeout();
+ HttpField acceptEncodingField = client.getAcceptEncodingField();
+ if (acceptEncodingField != null)
+ headers.put(acceptEncodingField);
+ headers.put(client.getUserAgentField());
}
@Override
@@ -126,13 +132,7 @@
}
@Override
- public HttpMethod getMethod()
- {
- return HttpMethod.fromString(method);
- }
-
- @Override
- public String method()
+ public String getMethod()
{
return method;
}
@@ -140,8 +140,7 @@
@Override
public Request method(HttpMethod method)
{
- this.method = method.asString();
- return this;
+ return method(method.asString());
}
@Override
@@ -201,7 +200,7 @@
@Override
public Request version(HttpVersion version)
{
- this.version = version;
+ this.version = Objects.requireNonNull(version);
return this;
}
@@ -595,6 +594,6 @@
@Override
public String toString()
{
- return String.format("%s[%s %s %s]@%x", HttpRequest.class.getSimpleName(), method(), getPath(), getVersion(), hashCode());
+ return String.format("%s[%s %s %s]@%x", HttpRequest.class.getSimpleName(), getMethod(), getPath(), getVersion(), hashCode());
}
}
diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/HttpResponse.java b/jetty-client/src/main/java/org/eclipse/jetty/client/HttpResponse.java
index 907f05d..ed126f9 100644
--- a/jetty-client/src/main/java/org/eclipse/jetty/client/HttpResponse.java
+++ b/jetty-client/src/main/java/org/eclipse/jetty/client/HttpResponse.java
@@ -106,6 +106,6 @@
@Override
public String toString()
{
- return String.format("%s[%s %d %s]", HttpResponse.class.getSimpleName(), getVersion(), getStatus(), getReason());
+ return String.format("%s[%s %d %s]@%x", HttpResponse.class.getSimpleName(), getVersion(), getStatus(), getReason(), hashCode());
}
}
diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/HttpSender.java b/jetty-client/src/main/java/org/eclipse/jetty/client/HttpSender.java
index eff0576..0b903b0 100644
--- a/jetty-client/src/main/java/org/eclipse/jetty/client/HttpSender.java
+++ b/jetty-client/src/main/java/org/eclipse/jetty/client/HttpSender.java
@@ -19,73 +19,119 @@
package org.eclipse.jetty.client;
import java.nio.ByteBuffer;
-import java.util.Collections;
-import java.util.Iterator;
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.Executor;
-import java.util.concurrent.atomic.AtomicMarkableReference;
import java.util.concurrent.atomic.AtomicReference;
import org.eclipse.jetty.client.api.ContentProvider;
import org.eclipse.jetty.client.api.Request;
import org.eclipse.jetty.client.api.Result;
-import org.eclipse.jetty.http.HttpGenerator;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpHeaderValue;
-import org.eclipse.jetty.io.ByteBufferPool;
-import org.eclipse.jetty.io.EndPoint;
import org.eclipse.jetty.util.BufferUtil;
import org.eclipse.jetty.util.Callback;
+import org.eclipse.jetty.util.IteratingCallback;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;
-public class HttpSender implements AsyncContentProvider.Listener
+/**
+ * {@link HttpSender} abstracts the algorithm to send HTTP requests, so that subclasses only implement
+ * the transport-specific code to send requests over the wire, implementing
+ * {@link #sendHeaders(HttpExchange, HttpContent, Callback)} and
+ * {@link #sendContent(HttpExchange, HttpContent, Callback)}.
+ * <p />
+ * {@link HttpSender} governs two state machines.
+ * <p />
+ * The request state machine is updated by {@link HttpSender} as the various steps of sending a request
+ * are executed, see {@link RequestState}.
+ * At any point in time, a user thread may abort the request, which may (if the request has not been
+ * completely sent yet) move the request state machine to {@link RequestState#FAILURE}.
+ * The request state machine guarantees that the request steps are executed (by I/O threads) only if
+ * the request has not been failed already.
+ * <p />
+ * The sender state machine is updated by {@link HttpSender} from three sources: deferred content notifications
+ * (via {@link #onContent()}), 100-continue notifications (via {@link #proceed(HttpExchange, boolean)})
+ * and normal request send (via {@link #sendContent(HttpExchange, HttpContent, Callback)}).
+ * This state machine must guarantee that the request sending is never executed concurrently: only one of
+ * those sources may trigger the call to {@link #sendContent(HttpExchange, HttpContent, Callback)}.
+ *
+ * @see HttpReceiver
+ */
+public abstract class HttpSender implements AsyncContentProvider.Listener
{
- private static final Logger LOG = Log.getLogger(HttpSender.class);
- private static final String EXPECT_100_ATTRIBUTE = HttpSender.class.getName() + ".expect100";
+ protected static final Logger LOG = Log.getLogger(new Object(){}.getClass().getEnclosingClass());
- private final AtomicReference<State> state = new AtomicReference<>(State.IDLE);
- private final AtomicReference<SendState> sendState = new AtomicReference<>(SendState.IDLE);
- private final HttpGenerator generator = new HttpGenerator();
- private final HttpConnection connection;
- private Iterator<ByteBuffer> contentIterator;
- private ContinueContentChunk continueContentChunk;
+ private final AtomicReference<RequestState> requestState = new AtomicReference<>(RequestState.QUEUED);
+ private final AtomicReference<SenderState> senderState = new AtomicReference<>(SenderState.IDLE);
+ private final Callback commitCallback = new CommitCallback();
+ private final Callback contentCallback = new ContentCallback();
+ private final Callback lastCallback = new LastContentCallback();
+ private final HttpChannel channel;
+ private volatile HttpContent content;
- public HttpSender(HttpConnection connection)
+ protected HttpSender(HttpChannel channel)
{
- this.connection = connection;
+ this.channel = channel;
+ }
+
+ protected HttpChannel getHttpChannel()
+ {
+ return channel;
+ }
+
+ protected HttpExchange getHttpExchange()
+ {
+ return channel.getHttpExchange();
}
@Override
public void onContent()
{
+ HttpExchange exchange = getHttpExchange();
+ if (exchange == null)
+ return;
+
while (true)
{
- SendState current = sendState.get();
+ SenderState current = senderState.get();
switch (current)
{
case IDLE:
{
- if (updateSendState(current, SendState.EXECUTE))
+ if (updateSenderState(current, SenderState.SENDING))
{
- LOG.debug("Deferred content available, sending");
- send();
+ LOG.debug("Deferred content available, idle -> sending");
+ HttpContent content = this.content;
+ content.advance();
+ sendContent(exchange, content, contentCallback);
return;
}
break;
}
- case EXECUTE:
+ case SENDING:
{
- if (updateSendState(current, SendState.SCHEDULE))
+ if (updateSenderState(current, SenderState.SCHEDULED))
{
- LOG.debug("Deferred content available, scheduling");
+ LOG.debug("Deferred content available, sending -> scheduled");
return;
}
break;
}
- case SCHEDULE:
+ case EXPECTING:
{
- LOG.debug("Deferred content available, queueing");
+ if (updateSenderState(current, SenderState.SCHEDULED))
+ {
+ LOG.debug("Deferred content available, expecting -> scheduled");
+ return;
+ }
+ break;
+ }
+ case WAITING:
+ {
+ LOG.debug("Deferred content available, waiting");
+ return;
+ }
+ case SCHEDULED:
+ {
+ LOG.debug("Deferred content available, scheduled");
return;
}
default:
@@ -98,9 +144,6 @@
public void send(HttpExchange exchange)
{
- if (!updateState(State.IDLE, State.BEGIN))
- throw new IllegalStateException();
-
Request request = exchange.getRequest();
Throwable cause = request.getAbortCause();
if (cause != null)
@@ -109,638 +152,610 @@
}
else
{
- LOG.debug("Sending {}", request);
- RequestNotifier notifier = connection.getDestination().getRequestNotifier();
- notifier.notifyBegin(request);
+ if (!queuedToBegin(request))
+ throw new IllegalStateException();
- ContentProvider content = request.getContent();
- this.contentIterator = content == null ? Collections.<ByteBuffer>emptyIterator() : content.iterator();
+ if (!updateSenderState(SenderState.IDLE, expects100Continue(request) ? SenderState.EXPECTING : SenderState.SENDING))
+ throw new IllegalStateException();
- boolean updated = updateSendState(SendState.IDLE, SendState.EXECUTE);
- assert updated;
+ ContentProvider contentProvider = request.getContent();
+ HttpContent content = this.content = new HttpContent(contentProvider);
// Setting the listener may trigger calls to onContent() by other
- // threads so we must set it only after the state has been updated
- if (content instanceof AsyncContentProvider)
- ((AsyncContentProvider)content).setListener(this);
+ // threads so we must set it only after the sender state has been updated
+ if (contentProvider instanceof AsyncContentProvider)
+ ((AsyncContentProvider)contentProvider).setListener(this);
- send();
+ if (!beginToHeaders(request))
+ return;
+
+ sendHeaders(exchange, content, commitCallback);
}
}
- private void send()
+ protected boolean expects100Continue(Request request)
{
- SendState currentSendState = sendState.get();
- assert currentSendState != SendState.IDLE : currentSendState;
+ return request.getHeaders().contains(HttpHeader.EXPECT, HttpHeaderValue.CONTINUE.asString());
+ }
- HttpClient client = connection.getHttpClient();
- ByteBufferPool bufferPool = client.getByteBufferPool();
- ByteBuffer header = null;
- ByteBuffer chunk = null;
- try
+ protected boolean queuedToBegin(Request request)
+ {
+ if (!updateRequestState(RequestState.QUEUED, RequestState.BEGIN))
+ return false;
+ LOG.debug("Request begin {}", request);
+ RequestNotifier notifier = getHttpChannel().getHttpDestination().getRequestNotifier();
+ notifier.notifyBegin(request);
+ return true;
+ }
+
+ protected boolean beginToHeaders(Request request)
+ {
+ if (!updateRequestState(RequestState.BEGIN, RequestState.HEADERS))
+ return false;
+ if (LOG.isDebugEnabled())
+ LOG.debug("Request headers {}{}{}", request, System.getProperty("line.separator"), request.getHeaders().toString().trim());
+ RequestNotifier notifier = getHttpChannel().getHttpDestination().getRequestNotifier();
+ notifier.notifyHeaders(request);
+ return true;
+ }
+
+ protected boolean headersToCommit(Request request)
+ {
+ if (!updateRequestState(RequestState.HEADERS, RequestState.COMMIT))
+ return false;
+ LOG.debug("Request committed {}", request);
+ RequestNotifier notifier = getHttpChannel().getHttpDestination().getRequestNotifier();
+ notifier.notifyCommit(request);
+ return true;
+ }
+
+ protected boolean someToContent(Request request, ByteBuffer content)
+ {
+ RequestState current = requestState.get();
+ switch (current)
{
- HttpExchange exchange = connection.getExchange();
- // The exchange may be null if it failed concurrently
- if (exchange == null)
- return;
+ case COMMIT:
+ case CONTENT:
+ {
+ if (!updateRequestState(current, RequestState.CONTENT))
+ return false;
+ if (LOG.isDebugEnabled())
+ LOG.debug("Request content {}{}{}", request, System.getProperty("line.separator"), BufferUtil.toDetailString(content));
+ RequestNotifier notifier = getHttpChannel().getHttpDestination().getRequestNotifier();
+ notifier.notifyContent(request, content);
+ return true;
+ }
+ case FAILURE:
+ {
+ return false;
+ }
+ default:
+ {
+ throw new IllegalStateException();
+ }
+ }
+ }
- final Request request = exchange.getRequest();
+ protected boolean someToSuccess(HttpExchange exchange)
+ {
+ RequestState current = requestState.get();
+ switch (current)
+ {
+ case COMMIT:
+ case CONTENT:
+ {
+ // Mark atomically the request as completed, with respect
+ // to concurrency between request success and request failure.
+ boolean completed = exchange.requestComplete();
+ if (!completed)
+ return false;
+
+ // Reset to be ready for another request
+ reset();
+
+ // Mark atomically the request as terminated and succeeded,
+ // with respect to concurrency between request and response.
+ Result result = exchange.terminateRequest(null);
+
+ // It is important to notify completion *after* we reset because
+ // the notification may trigger another request/response
+ Request request = exchange.getRequest();
+ LOG.debug("Request success {}", request);
+ HttpDestination destination = getHttpChannel().getHttpDestination();
+ destination.getRequestNotifier().notifySuccess(exchange.getRequest());
+
+ if (result != null)
+ {
+ boolean ordered = destination.getHttpClient().isStrictEventOrdering();
+ if (!ordered)
+ channel.exchangeTerminated(result);
+ LOG.debug("Request/Response succeded {}", request);
+ HttpConversation conversation = exchange.getConversation();
+ destination.getResponseNotifier().notifyComplete(conversation.getResponseListeners(), result);
+ if (ordered)
+ channel.exchangeTerminated(result);
+ }
+
+ return true;
+ }
+ case FAILURE:
+ {
+ return false;
+ }
+ default:
+ {
+ throw new IllegalStateException();
+ }
+ }
+ }
+
+ protected boolean anyToFailure(Throwable failure)
+ {
+ HttpExchange exchange = getHttpExchange();
+ if (exchange == null)
+ return false;
+
+ // Mark atomically the request as completed, with respect
+ // to concurrency between request success and request failure.
+ boolean completed = exchange.requestComplete();
+ if (!completed)
+ return false;
+
+ // Dispose to avoid further requests
+ RequestState requestState = dispose();
+
+ // Mark atomically the request as terminated and failed,
+ // with respect to concurrency between request and response.
+ Result result = exchange.terminateRequest(failure);
+
+ Request request = exchange.getRequest();
+ LOG.debug("Request failure {} {}", exchange, failure);
+ HttpDestination destination = getHttpChannel().getHttpDestination();
+ destination.getRequestNotifier().notifyFailure(request, failure);
+
+ boolean notCommitted = isBeforeCommit(requestState);
+ if (result == null && notCommitted && request.getAbortCause() == null)
+ {
+ // Complete the response from here
+ if (exchange.responseComplete())
+ {
+ result = exchange.terminateResponse(failure);
+ LOG.debug("Failed response from request {}", exchange);
+ }
+ }
+
+ if (result != null)
+ {
+ boolean ordered = destination.getHttpClient().isStrictEventOrdering();
+ if (!ordered)
+ channel.exchangeTerminated(result);
+ LOG.debug("Request/Response failed {}", request);
HttpConversation conversation = exchange.getConversation();
- HttpGenerator.RequestInfo requestInfo = null;
+ destination.getResponseNotifier().notifyComplete(conversation.getResponseListeners(), result);
+ if (ordered)
+ channel.exchangeTerminated(result);
+ }
- // Determine whether we have already received the 100 Continue response or not
- // If it was not received yet, we need to save the content and wait for it
- boolean expect100HeaderPresent = request.getHeaders().contains(HttpHeader.EXPECT, HttpHeaderValue.CONTINUE.asString());
- final boolean expecting100ContinueResponse = expect100HeaderPresent && conversation.getAttribute(EXPECT_100_ATTRIBUTE) == null;
- if (expecting100ContinueResponse)
- conversation.setAttribute(EXPECT_100_ATTRIBUTE, Boolean.TRUE);
+ return true;
+ }
- ContentChunk contentChunk = continueContentChunk;
- continueContentChunk = null;
- if (contentChunk == null)
- contentChunk = new ContentChunk(contentIterator);
+ /**
+ * Implementations should send the HTTP headers over the wire, possibly with some content,
+ * in a single write, and notify the given {@code callback} of the result of this operation.
+ * <p />
+ * If there is more content to send, then {@link #sendContent(HttpExchange, HttpContent, Callback)}
+ * will be invoked.
+ *
+ * @param exchange the exchange to send
+ * @param content the content to send
+ * @param callback the callback to notify
+ */
+ protected abstract void sendHeaders(HttpExchange exchange, HttpContent content, Callback callback);
+ /**
+ * Implementations should send the content at the {@link HttpContent} cursor position over the wire.
+ * <p />
+ * The {@link HttpContent} cursor is advanced by {@link HttpSender} at the right time, and if more
+ * content needs to be sent, this method is invoked again; subclasses need only to send the content
+ * at the {@link HttpContent} cursor position.
+ * <p />
+ * This method is invoked one last time when {@link HttpContent#isConsumed()} is true; subclasses
+ * needs to skip sending content in this case, and just complete their content generation.
+ *
+ * @param exchange the exchange to send
+ * @param content the content to send
+ * @param callback the callback to notify
+ */
+ protected abstract void sendContent(HttpExchange exchange, HttpContent content, Callback callback);
+
+ protected void reset()
+ {
+ content = null;
+ requestState.set(RequestState.QUEUED);
+ senderState.set(SenderState.IDLE);
+ }
+
+ protected RequestState dispose()
+ {
+ while (true)
+ {
+ RequestState current = requestState.get();
+ if (updateRequestState(current, RequestState.FAILURE))
+ return current;
+ }
+ }
+
+ public void proceed(HttpExchange exchange, boolean proceed)
+ {
+ if (!expects100Continue(exchange.getRequest()))
+ return;
+
+ if (proceed)
+ {
while (true)
{
- ByteBuffer content = contentChunk.content;
- final ByteBuffer contentBuffer = content == null ? null : content.slice();
-
- HttpGenerator.Result result = generator.generateRequest(requestInfo, header, chunk, content, contentChunk.lastContent);
- switch (result)
+ SenderState current = senderState.get();
+ switch (current)
{
- case NEED_INFO:
+ case EXPECTING:
{
- ContentProvider requestContent = request.getContent();
- long contentLength = requestContent == null ? -1 : requestContent.getLength();
- String path = request.getPath();
- String query = request.getQuery();
- if (query != null)
- path += "?" + query;
- requestInfo = new HttpGenerator.RequestInfo(request.getVersion(), request.getHeaders(), contentLength, request.method(), path);
- break;
+ // We are still sending the headers, but we already got the 100 Continue.
+ // Move to SEND so that the commit callback can send the content.
+ if (!updateSenderState(current, SenderState.SENDING))
+ break;
+ LOG.debug("Proceed while expecting");
+ return;
}
- case NEED_HEADER:
+ case WAITING:
{
- header = bufferPool.acquire(client.getRequestBufferSize(), false);
- break;
- }
- case NEED_CHUNK:
- {
- chunk = bufferPool.acquire(HttpGenerator.CHUNK_SIZE, false);
- break;
- }
- case FLUSH:
- {
- out:
- while (true)
+ // We received the 100 Continue, send the content if any.
+ // First update the sender state to be sure to be the one
+ // to call sendContent() since we race with onContent().
+ if (!updateSenderState(current, SenderState.SENDING))
+ break;
+ HttpContent content = this.content;
+ if (content.advance())
{
- State currentState = state.get();
- switch (currentState)
- {
- case BEGIN:
- {
- if (!updateState(currentState, State.HEADERS))
- continue;
- RequestNotifier notifier = connection.getDestination().getRequestNotifier();
- notifier.notifyHeaders(request);
- break out;
- }
- case HEADERS:
- case COMMIT:
- {
- // State update is performed after the write in commit()
- break out;
- }
- case FAILURE:
- {
- // Failed concurrently, avoid the write since
- // the connection is probably already closed
- return;
- }
- default:
- {
- throw new IllegalStateException();
- }
- }
+ // There is content to send
+ LOG.debug("Proceed while waiting");
+ sendContent(exchange, content, contentCallback);
}
-
- StatefulExecutorCallback callback = new StatefulExecutorCallback(client.getExecutor())
+ else
{
- @Override
- protected void onSucceeded()
- {
- LOG.debug("Write succeeded for {}", request);
-
- if (!processWrite(request, contentBuffer, expecting100ContinueResponse))
- return;
-
- send();
- }
-
- @Override
- protected void onFailed(Throwable x)
- {
- fail(x);
- }
- };
-
- if (expecting100ContinueResponse)
- {
- // Save the content waiting for the 100 Continue response
- continueContentChunk = new ContinueContentChunk(contentChunk);
- }
-
- write(callback, header, chunk, expecting100ContinueResponse ? null : content);
-
- if (callback.process())
- {
- LOG.debug("Write pending for {}", request);
- return;
- }
-
- if (callback.isSucceeded())
- {
- if (!processWrite(request, contentBuffer, expecting100ContinueResponse))
- return;
-
- // Send further content
- contentChunk = new ContentChunk(contentIterator);
-
- if (contentChunk.isDeferred())
- {
- out:
- while (true)
- {
- currentSendState = sendState.get();
- switch (currentSendState)
- {
- case EXECUTE:
- {
- if (updateSendState(currentSendState, SendState.IDLE))
- {
- LOG.debug("Waiting for deferred content for {}", request);
- return;
- }
- break;
- }
- case SCHEDULE:
- {
- if (updateSendState(currentSendState, SendState.EXECUTE))
- {
- LOG.debug("Deferred content available for {}", request);
- break out;
- }
- break;
- }
- default:
- {
- throw new IllegalStateException();
- }
- }
- }
- }
- }
- break;
- }
- case SHUTDOWN_OUT:
- {
- shutdownOutput();
- break;
- }
- case CONTINUE:
- {
- break;
- }
- case DONE:
- {
- if (generator.isEnd())
- {
- out: while (true)
- {
- currentSendState = sendState.get();
- switch (currentSendState)
- {
- case EXECUTE:
- case SCHEDULE:
- {
- if (!updateSendState(currentSendState, SendState.IDLE))
- throw new IllegalStateException();
- break out;
- }
- default:
- {
- throw new IllegalStateException();
- }
- }
- }
- success();
+ // No content to send yet - it's deferred.
+ // We may fail the update as onContent() moved to SCHEDULE.
+ if (!updateSenderState(SenderState.SENDING, SenderState.IDLE))
+ break;
+ LOG.debug("Proceed deferred");
}
return;
}
+ case SCHEDULED:
+ {
+ // We lost the race with onContent() to update the state, try again
+ if (!updateSenderState(current, SenderState.WAITING))
+ throw new IllegalStateException();
+ LOG.debug("Proceed while scheduled");
+ break;
+ }
default:
{
- throw new IllegalStateException("Unknown result " + result);
+ throw new IllegalStateException();
}
}
}
}
- catch (Exception x)
+ else
{
- LOG.debug(x);
- fail(x);
- }
- finally
- {
- releaseBuffers(bufferPool, header, chunk);
+ anyToFailure(new HttpRequestException("Expectation failed", exchange.getRequest()));
}
}
- private boolean processWrite(Request request, ByteBuffer content, boolean expecting100ContinueResponse)
+ public boolean abort(Throwable failure)
{
- if (!commit(request))
- return false;
-
- if (content != null)
- {
- RequestNotifier notifier = connection.getDestination().getRequestNotifier();
- notifier.notifyContent(request, content);
- }
-
- if (expecting100ContinueResponse)
- {
- LOG.debug("Expecting 100 Continue for {}", request);
- continueContentChunk.signal();
- return false;
- }
-
- return true;
- }
-
- public void proceed(boolean proceed)
- {
- ContinueContentChunk contentChunk = continueContentChunk;
- if (contentChunk != null)
- {
- if (proceed)
- {
- // Method send() must not be executed concurrently.
- // The write in send() may arrive to the server and the server reply with 100 Continue
- // before send() exits; the processing of the 100 Continue will invoke this method
- // which in turn invokes send(), with the risk of a concurrent invocation of send().
- // Therefore we wait here on the ContinueContentChunk to send, and send() will signal
- // when it is ok to proceed.
- LOG.debug("Proceeding {}", connection.getExchange());
- contentChunk.await();
- send();
- }
- else
- {
- HttpExchange exchange = connection.getExchange();
- if (exchange != null)
- fail(new HttpRequestException("Expectation failed", exchange.getRequest()));
- }
- }
- }
-
- private void write(Callback callback, ByteBuffer header, ByteBuffer chunk, ByteBuffer content)
- {
- int mask = 0;
- if (header != null)
- mask += 1;
- if (chunk != null)
- mask += 2;
- if (content != null)
- mask += 4;
-
- EndPoint endPoint = connection.getEndPoint();
- switch (mask)
- {
- case 0:
- endPoint.write(callback, BufferUtil.EMPTY_BUFFER);
- break;
- case 1:
- endPoint.write(callback, header);
- break;
- case 2:
- endPoint.write(callback, chunk);
- break;
- case 3:
- endPoint.write(callback, header, chunk);
- break;
- case 4:
- endPoint.write(callback, content);
- break;
- case 5:
- endPoint.write(callback, header, content);
- break;
- case 6:
- endPoint.write(callback, chunk, content);
- break;
- case 7:
- endPoint.write(callback, header, chunk, content);
- break;
- default:
- throw new IllegalStateException();
- }
- }
-
- protected boolean commit(Request request)
- {
- while (true)
- {
- State current = state.get();
- switch (current)
- {
- case HEADERS:
- if (!updateState(current, State.COMMIT))
- continue;
- LOG.debug("Committed {}", request);
- RequestNotifier notifier = connection.getDestination().getRequestNotifier();
- notifier.notifyCommit(request);
- return true;
- case COMMIT:
- if (!updateState(current, State.COMMIT))
- continue;
- return true;
- case FAILURE:
- return false;
- default:
- throw new IllegalStateException();
- }
- }
- }
-
- protected boolean success()
- {
- HttpExchange exchange = connection.getExchange();
- if (exchange == null)
- return false;
-
- AtomicMarkableReference<Result> completion = exchange.requestComplete(null);
- if (!completion.isMarked())
- return false;
-
- generator.reset();
-
- if (!updateState(State.COMMIT, State.IDLE))
- throw new IllegalStateException();
-
- exchange.terminateRequest();
-
- // It is important to notify completion *after* we reset because
- // the notification may trigger another request/response
-
- HttpDestination destination = connection.getDestination();
- Request request = exchange.getRequest();
- destination.getRequestNotifier().notifySuccess(request);
- LOG.debug("Sent {}", request);
-
- Result result = completion.getReference();
- if (result != null)
- {
- connection.complete(exchange, !result.isFailed());
-
- HttpConversation conversation = exchange.getConversation();
- destination.getResponseNotifier().notifyComplete(conversation.getResponseListeners(), result);
- }
-
- return true;
- }
-
- protected boolean fail(Throwable failure)
- {
- HttpExchange exchange = connection.getExchange();
- if (exchange == null)
- return false;
-
- AtomicMarkableReference<Result> completion = exchange.requestComplete(failure);
- if (!completion.isMarked())
- return false;
-
- generator.abort();
-
- State current;
- while (true)
- {
- current = state.get();
- if (updateState(current, State.FAILURE))
- break;
- }
-
- shutdownOutput();
-
- exchange.terminateRequest();
-
- HttpDestination destination = connection.getDestination();
- Request request = exchange.getRequest();
- destination.getRequestNotifier().notifyFailure(request, failure);
- LOG.debug("Failed {} {}", request, failure);
-
- Result result = completion.getReference();
- boolean notCommitted = isBeforeCommit(current);
- if (result == null && notCommitted && request.getAbortCause() == null)
- {
- completion = exchange.responseComplete(failure);
- if (completion.isMarked())
- {
- result = completion.getReference();
- exchange.terminateResponse();
- LOG.debug("Failed on behalf {}", exchange);
- }
- }
-
- if (result != null)
- {
- connection.complete(exchange, false);
-
- HttpConversation conversation = exchange.getConversation();
- destination.getResponseNotifier().notifyComplete(conversation.getResponseListeners(), result);
- }
-
- return true;
- }
-
- private void shutdownOutput()
- {
- connection.getEndPoint().shutdownOutput();
- }
-
- public boolean abort(Throwable cause)
- {
- State current = state.get();
+ RequestState current = requestState.get();
boolean abortable = isBeforeCommit(current) ||
- current == State.COMMIT && contentIterator.hasNext();
- return abortable && fail(cause);
+ isSending(current) && !content.isLast();
+ return abortable && anyToFailure(failure);
}
- private boolean isBeforeCommit(State state)
+ protected boolean updateRequestState(RequestState from, RequestState to)
{
- return state == State.IDLE || state == State.BEGIN || state == State.HEADERS;
- }
-
- private void releaseBuffers(ByteBufferPool bufferPool, ByteBuffer header, ByteBuffer chunk)
- {
- if (!BufferUtil.hasContent(header))
- bufferPool.release(header);
- if (!BufferUtil.hasContent(chunk))
- bufferPool.release(chunk);
- }
-
- private boolean updateState(State from, State to)
- {
- boolean updated = state.compareAndSet(from, to);
+ boolean updated = requestState.compareAndSet(from, to);
if (!updated)
- LOG.debug("State update failed: {} -> {}: {}", from, to, state.get());
+ LOG.debug("RequestState update failed: {} -> {}: {}", from, to, requestState.get());
return updated;
}
- private boolean updateSendState(SendState from, SendState to)
+ private boolean updateSenderState(SenderState from, SenderState to)
{
- boolean updated = sendState.compareAndSet(from, to);
+ boolean updated = senderState.compareAndSet(from, to);
if (!updated)
- LOG.debug("Send state update failed: {} -> {}: {}", from, to, sendState.get());
+ LOG.debug("SenderState update failed: {} -> {}: {}", from, to, senderState.get());
return updated;
}
- private enum State
+ private boolean isBeforeCommit(RequestState requestState)
{
- IDLE, BEGIN, HEADERS, COMMIT, FAILURE
- }
-
- private enum SendState
- {
- IDLE, EXECUTE, SCHEDULE
- }
-
- private static abstract class StatefulExecutorCallback implements Callback, Runnable
- {
- private final AtomicReference<State> state = new AtomicReference<>(State.INCOMPLETE);
- private final Executor executor;
-
- private StatefulExecutorCallback(Executor executor)
+ switch (requestState)
{
- this.executor = executor;
+ case QUEUED:
+ case BEGIN:
+ case HEADERS:
+ return true;
+ default:
+ return false;
}
+ }
+ private boolean isSending(RequestState requestState)
+ {
+ switch (requestState)
+ {
+ case COMMIT:
+ case CONTENT:
+ return true;
+ default:
+ return false;
+ }
+ }
+
+ /**
+ * The request states {@link HttpSender} goes through when sending a request.
+ */
+ protected enum RequestState
+ {
+ /**
+ * The request is queued, the initial state
+ */
+ QUEUED,
+ /**
+ * The request has been dequeued
+ */
+ BEGIN,
+ /**
+ * The request headers (and possibly some content) is about to be sent
+ */
+ HEADERS,
+ /**
+ * The request headers (and possibly some content) have been sent
+ */
+ COMMIT,
+ /**
+ * The request content is being sent
+ */
+ CONTENT,
+ /**
+ * The request is failed
+ */
+ FAILURE
+ }
+
+ /**
+ * The sender states {@link HttpSender} goes through when sending a request.
+ */
+ private enum SenderState
+ {
+ /**
+ * {@link HttpSender} is not sending the request
+ */
+ IDLE,
+ /**
+ * {@link HttpSender} is sending the request
+ */
+ SENDING,
+ /**
+ * {@link HttpSender} is sending the headers but will wait for 100-Continue before sending the content
+ */
+ EXPECTING,
+ /**
+ * {@link HttpSender} is waiting for 100-Continue
+ */
+ WAITING,
+ /**
+ * {@link HttpSender} is currently sending the request, and deferred content is available to be sent
+ */
+ SCHEDULED
+ }
+
+ private class CommitCallback implements Callback
+ {
@Override
- public final void succeeded()
- {
- State previous = state.get();
- while (true)
- {
- if (state.compareAndSet(previous, State.SUCCEEDED))
- break;
- previous = state.get();
- }
- if (previous == State.PENDING)
- executor.execute(this);
- }
-
- @Override
- public final void run()
- {
- onSucceeded();
- }
-
- protected abstract void onSucceeded();
-
- @Override
- public final void failed(final Throwable x)
- {
- State previous = state.get();
- while (true)
- {
- if (state.compareAndSet(previous, State.FAILED))
- break;
- previous = state.get();
- }
- if (previous == State.PENDING)
- {
- executor.execute(new Runnable()
- {
- @Override
- public void run()
- {
- onFailed(x);
- }
- });
- }
- else
- {
- onFailed(x);
- }
- }
-
- protected abstract void onFailed(Throwable x);
-
- public boolean process()
- {
- return state.compareAndSet(State.INCOMPLETE, State.PENDING);
- }
-
- public boolean isSucceeded()
- {
- return state.get() == State.SUCCEEDED;
- }
-
- public boolean isFailed()
- {
- return state.get() == State.FAILED;
- }
-
- private enum State
- {
- INCOMPLETE, PENDING, SUCCEEDED, FAILED
- }
- }
-
- private class ContentChunk
- {
- private final boolean lastContent;
- private final ByteBuffer content;
-
- private ContentChunk(ContentChunk chunk)
- {
- lastContent = chunk.lastContent;
- content = chunk.content;
- }
-
- private ContentChunk(Iterator<ByteBuffer> contentIterator)
- {
- lastContent = !contentIterator.hasNext();
- content = lastContent ? BufferUtil.EMPTY_BUFFER : contentIterator.next();
- }
-
- private boolean isDeferred()
- {
- return content == null && !lastContent;
- }
- }
-
- private class ContinueContentChunk extends ContentChunk
- {
- private final CountDownLatch latch = new CountDownLatch(1);
-
- private ContinueContentChunk(ContentChunk chunk)
- {
- super(chunk);
- }
-
- private void signal()
- {
- latch.countDown();
- }
-
- private void await()
+ public void succeeded()
{
try
{
- latch.await();
+ process();
}
- catch (InterruptedException x)
+ // Catch-all for runtime exceptions
+ catch (Exception x)
{
- LOG.ignore(x);
+ anyToFailure(x);
}
}
+
+ private void process() throws Exception
+ {
+ HttpExchange exchange = getHttpExchange();
+ if (exchange == null)
+ return;
+
+ Request request = exchange.getRequest();
+ if (!headersToCommit(request))
+ return;
+
+ HttpContent content = HttpSender.this.content;
+
+ if (!content.hasContent())
+ {
+ // No content to send, we are done.
+ someToSuccess(exchange);
+ }
+ else
+ {
+ // Was any content sent while committing ?
+ ByteBuffer contentBuffer = content.getContent();
+ if (contentBuffer != null)
+ {
+ if (!someToContent(request, contentBuffer))
+ return;
+ }
+
+ while (true)
+ {
+ SenderState current = senderState.get();
+ switch (current)
+ {
+ case SENDING:
+ {
+ // We have content to send ?
+ if (content.advance())
+ {
+ sendContent(exchange, content, contentCallback);
+ }
+ else
+ {
+ if (content.isLast())
+ {
+ sendContent(exchange, content, lastCallback);
+ }
+ else
+ {
+ if (!updateSenderState(current, SenderState.IDLE))
+ break;
+ LOG.debug("Waiting for deferred content for {}", request);
+ }
+ }
+ return;
+ }
+ case EXPECTING:
+ {
+ // Wait for the 100 Continue response
+ if (!updateSenderState(current, SenderState.WAITING))
+ break;
+ return;
+ }
+ case SCHEDULED:
+ {
+ if (expects100Continue(request))
+ return;
+ // We have deferred content to send.
+ updateSenderState(current, SenderState.SENDING);
+ break;
+ }
+ default:
+ {
+ throw new IllegalStateException();
+ }
+ }
+ }
+ }
+ }
+
+ @Override
+ public void failed(Throwable failure)
+ {
+ anyToFailure(failure);
+ }
+ }
+
+ private class ContentCallback extends IteratingCallback
+ {
+ @Override
+ protected boolean process() throws Exception
+ {
+ HttpExchange exchange = getHttpExchange();
+ if (exchange == null)
+ return false;
+
+ Request request = exchange.getRequest();
+ HttpContent content = HttpSender.this.content;
+
+ ByteBuffer contentBuffer = content.getContent();
+ if (contentBuffer != null)
+ {
+ if (!someToContent(request, contentBuffer))
+ return false;
+ }
+
+ if (content.advance())
+ {
+ // There is more content to send
+ sendContent(exchange, content, this);
+ }
+ else
+ {
+ if (content.isLast())
+ {
+ sendContent(exchange, content, lastCallback);
+ }
+ else
+ {
+ while (true)
+ {
+ SenderState current = senderState.get();
+ switch (current)
+ {
+ case SENDING:
+ {
+ if (updateSenderState(current, SenderState.IDLE))
+ {
+ LOG.debug("Waiting for deferred content for {}", request);
+ return false;
+ }
+ break;
+ }
+ case SCHEDULED:
+ {
+ if (updateSenderState(current, SenderState.SENDING))
+ {
+ LOG.debug("Deferred content available for {}", request);
+ // TODO: this case is not covered by tests
+ sendContent(exchange, content, this);
+ return false;
+ }
+ break;
+ }
+ default:
+ {
+ throw new IllegalStateException();
+ }
+ }
+ }
+ }
+ }
+ return false;
+ }
+
+ @Override
+ protected void completed()
+ {
+ // Nothing to do, since we always return false from process().
+ // Termination is obtained via LastContentCallback.
+ }
+
+ @Override
+ public void failed(Throwable failure)
+ {
+ anyToFailure(failure);
+ }
+ }
+
+ private class LastContentCallback implements Callback
+ {
+ @Override
+ public void succeeded()
+ {
+ HttpExchange exchange = getHttpExchange();
+ if (exchange == null)
+ return;
+ someToSuccess(exchange);
+ }
+
+ @Override
+ public void failed(Throwable failure)
+ {
+ anyToFailure(failure);
+ }
}
}
diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/RedirectProtocolHandler.java b/jetty-client/src/main/java/org/eclipse/jetty/client/RedirectProtocolHandler.java
index 7b9c42a..125ef8c 100644
--- a/jetty-client/src/main/java/org/eclipse/jetty/client/RedirectProtocolHandler.java
+++ b/jetty-client/src/main/java/org/eclipse/jetty/client/RedirectProtocolHandler.java
@@ -95,7 +95,7 @@
{
case 301:
{
- String method = request.method();
+ String method = request.getMethod();
if (HttpMethod.GET.is(method) || HttpMethod.HEAD.is(method))
redirect(result, method, newURI);
else
@@ -112,7 +112,7 @@
case 307:
{
// Keep same method
- redirect(result, request.method(), newURI);
+ redirect(result, request.getMethod(), newURI);
break;
}
default:
diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/api/Request.java b/jetty-client/src/main/java/org/eclipse/jetty/client/api/Request.java
index 225137a..ebe1176 100644
--- a/jetty-client/src/main/java/org/eclipse/jetty/client/api/Request.java
+++ b/jetty-client/src/main/java/org/eclipse/jetty/client/api/Request.java
@@ -76,16 +76,9 @@
int getPort();
/**
- * @return the method of this request, such as GET or POST, or null if the method is not a standard HTTP method
- * @deprecated use {@link #method()} instead
- */
- @Deprecated
- HttpMethod getMethod();
-
- /**
* @return the method of this request, such as GET or POST, as a String
*/
- String method();
+ String getMethod();
/**
* @param method the method of this request, such as GET or POST
diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/http/HttpChannelOverHTTP.java b/jetty-client/src/main/java/org/eclipse/jetty/client/http/HttpChannelOverHTTP.java
new file mode 100644
index 0000000..ebca079
--- /dev/null
+++ b/jetty-client/src/main/java/org/eclipse/jetty/client/http/HttpChannelOverHTTP.java
@@ -0,0 +1,116 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.client.http;
+
+import java.util.Enumeration;
+
+import org.eclipse.jetty.client.HttpChannel;
+import org.eclipse.jetty.client.HttpExchange;
+import org.eclipse.jetty.client.api.Result;
+import org.eclipse.jetty.http.HttpFields;
+import org.eclipse.jetty.http.HttpHeader;
+import org.eclipse.jetty.http.HttpHeaderValue;
+
+public class HttpChannelOverHTTP extends HttpChannel
+{
+ private final HttpConnectionOverHTTP connection;
+ private final HttpSenderOverHTTP sender;
+ private final HttpReceiverOverHTTP receiver;
+
+ public HttpChannelOverHTTP(HttpConnectionOverHTTP connection)
+ {
+ super(connection.getHttpDestination());
+ this.connection = connection;
+ this.sender = new HttpSenderOverHTTP(this);
+ this.receiver = new HttpReceiverOverHTTP(this);
+ }
+
+ public HttpConnectionOverHTTP getHttpConnection()
+ {
+ return connection;
+ }
+
+ @Override
+ public void send()
+ {
+ HttpExchange exchange = getHttpExchange();
+ if (exchange != null)
+ sender.send(exchange);
+ }
+
+ @Override
+ public void proceed(HttpExchange exchange, boolean proceed)
+ {
+ sender.proceed(exchange, proceed);
+ }
+
+ @Override
+ public boolean abort(Throwable cause)
+ {
+ // We want the return value to be that of the response
+ // because if the response has already successfully
+ // arrived then we failed to abort the exchange
+ sender.abort(cause);
+ return receiver.abort(cause);
+ }
+
+ public void receive()
+ {
+ receiver.receive();
+ }
+
+ @Override
+ public void exchangeTerminated(Result result)
+ {
+ super.exchangeTerminated(result);
+
+ if (result.isSucceeded())
+ {
+ HttpFields responseHeaders = result.getResponse().getHeaders();
+ Enumeration<String> values = responseHeaders.getValues(HttpHeader.CONNECTION.asString(), ",");
+ if (values != null)
+ {
+ while (values.hasMoreElements())
+ {
+ if (HttpHeaderValue.CLOSE.asString().equalsIgnoreCase(values.nextElement()))
+ {
+ connection.close();
+ return;
+ }
+ }
+ }
+ connection.release();
+ }
+ else
+ {
+ connection.close();
+ }
+ }
+
+ public void idleTimeout()
+ {
+ receiver.idleTimeout();
+ }
+
+ @Override
+ public String toString()
+ {
+ return String.format("%s@%x", getClass().getSimpleName(), hashCode());
+ }
+}
diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/http/HttpClientTransportOverHTTP.java b/jetty-client/src/main/java/org/eclipse/jetty/client/http/HttpClientTransportOverHTTP.java
new file mode 100644
index 0000000..391e2c8
--- /dev/null
+++ b/jetty-client/src/main/java/org/eclipse/jetty/client/http/HttpClientTransportOverHTTP.java
@@ -0,0 +1,237 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.client.http;
+
+import java.io.IOException;
+import java.net.ConnectException;
+import java.net.SocketAddress;
+import java.net.SocketException;
+import java.nio.channels.SelectionKey;
+import java.nio.channels.SocketChannel;
+import javax.net.ssl.SSLEngine;
+
+import org.eclipse.jetty.client.HttpClient;
+import org.eclipse.jetty.client.HttpClientTransport;
+import org.eclipse.jetty.client.HttpDestination;
+import org.eclipse.jetty.http.HttpScheme;
+import org.eclipse.jetty.io.Connection;
+import org.eclipse.jetty.io.EndPoint;
+import org.eclipse.jetty.io.SelectChannelEndPoint;
+import org.eclipse.jetty.io.SelectorManager;
+import org.eclipse.jetty.io.ssl.SslConnection;
+import org.eclipse.jetty.util.Promise;
+import org.eclipse.jetty.util.component.ContainerLifeCycle;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+import org.eclipse.jetty.util.ssl.SslContextFactory;
+
+public class HttpClientTransportOverHTTP extends ContainerLifeCycle implements HttpClientTransport
+{
+ private static final Logger LOG = Log.getLogger(HttpClientTransportOverHTTP.class);
+
+ private volatile HttpClient client;
+ private volatile SelectorManager selectorManager;
+
+ @Override
+ public void setHttpClient(HttpClient client)
+ {
+ this.client = client;
+ }
+
+ @Override
+ protected void doStart() throws Exception
+ {
+ selectorManager = newSelectorManager(client);
+ selectorManager.setConnectTimeout(client.getConnectTimeout());
+ addBean(selectorManager);
+ super.doStart();
+ }
+
+ @Override
+ public HttpDestination newHttpDestination(HttpClient client, String scheme, String host, int port)
+ {
+ return new HttpDestinationOverHTTP(client, scheme, host, port);
+ }
+
+ @Override
+ public void connect(HttpDestination destination, SocketAddress address, Promise<org.eclipse.jetty.client.api.Connection> promise)
+ {
+ SocketChannel channel = null;
+ try
+ {
+ channel = SocketChannel.open();
+ HttpClient client = destination.getHttpClient();
+ SocketAddress bindAddress = client.getBindAddress();
+ if (bindAddress != null)
+ channel.bind(bindAddress);
+ configure(client, channel);
+ channel.configureBlocking(false);
+ channel.connect(address);
+
+ ConnectionCallback callback = new ConnectionCallback(destination, promise);
+ selectorManager.connect(channel, callback);
+ }
+ // Must catch all exceptions, since some like
+ // UnresolvedAddressException are not IOExceptions.
+ catch (Throwable x)
+ {
+ try
+ {
+ if (channel != null)
+ channel.close();
+ }
+ catch (IOException xx)
+ {
+ LOG.ignore(xx);
+ }
+ finally
+ {
+ promise.failed(x);
+ }
+ }
+ }
+
+ @Override
+ public org.eclipse.jetty.client.api.Connection tunnel(org.eclipse.jetty.client.api.Connection connection)
+ {
+ HttpConnectionOverHTTP httpConnection = (HttpConnectionOverHTTP)connection;
+ HttpDestination destination = httpConnection.getHttpDestination();
+ SslConnection sslConnection = createSslConnection(destination, httpConnection.getEndPoint());
+ HttpConnectionOverHTTP result = (HttpConnectionOverHTTP)sslConnection.getDecryptedEndPoint().getConnection();
+ selectorManager.connectionClosed(httpConnection);
+ selectorManager.connectionOpened(sslConnection);
+ LOG.debug("Tunnelled {} over {}", connection, result);
+ return result;
+ }
+
+ protected void configure(HttpClient client, SocketChannel channel) throws SocketException
+ {
+ channel.socket().setTcpNoDelay(client.isTCPNoDelay());
+ }
+
+ protected SelectorManager newSelectorManager(HttpClient client)
+ {
+ return new ClientSelectorManager(client, 1);
+ }
+
+ protected Connection newHttpConnection(HttpClient httpClient, EndPoint endPoint, HttpDestination destination)
+ {
+ return new HttpConnectionOverHTTP(httpClient, endPoint, destination);
+ }
+
+ protected SslConnection newSslConnection(HttpClient httpClient, EndPoint endPoint, SSLEngine engine)
+ {
+ return new SslConnection(httpClient.getByteBufferPool(), httpClient.getExecutor(), endPoint, engine);
+ }
+
+ private SslConnection createSslConnection(HttpDestination destination, EndPoint endPoint)
+ {
+ HttpClient httpClient = destination.getHttpClient();
+ SslContextFactory sslContextFactory = httpClient.getSslContextFactory();
+ SSLEngine engine = sslContextFactory.newSSLEngine(destination.getHost(), destination.getPort());
+ engine.setUseClientMode(true);
+
+ SslConnection sslConnection = newSslConnection(httpClient, endPoint, engine);
+ sslConnection.setRenegotiationAllowed(sslContextFactory.isRenegotiationAllowed());
+ endPoint.setConnection(sslConnection);
+ EndPoint appEndPoint = sslConnection.getDecryptedEndPoint();
+ Connection connection = newHttpConnection(httpClient, appEndPoint, destination);
+ appEndPoint.setConnection(connection);
+
+ return sslConnection;
+ }
+
+ protected class ClientSelectorManager extends SelectorManager
+ {
+ private final HttpClient client;
+
+ protected ClientSelectorManager(HttpClient client, int selectors)
+ {
+ super(client.getExecutor(), client.getScheduler(), selectors);
+ this.client = client;
+ }
+
+ @Override
+ protected EndPoint newEndPoint(SocketChannel channel, ManagedSelector selector, SelectionKey key)
+ {
+ return new SelectChannelEndPoint(channel, selector, key, getScheduler(), client.getIdleTimeout());
+ }
+
+ @Override
+ public Connection newConnection(SocketChannel channel, EndPoint endPoint, Object attachment) throws IOException
+ {
+ ConnectionCallback callback = (ConnectionCallback)attachment;
+ HttpDestination destination = callback.destination;
+
+ SslContextFactory sslContextFactory = client.getSslContextFactory();
+ if (!destination.isProxied() && HttpScheme.HTTPS.is(destination.getScheme()))
+ {
+ if (sslContextFactory == null)
+ {
+ IOException failure = new ConnectException("Missing " + SslContextFactory.class.getSimpleName() + " for " + destination.getScheme() + " requests");
+ callback.failed(failure);
+ throw failure;
+ }
+ else
+ {
+ SslConnection sslConnection = createSslConnection(destination, endPoint);
+ callback.succeeded((org.eclipse.jetty.client.api.Connection)sslConnection.getDecryptedEndPoint().getConnection());
+ return sslConnection;
+ }
+ }
+ else
+ {
+ Connection connection = newHttpConnection(client, endPoint, destination);
+ callback.succeeded((org.eclipse.jetty.client.api.Connection)connection);
+ return connection;
+ }
+ }
+
+ @Override
+ protected void connectionFailed(SocketChannel channel, Throwable ex, Object attachment)
+ {
+ ConnectionCallback callback = (ConnectionCallback)attachment;
+ callback.failed(ex);
+ }
+ }
+
+ private class ConnectionCallback implements Promise<org.eclipse.jetty.client.api.Connection>
+ {
+ private final HttpDestination destination;
+ private final Promise<org.eclipse.jetty.client.api.Connection> promise;
+
+ private ConnectionCallback(HttpDestination destination, Promise<org.eclipse.jetty.client.api.Connection> promise)
+ {
+ this.destination = destination;
+ this.promise = promise;
+ }
+
+ @Override
+ public void succeeded(org.eclipse.jetty.client.api.Connection result)
+ {
+ promise.succeeded(result);
+ }
+
+ @Override
+ public void failed(Throwable x)
+ {
+ promise.failed(x);
+ }
+ }
+}
diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/http/HttpConnectionOverHTTP.java b/jetty-client/src/main/java/org/eclipse/jetty/client/http/HttpConnectionOverHTTP.java
new file mode 100644
index 0000000..8981a66
--- /dev/null
+++ b/jetty-client/src/main/java/org/eclipse/jetty/client/http/HttpConnectionOverHTTP.java
@@ -0,0 +1,189 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.client.http;
+
+import org.eclipse.jetty.client.HttpClient;
+import org.eclipse.jetty.client.HttpConnection;
+import org.eclipse.jetty.client.HttpDestination;
+import org.eclipse.jetty.client.HttpExchange;
+import org.eclipse.jetty.client.api.Connection;
+import org.eclipse.jetty.client.api.Request;
+import org.eclipse.jetty.client.api.Response;
+import org.eclipse.jetty.io.AbstractConnection;
+import org.eclipse.jetty.io.EndPoint;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+
+public class HttpConnectionOverHTTP extends AbstractConnection implements Connection
+{
+ private static final Logger LOG = Log.getLogger(HttpConnectionOverHTTP.class);
+
+ private final Delegate delegate;
+ private final HttpChannelOverHTTP channel;
+ private volatile boolean closed;
+ private volatile long idleTimeout;
+
+ public HttpConnectionOverHTTP(HttpClient client, EndPoint endPoint, HttpDestination destination)
+ {
+ super(endPoint, client.getExecutor(), client.isDispatchIO());
+ this.delegate = new Delegate(client, destination);
+ this.channel = new HttpChannelOverHTTP(this);
+ }
+
+ public HttpChannelOverHTTP getHttpChannel()
+ {
+ return channel;
+ }
+
+ public HttpDestinationOverHTTP getHttpDestination()
+ {
+ return (HttpDestinationOverHTTP)delegate.getHttpDestination();
+ }
+
+ @Override
+ public void send(Request request, Response.CompleteListener listener)
+ {
+ delegate.send(request, listener);
+ }
+
+ protected void send(HttpExchange exchange)
+ {
+ delegate.send(exchange);
+ }
+
+ @Override
+ public void onOpen()
+ {
+ super.onOpen();
+ fillInterested();
+ }
+
+ @Override
+ public void onClose()
+ {
+ closed = true;
+ super.onClose();
+ }
+
+ @Override
+ public void fillInterested()
+ {
+ // This is necessary when "upgrading" the connection for example after proxied
+ // CONNECT requests, because the old connection will read the CONNECT response
+ // and then set the read interest, while the new connection attached to the same
+ // EndPoint also will set the read interest, causing a ReadPendingException.
+ if (!closed)
+ super.fillInterested();
+ }
+
+ @Override
+ protected boolean onReadTimeout()
+ {
+ LOG.debug("{} idle timeout", this);
+
+ HttpExchange exchange = channel.getHttpExchange();
+ if (exchange != null)
+ idleTimeout();
+ else
+ getHttpDestination().remove(this);
+
+ return true;
+ }
+
+ protected void idleTimeout()
+ {
+ // TODO: we need to fail the exchange if we did not get an answer from the server
+ // TODO: however this mechanism does not seem to be available in SPDY if not subclassing SPDYConnection
+ // TODO: but the API (Session) does not have such facilities; perhaps we need to add a callback to ISession
+ channel.idleTimeout();
+ }
+
+ @Override
+ public void onFillable()
+ {
+ HttpExchange exchange = channel.getHttpExchange();
+ if (exchange != null)
+ {
+ channel.receive();
+ }
+ else
+ {
+ // If there is no exchange, then could be either a remote close,
+ // or garbage bytes; in both cases we close the connection
+ close();
+ }
+ }
+
+ public void release()
+ {
+ // Restore idle timeout
+ getEndPoint().setIdleTimeout(idleTimeout);
+ getHttpDestination().release(this);
+ }
+
+ @Override
+ public void close()
+ {
+ getHttpDestination().remove(this);
+ getEndPoint().shutdownOutput();
+ LOG.debug("{} oshut", this);
+ getEndPoint().close();
+ LOG.debug("{} closed", this);
+ }
+
+ @Override
+ public String toString()
+ {
+ return String.format("%s@%x(l:%s <-> r:%s)",
+ HttpConnection.class.getSimpleName(),
+ hashCode(),
+ getEndPoint().getLocalAddress(),
+ getEndPoint().getRemoteAddress());
+ }
+
+ private class Delegate extends HttpConnection
+ {
+ private Delegate(HttpClient client, HttpDestination destination)
+ {
+ super(client, destination);
+ }
+
+ @Override
+ protected void send(HttpExchange exchange)
+ {
+ Request request = exchange.getRequest();
+ normalizeRequest(request);
+
+ // Save the old idle timeout to restore it
+ EndPoint endPoint = getEndPoint();
+ idleTimeout = endPoint.getIdleTimeout();
+ endPoint.setIdleTimeout(request.getIdleTimeout());
+
+ // One channel per connection, just delegate the send
+ channel.associate(exchange);
+ channel.send();
+ }
+
+ @Override
+ public void close()
+ {
+ HttpConnectionOverHTTP.this.close();
+ }
+ }
+}
diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/http/HttpConnectionPool.java b/jetty-client/src/main/java/org/eclipse/jetty/client/http/HttpConnectionPool.java
new file mode 100644
index 0000000..a9d9841
--- /dev/null
+++ b/jetty-client/src/main/java/org/eclipse/jetty/client/http/HttpConnectionPool.java
@@ -0,0 +1,207 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.client.http;
+
+import java.io.IOException;
+import java.util.concurrent.BlockingDeque;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.LinkedBlockingDeque;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.eclipse.jetty.client.api.Connection;
+import org.eclipse.jetty.client.api.Destination;
+import org.eclipse.jetty.util.BlockingArrayQueue;
+import org.eclipse.jetty.util.Promise;
+import org.eclipse.jetty.util.component.ContainerLifeCycle;
+import org.eclipse.jetty.util.component.Dumpable;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+
+public class HttpConnectionPool implements Dumpable
+{
+ private static final Logger LOG = Log.getLogger(HttpConnectionPool.class);
+
+ private final AtomicInteger connectionCount = new AtomicInteger();
+ private final Destination destination;
+ private final int maxConnections;
+ private final Promise<Connection> connectionPromise;
+ private final BlockingDeque<Connection> idleConnections;
+ private final BlockingQueue<Connection> activeConnections;
+
+ public HttpConnectionPool(Destination destination, int maxConnections, Promise<Connection> connectionPromise)
+ {
+ this.destination = destination;
+ this.maxConnections = maxConnections;
+ this.connectionPromise = connectionPromise;
+ this.idleConnections = new LinkedBlockingDeque<>(maxConnections);
+ this.activeConnections = new BlockingArrayQueue<>(maxConnections);
+ }
+
+ public BlockingQueue<Connection> getIdleConnections()
+ {
+ return idleConnections;
+ }
+
+ public BlockingQueue<Connection> getActiveConnections()
+ {
+ return activeConnections;
+ }
+
+ public Connection acquire()
+ {
+ Connection result = acquireIdleConnection();
+ if (result != null)
+ return result;
+
+ while (true)
+ {
+ int current = connectionCount.get();
+ final int next = current + 1;
+
+ if (next > maxConnections)
+ {
+ LOG.debug("Max connections {}/{} reached", current, maxConnections);
+ // Try again the idle connections
+ return acquireIdleConnection();
+ }
+
+ if (connectionCount.compareAndSet(current, next))
+ {
+ LOG.debug("Connection {}/{} creation", next, maxConnections);
+
+ destination.newConnection(new Promise<Connection>()
+ {
+ @Override
+ public void succeeded(Connection connection)
+ {
+ LOG.debug("Connection {}/{} creation succeeded {}", next, maxConnections, connection);
+ activate(connection);
+ connectionPromise.succeeded(connection);
+ }
+
+ @Override
+ public void failed(Throwable x)
+ {
+ LOG.debug("Connection " + next + "/" + maxConnections + " creation failed", x);
+ connectionCount.decrementAndGet();
+ connectionPromise.failed(x);
+ }
+ });
+
+ // Try again the idle connections
+ return acquireIdleConnection();
+ }
+ }
+ }
+
+ private Connection acquireIdleConnection()
+ {
+ Connection connection = idleConnections.pollFirst();
+ if (connection != null)
+ activate(connection);
+ return connection;
+ }
+
+ private boolean activate(Connection connection)
+ {
+ if (activeConnections.offer(connection))
+ {
+ LOG.debug("Connection active {}", connection);
+ return true;
+ }
+ else
+ {
+ LOG.debug("Connection active overflow {}", connection);
+ return false;
+ }
+ }
+
+ public boolean release(Connection connection)
+ {
+ if (activeConnections.remove(connection))
+ {
+ // Make sure we use "hot" connections first
+ if (idleConnections.offerFirst(connection))
+ {
+ LOG.debug("Connection idle {}", connection);
+ return true;
+ }
+ else
+ {
+ LOG.debug("Connection idle overflow {}", connection);
+ }
+ }
+ return false;
+ }
+
+ public boolean remove(Connection connection)
+ {
+ boolean removed = activeConnections.remove(connection);
+ removed |= idleConnections.remove(connection);
+ if (removed)
+ {
+ int pooled = connectionCount.decrementAndGet();
+ LOG.debug("Connection removed {} - pooled: {}", connection, pooled);
+ }
+ return removed;
+ }
+
+ public boolean isActive(Connection connection)
+ {
+ return activeConnections.contains(connection);
+ }
+
+ public boolean isIdle(Connection connection)
+ {
+ return idleConnections.contains(connection);
+ }
+
+ public void close()
+ {
+ for (Connection connection : idleConnections)
+ connection.close();
+ idleConnections.clear();
+
+ // A bit drastic, but we cannot wait for all requests to complete
+ for (Connection connection : activeConnections)
+ connection.close();
+ activeConnections.clear();
+
+ connectionCount.set(0);
+ }
+
+ @Override
+ public String dump()
+ {
+ return ContainerLifeCycle.dump(this);
+ }
+
+ @Override
+ public void dump(Appendable out, String indent) throws IOException
+ {
+ ContainerLifeCycle.dumpObject(out, this);
+ ContainerLifeCycle.dump(out, indent, activeConnections, idleConnections);
+ }
+
+ @Override
+ public String toString()
+ {
+ return String.format("%s %d/%d", getClass().getSimpleName(), connectionCount.get(), maxConnections);
+ }
+}
diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/http/HttpDestinationOverHTTP.java b/jetty-client/src/main/java/org/eclipse/jetty/client/http/HttpDestinationOverHTTP.java
new file mode 100644
index 0000000..e7f4f19
--- /dev/null
+++ b/jetty-client/src/main/java/org/eclipse/jetty/client/http/HttpDestinationOverHTTP.java
@@ -0,0 +1,186 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.client.http;
+
+import java.io.IOException;
+import java.util.Arrays;
+
+import org.eclipse.jetty.client.HttpClient;
+import org.eclipse.jetty.client.HttpDestination;
+import org.eclipse.jetty.client.HttpExchange;
+import org.eclipse.jetty.client.api.Connection;
+import org.eclipse.jetty.client.api.Request;
+import org.eclipse.jetty.util.Promise;
+import org.eclipse.jetty.util.component.ContainerLifeCycle;
+
+public class HttpDestinationOverHTTP extends HttpDestination implements Promise<Connection>
+{
+ private final HttpConnectionPool connectionPool;
+
+ public HttpDestinationOverHTTP(HttpClient client, String scheme, String host, int port)
+ {
+ super(client, scheme, host, port);
+ this.connectionPool = newHttpConnectionPool(client);
+ }
+
+ protected HttpConnectionPool newHttpConnectionPool(HttpClient client)
+ {
+ return new HttpConnectionPool(this, client.getMaxConnectionsPerDestination(), this);
+ }
+
+ public HttpConnectionPool getHttpConnectionPool()
+ {
+ return connectionPool;
+ }
+
+ @Override
+ public void succeeded(Connection connection)
+ {
+ process((HttpConnectionOverHTTP)connection, true);
+ }
+
+ @Override
+ public void failed(final Throwable x)
+ {
+ getHttpClient().getExecutor().execute(new Runnable()
+ {
+ @Override
+ public void run()
+ {
+ abort(x);
+ }
+ });
+ }
+
+ protected void send()
+ {
+ HttpConnectionOverHTTP connection = acquire();
+ if (connection != null)
+ process(connection, false);
+ }
+
+ protected HttpConnectionOverHTTP acquire()
+ {
+ return (HttpConnectionOverHTTP)connectionPool.acquire();
+ }
+
+ /**
+ * <p>Processes a new connection making it idle or active depending on whether requests are waiting to be sent.</p>
+ * <p>A new connection is created when a request needs to be executed; it is possible that the request that
+ * triggered the request creation is executed by another connection that was just released, so the new connection
+ * may become idle.</p>
+ * <p>If a request is waiting to be executed, it will be dequeued and executed by the new connection.</p>
+ *
+ * @param connection the new connection
+ * @param dispatch whether to dispatch the processing to another thread
+ */
+ protected void process(final HttpConnectionOverHTTP connection, boolean dispatch)
+ {
+ HttpClient client = getHttpClient();
+ final HttpExchange exchange = getHttpExchanges().poll();
+ LOG.debug("Processing exchange {} on connection {}", exchange, connection);
+ if (exchange == null)
+ {
+ // TODO: review this part... may not be 100% correct
+ // TODO: e.g. is client is not running, there should be no need to close the connection
+
+ if (!connectionPool.release(connection))
+ connection.close();
+
+ if (!client.isRunning())
+ {
+ LOG.debug("{} is stopping", client);
+ connection.close();
+ }
+ }
+ else
+ {
+ final Request request = exchange.getRequest();
+ Throwable cause = request.getAbortCause();
+ if (cause != null)
+ {
+ abort(exchange, cause);
+ LOG.debug("Aborted before processing {}: {}", exchange, cause);
+ }
+ else
+ {
+ if (dispatch)
+ {
+ client.getExecutor().execute(new Runnable()
+ {
+ @Override
+ public void run()
+ {
+ connection.send(exchange);
+ }
+ });
+ }
+ else
+ {
+ connection.send(exchange);
+ }
+ }
+ }
+ }
+
+ protected void release(HttpConnectionOverHTTP connection)
+ {
+ LOG.debug("{} released", connection);
+ HttpClient client = getHttpClient();
+ if (client.isRunning())
+ {
+ if (connectionPool.isActive(connection))
+ process(connection, false);
+ else
+ LOG.debug("{} explicit", connection);
+ }
+ else
+ {
+ LOG.debug("{} is stopped", client);
+ remove(connection);
+ connection.close();
+ }
+ }
+
+ protected void remove(HttpConnectionOverHTTP connection)
+ {
+ connectionPool.remove(connection);
+
+ // We need to execute queued requests even if this connection failed.
+ // We may create a connection that is not needed, but it will eventually
+ // idle timeout, so no worries
+ if (!getHttpExchanges().isEmpty())
+ {
+ connection = acquire();
+ if (connection != null)
+ process(connection, false);
+ }
+ }
+
+ public void close()
+ {
+ connectionPool.close();
+ }
+
+ @Override
+ public void dump(Appendable out, String indent) throws IOException
+ {
+ ContainerLifeCycle.dump(out, indent, Arrays.asList(connectionPool));
+ }
+}
diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/http/HttpReceiverOverHTTP.java b/jetty-client/src/main/java/org/eclipse/jetty/client/http/HttpReceiverOverHTTP.java
new file mode 100644
index 0000000..9e939fe
--- /dev/null
+++ b/jetty-client/src/main/java/org/eclipse/jetty/client/http/HttpReceiverOverHTTP.java
@@ -0,0 +1,221 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.client.http;
+
+import java.io.EOFException;
+import java.nio.ByteBuffer;
+
+import org.eclipse.jetty.client.HttpClient;
+import org.eclipse.jetty.client.HttpExchange;
+import org.eclipse.jetty.client.HttpReceiver;
+import org.eclipse.jetty.client.HttpResponse;
+import org.eclipse.jetty.client.HttpResponseException;
+import org.eclipse.jetty.http.HttpField;
+import org.eclipse.jetty.http.HttpMethod;
+import org.eclipse.jetty.http.HttpParser;
+import org.eclipse.jetty.http.HttpVersion;
+import org.eclipse.jetty.io.ByteBufferPool;
+import org.eclipse.jetty.io.EndPoint;
+import org.eclipse.jetty.io.EofException;
+import org.eclipse.jetty.util.BufferUtil;
+
+public class HttpReceiverOverHTTP extends HttpReceiver implements HttpParser.ResponseHandler<ByteBuffer>
+{
+ private final HttpParser parser = new HttpParser(this);
+
+ public HttpReceiverOverHTTP(HttpChannelOverHTTP channel)
+ {
+ super(channel);
+ }
+
+ @Override
+ public HttpChannelOverHTTP getHttpChannel()
+ {
+ return (HttpChannelOverHTTP)super.getHttpChannel();
+ }
+
+ public void receive()
+ {
+ EndPoint endPoint = getHttpChannel().getHttpConnection().getEndPoint();
+ HttpClient client = getHttpDestination().getHttpClient();
+ ByteBufferPool bufferPool = client.getByteBufferPool();
+ ByteBuffer buffer = bufferPool.acquire(client.getResponseBufferSize(), true);
+ try
+ {
+ while (true)
+ {
+ int read = endPoint.fill(buffer);
+ LOG.debug("Read {} bytes from {}", read, endPoint);
+ if (read > 0)
+ {
+ parse(buffer);
+ }
+ else if (read == 0)
+ {
+ fillInterested();
+ break;
+ }
+ else
+ {
+ shutdown();
+ break;
+ }
+ }
+ }
+ catch (EofException x)
+ {
+ LOG.ignore(x);
+ failAndClose(x);
+ }
+ catch (Exception x)
+ {
+ LOG.debug(x);
+ failAndClose(x);
+ }
+ finally
+ {
+ bufferPool.release(buffer);
+ }
+ }
+
+ private void parse(ByteBuffer buffer)
+ {
+ while (buffer.hasRemaining())
+ parser.parseNext(buffer);
+ }
+
+ private void fillInterested()
+ {
+ // TODO: do we need to call fillInterested() only if we are not failed (or we have an exchange) ?
+ getHttpChannel().getHttpConnection().fillInterested();
+ }
+
+ private void shutdown()
+ {
+ // Shutting down the parser may invoke messageComplete() or earlyEOF()
+ parser.atEOF();
+ parser.parseNext(BufferUtil.EMPTY_BUFFER);
+ if (!responseFailure(new EOFException()))
+ getHttpChannel().getHttpConnection().close();
+ }
+
+ @Override
+ public int getHeaderCacheSize()
+ {
+ // TODO get from configuration
+ return 256;
+ }
+
+ @Override
+ public boolean startResponse(HttpVersion version, int status, String reason)
+ {
+ HttpExchange exchange = getHttpExchange();
+ if (exchange == null)
+ return false;
+
+ String method = exchange.getRequest().getMethod();
+ parser.setHeadResponse(HttpMethod.HEAD.is(method) || HttpMethod.CONNECT.is(method));
+ exchange.getResponse().version(version).status(status).reason(reason);
+
+ responseBegin(exchange);
+ return false;
+ }
+
+ @Override
+ public boolean parsedHeader(HttpField field)
+ {
+ HttpExchange exchange = getHttpExchange();
+ if (exchange == null)
+ return false;
+
+ responseHeader(exchange, field);
+ return false;
+ }
+
+ @Override
+ public boolean headerComplete()
+ {
+ HttpExchange exchange = getHttpExchange();
+ if (exchange == null)
+ return false;
+
+ responseHeaders(exchange);
+ return false;
+ }
+
+ @Override
+ public boolean content(ByteBuffer buffer)
+ {
+ HttpExchange exchange = getHttpExchange();
+ if (exchange == null)
+ return false;
+
+ responseContent(exchange, buffer);
+ return false;
+ }
+
+ @Override
+ public boolean messageComplete()
+ {
+ HttpExchange exchange = getHttpExchange();
+ if (exchange == null)
+ return false;
+
+ responseSuccess(exchange);
+ return true;
+ }
+
+ @Override
+ public void earlyEOF()
+ {
+ failAndClose(new EOFException());
+ }
+
+ @Override
+ public void badMessage(int status, String reason)
+ {
+ HttpExchange exchange = getHttpExchange();
+ if (exchange != null)
+ {
+ HttpResponse response = exchange.getResponse();
+ response.status(status).reason(reason);
+ failAndClose(new HttpResponseException("HTTP protocol violation: bad response", response));
+ }
+ }
+
+ @Override
+ protected void reset()
+ {
+ super.reset();
+ parser.reset();
+ }
+
+ @Override
+ protected void dispose()
+ {
+ super.dispose();
+ parser.close();
+ }
+
+ private void failAndClose(Throwable failure)
+ {
+ if (responseFailure(failure))
+ getHttpChannel().getHttpConnection().close();
+ }
+}
diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/http/HttpSenderOverHTTP.java b/jetty-client/src/main/java/org/eclipse/jetty/client/http/HttpSenderOverHTTP.java
new file mode 100644
index 0000000..c18a92a
--- /dev/null
+++ b/jetty-client/src/main/java/org/eclipse/jetty/client/http/HttpSenderOverHTTP.java
@@ -0,0 +1,235 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.client.http;
+
+import java.nio.ByteBuffer;
+
+import org.eclipse.jetty.client.HttpClient;
+import org.eclipse.jetty.client.HttpContent;
+import org.eclipse.jetty.client.HttpExchange;
+import org.eclipse.jetty.client.HttpSender;
+import org.eclipse.jetty.client.api.ContentProvider;
+import org.eclipse.jetty.client.api.Request;
+import org.eclipse.jetty.http.HttpGenerator;
+import org.eclipse.jetty.io.ByteBufferPool;
+import org.eclipse.jetty.io.EndPoint;
+import org.eclipse.jetty.util.Callback;
+
+public class HttpSenderOverHTTP extends HttpSender
+{
+ private final HttpGenerator generator = new HttpGenerator();
+
+ public HttpSenderOverHTTP(HttpChannelOverHTTP channel)
+ {
+ super(channel);
+ }
+
+ @Override
+ public HttpChannelOverHTTP getHttpChannel()
+ {
+ return (HttpChannelOverHTTP)super.getHttpChannel();
+ }
+
+ @Override
+ protected void sendHeaders(HttpExchange exchange, HttpContent content, Callback callback)
+ {
+ Request request = exchange.getRequest();
+ ContentProvider requestContent = request.getContent();
+ long contentLength = requestContent == null ? -1 : requestContent.getLength();
+ String path = request.getPath();
+ String query = request.getQuery();
+ if (query != null)
+ path += "?" + query;
+ HttpGenerator.RequestInfo requestInfo = new HttpGenerator.RequestInfo(request.getVersion(), request.getHeaders(), contentLength, request.getMethod(), path);
+
+ try
+ {
+ HttpClient client = getHttpChannel().getHttpDestination().getHttpClient();
+ ByteBufferPool bufferPool = client.getByteBufferPool();
+ ByteBuffer header = bufferPool.acquire(client.getRequestBufferSize(), false);
+ ByteBuffer chunk = null;
+
+ ByteBuffer contentBuffer = null;
+ boolean lastContent = false;
+ if (!expects100Continue(request))
+ {
+ content.advance();
+ contentBuffer = content.getByteBuffer();
+ lastContent = content.isLast();
+ }
+ while (true)
+ {
+ HttpGenerator.Result result = generator.generateRequest(requestInfo, header, chunk, contentBuffer, lastContent);
+ switch (result)
+ {
+ case NEED_CHUNK:
+ {
+ chunk = bufferPool.acquire(HttpGenerator.CHUNK_SIZE, false);
+ break;
+ }
+ case FLUSH:
+ {
+ int size = 1;
+ boolean hasChunk = chunk != null;
+ if (hasChunk)
+ ++size;
+ boolean hasContent = contentBuffer != null;
+ if (hasContent)
+ ++size;
+ ByteBuffer[] toWrite = new ByteBuffer[size];
+ ByteBuffer[] toRecycle = new ByteBuffer[hasChunk ? 2 : 1];
+ toWrite[0] = header;
+ toRecycle[0] = header;
+ if (hasChunk)
+ {
+ toWrite[1] = chunk;
+ toRecycle[1] = chunk;
+ }
+ if (hasContent)
+ toWrite[toWrite.length - 1] = contentBuffer;
+ EndPoint endPoint = getHttpChannel().getHttpConnection().getEndPoint();
+ endPoint.write(new ByteBufferRecyclerCallback(callback, bufferPool, toRecycle), toWrite);
+ return;
+ }
+ default:
+ {
+ throw new IllegalStateException();
+ }
+ }
+ }
+ }
+ catch (Exception x)
+ {
+ LOG.debug(x);
+ callback.failed(x);
+ }
+ }
+
+ @Override
+ protected void sendContent(HttpExchange exchange, HttpContent content, Callback callback)
+ {
+ try
+ {
+ HttpClient client = getHttpChannel().getHttpDestination().getHttpClient();
+ ByteBufferPool bufferPool = client.getByteBufferPool();
+ ByteBuffer chunk = null;
+ while (true)
+ {
+ ByteBuffer contentBuffer = content.getByteBuffer();
+ boolean lastContent = content.isLast();
+ HttpGenerator.Result result = generator.generateRequest(null, null, chunk, contentBuffer, lastContent);
+ switch (result)
+ {
+ case NEED_CHUNK:
+ {
+ chunk = bufferPool.acquire(HttpGenerator.CHUNK_SIZE, false);
+ break;
+ }
+ case FLUSH:
+ {
+ EndPoint endPoint = getHttpChannel().getHttpConnection().getEndPoint();
+ if (chunk != null)
+ endPoint.write(new ByteBufferRecyclerCallback(callback, bufferPool, chunk), chunk, contentBuffer);
+ else
+ endPoint.write(callback, contentBuffer);
+ return;
+ }
+ case SHUTDOWN_OUT:
+ {
+ shutdownOutput();
+ break;
+ }
+ case CONTINUE:
+ {
+ break;
+ }
+ case DONE:
+ {
+ assert generator.isEnd();
+ callback.succeeded();
+ return;
+ }
+ default:
+ {
+ throw new IllegalStateException();
+ }
+ }
+ }
+ }
+ catch (Exception x)
+ {
+ LOG.debug(x);
+ callback.failed(x);
+ }
+ }
+
+ @Override
+ protected void reset()
+ {
+ generator.reset();
+ super.reset();
+ }
+
+ @Override
+ protected RequestState dispose()
+ {
+ generator.abort();
+ RequestState result = super.dispose();
+ shutdownOutput();
+ return result;
+ }
+
+ private void shutdownOutput()
+ {
+ getHttpChannel().getHttpConnection().getEndPoint().shutdownOutput();
+ }
+
+ private class ByteBufferRecyclerCallback implements Callback
+ {
+ private final Callback callback;
+ private final ByteBufferPool pool;
+ private final ByteBuffer[] buffers;
+
+ private ByteBufferRecyclerCallback(Callback callback, ByteBufferPool pool, ByteBuffer... buffers)
+ {
+ this.callback = callback;
+ this.pool = pool;
+ this.buffers = buffers;
+ }
+
+ @Override
+ public void succeeded()
+ {
+ for (ByteBuffer buffer : buffers)
+ {
+ assert !buffer.hasRemaining();
+ pool.release(buffer);
+ }
+ callback.succeeded();
+ }
+
+ @Override
+ public void failed(Throwable x)
+ {
+ for (ByteBuffer buffer : buffers)
+ pool.release(buffer);
+ callback.failed(x);
+ }
+ }
+}
diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/util/DigestAuthentication.java b/jetty-client/src/main/java/org/eclipse/jetty/client/util/DigestAuthentication.java
index 55ac0ec..f45e4d2 100644
--- a/jetty-client/src/main/java/org/eclipse/jetty/client/util/DigestAuthentication.java
+++ b/jetty-client/src/main/java/org/eclipse/jetty/client/util/DigestAuthentication.java
@@ -221,7 +221,7 @@
String A1 = user + ":" + realm + ":" + password;
String hashA1 = toHexString(digester.digest(A1.getBytes(charset)));
- String A2 = request.method() + ":" + request.getURI();
+ String A2 = request.getMethod() + ":" + request.getURI();
if ("auth-int".equals(qop))
A2 += ":" + toHexString(digester.digest(content));
String hashA2 = toHexString(digester.digest(A2.getBytes(charset)));
diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/util/InputStreamContentProvider.java b/jetty-client/src/main/java/org/eclipse/jetty/client/util/InputStreamContentProvider.java
index eacbb09..8fdc0cc 100644
--- a/jetty-client/src/main/java/org/eclipse/jetty/client/util/InputStreamContentProvider.java
+++ b/jetty-client/src/main/java/org/eclipse/jetty/client/util/InputStreamContentProvider.java
@@ -25,6 +25,8 @@
import org.eclipse.jetty.client.api.ContentProvider;
import org.eclipse.jetty.util.BufferUtil;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
/**
* A {@link ContentProvider} for an {@link InputStream}.
@@ -44,6 +46,8 @@
*/
public class InputStreamContentProvider implements ContentProvider
{
+ private static final Logger LOG = Log.getLogger(InputStreamContentProvider.class);
+
private final InputStream stream;
private final int bufferSize;
@@ -88,63 +92,91 @@
@Override
public Iterator<ByteBuffer> iterator()
{
- return new Iterator<ByteBuffer>()
- {
- private final byte[] bytes = new byte[bufferSize];
- private Exception failure;
- private ByteBuffer buffer;
+ return new InputStreamIterator();
+ }
- @Override
- public boolean hasNext()
+ /**
+ * Iterating over an {@link InputStream} is tricky, because {@link #hasNext()} must return false
+ * if the stream reads -1. However, we don't know what to return until we read the stream, which
+ * means that stream reading must be performed by {@link #hasNext()}, which introduces a side-effect
+ * on what is supposed to be a simple query method (with respect to the Query Command Separation
+ * Principle).
+ * <p />
+ * Alternatively, we could return {@code true} from {@link #hasNext()} even if we don't know that
+ * we will read -1, but then when {@link #next()} reads -1 it must return an empty buffer.
+ * However this is problematic, since GETs with no content indication would become GET with chunked
+ * content, and not understood by servers.
+ * <p />
+ * Therefore we need to make sure that {@link #hasNext()} does not perform any side effect (so that
+ * it can be called multiple times) until {@link #next()} is called.
+ */
+ private class InputStreamIterator implements Iterator<ByteBuffer>
+ {
+ private final byte[] bytes = new byte[bufferSize];
+ private Exception failure;
+ private ByteBuffer buffer;
+ private Boolean hasNext;
+
+ @Override
+ public boolean hasNext()
+ {
+ try
{
- try
+ if (hasNext != null)
+ return hasNext;
+
+ int read = stream.read(bytes);
+ LOG.debug("Read {} bytes from {}", read, stream);
+ if (read > 0)
{
- int read = stream.read(bytes);
- if (read > 0)
- {
- buffer = onRead(bytes, 0, read);
- return true;
- }
- else if (read < 0)
- {
- return false;
- }
- else
- {
- buffer = BufferUtil.EMPTY_BUFFER;
- return true;
- }
+ buffer = onRead(bytes, 0, read);
+ hasNext = Boolean.TRUE;
+ return true;
}
- catch (Exception x)
+ else if (read < 0)
{
- if (failure == null)
- {
- failure = x;
- // Signal we have more content to cause a call to
- // next() which will throw NoSuchElementException.
- return true;
- }
+ hasNext = Boolean.FALSE;
return false;
}
+ else
+ {
+ buffer = BufferUtil.EMPTY_BUFFER;
+ hasNext = Boolean.TRUE;
+ return true;
+ }
}
-
- @Override
- public ByteBuffer next()
+ catch (Exception x)
{
- ByteBuffer result = buffer;
- buffer = null;
- if (failure != null)
- throw (NoSuchElementException)new NoSuchElementException().initCause(failure);
- if (result == null)
- throw new NoSuchElementException();
- return result;
+ LOG.debug(x);
+ if (failure == null)
+ {
+ failure = x;
+ // Signal we have more content to cause a call to
+ // next() which will throw NoSuchElementException.
+ hasNext = Boolean.TRUE;
+ return true;
+ }
+ throw new IllegalStateException();
}
+ }
- @Override
- public void remove()
- {
- throw new UnsupportedOperationException();
- }
- };
+ @Override
+ public ByteBuffer next()
+ {
+ if (failure != null)
+ throw (NoSuchElementException)new NoSuchElementException().initCause(failure);
+ ByteBuffer result = buffer;
+ if (result == null)
+ throw new NoSuchElementException();
+ buffer = null;
+ hasNext = null;
+ return result;
+ }
+
+ @Override
+ public void remove()
+ {
+ throw new UnsupportedOperationException();
+ }
}
}
diff --git a/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientAuthenticationTest.java b/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientAuthenticationTest.java
index 23b6926..0c3eafe 100644
--- a/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientAuthenticationTest.java
+++ b/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientAuthenticationTest.java
@@ -81,7 +81,7 @@
Constraint constraint = new Constraint();
constraint.setAuthenticate(true);
- constraint.setRoles(new String[]{"*"});
+ constraint.setRoles(new String[]{"**"}); //allow any authenticated user
ConstraintMapping mapping = new ConstraintMapping();
mapping.setPathSpec("/secure");
mapping.setConstraint(constraint);
@@ -89,7 +89,6 @@
securityHandler.addConstraintMapping(mapping);
securityHandler.setAuthenticator(authenticator);
securityHandler.setLoginService(loginService);
- securityHandler.setStrict(false);
securityHandler.setHandler(handler);
start(securityHandler);
diff --git a/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientContinueTest.java b/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientContinueTest.java
index e281ac4..fd08f6f 100644
--- a/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientContinueTest.java
+++ b/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientContinueTest.java
@@ -564,40 +564,7 @@
});
final byte[] data = new byte[]{0, 1, 2, 3, 4, 5, 6, 7};
- final DeferredContentProvider content = new DeferredContentProvider()
- {
- @Override
- public Iterator<ByteBuffer> iterator()
- {
- final Iterator<ByteBuffer> delegate = super.iterator();
- return new Iterator<ByteBuffer>()
- {
- private int count;
-
- @Override
- public boolean hasNext()
- {
- return delegate.hasNext();
- }
-
- @Override
- public ByteBuffer next()
- {
- // Fake that it returns null for two times,
- // to trigger particular branches in HttpSender
- if (++count <= 2)
- return null;
- return delegate.next();
- }
-
- @Override
- public void remove()
- {
- delegate.remove();
- }
- };
- }
- };
+ final DeferredContentProvider content = new DeferredContentProvider();
final CountDownLatch latch = new CountDownLatch(1);
client.newRequest("localhost", connector.getLocalPort())
diff --git a/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientExplicitConnectionTest.java b/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientExplicitConnectionTest.java
index c49b37b..6b7acc6 100644
--- a/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientExplicitConnectionTest.java
+++ b/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientExplicitConnectionTest.java
@@ -24,6 +24,9 @@
import org.eclipse.jetty.client.api.ContentResponse;
import org.eclipse.jetty.client.api.Destination;
import org.eclipse.jetty.client.api.Request;
+import org.eclipse.jetty.client.http.HttpConnectionOverHTTP;
+import org.eclipse.jetty.client.http.HttpConnectionPool;
+import org.eclipse.jetty.client.http.HttpDestinationOverHTTP;
import org.eclipse.jetty.client.util.FutureResponseListener;
import org.eclipse.jetty.toolchain.test.annotation.Slow;
import org.eclipse.jetty.util.FuturePromise;
@@ -56,9 +59,10 @@
Assert.assertNotNull(response);
Assert.assertEquals(200, response.getStatus());
- HttpDestination httpDestination = (HttpDestination)destination;
- Assert.assertTrue(httpDestination.getActiveConnections().isEmpty());
- Assert.assertTrue(httpDestination.getIdleConnections().isEmpty());
+ HttpDestinationOverHTTP httpDestination = (HttpDestinationOverHTTP)destination;
+ HttpConnectionPool connectionPool = httpDestination.getHttpConnectionPool();
+ Assert.assertTrue(connectionPool.getActiveConnections().isEmpty());
+ Assert.assertTrue(connectionPool.getIdleConnections().isEmpty());
}
}
@@ -84,11 +88,12 @@
// Give the connection some time to process the remote close
TimeUnit.SECONDS.sleep(1);
- HttpConnection httpConnection = (HttpConnection)connection;
+ HttpConnectionOverHTTP httpConnection = (HttpConnectionOverHTTP)connection;
Assert.assertFalse(httpConnection.getEndPoint().isOpen());
- HttpDestination httpDestination = (HttpDestination)destination;
- Assert.assertTrue(httpDestination.getActiveConnections().isEmpty());
- Assert.assertTrue(httpDestination.getIdleConnections().isEmpty());
+ HttpDestinationOverHTTP httpDestination = (HttpDestinationOverHTTP)destination;
+ HttpConnectionPool connectionPool = httpDestination.getHttpConnectionPool();
+ Assert.assertTrue(connectionPool.getActiveConnections().isEmpty());
+ Assert.assertTrue(connectionPool.getIdleConnections().isEmpty());
}
}
diff --git a/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientLoadTest.java b/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientLoadTest.java
index 601fd1c..1f76548 100644
--- a/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientLoadTest.java
+++ b/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientLoadTest.java
@@ -19,7 +19,6 @@
package org.eclipse.jetty.client;
import java.io.IOException;
-import java.net.URI;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Arrays;
@@ -37,6 +36,9 @@
import org.eclipse.jetty.client.api.Request;
import org.eclipse.jetty.client.api.Response;
import org.eclipse.jetty.client.api.Result;
+import org.eclipse.jetty.client.http.HttpConnectionOverHTTP;
+import org.eclipse.jetty.client.http.HttpConnectionPool;
+import org.eclipse.jetty.client.http.HttpDestinationOverHTTP;
import org.eclipse.jetty.client.util.BytesContentProvider;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpMethod;
@@ -48,6 +50,7 @@
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;
import org.eclipse.jetty.util.ssl.SslContextFactory;
+import org.eclipse.jetty.util.thread.Scheduler;
import org.junit.Assert;
import org.junit.Test;
@@ -70,9 +73,27 @@
client.setMaxConnectionsPerDestination(32768);
client.setMaxRequestsQueuedPerDestination(1024 * 1024);
client.setDispatchIO(false);
+ client.setStrictEventOrdering(false);
Random random = new Random();
+ // At least 25k requests to warmup properly (use -XX:+PrintCompilation to verify JIT activity)
+ int runs = 1;
int iterations = 500;
+ for (int i = 0; i < runs; ++i)
+ {
+ run(random, iterations);
+ }
+
+ // Re-run after warmup
+ iterations = 50_000;
+ for (int i = 0; i < runs; ++i)
+ {
+ run(random, iterations);
+ }
+ }
+
+ private void run(Random random, int iterations) throws InterruptedException
+ {
CountDownLatch latch = new CountDownLatch(iterations);
List<String> failures = new ArrayList<>();
@@ -81,7 +102,7 @@
// Dumps the state of the client if the test takes too long
final Thread testThread = Thread.currentThread();
- client.getScheduler().schedule(new Runnable()
+ Scheduler.Task task = client.getScheduler().schedule(new Runnable()
{
@Override
public void run()
@@ -89,11 +110,12 @@
logger.warn("Interrupting test, it is taking too long");
for (String host : Arrays.asList("localhost", "127.0.0.1"))
{
- HttpDestination destination = (HttpDestination)client.getDestination(scheme, host, connector.getLocalPort());
- for (Connection connection : new ArrayList<>(destination.getActiveConnections()))
+ HttpDestinationOverHTTP destination = (HttpDestinationOverHTTP)client.getDestination(scheme, host, connector.getLocalPort());
+ HttpConnectionPool connectionPool = destination.getHttpConnectionPool();
+ for (Connection connection : new ArrayList<>(connectionPool.getActiveConnections()))
{
- HttpConnection active = (HttpConnection)connection;
- logger.warn(active.getEndPoint() + " exchange " + active.getExchange());
+ HttpConnectionOverHTTP active = (HttpConnectionOverHTTP)connection;
+ logger.warn(active.getEndPoint() + " exchange " + active.getHttpChannel().getHttpExchange());
}
}
testThread.interrupt();
@@ -104,9 +126,11 @@
for (int i = 0; i < iterations; ++i)
{
test(random, latch, failures);
+// test("http", "localhost", "GET", false, false, 64 * 1024, false, latch, failures);
}
Assert.assertTrue(latch.await(iterations, TimeUnit.SECONDS));
long end = System.nanoTime();
+ task.cancel();
long elapsed = TimeUnit.NANOSECONDS.toMillis(end - begin);
logger.info("{} requests in {} ms, {} req/s", iterations, elapsed, elapsed > 0 ? iterations * 1000 / elapsed : -1);
@@ -118,34 +142,44 @@
private void test(Random random, final CountDownLatch latch, final List<String> failures) throws InterruptedException
{
- int maxContentLength = 64 * 1024;
-
// Choose a random destination
String host = random.nextBoolean() ? "localhost" : "127.0.0.1";
- URI uri = URI.create(scheme + "://" + host + ":" + connector.getLocalPort());
- Request request = client.newRequest(uri);
-
// Choose a random method
HttpMethod method = random.nextBoolean() ? HttpMethod.GET : HttpMethod.POST;
- request.method(method);
boolean ssl = HttpScheme.HTTPS.is(scheme);
// Choose randomly whether to close the connection on the client or on the server
+ boolean clientClose = false;
if (!ssl && random.nextBoolean())
+ clientClose = true;
+ boolean serverClose = false;
+ if (!ssl && random.nextBoolean())
+ serverClose = true;
+
+ int maxContentLength = 64 * 1024;
+ int contentLength = random.nextInt(maxContentLength) + 1;
+
+ test(scheme, host, method.asString(), clientClose, serverClose, contentLength, true, latch, failures);
+ }
+
+ private void test(String scheme, String host, String method, boolean clientClose, boolean serverClose, int contentLength, final boolean checkContentLength, final CountDownLatch latch, final List<String> failures) throws InterruptedException
+ {
+ Request request = client.newRequest(host, connector.getLocalPort())
+ .scheme(scheme)
+ .method(method);
+
+ if (clientClose)
request.header(HttpHeader.CONNECTION, "close");
- else if (!ssl && random.nextBoolean())
+ else if (serverClose)
request.header("X-Close", "true");
- int contentLength = random.nextInt(maxContentLength) + 1;
switch (method)
{
- case GET:
- // Randomly ask the server to download data upon this GET request
- if (random.nextBoolean())
- request.header("X-Download", String.valueOf(contentLength));
+ case "GET":
+ request.header("X-Download", String.valueOf(contentLength));
break;
- case POST:
+ case "POST":
request.header("X-Upload", String.valueOf(contentLength));
request.content(new BytesContentProvider(new byte[contentLength]));
break;
@@ -159,15 +193,19 @@
@Override
public void onHeaders(Response response)
{
- String content = response.getHeaders().get("X-Content");
- if (content != null)
- contentLength.set(Integer.parseInt(content));
+ if (checkContentLength)
+ {
+ String content = response.getHeaders().get("X-Content");
+ if (content != null)
+ contentLength.set(Integer.parseInt(content));
+ }
}
@Override
public void onContent(Response response, ByteBuffer content)
{
- contentLength.addAndGet(-content.remaining());
+ if (checkContentLength)
+ contentLength.addAndGet(-content.remaining());
}
@Override
@@ -178,8 +216,10 @@
result.getFailure().printStackTrace();
failures.add("Result failed " + result);
}
- if (contentLength.get() != 0)
+
+ if (checkContentLength && contentLength.get() != 0)
failures.add("Content length mismatch " + contentLength);
+
requestLatch.countDown();
latch.countDown();
}
diff --git a/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientTest.java b/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientTest.java
index aa7db6b..4e1bc24 100644
--- a/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientTest.java
+++ b/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientTest.java
@@ -50,6 +50,9 @@
import org.eclipse.jetty.client.api.Request;
import org.eclipse.jetty.client.api.Response;
import org.eclipse.jetty.client.api.Result;
+import org.eclipse.jetty.client.http.HttpConnectionOverHTTP;
+import org.eclipse.jetty.client.http.HttpConnectionPool;
+import org.eclipse.jetty.client.http.HttpDestinationOverHTTP;
import org.eclipse.jetty.client.util.BytesContentProvider;
import org.eclipse.jetty.http.HttpField;
import org.eclipse.jetty.http.HttpMethod;
@@ -81,13 +84,14 @@
Response response = client.GET(scheme + "://" + host + ":" + port + path);
Assert.assertEquals(200, response.getStatus());
- HttpDestination destination = (HttpDestination)client.getDestination(scheme, host, port);
+ HttpDestinationOverHTTP destination = (HttpDestinationOverHTTP)client.getDestination(scheme, host, port);
+ HttpConnectionPool connectionPool = destination.getHttpConnectionPool();
long start = System.nanoTime();
- HttpConnection connection = null;
+ HttpConnectionOverHTTP connection = null;
while (connection == null && TimeUnit.NANOSECONDS.toSeconds(System.nanoTime() - start) < 5)
{
- connection = (HttpConnection)destination.getIdleConnections().peek();
+ connection = (HttpConnectionOverHTTP)connectionPool.getIdleConnections().peek();
TimeUnit.MILLISECONDS.sleep(10);
}
Assert.assertNotNull(connection);
@@ -98,8 +102,8 @@
client.stop();
Assert.assertEquals(0, client.getDestinations().size());
- Assert.assertEquals(0, destination.getIdleConnections().size());
- Assert.assertEquals(0, destination.getActiveConnections().size());
+ Assert.assertEquals(0, connectionPool.getIdleConnections().size());
+ Assert.assertEquals(0, connectionPool.getActiveConnections().size());
Assert.assertFalse(connection.getEndPoint().isOpen());
}
@@ -632,8 +636,8 @@
@Override
public void onBegin(Request request)
{
- HttpDestination destination = (HttpDestination)client.getDestination(scheme, host, port);
- destination.getActiveConnections().peek().close();
+ HttpDestinationOverHTTP destination = (HttpDestinationOverHTTP)client.getDestination(scheme, host, port);
+ destination.getHttpConnectionPool().getActiveConnections().peek().close();
}
})
.send(new Response.Listener.Empty()
diff --git a/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientTimeoutTest.java b/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientTimeoutTest.java
index 3297b80..ccb4842 100644
--- a/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientTimeoutTest.java
+++ b/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientTimeoutTest.java
@@ -34,6 +34,7 @@
import org.eclipse.jetty.client.api.Request;
import org.eclipse.jetty.client.api.Response;
import org.eclipse.jetty.client.api.Result;
+import org.eclipse.jetty.client.http.HttpClientTransportOverHTTP;
import org.eclipse.jetty.client.util.BufferingResponseListener;
import org.eclipse.jetty.client.util.InputStreamContentProvider;
import org.eclipse.jetty.io.EndPoint;
@@ -240,7 +241,7 @@
start(new TimeoutHandler(2 * timeout));
client.stop();
final AtomicBoolean sslIdle = new AtomicBoolean();
- client = new HttpClient(sslContextFactory)
+ client = new HttpClient(new HttpClientTransportOverHTTP()
{
@Override
protected SslConnection newSslConnection(HttpClient httpClient, EndPoint endPoint, SSLEngine engine)
@@ -255,7 +256,7 @@
}
};
}
- };
+ }, sslContextFactory);
client.setIdleTimeout(timeout);
client.start();
diff --git a/jetty-client/src/test/java/org/eclipse/jetty/client/HttpConnectionLifecycleTest.java b/jetty-client/src/test/java/org/eclipse/jetty/client/HttpConnectionLifecycleTest.java
index 4d2aa2f..0f8afe6 100644
--- a/jetty-client/src/test/java/org/eclipse/jetty/client/HttpConnectionLifecycleTest.java
+++ b/jetty-client/src/test/java/org/eclipse/jetty/client/HttpConnectionLifecycleTest.java
@@ -33,8 +33,11 @@
import org.eclipse.jetty.client.api.Request;
import org.eclipse.jetty.client.api.Response;
import org.eclipse.jetty.client.api.Result;
+import org.eclipse.jetty.client.http.HttpConnectionPool;
+import org.eclipse.jetty.client.http.HttpDestinationOverHTTP;
import org.eclipse.jetty.client.util.ByteBufferContentProvider;
import org.eclipse.jetty.http.HttpHeader;
+import org.eclipse.jetty.server.Handler;
import org.eclipse.jetty.server.handler.AbstractHandler;
import org.eclipse.jetty.toolchain.test.annotation.Slow;
import org.eclipse.jetty.util.log.Log;
@@ -50,6 +53,13 @@
super(sslContextFactory);
}
+ @Override
+ public void start(Handler handler) throws Exception
+ {
+ super.start(handler);
+ client.setStrictEventOrdering(false);
+ }
+
@Test
public void test_SuccessfulRequest_ReturnsConnection() throws Exception
{
@@ -57,12 +67,13 @@
String host = "localhost";
int port = connector.getLocalPort();
- HttpDestination destination = (HttpDestination)client.getDestination(scheme, host, port);
+ HttpDestinationOverHTTP destination = (HttpDestinationOverHTTP)client.getDestination(scheme, host, port);
+ HttpConnectionPool connectionPool = destination.getHttpConnectionPool();
- final BlockingQueue<Connection> idleConnections = destination.getIdleConnections();
+ final BlockingQueue<Connection> idleConnections = connectionPool.getIdleConnections();
Assert.assertEquals(0, idleConnections.size());
- final BlockingQueue<Connection> activeConnections = destination.getActiveConnections();
+ final BlockingQueue<Connection> activeConnections = connectionPool.getActiveConnections();
Assert.assertEquals(0, activeConnections.size());
final CountDownLatch headersLatch = new CountDownLatch(1);
@@ -117,12 +128,13 @@
String host = "localhost";
int port = connector.getLocalPort();
- HttpDestination destination = (HttpDestination)client.getDestination(scheme, host, port);
+ HttpDestinationOverHTTP destination = (HttpDestinationOverHTTP)client.getDestination(scheme, host, port);
+ HttpConnectionPool connectionPool = destination.getHttpConnectionPool();
- final BlockingQueue<Connection> idleConnections = destination.getIdleConnections();
+ final BlockingQueue<Connection> idleConnections = connectionPool.getIdleConnections();
Assert.assertEquals(0, idleConnections.size());
- final BlockingQueue<Connection> activeConnections = destination.getActiveConnections();
+ final BlockingQueue<Connection> activeConnections = connectionPool.getActiveConnections();
Assert.assertEquals(0, activeConnections.size());
final CountDownLatch beginLatch = new CountDownLatch(1);
@@ -167,12 +179,13 @@
String host = "localhost";
int port = connector.getLocalPort();
- HttpDestination destination = (HttpDestination)client.getDestination(scheme, host, port);
+ HttpDestinationOverHTTP destination = (HttpDestinationOverHTTP)client.getDestination(scheme, host, port);
+ HttpConnectionPool connectionPool = destination.getHttpConnectionPool();
- final BlockingQueue<Connection> idleConnections = destination.getIdleConnections();
+ final BlockingQueue<Connection> idleConnections = connectionPool.getIdleConnections();
Assert.assertEquals(0, idleConnections.size());
- final BlockingQueue<Connection> activeConnections = destination.getActiveConnections();
+ final BlockingQueue<Connection> activeConnections = connectionPool.getActiveConnections();
Assert.assertEquals(0, activeConnections.size());
final CountDownLatch successLatch = new CountDownLatch(3);
@@ -226,12 +239,13 @@
String host = "localhost";
int port = connector.getLocalPort();
- HttpDestination destination = (HttpDestination)client.getDestination(scheme, host, port);
+ HttpDestinationOverHTTP destination = (HttpDestinationOverHTTP)client.getDestination(scheme, host, port);
+ HttpConnectionPool connectionPool = destination.getHttpConnectionPool();
- final BlockingQueue<Connection> idleConnections = destination.getIdleConnections();
+ final BlockingQueue<Connection> idleConnections = connectionPool.getIdleConnections();
Assert.assertEquals(0, idleConnections.size());
- final BlockingQueue<Connection> activeConnections = destination.getActiveConnections();
+ final BlockingQueue<Connection> activeConnections = connectionPool.getActiveConnections();
Assert.assertEquals(0, activeConnections.size());
final long delay = 1000;
@@ -298,12 +312,13 @@
String host = "localhost";
int port = connector.getLocalPort();
- HttpDestination destination = (HttpDestination)client.getDestination(scheme, host, port);
+ HttpDestinationOverHTTP destination = (HttpDestinationOverHTTP)client.getDestination(scheme, host, port);
+ HttpConnectionPool connectionPool = destination.getHttpConnectionPool();
- final BlockingQueue<Connection> idleConnections = destination.getIdleConnections();
+ final BlockingQueue<Connection> idleConnections = connectionPool.getIdleConnections();
Assert.assertEquals(0, idleConnections.size());
- final BlockingQueue<Connection> activeConnections = destination.getActiveConnections();
+ final BlockingQueue<Connection> activeConnections = connectionPool.getActiveConnections();
Assert.assertEquals(0, activeConnections.size());
server.stop();
@@ -350,12 +365,13 @@
String host = "localhost";
int port = connector.getLocalPort();
- HttpDestination destination = (HttpDestination)client.getDestination(scheme, host, port);
+ HttpDestinationOverHTTP destination = (HttpDestinationOverHTTP)client.getDestination(scheme, host, port);
+ HttpConnectionPool connectionPool = destination.getHttpConnectionPool();
- final BlockingQueue<Connection> idleConnections = destination.getIdleConnections();
+ final BlockingQueue<Connection> idleConnections = connectionPool.getIdleConnections();
Assert.assertEquals(0, idleConnections.size());
- final BlockingQueue<Connection> activeConnections = destination.getActiveConnections();
+ final BlockingQueue<Connection> activeConnections = connectionPool.getActiveConnections();
Assert.assertEquals(0, activeConnections.size());
final CountDownLatch latch = new CountDownLatch(1);
@@ -399,12 +415,13 @@
String host = "localhost";
int port = connector.getLocalPort();
- HttpDestination destination = (HttpDestination)client.getDestination(scheme, host, port);
+ HttpDestinationOverHTTP destination = (HttpDestinationOverHTTP)client.getDestination(scheme, host, port);
+ HttpConnectionPool connectionPool = destination.getHttpConnectionPool();
- final BlockingQueue<Connection> idleConnections = destination.getIdleConnections();
+ final BlockingQueue<Connection> idleConnections = connectionPool.getIdleConnections();
Assert.assertEquals(0, idleConnections.size());
- final BlockingQueue<Connection> activeConnections = destination.getActiveConnections();
+ final BlockingQueue<Connection> activeConnections = connectionPool.getActiveConnections();
Assert.assertEquals(0, activeConnections.size());
Log.getLogger(HttpConnection.class).info("Expecting java.lang.IllegalStateException: HttpParser{s=CLOSED,...");
@@ -420,6 +437,7 @@
@Override
public void onComplete(Result result)
{
+ Assert.assertEquals(1, latch.getCount());
Assert.assertEquals(0, idleConnections.size());
Assert.assertEquals(0, activeConnections.size());
latch.countDown();
@@ -430,6 +448,7 @@
Assert.assertEquals(0, idleConnections.size());
Assert.assertEquals(0, activeConnections.size());
+
server.stop();
}
finally
@@ -446,12 +465,13 @@
String host = "localhost";
int port = connector.getLocalPort();
- HttpDestination destination = (HttpDestination)client.getDestination(scheme, host, port);
+ HttpDestinationOverHTTP destination = (HttpDestinationOverHTTP)client.getDestination(scheme, host, port);
+ HttpConnectionPool connectionPool = destination.getHttpConnectionPool();
- final BlockingQueue<Connection> idleConnections = destination.getIdleConnections();
+ final BlockingQueue<Connection> idleConnections = connectionPool.getIdleConnections();
Assert.assertEquals(0, idleConnections.size());
- final BlockingQueue<Connection> activeConnections = destination.getActiveConnections();
+ final BlockingQueue<Connection> activeConnections = connectionPool.getActiveConnections();
Assert.assertEquals(0, activeConnections.size());
ContentResponse response = client.newRequest(host, port)
diff --git a/jetty-client/src/test/java/org/eclipse/jetty/client/HttpDestinationTest.java b/jetty-client/src/test/java/org/eclipse/jetty/client/HttpDestinationTest.java
deleted file mode 100644
index 3be2eae..0000000
--- a/jetty-client/src/test/java/org/eclipse/jetty/client/HttpDestinationTest.java
+++ /dev/null
@@ -1,216 +0,0 @@
-//
-// ========================================================================
-// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
-// ------------------------------------------------------------------------
-// All rights reserved. This program and the accompanying materials
-// are made available under the terms of the Eclipse Public License v1.0
-// and Apache License v2.0 which accompanies this distribution.
-//
-// The Eclipse Public License is available at
-// http://www.eclipse.org/legal/epl-v10.html
-//
-// The Apache License v2.0 is available at
-// http://www.opensource.org/licenses/apache2.0.php
-//
-// You may elect to redistribute this code under either of these licenses.
-// ========================================================================
-//
-
-package org.eclipse.jetty.client;
-
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.RejectedExecutionException;
-import java.util.concurrent.TimeUnit;
-
-import org.eclipse.jetty.client.api.Connection;
-import org.eclipse.jetty.client.api.ContentResponse;
-import org.eclipse.jetty.client.api.Request;
-import org.eclipse.jetty.client.api.Response;
-import org.eclipse.jetty.client.api.Result;
-import org.eclipse.jetty.toolchain.test.annotation.Slow;
-import org.eclipse.jetty.util.ssl.SslContextFactory;
-import org.hamcrest.Matchers;
-import org.junit.Assert;
-import org.junit.Before;
-import org.junit.Test;
-
-public class HttpDestinationTest extends AbstractHttpClientServerTest
-{
- public HttpDestinationTest(SslContextFactory sslContextFactory)
- {
- super(sslContextFactory);
- }
-
- @Before
- public void init() throws Exception
- {
- start(new EmptyServerHandler());
- }
-
- @Test
- public void test_FirstAcquire_WithEmptyQueue() throws Exception
- {
- HttpDestination destination = new HttpDestination(client, "http", "localhost", connector.getLocalPort());
- Connection connection = destination.acquire();
- if (connection == null)
- {
- // There are no queued requests, so the newly created connection will be idle
- connection = destination.getIdleConnections().poll(5, TimeUnit.SECONDS);
- }
- Assert.assertNotNull(connection);
- }
-
- @Test
- public void test_SecondAcquire_AfterFirstAcquire_WithEmptyQueue_ReturnsSameConnection() throws Exception
- {
- HttpDestination destination = new HttpDestination(client, "http", "localhost", connector.getLocalPort());
- Connection connection1 = destination.acquire();
- if (connection1 == null)
- {
- // There are no queued requests, so the newly created connection will be idle
- long start = System.nanoTime();
- while (connection1 == null && TimeUnit.NANOSECONDS.toSeconds(System.nanoTime() - start) < 5)
- {
- connection1 = destination.getIdleConnections().peek();
- TimeUnit.MILLISECONDS.sleep(50);
- }
- Assert.assertNotNull(connection1);
-
- Connection connection2 = destination.acquire();
- Assert.assertSame(connection1, connection2);
- }
- }
-
- @Test
- public void test_SecondAcquire_ConcurrentWithFirstAcquire_WithEmptyQueue_CreatesTwoConnections() throws Exception
- {
- final CountDownLatch latch = new CountDownLatch(1);
- HttpDestination destination = new HttpDestination(client, "http", "localhost", connector.getLocalPort())
- {
- @Override
- protected void process(Connection connection, boolean dispatch)
- {
- try
- {
- latch.await(5, TimeUnit.SECONDS);
- super.process(connection, dispatch);
- }
- catch (InterruptedException x)
- {
- x.printStackTrace();
- }
- }
- };
- Connection connection1 = destination.acquire();
-
- // There are no available existing connections, so acquire()
- // returns null because we delayed process() above
- Assert.assertNull(connection1);
-
- Connection connection2 = destination.acquire();
- Assert.assertNull(connection2);
-
- latch.countDown();
-
- // There must be 2 idle connections
- Connection connection = destination.getIdleConnections().poll(5, TimeUnit.SECONDS);
- Assert.assertNotNull(connection);
- connection = destination.getIdleConnections().poll(5, TimeUnit.SECONDS);
- Assert.assertNotNull(connection);
- }
-
- @Test
- public void test_Acquire_Process_Release_Acquire_ReturnsSameConnection() throws Exception
- {
- HttpDestination destination = new HttpDestination(client, "http", "localhost", connector.getLocalPort());
- Connection connection1 = destination.acquire();
- if (connection1 == null)
- connection1 = destination.getIdleConnections().poll(5, TimeUnit.SECONDS);
- Assert.assertNotNull(connection1);
-
- destination.process(connection1, false);
- destination.release(connection1);
-
- Connection connection2 = destination.acquire();
- Assert.assertSame(connection1, connection2);
- }
-
- @Slow
- @Test
- public void test_IdleConnection_IdleTimeout() throws Exception
- {
- long idleTimeout = 1000;
- client.setIdleTimeout(idleTimeout);
-
- HttpDestination destination = new HttpDestination(client, "http", "localhost", connector.getLocalPort());
- Connection connection1 = destination.acquire();
- if (connection1 == null)
- {
- // There are no queued requests, so the newly created connection will be idle
- long start = System.nanoTime();
- while (connection1 == null && TimeUnit.NANOSECONDS.toSeconds(System.nanoTime() - start) < 5)
- {
- connection1 = destination.getIdleConnections().peek();
- TimeUnit.MILLISECONDS.sleep(50);
- }
- Assert.assertNotNull(connection1);
-
- TimeUnit.MILLISECONDS.sleep(2 * idleTimeout);
-
- connection1 = destination.getIdleConnections().poll();
- Assert.assertNull(connection1);
- }
- }
-
- @Test
- public void test_Request_Failed_If_MaxRequestsQueuedPerDestination_Exceeded() throws Exception
- {
- int maxQueued = 1;
- client.setMaxRequestsQueuedPerDestination(maxQueued);
- client.setMaxConnectionsPerDestination(1);
-
- // Make one request to open the connection and be sure everything is setup properly
- ContentResponse response = client.newRequest("localhost", connector.getLocalPort())
- .scheme(scheme)
- .send();
- Assert.assertEquals(200, response.getStatus());
-
- // Send another request that is sent immediately
- final CountDownLatch successLatch = new CountDownLatch(1);
- final CountDownLatch failureLatch = new CountDownLatch(1);
- client.newRequest("localhost", connector.getLocalPort())
- .scheme(scheme)
- .onRequestQueued(new Request.QueuedListener()
- {
- @Override
- public void onQueued(Request request)
- {
- // This request exceeds the maximum queued, should fail
- client.newRequest("localhost", connector.getLocalPort())
- .scheme(scheme)
- .send(new Response.CompleteListener()
- {
- @Override
- public void onComplete(Result result)
- {
- Assert.assertTrue(result.isFailed());
- Assert.assertThat(result.getRequestFailure(), Matchers.instanceOf(RejectedExecutionException.class));
- failureLatch.countDown();
- }
- });
- }
- })
- .send(new Response.CompleteListener()
- {
- @Override
- public void onComplete(Result result)
- {
- if (result.isSucceeded())
- successLatch.countDown();
- }
- });
-
- Assert.assertTrue(failureLatch.await(5, TimeUnit.SECONDS));
- Assert.assertTrue(successLatch.await(5, TimeUnit.SECONDS));
- }
-}
diff --git a/jetty-client/src/test/java/org/eclipse/jetty/client/HttpReceiverTest.java b/jetty-client/src/test/java/org/eclipse/jetty/client/HttpReceiverTest.java
index ad78b1e..d0cd047 100644
--- a/jetty-client/src/test/java/org/eclipse/jetty/client/HttpReceiverTest.java
+++ b/jetty-client/src/test/java/org/eclipse/jetty/client/HttpReceiverTest.java
@@ -18,227 +18,203 @@
package org.eclipse.jetty.client;
-import java.io.ByteArrayOutputStream;
-import java.io.EOFException;
-import java.net.URI;
-import java.nio.ByteBuffer;
-import java.util.Collections;
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.TimeoutException;
-import java.util.zip.GZIPOutputStream;
-
-import org.eclipse.jetty.client.api.ContentResponse;
-import org.eclipse.jetty.client.api.Response;
-import org.eclipse.jetty.client.util.FutureResponseListener;
-import org.eclipse.jetty.http.HttpFields;
-import org.eclipse.jetty.http.HttpHeader;
-import org.eclipse.jetty.http.HttpVersion;
-import org.eclipse.jetty.io.ByteArrayEndPoint;
-import org.eclipse.jetty.toolchain.test.TestTracker;
-import org.junit.After;
-import org.junit.Assert;
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-
public class HttpReceiverTest
{
- @Rule
- public final TestTracker tracker = new TestTracker();
-
- private HttpClient client;
- private HttpDestination destination;
- private ByteArrayEndPoint endPoint;
- private HttpConnection connection;
- private HttpConversation conversation;
-
- @Before
- public void init() throws Exception
- {
- client = new HttpClient();
- client.start();
- destination = new HttpDestination(client, "http", "localhost", 8080);
- endPoint = new ByteArrayEndPoint();
- connection = new HttpConnection(client, endPoint, destination);
- conversation = new HttpConversation(client, 1);
- }
-
- @After
- public void destroy() throws Exception
- {
- client.stop();
- }
-
- protected HttpExchange newExchange()
- {
- HttpRequest request = new HttpRequest(client, URI.create("http://localhost"));
- FutureResponseListener listener = new FutureResponseListener(request);
- HttpExchange exchange = new HttpExchange(conversation, destination, request, Collections.<Response.ResponseListener>singletonList(listener));
- conversation.getExchanges().offer(exchange);
- connection.associate(exchange);
- exchange.requestComplete(null);
- exchange.terminateRequest();
- return exchange;
- }
-
- @Test
- public void test_Receive_NoResponseContent() throws Exception
- {
- endPoint.setInput("" +
- "HTTP/1.1 200 OK\r\n" +
- "Content-length: 0\r\n" +
- "\r\n");
- HttpExchange exchange = newExchange();
- FutureResponseListener listener = (FutureResponseListener)exchange.getResponseListeners().get(0);
- connection.receive();
-
- Response response = listener.get(5, TimeUnit.SECONDS);
- Assert.assertNotNull(response);
- Assert.assertEquals(200, response.getStatus());
- Assert.assertEquals("OK", response.getReason());
- Assert.assertSame(HttpVersion.HTTP_1_1, response.getVersion());
- HttpFields headers = response.getHeaders();
- Assert.assertNotNull(headers);
- Assert.assertEquals(1, headers.size());
- Assert.assertEquals("0", headers.get(HttpHeader.CONTENT_LENGTH));
- }
-
- @Test
- public void test_Receive_ResponseContent() throws Exception
- {
- String content = "0123456789ABCDEF";
- endPoint.setInput("" +
- "HTTP/1.1 200 OK\r\n" +
- "Content-length: " + content.length() + "\r\n" +
- "\r\n" +
- content);
- HttpExchange exchange = newExchange();
- FutureResponseListener listener = (FutureResponseListener)exchange.getResponseListeners().get(0);
- connection.receive();
-
- Response response = listener.get(5, TimeUnit.SECONDS);
- Assert.assertNotNull(response);
- Assert.assertEquals(200, response.getStatus());
- Assert.assertEquals("OK", response.getReason());
- Assert.assertSame(HttpVersion.HTTP_1_1, response.getVersion());
- HttpFields headers = response.getHeaders();
- Assert.assertNotNull(headers);
- Assert.assertEquals(1, headers.size());
- Assert.assertEquals(String.valueOf(content.length()), headers.get(HttpHeader.CONTENT_LENGTH));
- String received = listener.getContentAsString("UTF-8");
- Assert.assertEquals(content, received);
- }
-
- @Test
- public void test_Receive_ResponseContent_EarlyEOF() throws Exception
- {
- String content1 = "0123456789";
- String content2 = "ABCDEF";
- endPoint.setInput("" +
- "HTTP/1.1 200 OK\r\n" +
- "Content-length: " + (content1.length() + content2.length()) + "\r\n" +
- "\r\n" +
- content1);
- HttpExchange exchange = newExchange();
- FutureResponseListener listener = (FutureResponseListener)exchange.getResponseListeners().get(0);
- connection.receive();
- endPoint.setInputEOF();
- connection.receive();
-
- try
- {
- listener.get(5, TimeUnit.SECONDS);
- Assert.fail();
- }
- catch (ExecutionException e)
- {
- Assert.assertTrue(e.getCause() instanceof EOFException);
- }
- }
-
- @Test
- public void test_Receive_ResponseContent_IdleTimeout() throws Exception
- {
- endPoint.setInput("" +
- "HTTP/1.1 200 OK\r\n" +
- "Content-length: 1\r\n" +
- "\r\n");
- HttpExchange exchange = newExchange();
- FutureResponseListener listener = (FutureResponseListener)exchange.getResponseListeners().get(0);
- connection.receive();
- // Simulate an idle timeout
- connection.idleTimeout();
-
- try
- {
- listener.get(5, TimeUnit.SECONDS);
- Assert.fail();
- }
- catch (ExecutionException e)
- {
- Assert.assertTrue(e.getCause() instanceof TimeoutException);
- }
- }
-
- @Test
- public void test_Receive_BadResponse() throws Exception
- {
- endPoint.setInput("" +
- "HTTP/1.1 200 OK\r\n" +
- "Content-length: A\r\n" +
- "\r\n");
- HttpExchange exchange = newExchange();
- FutureResponseListener listener = (FutureResponseListener)exchange.getResponseListeners().get(0);
- connection.receive();
-
- try
- {
- listener.get(5, TimeUnit.SECONDS);
- Assert.fail();
- }
- catch (ExecutionException e)
- {
- Assert.assertTrue(e.getCause() instanceof HttpResponseException);
- }
- }
-
- @Test
- public void test_Receive_GZIPResponseContent_Fragmented() throws Exception
- {
- byte[] data = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
- ByteArrayOutputStream baos = new ByteArrayOutputStream();
- try (GZIPOutputStream gzipOutput = new GZIPOutputStream(baos))
- {
- gzipOutput.write(data);
- }
- byte[] gzip = baos.toByteArray();
-
- endPoint.setInput("" +
- "HTTP/1.1 200 OK\r\n" +
- "Content-Length: " + gzip.length + "\r\n" +
- "Content-Encoding: gzip\r\n" +
- "\r\n");
- HttpExchange exchange = newExchange();
- FutureResponseListener listener = (FutureResponseListener)exchange.getResponseListeners().get(0);
- connection.receive();
- endPoint.reset();
-
- ByteBuffer buffer = ByteBuffer.wrap(gzip);
- int fragment = buffer.limit() - 1;
- buffer.limit(fragment);
- endPoint.setInput(buffer);
- connection.receive();
- endPoint.reset();
-
- buffer.limit(gzip.length);
- buffer.position(fragment);
- endPoint.setInput(buffer);
- connection.receive();
-
- ContentResponse response = listener.get(5, TimeUnit.SECONDS);
- Assert.assertNotNull(response);
- Assert.assertEquals(200, response.getStatus());
- Assert.assertArrayEquals(data, response.getContent());
- }
+// @Rule
+// public final TestTracker tracker = new TestTracker();
+//
+// private HttpClient client;
+// private HttpDestination destination;
+// private ByteArrayEndPoint endPoint;
+// private HttpConnection connection;
+// private HttpConversation conversation;
+//
+// @Before
+// public void init() throws Exception
+// {
+// client = new HttpClient();
+// client.start();
+// destination = new HttpDestination(client, "http", "localhost", 8080);
+// endPoint = new ByteArrayEndPoint();
+// connection = new HttpConnection(client, endPoint, destination);
+// conversation = new HttpConversation(client, 1);
+// }
+//
+// @After
+// public void destroy() throws Exception
+// {
+// client.stop();
+// }
+//
+// protected HttpExchange newExchange()
+// {
+// HttpRequest request = new HttpRequest(client, URI.create("http://localhost"));
+// FutureResponseListener listener = new FutureResponseListener(request);
+// HttpExchange exchange = new HttpExchange(conversation, destination, request, Collections.<Response.ResponseListener>singletonList(listener));
+// conversation.getExchanges().offer(exchange);
+// connection.associate(exchange);
+// exchange.requestComplete();
+// exchange.terminateRequest();
+// return exchange;
+// }
+//
+// @Test
+// public void test_Receive_NoResponseContent() throws Exception
+// {
+// endPoint.setInput("" +
+// "HTTP/1.1 200 OK\r\n" +
+// "Content-length: 0\r\n" +
+// "\r\n");
+// HttpExchange exchange = newExchange();
+// FutureResponseListener listener = (FutureResponseListener)exchange.getResponseListeners().get(0);
+// connection.receive();
+//
+// Response response = listener.get(5, TimeUnit.SECONDS);
+// Assert.assertNotNull(response);
+// Assert.assertEquals(200, response.getStatus());
+// Assert.assertEquals("OK", response.getReason());
+// Assert.assertSame(HttpVersion.HTTP_1_1, response.getVersion());
+// HttpFields headers = response.getHeaders();
+// Assert.assertNotNull(headers);
+// Assert.assertEquals(1, headers.size());
+// Assert.assertEquals("0", headers.get(HttpHeader.CONTENT_LENGTH));
+// }
+//
+// @Test
+// public void test_Receive_ResponseContent() throws Exception
+// {
+// String content = "0123456789ABCDEF";
+// endPoint.setInput("" +
+// "HTTP/1.1 200 OK\r\n" +
+// "Content-length: " + content.length() + "\r\n" +
+// "\r\n" +
+// content);
+// HttpExchange exchange = newExchange();
+// FutureResponseListener listener = (FutureResponseListener)exchange.getResponseListeners().get(0);
+// connection.receive();
+//
+// Response response = listener.get(5, TimeUnit.SECONDS);
+// Assert.assertNotNull(response);
+// Assert.assertEquals(200, response.getStatus());
+// Assert.assertEquals("OK", response.getReason());
+// Assert.assertSame(HttpVersion.HTTP_1_1, response.getVersion());
+// HttpFields headers = response.getHeaders();
+// Assert.assertNotNull(headers);
+// Assert.assertEquals(1, headers.size());
+// Assert.assertEquals(String.valueOf(content.length()), headers.get(HttpHeader.CONTENT_LENGTH));
+// String received = listener.getContentAsString("UTF-8");
+// Assert.assertEquals(content, received);
+// }
+//
+// @Test
+// public void test_Receive_ResponseContent_EarlyEOF() throws Exception
+// {
+// String content1 = "0123456789";
+// String content2 = "ABCDEF";
+// endPoint.setInput("" +
+// "HTTP/1.1 200 OK\r\n" +
+// "Content-length: " + (content1.length() + content2.length()) + "\r\n" +
+// "\r\n" +
+// content1);
+// HttpExchange exchange = newExchange();
+// FutureResponseListener listener = (FutureResponseListener)exchange.getResponseListeners().get(0);
+// connection.receive();
+// endPoint.setInputEOF();
+// connection.receive();
+//
+// try
+// {
+// listener.get(5, TimeUnit.SECONDS);
+// Assert.fail();
+// }
+// catch (ExecutionException e)
+// {
+// Assert.assertTrue(e.getCause() instanceof EOFException);
+// }
+// }
+//
+// @Test
+// public void test_Receive_ResponseContent_IdleTimeout() throws Exception
+// {
+// endPoint.setInput("" +
+// "HTTP/1.1 200 OK\r\n" +
+// "Content-length: 1\r\n" +
+// "\r\n");
+// HttpExchange exchange = newExchange();
+// FutureResponseListener listener = (FutureResponseListener)exchange.getResponseListeners().get(0);
+// connection.receive();
+// // Simulate an idle timeout
+// connection.idleTimeout();
+//
+// try
+// {
+// listener.get(5, TimeUnit.SECONDS);
+// Assert.fail();
+// }
+// catch (ExecutionException e)
+// {
+// Assert.assertTrue(e.getCause() instanceof TimeoutException);
+// }
+// }
+//
+// @Test
+// public void test_Receive_BadResponse() throws Exception
+// {
+// endPoint.setInput("" +
+// "HTTP/1.1 200 OK\r\n" +
+// "Content-length: A\r\n" +
+// "\r\n");
+// HttpExchange exchange = newExchange();
+// FutureResponseListener listener = (FutureResponseListener)exchange.getResponseListeners().get(0);
+// connection.receive();
+//
+// try
+// {
+// listener.get(5, TimeUnit.SECONDS);
+// Assert.fail();
+// }
+// catch (ExecutionException e)
+// {
+// Assert.assertTrue(e.getCause() instanceof HttpResponseException);
+// }
+// }
+//
+// @Test
+// public void test_Receive_GZIPResponseContent_Fragmented() throws Exception
+// {
+// byte[] data = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
+// ByteArrayOutputStream baos = new ByteArrayOutputStream();
+// try (GZIPOutputStream gzipOutput = new GZIPOutputStream(baos))
+// {
+// gzipOutput.write(data);
+// }
+// byte[] gzip = baos.toByteArray();
+//
+// endPoint.setInput("" +
+// "HTTP/1.1 200 OK\r\n" +
+// "Content-Length: " + gzip.length + "\r\n" +
+// "Content-Encoding: gzip\r\n" +
+// "\r\n");
+// HttpExchange exchange = newExchange();
+// FutureResponseListener listener = (FutureResponseListener)exchange.getResponseListeners().get(0);
+// connection.receive();
+// endPoint.reset();
+//
+// ByteBuffer buffer = ByteBuffer.wrap(gzip);
+// int fragment = buffer.limit() - 1;
+// buffer.limit(fragment);
+// endPoint.setInput(buffer);
+// connection.receive();
+// endPoint.reset();
+//
+// buffer.limit(gzip.length);
+// buffer.position(fragment);
+// endPoint.setInput(buffer);
+// connection.receive();
+//
+// ContentResponse response = listener.get(5, TimeUnit.SECONDS);
+// Assert.assertNotNull(response);
+// Assert.assertEquals(200, response.getStatus());
+// Assert.assertArrayEquals(data, response.getContent());
+// }
}
diff --git a/jetty-client/src/test/java/org/eclipse/jetty/client/HttpSenderTest.java b/jetty-client/src/test/java/org/eclipse/jetty/client/HttpSenderTest.java
index e4cb6a0..5fb69c0 100644
--- a/jetty-client/src/test/java/org/eclipse/jetty/client/HttpSenderTest.java
+++ b/jetty-client/src/test/java/org/eclipse/jetty/client/HttpSenderTest.java
@@ -18,282 +18,263 @@
package org.eclipse.jetty.client;
-import java.net.URI;
-import java.nio.ByteBuffer;
-import java.util.Locale;
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.TimeUnit;
-
-import org.eclipse.jetty.client.api.Request;
-import org.eclipse.jetty.client.api.Response;
-import org.eclipse.jetty.client.api.Result;
-import org.eclipse.jetty.client.util.ByteBufferContentProvider;
-import org.eclipse.jetty.io.ByteArrayEndPoint;
-import org.eclipse.jetty.toolchain.test.TestTracker;
-import org.eclipse.jetty.toolchain.test.annotation.Slow;
-import org.junit.After;
-import org.junit.Assert;
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-
public class HttpSenderTest
{
- @Rule
- public final TestTracker tracker = new TestTracker();
-
- private HttpClient client;
-
- @Before
- public void init() throws Exception
- {
- client = new HttpClient();
- client.start();
- }
-
- @After
- public void destroy() throws Exception
- {
- client.stop();
- }
-
- @Test
- public void test_Send_NoRequestContent() throws Exception
- {
- ByteArrayEndPoint endPoint = new ByteArrayEndPoint();
- HttpDestination destination = new HttpDestination(client, "http", "localhost", 8080);
- HttpConnection connection = new HttpConnection(client, endPoint, destination);
- Request request = client.newRequest(URI.create("http://localhost/"));
- final CountDownLatch headersLatch = new CountDownLatch(1);
- final CountDownLatch successLatch = new CountDownLatch(1);
- request.listener(new Request.Listener.Empty()
- {
- @Override
- public void onHeaders(Request request)
- {
- headersLatch.countDown();
- }
-
- @Override
- public void onSuccess(Request request)
- {
- successLatch.countDown();
- }
- });
- connection.send(request, (Response.CompleteListener)null);
-
- String requestString = endPoint.takeOutputString();
- Assert.assertTrue(requestString.startsWith("GET "));
- Assert.assertTrue(requestString.endsWith("\r\n\r\n"));
- Assert.assertTrue(headersLatch.await(5, TimeUnit.SECONDS));
- Assert.assertTrue(successLatch.await(5, TimeUnit.SECONDS));
- }
-
- @Slow
- @Test
- public void test_Send_NoRequestContent_IncompleteFlush() throws Exception
- {
- ByteArrayEndPoint endPoint = new ByteArrayEndPoint("", 16);
- HttpDestination destination = new HttpDestination(client, "http", "localhost", 8080);
- HttpConnection connection = new HttpConnection(client, endPoint, destination);
- Request request = client.newRequest(URI.create("http://localhost/"));
- connection.send(request, (Response.CompleteListener)null);
-
- // This take will free space in the buffer and allow for the write to complete
- StringBuilder builder = new StringBuilder(endPoint.takeOutputString());
-
- // Wait for the write to complete
- TimeUnit.SECONDS.sleep(1);
-
- String chunk = endPoint.takeOutputString();
- while (chunk.length() > 0)
- {
- builder.append(chunk);
- chunk = endPoint.takeOutputString();
- }
-
- String requestString = builder.toString();
- Assert.assertTrue(requestString.startsWith("GET "));
- Assert.assertTrue(requestString.endsWith("\r\n\r\n"));
- }
-
- @Test
- public void test_Send_NoRequestContent_Exception() throws Exception
- {
- ByteArrayEndPoint endPoint = new ByteArrayEndPoint();
- // Shutdown output to trigger the exception on write
- endPoint.shutdownOutput();
- HttpDestination destination = new HttpDestination(client, "http", "localhost", 8080);
- HttpConnection connection = new HttpConnection(client, endPoint, destination);
- Request request = client.newRequest(URI.create("http://localhost/"));
- final CountDownLatch failureLatch = new CountDownLatch(2);
- request.listener(new Request.Listener.Empty()
- {
- @Override
- public void onFailure(Request request, Throwable x)
- {
- failureLatch.countDown();
- }
- });
- connection.send(request, new Response.Listener.Empty()
- {
- @Override
- public void onComplete(Result result)
- {
- Assert.assertTrue(result.isFailed());
- failureLatch.countDown();
- }
- });
-
- Assert.assertTrue(failureLatch.await(5, TimeUnit.SECONDS));
- }
-
- @Test
- public void test_Send_NoRequestContent_IncompleteFlush_Exception() throws Exception
- {
- ByteArrayEndPoint endPoint = new ByteArrayEndPoint("", 16);
- HttpDestination destination = new HttpDestination(client, "http", "localhost", 8080);
- HttpConnection connection = new HttpConnection(client, endPoint, destination);
- Request request = client.newRequest(URI.create("http://localhost/"));
- final CountDownLatch failureLatch = new CountDownLatch(2);
- request.listener(new Request.Listener.Empty()
- {
- @Override
- public void onFailure(Request request, Throwable x)
- {
- failureLatch.countDown();
- }
- });
- connection.send(request, new Response.Listener.Empty()
- {
- @Override
- public void onComplete(Result result)
- {
- Assert.assertTrue(result.isFailed());
- failureLatch.countDown();
- }
- });
-
- // Shutdown output to trigger the exception on write
- endPoint.shutdownOutput();
- // This take will free space in the buffer and allow for the write to complete
- // although it will fail because we shut down the output
- endPoint.takeOutputString();
-
- Assert.assertTrue(failureLatch.await(5, TimeUnit.SECONDS));
- }
-
- @Test
- public void test_Send_SmallRequestContent_InOneBuffer() throws Exception
- {
- ByteArrayEndPoint endPoint = new ByteArrayEndPoint();
- HttpDestination destination = new HttpDestination(client, "http", "localhost", 8080);
- HttpConnection connection = new HttpConnection(client, endPoint, destination);
- Request request = client.newRequest(URI.create("http://localhost/"));
- String content = "abcdef";
- request.content(new ByteBufferContentProvider(ByteBuffer.wrap(content.getBytes("UTF-8"))));
- final CountDownLatch headersLatch = new CountDownLatch(1);
- final CountDownLatch successLatch = new CountDownLatch(1);
- request.listener(new Request.Listener.Empty()
- {
- @Override
- public void onHeaders(Request request)
- {
- headersLatch.countDown();
- }
-
- @Override
- public void onSuccess(Request request)
- {
- successLatch.countDown();
- }
- });
- connection.send(request, (Response.CompleteListener)null);
-
- String requestString = endPoint.takeOutputString();
- Assert.assertTrue(requestString.startsWith("GET "));
- Assert.assertTrue(requestString.endsWith("\r\n\r\n" + content));
- Assert.assertTrue(headersLatch.await(5, TimeUnit.SECONDS));
- Assert.assertTrue(successLatch.await(5, TimeUnit.SECONDS));
- }
-
- @Test
- public void test_Send_SmallRequestContent_InTwoBuffers() throws Exception
- {
- ByteArrayEndPoint endPoint = new ByteArrayEndPoint();
- HttpDestination destination = new HttpDestination(client, "http", "localhost", 8080);
- HttpConnection connection = new HttpConnection(client, endPoint, destination);
- Request request = client.newRequest(URI.create("http://localhost/"));
- String content1 = "0123456789";
- String content2 = "abcdef";
- request.content(new ByteBufferContentProvider(ByteBuffer.wrap(content1.getBytes("UTF-8")), ByteBuffer.wrap(content2.getBytes("UTF-8"))));
- final CountDownLatch headersLatch = new CountDownLatch(1);
- final CountDownLatch successLatch = new CountDownLatch(1);
- request.listener(new Request.Listener.Empty()
- {
- @Override
- public void onHeaders(Request request)
- {
- headersLatch.countDown();
- }
-
- @Override
- public void onSuccess(Request request)
- {
- successLatch.countDown();
- }
- });
- connection.send(request, (Response.CompleteListener)null);
-
- String requestString = endPoint.takeOutputString();
- Assert.assertTrue(requestString.startsWith("GET "));
- Assert.assertTrue(requestString.endsWith("\r\n\r\n" + content1 + content2));
- Assert.assertTrue(headersLatch.await(5, TimeUnit.SECONDS));
- Assert.assertTrue(successLatch.await(5, TimeUnit.SECONDS));
- }
-
- @Test
- public void test_Send_SmallRequestContent_Chunked_InTwoChunks() throws Exception
- {
- ByteArrayEndPoint endPoint = new ByteArrayEndPoint();
- HttpDestination destination = new HttpDestination(client, "http", "localhost", 8080);
- HttpConnection connection = new HttpConnection(client, endPoint, destination);
- Request request = client.newRequest(URI.create("http://localhost/"));
- String content1 = "0123456789";
- String content2 = "ABCDEF";
- request.content(new ByteBufferContentProvider(ByteBuffer.wrap(content1.getBytes("UTF-8")), ByteBuffer.wrap(content2.getBytes("UTF-8")))
- {
- @Override
- public long getLength()
- {
- return -1;
- }
- });
- final CountDownLatch headersLatch = new CountDownLatch(1);
- final CountDownLatch successLatch = new CountDownLatch(1);
- request.listener(new Request.Listener.Empty()
- {
- @Override
- public void onHeaders(Request request)
- {
- headersLatch.countDown();
- }
-
- @Override
- public void onSuccess(Request request)
- {
- successLatch.countDown();
- }
- });
- connection.send(request, (Response.CompleteListener)null);
-
- String requestString = endPoint.takeOutputString();
- Assert.assertTrue(requestString.startsWith("GET "));
- String content = Integer.toHexString(content1.length()).toUpperCase(Locale.ENGLISH) + "\r\n" + content1 + "\r\n";
- content += Integer.toHexString(content2.length()).toUpperCase(Locale.ENGLISH) + "\r\n" + content2 + "\r\n";
- content += "0\r\n\r\n";
- Assert.assertTrue(requestString.endsWith("\r\n\r\n" + content));
- Assert.assertTrue(headersLatch.await(5, TimeUnit.SECONDS));
- Assert.assertTrue(successLatch.await(5, TimeUnit.SECONDS));
- }
+// @Rule
+// public final TestTracker tracker = new TestTracker();
+//
+// private HttpClient client;
+//
+// @Before
+// public void init() throws Exception
+// {
+// client = new HttpClient();
+// client.start();
+// }
+//
+// @After
+// public void destroy() throws Exception
+// {
+// client.stop();
+// }
+//
+// @Test
+// public void test_Send_NoRequestContent() throws Exception
+// {
+// ByteArrayEndPoint endPoint = new ByteArrayEndPoint();
+// HttpDestination destination = new HttpDestination(client, "http", "localhost", 8080);
+// HttpConnection connection = new HttpConnection(client, endPoint, destination);
+// Request request = client.newRequest(URI.create("http://localhost/"));
+// final CountDownLatch headersLatch = new CountDownLatch(1);
+// final CountDownLatch successLatch = new CountDownLatch(1);
+// request.listener(new Request.Listener.Empty()
+// {
+// @Override
+// public void onHeaders(Request request)
+// {
+// headersLatch.countDown();
+// }
+//
+// @Override
+// public void onSuccess(Request request)
+// {
+// successLatch.countDown();
+// }
+// });
+// connection.send(request, (Response.CompleteListener)null);
+//
+// String requestString = endPoint.takeOutputString();
+// Assert.assertTrue(requestString.startsWith("GET "));
+// Assert.assertTrue(requestString.endsWith("\r\n\r\n"));
+// Assert.assertTrue(headersLatch.await(5, TimeUnit.SECONDS));
+// Assert.assertTrue(successLatch.await(5, TimeUnit.SECONDS));
+// }
+//
+// @Slow
+// @Test
+// public void test_Send_NoRequestContent_IncompleteFlush() throws Exception
+// {
+// ByteArrayEndPoint endPoint = new ByteArrayEndPoint("", 16);
+// HttpDestination destination = new HttpDestination(client, "http", "localhost", 8080);
+// HttpConnection connection = new HttpConnection(client, endPoint, destination);
+// Request request = client.newRequest(URI.create("http://localhost/"));
+// connection.send(request, (Response.CompleteListener)null);
+//
+// // This take will free space in the buffer and allow for the write to complete
+// StringBuilder builder = new StringBuilder(endPoint.takeOutputString());
+//
+// // Wait for the write to complete
+// TimeUnit.SECONDS.sleep(1);
+//
+// String chunk = endPoint.takeOutputString();
+// while (chunk.length() > 0)
+// {
+// builder.append(chunk);
+// chunk = endPoint.takeOutputString();
+// }
+//
+// String requestString = builder.toString();
+// Assert.assertTrue(requestString.startsWith("GET "));
+// Assert.assertTrue(requestString.endsWith("\r\n\r\n"));
+// }
+//
+// @Test
+// public void test_Send_NoRequestContent_Exception() throws Exception
+// {
+// ByteArrayEndPoint endPoint = new ByteArrayEndPoint();
+// // Shutdown output to trigger the exception on write
+// endPoint.shutdownOutput();
+// HttpDestination destination = new HttpDestination(client, "http", "localhost", 8080);
+// HttpConnection connection = new HttpConnection(client, endPoint, destination);
+// Request request = client.newRequest(URI.create("http://localhost/"));
+// final CountDownLatch failureLatch = new CountDownLatch(2);
+// request.listener(new Request.Listener.Empty()
+// {
+// @Override
+// public void onFailure(Request request, Throwable x)
+// {
+// failureLatch.countDown();
+// }
+// });
+// connection.send(request, new Response.Listener.Empty()
+// {
+// @Override
+// public void onComplete(Result result)
+// {
+// Assert.assertTrue(result.isFailed());
+// failureLatch.countDown();
+// }
+// });
+//
+// Assert.assertTrue(failureLatch.await(5, TimeUnit.SECONDS));
+// }
+//
+// @Test
+// public void test_Send_NoRequestContent_IncompleteFlush_Exception() throws Exception
+// {
+// ByteArrayEndPoint endPoint = new ByteArrayEndPoint("", 16);
+// HttpDestination destination = new HttpDestination(client, "http", "localhost", 8080);
+// HttpConnection connection = new HttpConnection(client, endPoint, destination);
+// Request request = client.newRequest(URI.create("http://localhost/"));
+// final CountDownLatch failureLatch = new CountDownLatch(2);
+// request.listener(new Request.Listener.Empty()
+// {
+// @Override
+// public void onFailure(Request request, Throwable x)
+// {
+// failureLatch.countDown();
+// }
+// });
+// connection.send(request, new Response.Listener.Empty()
+// {
+// @Override
+// public void onComplete(Result result)
+// {
+// Assert.assertTrue(result.isFailed());
+// failureLatch.countDown();
+// }
+// });
+//
+// // Shutdown output to trigger the exception on write
+// endPoint.shutdownOutput();
+// // This take will free space in the buffer and allow for the write to complete
+// // although it will fail because we shut down the output
+// endPoint.takeOutputString();
+//
+// Assert.assertTrue(failureLatch.await(5, TimeUnit.SECONDS));
+// }
+//
+// @Test
+// public void test_Send_SmallRequestContent_InOneBuffer() throws Exception
+// {
+// ByteArrayEndPoint endPoint = new ByteArrayEndPoint();
+// HttpDestination destination = new HttpDestination(client, "http", "localhost", 8080);
+// HttpConnection connection = new HttpConnection(client, endPoint, destination);
+// Request request = client.newRequest(URI.create("http://localhost/"));
+// String content = "abcdef";
+// request.content(new ByteBufferContentProvider(ByteBuffer.wrap(content.getBytes("UTF-8"))));
+// final CountDownLatch headersLatch = new CountDownLatch(1);
+// final CountDownLatch successLatch = new CountDownLatch(1);
+// request.listener(new Request.Listener.Empty()
+// {
+// @Override
+// public void onHeaders(Request request)
+// {
+// headersLatch.countDown();
+// }
+//
+// @Override
+// public void onSuccess(Request request)
+// {
+// successLatch.countDown();
+// }
+// });
+// connection.send(request, (Response.CompleteListener)null);
+//
+// String requestString = endPoint.takeOutputString();
+// Assert.assertTrue(requestString.startsWith("GET "));
+// Assert.assertTrue(requestString.endsWith("\r\n\r\n" + content));
+// Assert.assertTrue(headersLatch.await(5, TimeUnit.SECONDS));
+// Assert.assertTrue(successLatch.await(5, TimeUnit.SECONDS));
+// }
+//
+// @Test
+// public void test_Send_SmallRequestContent_InTwoBuffers() throws Exception
+// {
+// ByteArrayEndPoint endPoint = new ByteArrayEndPoint();
+// HttpDestination destination = new HttpDestination(client, "http", "localhost", 8080);
+// HttpConnection connection = new HttpConnection(client, endPoint, destination);
+// Request request = client.newRequest(URI.create("http://localhost/"));
+// String content1 = "0123456789";
+// String content2 = "abcdef";
+// request.content(new ByteBufferContentProvider(ByteBuffer.wrap(content1.getBytes("UTF-8")), ByteBuffer.wrap(content2.getBytes("UTF-8"))));
+// final CountDownLatch headersLatch = new CountDownLatch(1);
+// final CountDownLatch successLatch = new CountDownLatch(1);
+// request.listener(new Request.Listener.Empty()
+// {
+// @Override
+// public void onHeaders(Request request)
+// {
+// headersLatch.countDown();
+// }
+//
+// @Override
+// public void onSuccess(Request request)
+// {
+// successLatch.countDown();
+// }
+// });
+// connection.send(request, (Response.CompleteListener)null);
+//
+// String requestString = endPoint.takeOutputString();
+// Assert.assertTrue(requestString.startsWith("GET "));
+// Assert.assertTrue(requestString.endsWith("\r\n\r\n" + content1 + content2));
+// Assert.assertTrue(headersLatch.await(5, TimeUnit.SECONDS));
+// Assert.assertTrue(successLatch.await(5, TimeUnit.SECONDS));
+// }
+//
+// @Test
+// public void test_Send_SmallRequestContent_Chunked_InTwoChunks() throws Exception
+// {
+// ByteArrayEndPoint endPoint = new ByteArrayEndPoint();
+// HttpDestination destination = new HttpDestination(client, "http", "localhost", 8080);
+// HttpConnection connection = new HttpConnection(client, endPoint, destination);
+// Request request = client.newRequest(URI.create("http://localhost/"));
+// String content1 = "0123456789";
+// String content2 = "ABCDEF";
+// request.content(new ByteBufferContentProvider(ByteBuffer.wrap(content1.getBytes("UTF-8")), ByteBuffer.wrap(content2.getBytes("UTF-8")))
+// {
+// @Override
+// public long getLength()
+// {
+// return -1;
+// }
+// });
+// final CountDownLatch headersLatch = new CountDownLatch(1);
+// final CountDownLatch successLatch = new CountDownLatch(1);
+// request.listener(new Request.Listener.Empty()
+// {
+// @Override
+// public void onHeaders(Request request)
+// {
+// headersLatch.countDown();
+// }
+//
+// @Override
+// public void onSuccess(Request request)
+// {
+// successLatch.countDown();
+// }
+// });
+// connection.send(request, (Response.CompleteListener)null);
+//
+// String requestString = endPoint.takeOutputString();
+// Assert.assertTrue(requestString.startsWith("GET "));
+// String content = Integer.toHexString(content1.length()).toUpperCase(Locale.ENGLISH) + "\r\n" + content1 + "\r\n";
+// content += Integer.toHexString(content2.length()).toUpperCase(Locale.ENGLISH) + "\r\n" + content2 + "\r\n";
+// content += "0\r\n\r\n";
+// Assert.assertTrue(requestString.endsWith("\r\n\r\n" + content));
+// Assert.assertTrue(headersLatch.await(5, TimeUnit.SECONDS));
+// Assert.assertTrue(successLatch.await(5, TimeUnit.SECONDS));
+// }
}
diff --git a/jetty-client/src/test/java/org/eclipse/jetty/client/HttpStalledServerConnectionTest.java b/jetty-client/src/test/java/org/eclipse/jetty/client/HttpStalledServerConnectionTest.java
new file mode 100644
index 0000000..282b2ce
--- /dev/null
+++ b/jetty-client/src/test/java/org/eclipse/jetty/client/HttpStalledServerConnectionTest.java
@@ -0,0 +1,197 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.client;
+
+import java.io.IOException;
+import java.net.URI;
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.UUID;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.eclipse.jetty.client.api.Response;
+import org.eclipse.jetty.client.util.DeferredContentProvider;
+import org.eclipse.jetty.http.HttpMethod;
+import org.eclipse.jetty.server.Request;
+import org.eclipse.jetty.server.Server;
+import org.eclipse.jetty.server.ServerConnector;
+import org.eclipse.jetty.server.handler.AbstractHandler;
+import org.eclipse.jetty.toolchain.test.annotation.Stress;
+import org.eclipse.jetty.util.BufferUtil;
+import org.eclipse.jetty.util.IO;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+import org.eclipse.jetty.util.thread.QueuedThreadPool;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.junit.Assert.assertThat;
+
+/**
+ * This test reproduces an issue on the server side, where the server threads might get stalled waiting for content,
+ * even if the content has been already fully read:
+ *
+ * "serverQTP-74" prio=5 tid=0x00007f9765aff000 nid=0x9303 waiting on condition [0x000000012bee3000]
+ java.lang.Thread.State: WAITING (parking)
+ at sun.misc.Unsafe.park(Native Method)
+ - parking to wait for <0x00000001170c0098> (a java.util.concurrent.Semaphore$NonfairSync)
+ at java.util.concurrent.locks.LockSupport.park(LockSupport.java:186)
+ at java.util.concurrent.locks.AbstractQueuedSynchronizer.parkAndCheckInterrupt(AbstractQueuedSynchronizer.java:834)
+ at java.util.concurrent.locks.AbstractQueuedSynchronizer.doAcquireSharedInterruptibly(AbstractQueuedSynchronizer.java:994)
+ at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireSharedInterruptibly(AbstractQueuedSynchronizer.java:1303)
+ at java.util.concurrent.Semaphore.acquire(Semaphore.java:317)
+ at org.eclipse.jetty.util.BlockingCallback.block(BlockingCallback.java:96)
+ at org.eclipse.jetty.server.HttpInputOverHTTP.blockForContent(HttpInputOverHTTP.java:64)
+ at org.eclipse.jetty.server.HttpInput$1.waitForContent(HttpInput.java:335)
+ at org.eclipse.jetty.server.HttpInput.read(HttpInput.java:131)
+ - locked <0x00000001170a3da0> (a org.eclipse.jetty.server.HttpInputOverHTTP)
+ at org.eclipse.jetty.util.IO.copy(IO.java:202)
+ at org.eclipse.jetty.util.IO.copy(IO.java:143)
+ at org.eclipse.jetty.client.HttpStalledClientConnectionTest$1.handle(HttpStalledClientConnectionTest.java:77)
+ at org.eclipse.jetty.server.handler.HandlerWrapper.handle(HandlerWrapper.java:97)
+ at org.eclipse.jetty.server.Server.handle(Server.java:445)
+ at org.eclipse.jetty.server.HttpChannel.handle(HttpChannel.java:272)
+ at org.eclipse.jetty.server.HttpConnection.onFillable(HttpConnection.java:220)
+ at org.eclipse.jetty.io.AbstractConnection$6.run(AbstractConnection.java:465)
+ at org.eclipse.jetty.util.thread.QueuedThreadPool.runJob(QueuedThreadPool.java:601)
+ at org.eclipse.jetty.util.thread.QueuedThreadPool$3.run(QueuedThreadPool.java:532)
+ at java.lang.Thread.run(Thread.java:724)
+
+ * This test actually doesn't belong to jetty-client. But for now it's ok. Once the issue is identified this test can
+ * be moved or removed.
+ */
+@RunWith(JUnit4.class)
+public class HttpStalledServerConnectionTest
+{
+ private static final Logger LOG = Log.getLogger(HttpStalledServerConnectionTest.class);
+
+ private Server server;
+ private ServerConnector connector;
+ private HttpClient httpClient;
+ private ExecutorService threadPool = Executors.newFixedThreadPool(256);
+
+ @Before
+ public void setUp() throws Exception
+ {
+ QueuedThreadPool threadPool = new QueuedThreadPool(256);
+ threadPool.setName("serverQTP");
+ server = new Server(threadPool);
+ server.setHandler(new AbstractHandler()
+ {
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
+ {
+ IO.copy(request.getInputStream(), response.getOutputStream());
+ baseRequest.setHandled(true);
+ }
+ });
+ connector = new ServerConnector(server);
+ connector.setPort(8080);
+ server.addConnector(connector);
+ server.start();
+ httpClient = new HttpClient();
+ httpClient.start();
+ }
+
+ @After
+ public void tearDown() throws Exception
+ {
+ server.stop();
+ server.join();
+ httpClient.stop();
+
+ }
+
+ @Stress("small loadtest")
+ @Test
+ public void simpleLoadTest() throws InterruptedException, ExecutionException, TimeoutException
+ {
+ int requests = 2000;
+ int timeout = 30;
+
+ CountDownLatch requestLatch = new CountDownLatch(requests);
+ for (int i = 0; i < requests; i++)
+ {
+ threadPool.execute(new executeSingleRequestRunnable(requestLatch));
+ }
+ threadPool.shutdown();
+ threadPool.awaitTermination(timeout, TimeUnit.SECONDS);
+ assertThat("all requests executed", requestLatch.await(timeout, TimeUnit.SECONDS), is(true));
+ }
+
+ private class executeSingleRequestRunnable implements Runnable
+ {
+ final CountDownLatch requestLatch;
+
+ private executeSingleRequestRunnable(CountDownLatch requestLatch)
+ {
+ this.requestLatch = requestLatch;
+ }
+
+ public void run()
+ {
+ URI uri = URI.create("http://localhost:" + connector.getLocalPort());
+ DeferredContentProvider deferredContentProvider = new DeferredContentProvider();
+ org.eclipse.jetty.client.api.Request request = httpClient.newRequest(uri).method(HttpMethod.POST).content
+ (deferredContentProvider);
+
+ ArrayList<Response.ResponseListener> listeners = new ArrayList<>();
+ // hack to avoid the test finishing before all requests have been answered. Test could as well be
+ // rewritten to use the blocking methods of httpClient
+ final CountDownLatch responseLatch = new CountDownLatch(1);
+ listeners.add(new Response.ContentListener()
+ {
+ @Override
+ public void onContent(Response response, ByteBuffer content)
+ {
+ assertThat("response status is 200", response.getStatus(), is(200));
+ requestLatch.countDown();
+ responseLatch.countDown();
+ LOG.debug("#{} status={},response={}", requestLatch.getCount(), response.getStatus(),
+ BufferUtil.toDetailString(content));
+ }
+ });
+ httpClient.send(request, listeners);
+ final String body = UUID.randomUUID().toString();
+
+ deferredContentProvider.offer(BufferUtil.toBuffer(body.getBytes()));
+ deferredContentProvider.close();
+ try
+ {
+ responseLatch.await(5, TimeUnit.SECONDS);
+ }
+ catch (InterruptedException e)
+ {
+ e.printStackTrace();
+ }
+ }
+ }
+}
diff --git a/jetty-client/src/test/java/org/eclipse/jetty/client/http/HttpDestinationOverHTTPTest.java b/jetty-client/src/test/java/org/eclipse/jetty/client/http/HttpDestinationOverHTTPTest.java
new file mode 100644
index 0000000..611335f
--- /dev/null
+++ b/jetty-client/src/test/java/org/eclipse/jetty/client/http/HttpDestinationOverHTTPTest.java
@@ -0,0 +1,226 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.client.http;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.RejectedExecutionException;
+import java.util.concurrent.TimeUnit;
+
+import org.eclipse.jetty.client.AbstractHttpClientServerTest;
+import org.eclipse.jetty.client.EmptyServerHandler;
+import org.eclipse.jetty.client.api.Connection;
+import org.eclipse.jetty.client.api.ContentResponse;
+import org.eclipse.jetty.client.api.Request;
+import org.eclipse.jetty.client.api.Response;
+import org.eclipse.jetty.client.api.Result;
+import org.eclipse.jetty.toolchain.test.annotation.Slow;
+import org.eclipse.jetty.util.ssl.SslContextFactory;
+import org.hamcrest.Matchers;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+
+public class HttpDestinationOverHTTPTest extends AbstractHttpClientServerTest
+{
+ public HttpDestinationOverHTTPTest(SslContextFactory sslContextFactory)
+ {
+ super(sslContextFactory);
+ }
+
+ @Before
+ public void init() throws Exception
+ {
+ start(new EmptyServerHandler());
+ }
+
+ @Test
+ public void test_FirstAcquire_WithEmptyQueue() throws Exception
+ {
+ HttpDestinationOverHTTP destination = new HttpDestinationOverHTTP(client, "http", "localhost", connector.getLocalPort());
+ Connection connection = destination.acquire();
+ if (connection == null)
+ {
+ // There are no queued requests, so the newly created connection will be idle
+ connection = destination.getHttpConnectionPool().getIdleConnections().poll(5, TimeUnit.SECONDS);
+ }
+ Assert.assertNotNull(connection);
+ }
+
+ @Test
+ public void test_SecondAcquire_AfterFirstAcquire_WithEmptyQueue_ReturnsSameConnection() throws Exception
+ {
+ HttpDestinationOverHTTP destination = new HttpDestinationOverHTTP(client, "http", "localhost", connector.getLocalPort());
+ Connection connection1 = destination.acquire();
+ if (connection1 == null)
+ {
+ // There are no queued requests, so the newly created connection will be idle
+ long start = System.nanoTime();
+ while (connection1 == null && TimeUnit.NANOSECONDS.toSeconds(System.nanoTime() - start) < 5)
+ {
+ TimeUnit.MILLISECONDS.sleep(50);
+ connection1 = destination.getHttpConnectionPool().getIdleConnections().peek();
+ }
+ Assert.assertNotNull(connection1);
+
+ Connection connection2 = destination.acquire();
+ Assert.assertSame(connection1, connection2);
+ }
+ }
+
+ @Test
+ public void test_SecondAcquire_ConcurrentWithFirstAcquire_WithEmptyQueue_CreatesTwoConnections() throws Exception
+ {
+ final CountDownLatch latch = new CountDownLatch(1);
+ HttpDestinationOverHTTP destination = new HttpDestinationOverHTTP(client, "http", "localhost", connector.getLocalPort())
+ {
+ @Override
+ protected void process(HttpConnectionOverHTTP connection, boolean dispatch)
+ {
+ try
+ {
+ latch.await(5, TimeUnit.SECONDS);
+ super.process(connection, dispatch);
+ }
+ catch (InterruptedException x)
+ {
+ x.printStackTrace();
+ }
+ }
+ };
+ Connection connection1 = destination.acquire();
+
+ // There are no available existing connections, so acquire()
+ // returns null because we delayed process() above
+ Assert.assertNull(connection1);
+
+ Connection connection2 = destination.acquire();
+ Assert.assertNull(connection2);
+
+ latch.countDown();
+
+ // There must be 2 idle connections
+ Connection connection = destination.getHttpConnectionPool().getIdleConnections().poll(5, TimeUnit.SECONDS);
+ Assert.assertNotNull(connection);
+ connection = destination.getHttpConnectionPool().getIdleConnections().poll(5, TimeUnit.SECONDS);
+ Assert.assertNotNull(connection);
+ }
+
+ @Test
+ public void test_Acquire_Process_Release_Acquire_ReturnsSameConnection() throws Exception
+ {
+ HttpDestinationOverHTTP destination = new HttpDestinationOverHTTP(client, "http", "localhost", connector.getLocalPort());
+ HttpConnectionOverHTTP connection1 = destination.acquire();
+
+ long start = System.nanoTime();
+ while (connection1 == null && TimeUnit.NANOSECONDS.toSeconds(System.nanoTime() - start) < 5)
+ {
+ TimeUnit.MILLISECONDS.sleep(50);
+ connection1 = (HttpConnectionOverHTTP)destination.getHttpConnectionPool().getIdleConnections().peek();
+ }
+ Assert.assertNotNull(connection1);
+
+ // Acquire the connection to make it active
+ Assert.assertSame(connection1, destination.acquire());
+
+ destination.process(connection1, false);
+ destination.release(connection1);
+
+ Connection connection2 = destination.acquire();
+ Assert.assertSame(connection1, connection2);
+ }
+
+ @Slow
+ @Test
+ public void test_IdleConnection_IdleTimeout() throws Exception
+ {
+ long idleTimeout = 1000;
+ client.setIdleTimeout(idleTimeout);
+
+ HttpDestinationOverHTTP destination = new HttpDestinationOverHTTP(client, "http", "localhost", connector.getLocalPort());
+ Connection connection1 = destination.acquire();
+ if (connection1 == null)
+ {
+ // There are no queued requests, so the newly created connection will be idle
+ long start = System.nanoTime();
+ while (connection1 == null && TimeUnit.NANOSECONDS.toSeconds(System.nanoTime() - start) < 5)
+ {
+ TimeUnit.MILLISECONDS.sleep(50);
+ connection1 = destination.getHttpConnectionPool().getIdleConnections().peek();
+ }
+ Assert.assertNotNull(connection1);
+
+ TimeUnit.MILLISECONDS.sleep(2 * idleTimeout);
+
+ connection1 = destination.getHttpConnectionPool().getIdleConnections().poll();
+ Assert.assertNull(connection1);
+ }
+ }
+
+ @Test
+ public void test_Request_Failed_If_MaxRequestsQueuedPerDestination_Exceeded() throws Exception
+ {
+ int maxQueued = 1;
+ client.setMaxRequestsQueuedPerDestination(maxQueued);
+ client.setMaxConnectionsPerDestination(1);
+
+ // Make one request to open the connection and be sure everything is setup properly
+ ContentResponse response = client.newRequest("localhost", connector.getLocalPort())
+ .scheme(scheme)
+ .send();
+ Assert.assertEquals(200, response.getStatus());
+
+ // Send another request that is sent immediately
+ final CountDownLatch successLatch = new CountDownLatch(1);
+ final CountDownLatch failureLatch = new CountDownLatch(1);
+ client.newRequest("localhost", connector.getLocalPort())
+ .scheme(scheme)
+ .onRequestQueued(new Request.QueuedListener()
+ {
+ @Override
+ public void onQueued(Request request)
+ {
+ // This request exceeds the maximum queued, should fail
+ client.newRequest("localhost", connector.getLocalPort())
+ .scheme(scheme)
+ .send(new Response.CompleteListener()
+ {
+ @Override
+ public void onComplete(Result result)
+ {
+ Assert.assertTrue(result.isFailed());
+ Assert.assertThat(result.getRequestFailure(), Matchers.instanceOf(RejectedExecutionException.class));
+ failureLatch.countDown();
+ }
+ });
+ }
+ })
+ .send(new Response.CompleteListener()
+ {
+ @Override
+ public void onComplete(Result result)
+ {
+ if (result.isSucceeded())
+ successLatch.countDown();
+ }
+ });
+
+ Assert.assertTrue(failureLatch.await(5, TimeUnit.SECONDS));
+ Assert.assertTrue(successLatch.await(5, TimeUnit.SECONDS));
+ }
+}
diff --git a/jetty-client/src/test/java/org/eclipse/jetty/client/ssl/SslBytesServerTest.java b/jetty-client/src/test/java/org/eclipse/jetty/client/ssl/SslBytesServerTest.java
index e0130dd..4ffab8f 100644
--- a/jetty-client/src/test/java/org/eclipse/jetty/client/ssl/SslBytesServerTest.java
+++ b/jetty-client/src/test/java/org/eclipse/jetty/client/ssl/SslBytesServerTest.java
@@ -18,6 +18,9 @@
package org.eclipse.jetty.client.ssl;
+import static org.hamcrest.Matchers.notNullValue;
+import static org.hamcrest.Matchers.nullValue;
+
import java.io.BufferedReader;
import java.io.EOFException;
import java.io.File;
@@ -48,6 +51,7 @@
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
+import org.eclipse.jetty.client.ssl.SslBytesTest.TLSRecord.Type;
import org.eclipse.jetty.http.HttpParser;
import org.eclipse.jetty.io.Connection;
import org.eclipse.jetty.io.EndPoint;
@@ -163,7 +167,7 @@
}
};
- ServerConnector connector = new ServerConnector(server, sslFactory, httpFactory)
+ ServerConnector connector = new ServerConnector(server, null,null,null,1,1,sslFactory, httpFactory)
{
@Override
protected SelectChannelEndPoint newEndPoint(SocketChannel channel, ManagedSelector selectSet, SelectionKey key) throws IOException
@@ -387,7 +391,14 @@
// Socket close
record = proxy.readFromServer();
- Assert.assertNull(String.valueOf(record), record);
+ if (record!=null)
+ {
+ Assert.assertEquals(record.getType(),Type.ALERT);
+
+ // Now should be a raw close
+ record = proxy.readFromServer();
+ Assert.assertNull(String.valueOf(record), record);
+ }
}
@Test
@@ -675,7 +686,15 @@
// Socket close
record = proxy.readFromServer();
- Assert.assertNull(String.valueOf(record), record);
+ // Raw close or alert
+ if (record!=null)
+ {
+ Assert.assertEquals(record.getType(),Type.ALERT);
+
+ // Now should be a raw close
+ record = proxy.readFromServer();
+ Assert.assertNull(String.valueOf(record), record);
+ }
}
@Test
@@ -730,7 +749,14 @@
// Socket close
record = proxy.readFromServer();
- Assert.assertNull(record);
+ if (record!=null)
+ {
+ Assert.assertEquals(record.getType(),Type.ALERT);
+
+ // Now should be a raw close
+ record = proxy.readFromServer();
+ Assert.assertNull(String.valueOf(record), record);
+ }
// Check that we did not spin
TimeUnit.MILLISECONDS.sleep(500);
@@ -798,7 +824,14 @@
// Socket close
record = proxy.readFromServer();
- Assert.assertNull(String.valueOf(record), record);
+ if (record!=null)
+ {
+ Assert.assertEquals(record.getType(),Type.ALERT);
+
+ // Now should be a raw close
+ record = proxy.readFromServer();
+ Assert.assertNull(String.valueOf(record), record);
+ }
// Check that we did not spin
TimeUnit.MILLISECONDS.sleep(500);
@@ -850,9 +883,17 @@
// Close the raw socket, this generates a truncation attack
proxy.flushToServer(null);
- // Expect raw close from server
+ // Expect raw close from server OR ALERT
record = proxy.readFromServer();
- Assert.assertNull(String.valueOf(record), record);
+ // TODO check that this is OK?
+ if (record!=null)
+ {
+ Assert.assertEquals(record.getType(),Type.ALERT);
+
+ // Now should be a raw close
+ record = proxy.readFromServer();
+ Assert.assertNull(String.valueOf(record), record);
+ }
// Check that we did not spin
TimeUnit.MILLISECONDS.sleep(500);
@@ -902,7 +943,14 @@
// Expect raw close from server
record = proxy.readFromServer();
- Assert.assertNull(String.valueOf(record), record);
+ if (record!=null)
+ {
+ Assert.assertEquals(record.getType(),Type.ALERT);
+
+ // Now should be a raw close
+ record = proxy.readFromServer();
+ Assert.assertNull(String.valueOf(record), record);
+ }
// Check that we did not spin
TimeUnit.MILLISECONDS.sleep(500);
@@ -1096,7 +1144,14 @@
// Socket close
record = proxy.readFromServer();
- Assert.assertNull(record);
+ if (record!=null)
+ {
+ Assert.assertEquals(record.getType(),Type.ALERT);
+
+ // Now should be a raw close
+ record = proxy.readFromServer();
+ Assert.assertNull(String.valueOf(record), record);
+ }
// Check that we did not spin
TimeUnit.MILLISECONDS.sleep(500);
@@ -1815,6 +1870,11 @@
// Socket close
record = proxy.readFromServer();
- Assert.assertNull(String.valueOf(record), record);
+ if (record!=null)
+ {
+ Assert.assertEquals(record.getType(),Type.ALERT);
+ record = proxy.readFromServer();
+ }
+ Assert.assertThat(record,nullValue());
}
}
diff --git a/jetty-continuation/pom.xml b/jetty-continuation/pom.xml
index 047c8f2..4683f8b 100644
--- a/jetty-continuation/pom.xml
+++ b/jetty-continuation/pom.xml
@@ -2,7 +2,7 @@
<parent>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-project</artifactId>
- <version>9.0.5-SNAPSHOT</version>
+ <version>9.1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>jetty-continuation</artifactId>
@@ -54,9 +54,9 @@
</build>
<dependencies>
<dependency>
- <groupId>org.eclipse.jetty.orbit</groupId>
- <artifactId>javax.servlet</artifactId>
+ <groupId>javax.servlet</groupId>
+ <artifactId>javax.servlet-api</artifactId>
<scope>provided</scope>
- </dependency>
+ </dependency>
</dependencies>
</project>
diff --git a/jetty-continuation/src/main/java/org/eclipse/jetty/continuation/Servlet3Continuation.java b/jetty-continuation/src/main/java/org/eclipse/jetty/continuation/Servlet3Continuation.java
index 8c1a66e..d370a34 100644
--- a/jetty-continuation/src/main/java/org/eclipse/jetty/continuation/Servlet3Continuation.java
+++ b/jetty-continuation/src/main/java/org/eclipse/jetty/continuation/Servlet3Continuation.java
@@ -34,7 +34,7 @@
/* ------------------------------------------------------------ */
/**
* This implementation of Continuation is used by {@link ContinuationSupport}
- * when it detects that the application has been deployed in a non-jetty Servlet 3
+ * when it detects that the application has been deployed in a Servlet 3
* server.
*/
public class Servlet3Continuation implements Continuation, AsyncListener
diff --git a/jetty-deploy/pom.xml b/jetty-deploy/pom.xml
index fc17414..204d680 100644
--- a/jetty-deploy/pom.xml
+++ b/jetty-deploy/pom.xml
@@ -2,7 +2,7 @@
<parent>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-project</artifactId>
- <version>9.0.5-SNAPSHOT</version>
+ <version>9.1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>jetty-deploy</artifactId>
@@ -25,7 +25,7 @@
</goals>
<configuration>
<instructions>
- <Import-Package>org.eclipse.jetty.jmx.*;version="9.0";resolution:=optional,*</Import-Package>
+ <Import-Package>org.eclipse.jetty.jmx.*;version="9.1";resolution:=optional,*</Import-Package>
<_nouses>true</_nouses>
</instructions>
</configuration>
@@ -92,12 +92,5 @@
<version>${project.version}</version>
<optional>true</optional>
</dependency>
- <!--
- <dependency>
- <groupId>org.eclipse.jetty</groupId>
- <artifactId>jetty-websocket</artifactId>
- <version>${project.version}</version>
- <scope>test</scope>
- </dependency> -->
</dependencies>
</project>
diff --git a/jetty-distribution/pom.xml b/jetty-distribution/pom.xml
index eebc1d3..e55ebe9 100644
--- a/jetty-distribution/pom.xml
+++ b/jetty-distribution/pom.xml
@@ -3,7 +3,7 @@
<parent>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-project</artifactId>
- <version>9.0.5-SNAPSHOT</version>
+ <version>9.1.0-SNAPSHOT</version>
</parent>
<artifactId>jetty-distribution</artifactId>
<name>Jetty :: Distribution Assemblies</name>
@@ -297,8 +297,8 @@
</goals>
<configuration>
<includeGroupIds>org.eclipse.jetty</includeGroupIds>
- <excludeGroupIds>org.eclipse.jetty.orbit,org.eclipse.jetty.spdy,org.eclipse.jetty.websocket,org.eclipse.jetty.drafts</excludeGroupIds>
- <excludeArtifactIds>jetty-all,jetty-start,jetty-monitor,jetty-jsp</excludeArtifactIds>
+ <excludeGroupIds>org.eclipse.jetty.orbit,org.eclipse.jetty.spdy,org.eclipse.jetty.websocket</excludeGroupIds>
+ <excludeArtifactIds>jetty-all,jetty-start,jetty-monitor</excludeArtifactIds>
<includeTypes>jar</includeTypes>
<outputDirectory>${assembly-directory}/lib</outputDirectory>
</configuration>
@@ -310,7 +310,8 @@
<goal>copy-dependencies</goal>
</goals>
<configuration>
- <includeGroupIds>org.eclipse.jetty.websocket,org.eclipse.jetty.drafts</includeGroupIds>
+ <includeGroupIds>javax.websocket,org.eclipse.jetty.websocket</includeGroupIds>
+ <excludeArtifactIds>javax.websocket-client-api</excludeArtifactIds>
<includeTypes>jar</includeTypes>
<outputDirectory>${assembly-directory}/lib/websocket</outputDirectory>
</configuration>
@@ -335,7 +336,7 @@
</configuration>
</execution>
<execution>
- <id>copy-orbit-servlet-api-deps</id>
+ <id>copy-servlet-api-deps</id>
<phase>generate-resources</phase>
<goals>
<goal>copy</goal>
@@ -343,12 +344,20 @@
<configuration>
<artifactItems>
<artifactItem>
- <groupId>org.eclipse.jetty.orbit</groupId>
- <artifactId>javax.servlet</artifactId>
- <version>${orbit-servlet-api-version}</version>
+ <groupId>javax.servlet</groupId>
+ <artifactId>javax.servlet-api</artifactId>
+ <version>3.1.0</version>
<overWrite>true</overWrite>
<outputDirectory>${assembly-directory}/lib</outputDirectory>
- <destFileName>servlet-api-3.0.jar</destFileName>
+ <destFileName>servlet-api-3.1.jar</destFileName>
+ </artifactItem>
+ <artifactItem>
+ <groupId>org.eclipse.jetty.toolchain</groupId>
+ <artifactId>jetty-schemas</artifactId>
+ <version>3.1.RC0</version>
+ <overWrite>true</overWrite>
+ <outputDirectory>${assembly-directory}/lib</outputDirectory>
+ <destFileName>jetty-schemas-3.1.jar</destFileName>
</artifactItem>
</artifactItems>
</configuration>
@@ -380,54 +389,54 @@
</configuration>
</execution>
<execution>
- <id>copy-orbit-lib-annotations-deps</id>
+ <id>copy-annotations-deps</id>
<phase>generate-resources</phase>
<goals>
<goal>copy-dependencies</goal>
</goals>
<configuration>
- <includeGroupIds>org.eclipse.jetty.orbit</includeGroupIds>
- <includeArtifactIds>javax.annotation,org.objectweb.asm</includeArtifactIds>
+ <includeGroupIds>javax.annotation,org.eclipse.jetty.orbit</includeGroupIds>
+ <includeArtifactIds>javax.annotation-api,org.objectweb.asm</includeArtifactIds>
<includeTypes>jar</includeTypes>
<outputDirectory>${assembly-directory}/lib/annotations</outputDirectory>
</configuration>
</execution>
<execution>
- <id>copy-orbit-lib-jta-deps</id>
+ <id>copy-jta-deps</id>
<phase>generate-resources</phase>
<goals>
<goal>copy-dependencies</goal>
</goals>
<configuration>
- <includeGroupIds>org.eclipse.jetty.orbit</includeGroupIds>
- <includeArtifactIds>javax.transaction</includeArtifactIds>
+ <includeGroupIds>javax.transaction</includeGroupIds>
+ <includeArtifactIds>javax.transaction-api</includeArtifactIds>
<includeTypes>jar</includeTypes>
<outputDirectory>${assembly-directory}/lib/jndi</outputDirectory>
</configuration>
</execution>
<execution>
- <id>copy-orbit-lib-jndi-deps</id>
+ <id>copy-jndi-deps</id>
<phase>generate-resources</phase>
<goals>
<goal>copy-dependencies</goal>
</goals>
<configuration>
<includeGroupIds>org.eclipse.jetty.orbit</includeGroupIds>
- <includeArtifactIds>javax.mail.glassfish,javax.activation</includeArtifactIds>
+ <includeArtifactIds>javax.activation</includeArtifactIds>
<includeTypes>jar</includeTypes>
<outputDirectory>${assembly-directory}/lib/jndi</outputDirectory>
</configuration>
</execution>
<execution>
- <id>copy-orbit-lib-jsp-deps</id>
+ <id>copy-jsp-deps</id>
<phase>generate-resources</phase>
<goals>
<goal>copy-dependencies</goal>
</goals>
<configuration>
- <includeGroupIds>org.eclipse.jetty.orbit</includeGroupIds>
- <includeArtifactIds>com.sun.el,javax.el,javax.servlet.jsp,javax.servlet.jsp.jstl,org.apache.jasper.glassfish,org.apache.taglibs.standard.glassfish,org.eclipse.jdt.core</includeArtifactIds>
+ <includeGroupIds>org.eclipse.jetty.orbit,org.glassfish.web, org.glassfish, javax.el, javax.servlet.jsp</includeGroupIds>
+ <includeArtifactIds>javax.servlet.jsp.jstl,org.apache.taglibs.standard.glassfish,org.eclipse.jdt.core, javax.servlet.jsp-api, javax.servlet.jsp, javax.el-api, javax.el</includeArtifactIds>
<includeTypes>jar</includeTypes>
<outputDirectory>${assembly-directory}/lib/jsp</outputDirectory>
</configuration>
@@ -439,7 +448,7 @@
<goal>unpack-dependencies</goal>
</goals>
<configuration>
- <includeGroupIds>org.eclipse.jetty</includeGroupIds>
+ <includeGroupIds>org.eclipse.jetty,org.eclipse.jetty.websocket</includeGroupIds>
<classifier>config</classifier>
<failOnMissingClassifierArtifact>false</failOnMissingClassifierArtifact>
<excludes>META-INF/**</excludes>
@@ -488,8 +497,8 @@
<dependencies>
<!-- Orbit Deps -->
<dependency>
- <groupId>org.eclipse.jetty.orbit</groupId>
- <artifactId>javax.annotation</artifactId>
+ <groupId>javax.annotation</groupId>
+ <artifactId>javax.annotation-api</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.jetty.orbit</groupId>
@@ -500,17 +509,30 @@
<artifactId>javax.activation</artifactId>
</dependency>
<dependency>
- <groupId>org.eclipse.jetty.orbit</groupId>
- <artifactId>javax.mail.glassfish</artifactId>
- </dependency>
- <dependency>
- <groupId>org.eclipse.jetty.orbit</groupId>
- <artifactId>javax.transaction</artifactId>
+ <groupId>javax.transaction</groupId>
+ <artifactId>javax.transaction-api</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.jetty.orbit</groupId>
<artifactId>javax.security.auth.message</artifactId>
</dependency>
+ <dependency>
+ <groupId>org.eclipse.jetty.orbit</groupId>
+ <artifactId>org.apache.taglibs.standard.glassfish</artifactId>
+ </dependency>
+
+ <dependency>
+ <groupId>org.glassfish.web</groupId>
+ <artifactId>javax.servlet.jsp</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>javax.servlet.jsp</groupId>
+ <artifactId>javax.servlet.jsp-api</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.glassfish</groupId>
+ <artifactId>javax.el</artifactId>
+ </dependency>
<!-- jetty deps -->
<dependency>
@@ -552,10 +574,20 @@
</dependency>
<dependency>
<groupId>org.eclipse.jetty.websocket</groupId>
+ <artifactId>websocket-servlet</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.eclipse.jetty.websocket</groupId>
<artifactId>websocket-server</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
+ <groupId>org.eclipse.jetty.websocket</groupId>
+ <artifactId>javax-websocket-server-impl</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+ <dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-jsp</artifactId>
<version>${project.version}</version>
diff --git a/jetty-distribution/src/main/resources/start.d/900-demo.ini b/jetty-distribution/src/main/resources/start.d/900-demo.ini
index ae04b10..3f94b63 100644
--- a/jetty-distribution/src/main/resources/start.d/900-demo.ini
+++ b/jetty-distribution/src/main/resources/start.d/900-demo.ini
@@ -43,14 +43,14 @@
# ===========================================================
# Enable additional webapp environment configurators
# -----------------------------------------------------------
-OPTIONS=plus
-etc/jetty-plus.xml
+# OPTIONS=plus
+# etc/jetty-plus.xml
# ===========================================================
# Enable servlet 3.1 annotations
# -----------------------------------------------------------
-OPTIONS=annotations
-etc/jetty-annotations.xml
+# OPTIONS=annotations
+# etc/jetty-annotations.xml
# ===========================================================
# Enable https listener
diff --git a/jetty-distribution/src/main/resources/start.ini b/jetty-distribution/src/main/resources/start.ini
index 3d7646d..92122b8 100644
--- a/jetty-distribution/src/main/resources/start.ini
+++ b/jetty-distribution/src/main/resources/start.ini
@@ -69,7 +69,7 @@
# Add jars discovered in lib/ext to the classpath
# Include the core jetty configuration file
#-----------------------------------------------------------
-OPTIONS=Server,websocket,resources,ext
+OPTIONS=Server,resources,ext
threads.min=10
threads.max=200
threads.timeout=60000
@@ -80,6 +80,22 @@
etc/jetty.xml
#===========================================================
+# Servlet 3.x Annotation Support
+#-----------------------------------------------------------
+OPTIONS=plus,annotations
+etc/jetty-plus.xml
+etc/jetty-annotations.xml
+
+#===========================================================
+# WebSocket Support
+# To enable websocket server support (both apis)
+# org.eclipse.jetty.websocket.api (API)
+# javax.websocket (API)
+#-----------------------------------------------------------
+OPTIONS=websocket
+etc/jetty-websockets.xml
+
+#===========================================================
# JMX Management
# To enable remote JMX access uncomment jmxremote and
# enable --exec
diff --git a/jetty-http-spi/pom.xml b/jetty-http-spi/pom.xml
index 263c91f..60544a3 100644
--- a/jetty-http-spi/pom.xml
+++ b/jetty-http-spi/pom.xml
@@ -2,7 +2,7 @@
<parent>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-project</artifactId>
- <version>9.0.5-SNAPSHOT</version>
+ <version>9.1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>jetty-http-spi</artifactId>
diff --git a/jetty-http/pom.xml b/jetty-http/pom.xml
index e8de81e..3da7358 100644
--- a/jetty-http/pom.xml
+++ b/jetty-http/pom.xml
@@ -3,7 +3,7 @@
<parent>
<artifactId>jetty-project</artifactId>
<groupId>org.eclipse.jetty</groupId>
- <version>9.0.5-SNAPSHOT</version>
+ <version>9.1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>jetty-http</artifactId>
@@ -37,7 +37,7 @@
</goals>
<configuration>
<instructions>
- <Import-Package>javax.servlet.*;version="2.6.0",javax.net.*,*</Import-Package>
+ <Import-Package>javax.servlet.*;version="[2.6.0,3.2)",javax.net.*,*</Import-Package>
</instructions>
</configuration>
</execution>
diff --git a/jetty-http/src/main/java/org/eclipse/jetty/http/HttpContent.java b/jetty-http/src/main/java/org/eclipse/jetty/http/HttpContent.java
index 61775e5..5a4cbf7 100644
--- a/jetty-http/src/main/java/org/eclipse/jetty/http/HttpContent.java
+++ b/jetty-http/src/main/java/org/eclipse/jetty/http/HttpContent.java
@@ -23,6 +23,7 @@
import java.nio.ByteBuffer;
import java.nio.channels.ReadableByteChannel;
+import org.eclipse.jetty.util.BufferUtil;
import org.eclipse.jetty.util.resource.Resource;
/* ------------------------------------------------------------ */
@@ -98,7 +99,16 @@
@Override
public ByteBuffer getDirectBuffer()
{
- return null;
+ if (_resource.length()<=0 || _maxBuffer<_resource.length())
+ return null;
+ try
+ {
+ return BufferUtil.toBuffer(_resource,true);
+ }
+ catch(IOException e)
+ {
+ throw new RuntimeException(e);
+ }
}
/* ------------------------------------------------------------ */
@@ -114,24 +124,9 @@
{
if (_resource.length()<=0 || _maxBuffer<_resource.length())
return null;
- int length=(int)_resource.length();
- byte[] array = new byte[length];
-
- int offset=0;
- try (InputStream in=_resource.getInputStream())
+ try
{
- do
- {
- int filled=in.read(array,offset,length);
- if (filled<0)
- break;
- length-=filled;
- offset+=filled;
- }
- while(length>0);
-
- ByteBuffer buffer = ByteBuffer.wrap(array);
- return buffer;
+ return BufferUtil.toBuffer(_resource,false);
}
catch(IOException e)
{
diff --git a/jetty-http/src/main/java/org/eclipse/jetty/http/HttpFields.java b/jetty-http/src/main/java/org/eclipse/jetty/http/HttpFields.java
index 98e3e59..6e326ba 100644
--- a/jetty-http/src/main/java/org/eclipse/jetty/http/HttpFields.java
+++ b/jetty-http/src/main/java/org/eclipse/jetty/http/HttpFields.java
@@ -826,6 +826,8 @@
if (value != null && value.length() > 0)
QuotedStringTokenizer.quoteIfNeeded(buf, value, delim);
+ if (version>0)
+ buf.append(";Version=").append(version);
if (path != null && path.length() > 0)
{
diff --git a/jetty-http/src/main/java/org/eclipse/jetty/http/HttpParser.java b/jetty-http/src/main/java/org/eclipse/jetty/http/HttpParser.java
index de08e65..968a966 100644
--- a/jetty-http/src/main/java/org/eclipse/jetty/http/HttpParser.java
+++ b/jetty-http/src/main/java/org/eclipse/jetty/http/HttpParser.java
@@ -18,6 +18,7 @@
package org.eclipse.jetty.http;
+import java.io.IOException;
import java.nio.ByteBuffer;
import org.eclipse.jetty.http.HttpTokens.EndOfContent;
@@ -86,17 +87,17 @@
HEADER_IN_NAME,
HEADER_VALUE,
HEADER_IN_VALUE,
- END,
- EOF_CONTENT,
CONTENT,
+ EOF_CONTENT,
CHUNKED_CONTENT,
CHUNK_SIZE,
CHUNK_PARAMS,
CHUNK,
+ END,
CLOSED
};
- private final boolean DEBUG=LOG.isDebugEnabled();
+ private final boolean DEBUG=LOG.isDebugEnabled(); // Cache debug to help branch prediction
private final HttpHandler<ByteBuffer> _handler;
private final RequestHandler<ByteBuffer> _requestHandler;
private final ResponseHandler<ByteBuffer> _responseHandler;
@@ -112,6 +113,8 @@
/* ------------------------------------------------------------------------------- */
private volatile State _state=State.START;
+ private volatile boolean _eof;
+ private volatile boolean _closed;
private HttpMethod _method;
private String _methodString;
private HttpVersion _version;
@@ -189,19 +192,13 @@
/* ------------------------------------------------------------------------------- */
public boolean inContentState()
{
- return _state.ordinal() > State.END.ordinal();
+ return _state.ordinal()>=State.CONTENT.ordinal() && _state.ordinal()<State.END.ordinal();
}
/* ------------------------------------------------------------------------------- */
public boolean inHeaderState()
{
- return _state.ordinal() < State.END.ordinal();
- }
-
- /* ------------------------------------------------------------------------------- */
- public boolean isInContent()
- {
- return _state.ordinal()>State.END.ordinal() && _state.ordinal()<State.CLOSED.ordinal();
+ return _state.ordinal() < State.CONTENT.ordinal();
}
/* ------------------------------------------------------------------------------- */
@@ -240,8 +237,10 @@
return _state == state;
}
+ /* ------------------------------------------------------------------------------- */
private static class BadMessage extends Error
{
+ private static final long serialVersionUID = 1L;
private final int _code;
private final String _message;
@@ -269,65 +268,43 @@
}
/* ------------------------------------------------------------------------------- */
- private byte next(ByteBuffer buffer)
+ private byte next(ByteBuffer buffer)
{
- byte ch=buffer.get();
-
- // If not a special character
- if (ch>=HttpTokens.SPACE || ch<0)
- {
- if (_cr)
- throw new BadMessage("Bad EOL");
-
- /*
- if (ch>HttpTokens.SPACE)
- System.err.println("Next "+(char)ch);
- else
- System.err.println("Next ["+ch+"]");*/
- return ch;
- }
-
+ byte ch = buffer.get();
- // Only a LF acceptable after CR
if (_cr)
{
+ if (ch!=HttpTokens.LINE_FEED)
+ throw new BadMessage("Bad EOL");
_cr=false;
- if (ch==HttpTokens.LINE_FEED)
- return ch;
-
- throw new BadMessage("Bad EOL");
+ return ch;
}
-
- // If it is a CR
- if (ch==HttpTokens.CARRIAGE_RETURN)
+
+ if (ch>=0 && ch<HttpTokens.SPACE)
{
- // Skip CR and look for a LF
- if (buffer.hasRemaining())
+ if (ch==HttpTokens.CARRIAGE_RETURN)
{
- if(_maxHeaderBytes>0 && _state.ordinal()<State.END.ordinal())
- _headerBytes++;
- ch=buffer.get();
- if (ch==HttpTokens.LINE_FEED)
- return ch;
-
- throw new BadMessage();
+ if (buffer.hasRemaining())
+ {
+ if(_maxHeaderBytes>0 && _state.ordinal()<State.END.ordinal())
+ _headerBytes++;
+ ch=buffer.get();
+ if (ch!=HttpTokens.LINE_FEED)
+ throw new BadMessage("Bad EOL");
+ }
+ else
+ {
+ _cr=true;
+ // Can return 0 here to indicate the need for more characters,
+ // because a real 0 in the buffer would cause a BadMessage below
+ return 0;
+ }
}
-
- // Defer lookup of LF
- _cr=true;
- return 0;
+ // Only LF or TAB acceptable special characters
+ else if (!(ch==HttpTokens.LINE_FEED || ch==HttpTokens.TAB))
+ throw new BadMessage();
}
- // Only LF or TAB acceptable special characters
- if (ch!=HttpTokens.LINE_FEED && ch!=HttpTokens.TAB)
- throw new BadMessage();
-
- /*
- if (ch>HttpTokens.SPACE)
- System.err.println("Next "+(char)ch);
- else
- System.err.println("Next ["+ch+"]");
- */
return ch;
}
@@ -337,33 +314,33 @@
*/
private boolean quickStart(ByteBuffer buffer)
{
+ if (_requestHandler!=null)
+ {
+ _method = HttpMethod.lookAheadGet(buffer);
+ if (_method!=null)
+ {
+ _methodString = _method.asString();
+ buffer.position(buffer.position()+_methodString.length()+1);
+ setState(State.SPACE1);
+ return false;
+ }
+ }
+ else if (_responseHandler!=null)
+ {
+ _version = HttpVersion.lookAheadGet(buffer);
+ if (_version!=null)
+ {
+ buffer.position(buffer.position()+_version.asString().length()+1);
+ setState(State.SPACE1);
+ return false;
+ }
+ }
+
// Quick start look
while (_state==State.START && buffer.hasRemaining())
{
- if (_requestHandler!=null)
- {
- _method = HttpMethod.lookAheadGet(buffer);
- if (_method!=null)
- {
- _methodString = _method.asString();
- buffer.position(buffer.position()+_methodString.length()+1);
- setState(State.SPACE1);
- return false;
- }
- }
- else if (_responseHandler!=null)
- {
- _version = HttpVersion.lookAheadGet(buffer);
- if (_version!=null)
- {
- buffer.position(buffer.position()+_version.asString().length()+1);
- setState(State.SPACE1);
- return false;
- }
- }
+ int ch=next(buffer);
- byte ch=next(buffer);
-
if (ch > HttpTokens.SPACE)
{
_string.setLength(0);
@@ -371,10 +348,13 @@
setState(_requestHandler!=null?State.METHOD:State.RESPONSE_VERSION);
return false;
}
+ else if (ch==0)
+ break;
}
return false;
}
+ /* ------------------------------------------------------------------------------- */
private String takeString()
{
String s =_string.toString();
@@ -382,6 +362,7 @@
return s;
}
+ /* ------------------------------------------------------------------------------- */
private String takeLengthString()
{
_string.setLength(_length);
@@ -396,17 +377,15 @@
*/
private boolean parseLine(ByteBuffer buffer)
{
- boolean return_from_parse=false;
+ boolean handle=false;
// Process headers
- while (_state.ordinal()<State.HEADER.ordinal() && buffer.hasRemaining() && !return_from_parse)
+ while (_state.ordinal()<State.HEADER.ordinal() && buffer.hasRemaining() && !handle)
{
// process each character
byte ch=next(buffer);
- if (ch==-1)
- return true;
if (ch==0)
- continue;
+ break;
if (_maxHeaderBytes>0 && ++_headerBytes>_maxHeaderBytes)
{
@@ -524,7 +503,7 @@
}
else if (ch < HttpTokens.SPACE && ch>=0)
{
- return_from_parse=_responseHandler.startResponse(_version, _responseStatus, null)||return_from_parse;
+ handle=_responseHandler.startResponse(_version, _responseStatus, null)||handle;
setState(State.HEADER);
}
else
@@ -542,11 +521,11 @@
{
// HTTP/0.9
_uri.flip();
- return_from_parse=_requestHandler.startRequest(_method,_methodString,_uri,null)||return_from_parse;
+ handle=_requestHandler.startRequest(_method,_methodString,_uri,null)||handle;
setState(State.END);
BufferUtil.clear(buffer);
- return_from_parse=_handler.headerComplete()||return_from_parse;
- return_from_parse=_handler.messageComplete()||return_from_parse;
+ handle=_handler.headerComplete()||handle;
+ handle=_handler.messageComplete()||handle;
}
else
{
@@ -576,28 +555,29 @@
setState(State.REQUEST_VERSION);
// try quick look ahead for HTTP Version
+ HttpVersion version;
if (buffer.position()>0 && buffer.hasArray())
+ version=HttpVersion.lookAheadGet(buffer.array(),buffer.arrayOffset()+buffer.position()-1,buffer.arrayOffset()+buffer.limit());
+ else
+ version=HttpVersion.CACHE.getBest(buffer,0,buffer.remaining());
+ if (version!=null)
{
- HttpVersion version=HttpVersion.lookAheadGet(buffer.array(),buffer.arrayOffset()+buffer.position()-1,buffer.arrayOffset()+buffer.limit());
- if (version!=null)
+ int pos = buffer.position()+version.asString().length()-1;
+ if (pos<buffer.limit())
{
- int pos = buffer.position()+version.asString().length()-1;
- if (pos<buffer.limit())
+ byte n=buffer.get(pos);
+ if (n==HttpTokens.CARRIAGE_RETURN)
{
- byte n=buffer.get(pos);
- if (n==HttpTokens.CARRIAGE_RETURN)
- {
- _cr=true;
- _version=version;
- _string.setLength(0);
- buffer.position(pos+1);
- }
- else if (n==HttpTokens.LINE_FEED)
- {
- _version=version;
- _string.setLength(0);
- buffer.position(pos);
- }
+ _cr=true;
+ _version=version;
+ _string.setLength(0);
+ buffer.position(pos+1);
+ }
+ else if (n==HttpTokens.LINE_FEED)
+ {
+ _version=version;
+ _string.setLength(0);
+ buffer.position(pos);
}
}
}
@@ -607,18 +587,18 @@
{
if (_responseHandler!=null)
{
- return_from_parse=_responseHandler.startResponse(_version, _responseStatus, null)||return_from_parse;
+ handle=_responseHandler.startResponse(_version, _responseStatus, null)||handle;
setState(State.HEADER);
}
else
{
// HTTP/0.9
_uri.flip();
- return_from_parse=_requestHandler.startRequest(_method,_methodString,_uri, null)||return_from_parse;
+ handle=_requestHandler.startRequest(_method,_methodString,_uri, null)||handle;
setState(State.END);
BufferUtil.clear(buffer);
- return_from_parse=_handler.headerComplete()||return_from_parse;
- return_from_parse=_handler.messageComplete()||return_from_parse;
+ handle=_handler.headerComplete()||handle;
+ handle=_handler.messageComplete()||handle;
}
}
break;
@@ -629,9 +609,7 @@
if (_version==null)
_version=HttpVersion.CACHE.get(takeString());
if (_version==null)
- {
throw new BadMessage(HttpStatus.BAD_REQUEST_400,"Unknown Version");
- }
// Should we try to cache header fields?
if (_connectionFields==null && _version.getVersion()>=HttpVersion.HTTP_1_1.getVersion())
@@ -643,7 +621,7 @@
setState(State.HEADER);
_uri.flip();
- return_from_parse=_requestHandler.startRequest(_method,_methodString,_uri, _version)||return_from_parse;
+ handle=_requestHandler.startRequest(_method,_methodString,_uri, _version)||handle;
continue;
}
else
@@ -657,7 +635,7 @@
String reason=takeLengthString();
setState(State.HEADER);
- return_from_parse=_responseHandler.startResponse(_version, _responseStatus, reason)||return_from_parse;
+ handle=_responseHandler.startResponse(_version, _responseStatus, reason)||handle;
continue;
}
else
@@ -674,7 +652,7 @@
}
}
- return return_from_parse;
+ return handle;
}
private boolean handleKnownHeaders(ByteBuffer buffer)
@@ -768,7 +746,10 @@
case CONNECTION:
// Don't cache if not persistent
if (_valueString!=null && _valueString.indexOf("close")>=0)
+ {
+ _closed=true;
_connectionFields=null;
+ }
break;
case AUTHORIZATION:
@@ -801,17 +782,15 @@
*/
private boolean parseHeaders(ByteBuffer buffer)
{
- boolean return_from_parse=false;
+ boolean handle=false;
// Process headers
- while (_state.ordinal()<State.END.ordinal() && buffer.hasRemaining() && !return_from_parse)
+ while (_state.ordinal()<State.CONTENT.ordinal() && buffer.hasRemaining() && !handle)
{
// process each character
byte ch=next(buffer);
- if (ch==-1)
- return true;
if (ch==0)
- continue;
+ break;
if (_maxHeaderBytes>0 && ++_headerBytes>_maxHeaderBytes)
{
@@ -855,7 +834,7 @@
_field=null;
return true;
}
- return_from_parse=_handler.parsedHeader(_field!=null?_field:new HttpField(_header,_headerString,_valueString))||return_from_parse;
+ handle=_handler.parsedHeader(_field!=null?_field:new HttpField(_header,_headerString,_valueString))||handle;
}
_headerString=_valueString=null;
_header=null;
@@ -899,23 +878,23 @@
{
case EOF_CONTENT:
setState(State.EOF_CONTENT);
- return_from_parse=_handler.headerComplete()||return_from_parse;
+ handle=_handler.headerComplete()||handle;
break;
case CHUNKED_CONTENT:
setState(State.CHUNKED_CONTENT);
- return_from_parse=_handler.headerComplete()||return_from_parse;
+ handle=_handler.headerComplete()||handle;
break;
case NO_CONTENT:
- return_from_parse=_handler.headerComplete()||return_from_parse;
+ handle=_handler.headerComplete()||handle;
setState(State.END);
- return_from_parse=_handler.messageComplete()||return_from_parse;
+ handle=_handler.messageComplete()||handle;
break;
default:
setState(State.CONTENT);
- return_from_parse=_handler.headerComplete()||return_from_parse;
+ handle=_handler.headerComplete()||handle;
break;
}
}
@@ -1153,7 +1132,7 @@
}
}
- return return_from_parse;
+ return handle;
}
/* ------------------------------------------------------------------------------- */
@@ -1163,208 +1142,107 @@
*/
public boolean parseNext(ByteBuffer buffer)
{
+ if (DEBUG)
+ LOG.debug("parseNext s={} {}",_state,BufferUtil.toDetailString(buffer));
try
{
- // handle initial state
- switch(_state)
+ boolean handle=false;
+
+ // Start a request/response
+ if (_state==State.START)
{
- case START:
- _version=null;
- _method=null;
- _methodString=null;
- _endOfContent=EndOfContent.UNKNOWN_CONTENT;
- _header=null;
- if(quickStart(buffer))
- return true;
- break;
-
- case CONTENT:
- if (_contentPosition==_contentLength)
- {
- setState(State.END);
- if(_handler.messageComplete())
- return true;
- }
- break;
-
- case END:
- // eat white space
- while (buffer.remaining()>0 && buffer.get(buffer.position())<=HttpTokens.SPACE)
- buffer.get();
- return false;
-
- case CLOSED:
- if (BufferUtil.hasContent(buffer))
- {
- // Just ignore data when closed
- _headerBytes+=buffer.remaining();
- BufferUtil.clear(buffer);
- if (_headerBytes>_maxHeaderBytes)
- {
- // Don't want to waste time reading data of a closed request
- throw new IllegalStateException("too much data after closed");
- }
- }
- return false;
- default: break;
-
+ _version=null;
+ _method=null;
+ _methodString=null;
+ _endOfContent=EndOfContent.UNKNOWN_CONTENT;
+ _header=null;
+ handle=quickStart(buffer);
}
-
+
// Request/response line
- if (_state.ordinal()<State.HEADER.ordinal())
- if (parseLine(buffer))
- return true;
+ if (!handle && _state.ordinal()>= State.START.ordinal() && _state.ordinal()<State.HEADER.ordinal())
+ handle=parseLine(buffer);
- if (_state.ordinal()<State.END.ordinal())
- if (parseHeaders(buffer))
- return true;
-
- // Handle HEAD response
- if (_responseStatus>0 && _headResponse)
+ // parse headers
+ if (!handle && _state.ordinal()>= State.HEADER.ordinal() && _state.ordinal()<State.CONTENT.ordinal())
+ handle=parseHeaders(buffer);
+
+ // parse content
+ if (!handle && _state.ordinal()>= State.CONTENT.ordinal() && _state.ordinal()<State.END.ordinal())
{
- setState(State.END);
- if (_handler.messageComplete())
- return true;
- }
-
-
- // Handle _content
- byte ch;
- while (_state.ordinal() > State.END.ordinal() && buffer.hasRemaining())
- {
- switch (_state)
+ // Handle HEAD response
+ if (_responseStatus>0 && _headResponse)
{
- case EOF_CONTENT:
- _contentChunk=buffer.asReadOnlyBuffer();
- _contentPosition += _contentChunk.remaining();
- buffer.position(buffer.position()+_contentChunk.remaining());
- if (_handler.content(_contentChunk))
- return true;
- break;
-
- case CONTENT:
+ setState(State.END);
+ handle=_handler.messageComplete();
+ }
+ else
+ handle=parseContent(buffer);
+ }
+
+ // handle end states
+ if (_state==State.END)
+ {
+ // eat white space
+ while (buffer.remaining()>0 && buffer.get(buffer.position())<=HttpTokens.SPACE)
+ buffer.get();
+ }
+ else if (_state==State.CLOSED)
+ {
+ if (BufferUtil.hasContent(buffer))
+ {
+ // Just ignore data when closed
+ _headerBytes+=buffer.remaining();
+ BufferUtil.clear(buffer);
+ if (_headerBytes>_maxHeaderBytes)
{
- long remaining=_contentLength - _contentPosition;
- if (remaining == 0)
- {
- setState(State.END);
- if (_handler.messageComplete())
- return true;
- }
- else
- {
- _contentChunk=buffer.asReadOnlyBuffer();
-
- // limit content by expected size
- if (_contentChunk.remaining() > remaining)
- {
- // We can cast remaining to an int as we know that it is smaller than
- // or equal to length which is already an int.
- _contentChunk.limit(_contentChunk.position()+(int)remaining);
- }
-
- _contentPosition += _contentChunk.remaining();
- buffer.position(buffer.position()+_contentChunk.remaining());
-
- if (_handler.content(_contentChunk))
- return true;
-
- if(_contentPosition == _contentLength)
- {
- setState(State.END);
- if (_handler.messageComplete())
- return true;
- }
- }
- break;
+ // Don't want to waste time reading data of a closed request
+ throw new IllegalStateException("too much data after closed");
}
-
- case CHUNKED_CONTENT:
- {
- ch=next(buffer);
- if (ch>HttpTokens.SPACE)
- {
- _chunkLength=TypeUtil.convertHexDigit(ch);
- _chunkPosition=0;
- setState(State.CHUNK_SIZE);
- }
-
- break;
- }
-
- case CHUNK_SIZE:
- {
- ch=next(buffer);
- if (ch == HttpTokens.LINE_FEED)
- {
- if (_chunkLength == 0)
- {
- setState(State.END);
- if (_handler.messageComplete())
- return true;
- }
- else
- setState(State.CHUNK);
- }
- else if (ch <= HttpTokens.SPACE || ch == HttpTokens.SEMI_COLON)
- setState(State.CHUNK_PARAMS);
- else
- _chunkLength=_chunkLength * 16 + TypeUtil.convertHexDigit(ch);
- break;
- }
-
- case CHUNK_PARAMS:
- {
- ch=next(buffer);
- if (ch == HttpTokens.LINE_FEED)
- {
- if (_chunkLength == 0)
- {
- setState(State.END);
- if (_handler.messageComplete())
- return true;
- }
- else
- setState(State.CHUNK);
- }
- break;
- }
-
- case CHUNK:
- {
- int remaining=_chunkLength - _chunkPosition;
- if (remaining == 0)
- {
- setState(State.CHUNKED_CONTENT);
- }
- else
- {
- _contentChunk=buffer.asReadOnlyBuffer();
-
- if (_contentChunk.remaining() > remaining)
- _contentChunk.limit(_contentChunk.position()+remaining);
- remaining=_contentChunk.remaining();
-
- _contentPosition += remaining;
- _chunkPosition += remaining;
- buffer.position(buffer.position()+remaining);
- if (_handler.content(_contentChunk))
- return true;
- }
- break;
- }
+ }
+ }
+
+ // Handle EOF
+ if (_eof && !buffer.hasRemaining())
+ {
+ switch(_state)
+ {
case CLOSED:
- {
- BufferUtil.clear(buffer);
- return false;
- }
-
- default:
+ break;
+
+ case START:
+ _handler.earlyEOF();
+ setState(State.CLOSED);
+ break;
+
+ case END:
+ setState(State.CLOSED);
+ break;
+
+ case EOF_CONTENT:
+ handle=_handler.messageComplete()||handle;
+ setState(State.CLOSED);
+ break;
+
+ case CONTENT:
+ case CHUNKED_CONTENT:
+ case CHUNK_SIZE:
+ case CHUNK_PARAMS:
+ case CHUNK:
+ _handler.earlyEOF();
+ setState(State.CLOSED);
+ break;
+
+ default:
+ if (DEBUG)
+ LOG.debug("{} EOF in {}",this,_state);
+ _handler.badMessage(400,null);
+ setState(State.CLOSED);
break;
}
}
-
- return false;
+
+ return handle;
}
catch(BadMessage e)
{
@@ -1382,7 +1260,7 @@
BufferUtil.clear(buffer);
LOG.warn("badMessage: "+e.toString()+" for "+_handler);
- if (DEBUG)
+ if (DEBUG)
LOG.debug(e);
if (_state.ordinal()<=State.END.ordinal())
@@ -1400,85 +1278,187 @@
}
}
- /**
- * Notifies this parser that I/O code read a -1 and therefore no more data will arrive to be parsed.
- * Calling this method may result in an invocation to {@link HttpHandler#messageComplete()}, for
- * example when the content is delimited by the close of the connection.
- * If the parser is already in a state that does not need data (for example, it is idle waiting for
- * a request/response to be parsed), then calling this method is a no-operation.
- *
- * @return the result of the invocation to {@link HttpHandler#messageComplete()} if there has been
- * one, or false otherwise.
- */
- public boolean shutdownInput()
+ private boolean parseContent(ByteBuffer buffer)
{
- if (DEBUG)
- LOG.debug("shutdownInput {}", this);
-
- // was this unexpected?
- switch(_state)
+ // Handle _content
+ byte ch;
+ while (_state.ordinal() < State.END.ordinal() && buffer.hasRemaining())
{
- case START:
- case END:
- break;
+ switch (_state)
+ {
+ case EOF_CONTENT:
+ _contentChunk=buffer.asReadOnlyBuffer();
+ _contentPosition += _contentChunk.remaining();
+ buffer.position(buffer.position()+_contentChunk.remaining());
+ if (_handler.content(_contentChunk))
+ return true;
+ break;
- case EOF_CONTENT:
- setState(State.END);
- return _handler.messageComplete();
+ case CONTENT:
+ {
+ long remaining=_contentLength - _contentPosition;
+ if (remaining == 0)
+ {
+ setState(State.END);
+ if (_handler.messageComplete())
+ return true;
+ }
+ else
+ {
+ _contentChunk=buffer.asReadOnlyBuffer();
- case CLOSED:
- break;
+ // limit content by expected size
+ if (_contentChunk.remaining() > remaining)
+ {
+ // We can cast remaining to an int as we know that it is smaller than
+ // or equal to length which is already an int.
+ _contentChunk.limit(_contentChunk.position()+(int)remaining);
+ }
- default:
- setState(State.END);
- if (!_headResponse)
- _handler.earlyEOF();
- return _handler.messageComplete();
+ _contentPosition += _contentChunk.remaining();
+ buffer.position(buffer.position()+_contentChunk.remaining());
+
+ boolean handle=_handler.content(_contentChunk);
+ if(_contentPosition == _contentLength)
+ {
+ setState(State.END);
+ if (_handler.messageComplete())
+ return true;
+ }
+ if (handle)
+ return true;
+ }
+ break;
+ }
+
+ case CHUNKED_CONTENT:
+ {
+ ch=next(buffer);
+ if (ch>HttpTokens.SPACE)
+ {
+ _chunkLength=TypeUtil.convertHexDigit(ch);
+ _chunkPosition=0;
+ setState(State.CHUNK_SIZE);
+ }
+
+ break;
+ }
+
+ case CHUNK_SIZE:
+ {
+ ch=next(buffer);
+ if (ch==0)
+ break;
+ if (ch == HttpTokens.LINE_FEED)
+ {
+ if (_chunkLength == 0)
+ {
+ setState(State.END);
+ if (_handler.messageComplete())
+ return true;
+ }
+ else
+ setState(State.CHUNK);
+ }
+ else if (ch <= HttpTokens.SPACE || ch == HttpTokens.SEMI_COLON)
+ setState(State.CHUNK_PARAMS);
+ else
+ _chunkLength=_chunkLength * 16 + TypeUtil.convertHexDigit(ch);
+ break;
+ }
+
+ case CHUNK_PARAMS:
+ {
+ ch=next(buffer);
+ if (ch == HttpTokens.LINE_FEED)
+ {
+ if (_chunkLength == 0)
+ {
+ setState(State.END);
+ if (_handler.messageComplete())
+ return true;
+ }
+ else
+ setState(State.CHUNK);
+ }
+ break;
+ }
+
+ case CHUNK:
+ {
+ int remaining=_chunkLength - _chunkPosition;
+ if (remaining == 0)
+ {
+ setState(State.CHUNKED_CONTENT);
+ }
+ else
+ {
+ _contentChunk=buffer.asReadOnlyBuffer();
+
+ if (_contentChunk.remaining() > remaining)
+ _contentChunk.limit(_contentChunk.position()+remaining);
+ remaining=_contentChunk.remaining();
+
+ _contentPosition += remaining;
+ _chunkPosition += remaining;
+ buffer.position(buffer.position()+remaining);
+ if (_handler.content(_contentChunk))
+ return true;
+ }
+ break;
+ }
+
+ case CLOSED:
+ {
+ BufferUtil.clear(buffer);
+ return false;
+ }
+
+ default:
+ break;
+ }
}
-
return false;
}
/* ------------------------------------------------------------------------------- */
+ public boolean isAtEOF()
+
+ {
+ return _eof;
+ }
+
+ /* ------------------------------------------------------------------------------- */
+ public void atEOF()
+
+ {
+ if (DEBUG)
+ LOG.debug("atEOF {}", this);
+ _eof=true;
+ }
+
+ /* ------------------------------------------------------------------------------- */
public void close()
{
if (DEBUG)
LOG.debug("close {}", this);
- switch(_state)
- {
- case START:
- case CLOSED:
- case END:
- break;
-
- case EOF_CONTENT:
- _handler.messageComplete();
- break;
-
- default:
- if (_state.ordinal()>State.END.ordinal())
- {
- _handler.earlyEOF();
- _handler.messageComplete();
- }
- else
- LOG.warn("Closing {}",this);
- }
setState(State.CLOSED);
- _endOfContent=EndOfContent.UNKNOWN_CONTENT;
- _contentLength=-1;
- _contentPosition=0;
- _responseStatus=0;
- _headerBytes=0;
- _contentChunk=null;
}
-
+
/* ------------------------------------------------------------------------------- */
public void reset()
{
if (DEBUG)
LOG.debug("reset {}", this);
// reset state
+ if (_state==State.CLOSED)
+ return;
+ if (_closed)
+ {
+ setState(State.CLOSED);
+ return;
+ }
+
setState(State.START);
_endOfContent=EndOfContent.UNKNOWN_CONTENT;
_contentLength=-1;
diff --git a/jetty-http/src/test/java/org/eclipse/jetty/http/HttpFieldsTest.java b/jetty-http/src/test/java/org/eclipse/jetty/http/HttpFieldsTest.java
index 3f8a91d..8cc0ad0 100644
--- a/jetty-http/src/test/java/org/eclipse/jetty/http/HttpFieldsTest.java
+++ b/jetty-http/src/test/java/org/eclipse/jetty/http/HttpFieldsTest.java
@@ -344,7 +344,7 @@
fields.clear();
fields.addSetCookie("ev erything","va lue","do main","pa th",1,"co mment",true,true,2);
String setCookie=fields.getStringField("Set-Cookie");
- assertThat(setCookie,Matchers.startsWith("\"ev erything\"=\"va lue\";Path=\"pa th\";Domain=\"do main\";Expires="));
+ assertThat(setCookie,Matchers.startsWith("\"ev erything\"=\"va lue\";Version=2;Path=\"pa th\";Domain=\"do main\";Expires="));
assertThat(setCookie,Matchers.endsWith(" GMT;Max-Age=1;Secure;HttpOnly;Comment=\"co mment\""));
fields.clear();
diff --git a/jetty-http/src/test/java/org/eclipse/jetty/http/HttpParserTest.java b/jetty-http/src/test/java/org/eclipse/jetty/http/HttpParserTest.java
index b65e10a..7c197a1 100644
--- a/jetty-http/src/test/java/org/eclipse/jetty/http/HttpParserTest.java
+++ b/jetty-http/src/test/java/org/eclipse/jetty/http/HttpParserTest.java
@@ -74,7 +74,7 @@
assertEquals("POST", _methodOrVersion);
assertEquals("/foo", _uriOrStatus);
assertEquals("HTTP/1.0", _versionOrReason);
- assertEquals(-1, _h);
+ assertEquals(-1, _headers);
}
@Test
@@ -89,7 +89,7 @@
assertEquals("GET", _methodOrVersion);
assertEquals("/999", _uriOrStatus);
assertEquals(null, _versionOrReason);
- assertEquals(-1, _h);
+ assertEquals(-1, _headers);
}
@Test
@@ -104,7 +104,7 @@
assertEquals("POST", _methodOrVersion);
assertEquals("/222", _uriOrStatus);
assertEquals(null, _versionOrReason);
- assertEquals(-1, _h);
+ assertEquals(-1, _headers);
}
@Test
@@ -118,7 +118,7 @@
assertEquals("POST", _methodOrVersion);
assertEquals("/fo\u0690", _uriOrStatus);
assertEquals("HTTP/1.0", _versionOrReason);
- assertEquals(-1, _h);
+ assertEquals(-1, _headers);
}
@Test
@@ -132,7 +132,7 @@
assertEquals("POST", _methodOrVersion);
assertEquals("/foo?param=\u0690", _uriOrStatus);
assertEquals("HTTP/1.0", _versionOrReason);
- assertEquals(-1, _h);
+ assertEquals(-1, _headers);
}
@Test
@@ -146,7 +146,7 @@
assertEquals("POST", _methodOrVersion);
assertEquals("/123456789abcdef/123456789abcdef/123456789abcdef/123456789abcdef/123456789abcdef/123456789abcdef/123456789abcdef/123456789abcdef/123456789abcdef/123456789abcdef/123456789abcdef/123456789abcdef/123456789abcdef/123456789abcdef/123456789abcdef/123456789abcdef/", _uriOrStatus);
assertEquals("HTTP/1.0", _versionOrReason);
- assertEquals(-1, _h);
+ assertEquals(-1, _headers);
}
@Test
@@ -159,7 +159,32 @@
assertEquals("CONNECT", _methodOrVersion);
assertEquals("192.168.1.2:80", _uriOrStatus);
assertEquals("HTTP/1.1", _versionOrReason);
- assertEquals(-1, _h);
+ assertEquals(-1, _headers);
+ }
+
+ @Test
+ public void testSimple() throws Exception
+ {
+ ByteBuffer buffer= BufferUtil.toBuffer(
+ "GET / HTTP/1.0\015\012" +
+ "Host: localhost\015\012" +
+ "Connection: close\015\012" +
+ "\015\012");
+
+ HttpParser.RequestHandler<ByteBuffer> handler = new Handler();
+ HttpParser parser= new HttpParser(handler);
+ parseAll(parser,buffer);
+
+ assertTrue(_headerCompleted);
+ assertTrue(_messageCompleted);
+ assertEquals("GET", _methodOrVersion);
+ assertEquals("/", _uriOrStatus);
+ assertEquals("HTTP/1.0", _versionOrReason);
+ assertEquals("Host", _hdr[0]);
+ assertEquals("localhost", _val[0]);
+ assertEquals("Connection", _hdr[1]);
+ assertEquals("close", _val[1]);
+ assertEquals(1, _headers);
}
@Test
@@ -212,7 +237,7 @@
assertEquals("gzip, deflated", _val[8]);
assertEquals("Accept", _hdr[9]);
assertEquals("unknown", _val[9]);
- assertEquals(9, _h);
+ assertEquals(9, _headers);
}
@Test
@@ -260,7 +285,7 @@
assertEquals("gzip, deflated", _val[8]);
assertEquals("Accept", _hdr[9]);
assertEquals("unknown", _val[9]);
- assertEquals(9, _h);
+ assertEquals(9, _headers);
}
@@ -310,7 +335,7 @@
assertEquals("gzip, deflated", _val[8]);
assertEquals("Accept", _hdr[9]);
assertEquals("unknown", _val[9]);
- assertEquals(9, _h);
+ assertEquals(9, _headers);
}
@Test
@@ -365,7 +390,7 @@
assertEquals("value4", _val[4]);
assertEquals("Server5", _hdr[5]);
assertEquals("notServer", _val[5]);
- assertEquals(5, _h);
+ assertEquals(5, _headers);
}
}
@@ -390,13 +415,74 @@
assertEquals("GET", _methodOrVersion);
assertEquals("/chunk", _uriOrStatus);
assertEquals("HTTP/1.0", _versionOrReason);
- assertEquals(1, _h);
+ assertEquals(1, _headers);
assertEquals("Header1", _hdr[0]);
assertEquals("value1", _val[0]);
assertEquals("0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ", _content);
}
@Test
+ public void testStartEOF() throws Exception
+ {
+ HttpParser.RequestHandler<ByteBuffer> handler = new Handler();
+ HttpParser parser= new HttpParser(handler);
+ parser.atEOF();
+ parser.parseNext(BufferUtil.EMPTY_BUFFER);
+
+ assertTrue(_early);
+ assertEquals(null,_bad);
+ }
+
+ @Test
+ public void testEarlyEOF() throws Exception
+ {
+ ByteBuffer buffer= BufferUtil.toBuffer(
+ "GET /uri HTTP/1.0\015\012"
+ + "Content-Length: 20\015\012"
+ + "\015\012"
+ + "0123456789");
+ HttpParser.RequestHandler<ByteBuffer> handler = new Handler();
+ HttpParser parser= new HttpParser(handler);
+ parser.atEOF();
+ parseAll(parser,buffer);
+
+ assertEquals("GET", _methodOrVersion);
+ assertEquals("/uri", _uriOrStatus);
+ assertEquals("HTTP/1.0", _versionOrReason);
+ assertEquals("0123456789", _content);
+
+ assertTrue(_early);
+ }
+
+ @Test
+ public void testChunkEarlyEOF() throws Exception
+ {
+ ByteBuffer buffer= BufferUtil.toBuffer(
+ "GET /chunk HTTP/1.0\015\012"
+ + "Header1: value1\015\012"
+ + "Transfer-Encoding: chunked\015\012"
+ + "\015\012"
+ + "a;\015\012"
+ + "0123456789\015\012");
+ HttpParser.RequestHandler<ByteBuffer> handler = new Handler();
+ HttpParser parser= new HttpParser(handler);
+ parser.atEOF();
+ parseAll(parser,buffer);
+
+ assertEquals("GET", _methodOrVersion);
+ assertEquals("/chunk", _uriOrStatus);
+ assertEquals("HTTP/1.0", _versionOrReason);
+ assertEquals(1, _headers);
+ assertEquals("Header1", _hdr[0]);
+ assertEquals("value1", _val[0]);
+ assertEquals("0123456789", _content);
+
+ assertTrue(_early);
+
+ }
+
+
+ @Test
public void testMultiParse() throws Exception
{
ByteBuffer buffer= BufferUtil.toBuffer(
@@ -433,7 +519,7 @@
assertEquals("GET", _methodOrVersion);
assertEquals("/mp", _uriOrStatus);
assertEquals("HTTP/1.0", _versionOrReason);
- assertEquals(2, _h);
+ assertEquals(2, _headers);
assertEquals("Header1", _hdr[1]);
assertEquals("value1", _val[1]);
assertEquals("0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ", _content);
@@ -444,7 +530,7 @@
assertEquals("POST", _methodOrVersion);
assertEquals("/foo", _uriOrStatus);
assertEquals("HTTP/1.0", _versionOrReason);
- assertEquals(2, _h);
+ assertEquals(2, _headers);
assertEquals("Header2", _hdr[1]);
assertEquals("value2", _val[1]);
assertEquals(null, _content);
@@ -452,17 +538,86 @@
parser.reset();
init();
parser.parseNext(buffer);
- parser.shutdownInput();
+ parser.atEOF();
assertEquals("PUT", _methodOrVersion);
assertEquals("/doodle", _uriOrStatus);
assertEquals("HTTP/1.0", _versionOrReason);
- assertEquals(2, _h);
+ assertEquals(2, _headers);
assertEquals("Header3", _hdr[1]);
assertEquals("value3", _val[1]);
assertEquals("0123456789", _content);
}
+
@Test
+ public void testMultiParseEarlyEOF() throws Exception
+ {
+ ByteBuffer buffer0= BufferUtil.toBuffer(
+ "GET /mp HTTP/1.0\015\012"
+ + "Connection: Keep-Alive\015\012");
+
+ ByteBuffer buffer1= BufferUtil.toBuffer("Header1: value1\015\012"
+ + "Transfer-Encoding: chunked\015\012"
+ + "\015\012"
+ + "a;\015\012"
+ + "0123456789\015\012"
+ + "1a\015\012"
+ + "ABCDEFGHIJKLMNOPQRSTUVWXYZ\015\012"
+ + "0\015\012"
+
+ + "\015\012"
+
+ + "POST /foo HTTP/1.0\015\012"
+ + "Connection: Keep-Alive\015\012"
+ + "Header2: value2\015\012"
+ + "Content-Length: 0\015\012"
+ + "\015\012"
+
+ + "PUT /doodle HTTP/1.0\015\012"
+ + "Connection: close\015\012"
+ + "Header3: value3\015\012"
+ + "Content-Length: 10\015\012"
+ + "\015\012"
+ + "0123456789\015\012");
+
+
+ HttpParser.RequestHandler<ByteBuffer> handler = new Handler();
+ HttpParser parser= new HttpParser(handler);
+ parser.parseNext(buffer0);
+ parser.atEOF();
+ parser.parseNext(buffer1);
+ assertEquals("GET", _methodOrVersion);
+ assertEquals("/mp", _uriOrStatus);
+ assertEquals("HTTP/1.0", _versionOrReason);
+ assertEquals(2, _headers);
+ assertEquals("Header1", _hdr[1]);
+ assertEquals("value1", _val[1]);
+ assertEquals("0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ", _content);
+
+ parser.reset();
+ init();
+ parser.parseNext(buffer1);
+ assertEquals("POST", _methodOrVersion);
+ assertEquals("/foo", _uriOrStatus);
+ assertEquals("HTTP/1.0", _versionOrReason);
+ assertEquals(2, _headers);
+ assertEquals("Header2", _hdr[1]);
+ assertEquals("value2", _val[1]);
+ assertEquals(null, _content);
+
+ parser.reset();
+ init();
+ parser.parseNext(buffer1);
+ assertEquals("PUT", _methodOrVersion);
+ assertEquals("/doodle", _uriOrStatus);
+ assertEquals("HTTP/1.0", _versionOrReason);
+ assertEquals(2, _headers);
+ assertEquals("Header3", _hdr[1]);
+ assertEquals("value3", _val[1]);
+ assertEquals("0123456789", _content);
+ }
+
+ @Test
public void testResponseParse0() throws Exception
{
ByteBuffer buffer= BufferUtil.toBuffer(
@@ -528,7 +683,7 @@
init();
parser.parseNext(buffer);
- parser.shutdownInput();
+ parser.atEOF();
assertEquals("HTTP/1.1", _methodOrVersion);
assertEquals("200", _uriOrStatus);
assertEquals("Correct", _versionOrReason);
@@ -581,6 +736,29 @@
}
@Test
+ public void testResponseEOFContent() throws Exception
+ {
+ ByteBuffer buffer= BufferUtil.toBuffer(
+ "HTTP/1.1 200 \015\012"
+ + "Content-Type: text/plain\015\012"
+ + "\015\012"
+ + "0123456789\015\012");
+
+ HttpParser.ResponseHandler<ByteBuffer> handler = new Handler();
+ HttpParser parser= new HttpParser(handler);
+ parser.atEOF();
+ parser.parseNext(buffer);
+
+ assertEquals("HTTP/1.1", _methodOrVersion);
+ assertEquals("200", _uriOrStatus);
+ assertEquals(null, _versionOrReason);
+ assertEquals(12,_content.length());
+ assertEquals("0123456789\015\012",_content);
+ assertTrue(_headerCompleted);
+ assertTrue(_messageCompleted);
+ }
+
+ @Test
public void testResponse304WithContentLength() throws Exception
{
ByteBuffer buffer= BufferUtil.toBuffer(
@@ -627,7 +805,7 @@
+ "Connection: close\015\012"
+ "\015\012"
+ "\015\012" // extra CRLF ignored
- + "HTTP/1.1 400 OK\015\012"); // extra data causes close
+ + "HTTP/1.1 400 OK\015\012"); // extra data causes close ??
HttpParser.ResponseHandler<ByteBuffer> handler = new Handler();
@@ -641,8 +819,13 @@
assertTrue(_headerCompleted);
assertTrue(_messageCompleted);
-
+ parser.reset();
+ parser.parseNext(buffer);
+ assertFalse(buffer.hasRemaining());
+ assertTrue(parser.isClosed());
}
+
+
@Test
public void testNoURI() throws Exception
@@ -1025,7 +1208,7 @@
_versionOrReason=null;
_hdr=null;
_val=null;
- _h=0;
+ _headers=0;
_headerCompleted=false;
_messageCompleted=false;
}
@@ -1040,15 +1223,15 @@
private List<HttpField> _fields=new ArrayList<>();
private String[] _hdr;
private String[] _val;
- private int _h;
-
+ private int _headers;
+
+ private boolean _early;
private boolean _headerCompleted;
private boolean _messageCompleted;
private class Handler implements HttpParser.RequestHandler<ByteBuffer>, HttpParser.ResponseHandler<ByteBuffer>
{
private HttpFields fields;
- private boolean request;
@Override
public boolean content(ByteBuffer ref)
@@ -1066,8 +1249,7 @@
public boolean startRequest(HttpMethod httpMethod, String method, ByteBuffer uri, HttpVersion version)
{
_fields.clear();
- request=true;
- _h= -1;
+ _headers= -1;
_hdr= new String[10];
_val= new String[10];
_methodOrVersion= method;
@@ -1077,6 +1259,7 @@
fields=new HttpFields();
_messageCompleted = false;
_headerCompleted = false;
+ _early=false;
return false;
}
@@ -1085,8 +1268,8 @@
{
_fields.add(field);
//System.err.println("header "+name+": "+value);
- _hdr[++_h]= field.getName();
- _val[_h]= field.getValue();
+ _hdr[++_headers]= field.getName();
+ _val[_headers]= field.getValue();
return false;
}
@@ -1125,14 +1308,13 @@
@Override
public void badMessage(int status, String reason)
{
- _bad=reason;
+ _bad=reason==null?(""+status):reason;
}
@Override
public boolean startResponse(HttpVersion version, int status, String reason)
{
_fields.clear();
- request=false;
_methodOrVersion = version.asString();
_uriOrStatus = Integer.toString(status);
_versionOrReason = reason==null?null:reason.toString();
@@ -1149,6 +1331,7 @@
@Override
public void earlyEOF()
{
+ _early=true;
}
@Override
diff --git a/jetty-io/pom.xml b/jetty-io/pom.xml
index 3da554a..6a698a7 100644
--- a/jetty-io/pom.xml
+++ b/jetty-io/pom.xml
@@ -2,7 +2,7 @@
<parent>
<artifactId>jetty-project</artifactId>
<groupId>org.eclipse.jetty</groupId>
- <version>9.0.5-SNAPSHOT</version>
+ <version>9.1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>jetty-io</artifactId>
diff --git a/jetty-io/src/main/java/org/eclipse/jetty/io/AbstractConnection.java b/jetty-io/src/main/java/org/eclipse/jetty/io/AbstractConnection.java
index 3fc9bef..11c4b1a 100644
--- a/jetty-io/src/main/java/org/eclipse/jetty/io/AbstractConnection.java
+++ b/jetty-io/src/main/java/org/eclipse/jetty/io/AbstractConnection.java
@@ -24,7 +24,6 @@
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicReference;
-import org.eclipse.jetty.util.BlockingCallback;
import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;
@@ -43,7 +42,7 @@
public static final boolean EXECUTE_ONFILLABLE=true;
private final List<Listener> listeners = new CopyOnWriteArrayList<>();
- private final AtomicReference<State> _state = new AtomicReference<>(State.IDLE);
+ private final AtomicReference<State> _state = new AtomicReference<>(IDLE);
private final long _created=System.currentTimeMillis();
private final EndPoint _endPoint;
private final Executor _executor;
@@ -64,6 +63,7 @@
_executor = executor;
_readCallback = new ReadCallback();
_executeOnfillable=executeOnfillable;
+ _state.set(IDLE);
}
@Override
@@ -95,149 +95,32 @@
*/
public void fillInterested()
{
+ LOG.debug("fillInterested {}",this);
+
+ while(true)
+ {
+ State state=_state.get();
+ if (next(state,state.fillInterested()))
+ break;
+ }
+ }
+
+ public void fillInterested(Callback callback)
+ {
LOG.debug("fillInterested {}",this);
- loop:while(true)
+ while(true)
{
- switch(_state.get())
- {
- case IDLE:
- if (_state.compareAndSet(State.IDLE,State.INTERESTED))
- {
- getEndPoint().fillInterested(_readCallback);
- break loop;
- }
- break;
-
- case FILLING:
- if (_state.compareAndSet(State.FILLING,State.FILLING_INTERESTED))
- break loop;
- break;
-
- case FILLING_BLOCKED:
- if (_state.compareAndSet(State.FILLING_BLOCKED,State.FILLING_BLOCKED_INTERESTED))
- break loop;
- break;
-
- case BLOCKED:
- if (_state.compareAndSet(State.BLOCKED,State.BLOCKED_INTERESTED))
- break loop;
- break;
-
- case FILLING_BLOCKED_INTERESTED:
- case FILLING_INTERESTED:
- case BLOCKED_INTERESTED:
- case INTERESTED:
- break loop;
- }
+ State state=_state.get();
+ // TODO yuck
+ if (state instanceof FillingInterestedCallback && ((FillingInterestedCallback)state)._callback==callback)
+ break;
+ State next=new FillingInterestedCallback(callback,state);
+ if (next(state,next))
+ break;
}
}
-
- private void unblock()
- {
- LOG.debug("unblock {}",this);
-
- loop:while(true)
- {
- switch(_state.get())
- {
- case FILLING_BLOCKED:
- if (_state.compareAndSet(State.FILLING_BLOCKED,State.FILLING))
- break loop;
- break;
-
- case FILLING_BLOCKED_INTERESTED:
- if (_state.compareAndSet(State.FILLING_BLOCKED_INTERESTED,State.FILLING_INTERESTED))
- break loop;
- break;
-
- case BLOCKED_INTERESTED:
- if (_state.compareAndSet(State.BLOCKED_INTERESTED,State.INTERESTED))
- {
- getEndPoint().fillInterested(_readCallback);
- break loop;
- }
- break;
-
- case BLOCKED:
- if (_state.compareAndSet(State.BLOCKED,State.IDLE))
- break loop;
- break;
-
- case FILLING:
- case IDLE:
- case FILLING_INTERESTED:
- case INTERESTED:
- break loop;
- }
- }
- }
-
-
- /**
- */
- protected void block(final BlockingCallback callback)
- {
- LOG.debug("block {}",this);
-
- final Callback blocked=new Callback()
- {
- @Override
- public void succeeded()
- {
- unblock();
- callback.succeeded();
- }
-
- @Override
- public void failed(Throwable x)
- {
- unblock();
- callback.failed(x);
- }
- };
-
- loop:while(true)
- {
- switch(_state.get())
- {
- case IDLE:
- if (_state.compareAndSet(State.IDLE,State.BLOCKED))
- {
- getEndPoint().fillInterested(blocked);
- break loop;
- }
- break;
-
- case FILLING:
- if (_state.compareAndSet(State.FILLING,State.FILLING_BLOCKED))
- {
- getEndPoint().fillInterested(blocked);
- break loop;
- }
- break;
-
- case FILLING_INTERESTED:
- if (_state.compareAndSet(State.FILLING_INTERESTED,State.FILLING_BLOCKED_INTERESTED))
- {
- getEndPoint().fillInterested(blocked);
- break loop;
- }
- break;
-
- case BLOCKED:
- case BLOCKED_INTERESTED:
- case FILLING_BLOCKED:
- case FILLING_BLOCKED_INTERESTED:
- throw new IllegalStateException("Already Blocked");
-
- case INTERESTED:
- throw new IllegalStateException();
- }
- }
- }
-
/**
* <p>Callback method invoked when the endpoint is ready to be read.</p>
* @see #fillInterested()
@@ -264,6 +147,9 @@
_endPoint.shutdownOutput();
}
}
+
+ if (_endPoint.isOpen())
+ fillInterested();
}
/**
@@ -340,85 +226,326 @@
{
return String.format("%s@%x{%s}", getClass().getSimpleName(), hashCode(), _state.get());
}
-
- private enum State
+
+ public boolean next(State state, State next)
{
- IDLE, INTERESTED, FILLING, FILLING_INTERESTED, FILLING_BLOCKED, BLOCKED, FILLING_BLOCKED_INTERESTED, BLOCKED_INTERESTED
+ if (next==null)
+ return true;
+ if(_state.compareAndSet(state,next))
+ {
+ LOG.debug("{}-->{} {}",state,next,this);
+ if (next!=state)
+ next.onEnter(AbstractConnection.this);
+ return true;
+ }
+ return false;
}
- private class ReadCallback implements Callback, Runnable
+ private static final class IdleState extends State
+ {
+ private IdleState()
+ {
+ super("IDLE");
+ }
+
+ @Override
+ State fillInterested()
+ {
+ return FILL_INTERESTED;
+ }
+ }
+
+
+ private static final class FillInterestedState extends State
+ {
+ private FillInterestedState()
+ {
+ super("FILL_INTERESTED");
+ }
+
+ @Override
+ public void onEnter(AbstractConnection connection)
+ {
+ connection.getEndPoint().fillInterested(connection._readCallback);
+ }
+
+ @Override
+ State fillInterested()
+ {
+ return this;
+ }
+
+ @Override
+ public State onFillable()
+ {
+ return FILLING;
+ }
+
+ @Override
+ State onFailed()
+ {
+ return IDLE;
+ }
+ }
+
+
+ private static final class RefillingState extends State
+ {
+ private RefillingState()
+ {
+ super("REFILLING");
+ }
+
+ @Override
+ State fillInterested()
+ {
+ return FILLING_FILL_INTERESTED;
+ }
+
+ @Override
+ public State onFilled()
+ {
+ return IDLE;
+ }
+ }
+
+
+ private static final class FillingFillInterestedState extends State
+ {
+ private FillingFillInterestedState(String name)
+ {
+ super(name);
+ }
+
+ @Override
+ State fillInterested()
+ {
+ return this;
+ }
+
+ State onFilled()
+ {
+ return FILL_INTERESTED;
+ }
+ }
+
+
+ private static final class FillingState extends State
+ {
+ private FillingState()
+ {
+ super("FILLING");
+ }
+
+ @Override
+ public void onEnter(AbstractConnection connection)
+ {
+ if (connection._executeOnfillable)
+ connection.getExecutor().execute(connection._runOnFillable);
+ else
+ connection._runOnFillable.run();
+ }
+
+ @Override
+ State fillInterested()
+ {
+ return FILLING_FILL_INTERESTED;
+ }
+
+ @Override
+ public State onFilled()
+ {
+ return IDLE;
+ }
+ }
+
+
+ public static class State
+ {
+ private final String _name;
+ State(String name)
+ {
+ _name=name;
+ }
+
+ @Override
+ public String toString()
+ {
+ return _name;
+ }
+
+ void onEnter(AbstractConnection connection)
+ {
+ }
+
+ State fillInterested()
+ {
+ throw new IllegalStateException(this.toString());
+ }
+
+ State onFillable()
+ {
+ throw new IllegalStateException(this.toString());
+ }
+
+ State onFilled()
+ {
+ throw new IllegalStateException(this.toString());
+ }
+
+ State onFailed()
+ {
+ throw new IllegalStateException(this.toString());
+ }
+ }
+
+
+ public static final State IDLE=new IdleState();
+
+ public static final State FILL_INTERESTED=new FillInterestedState();
+
+ public static final State FILLING=new FillingState();
+
+ public static final State REFILLING=new RefillingState();
+
+ public static final State FILLING_FILL_INTERESTED=new FillingFillInterestedState("FILLING_FILL_INTERESTED");
+
+ public class NestedState extends State
+ {
+ private final State _nested;
+
+ NestedState(State nested)
+ {
+ super("NESTED("+nested+")");
+ _nested=nested;
+ }
+ NestedState(String name,State nested)
+ {
+ super(name+"("+nested+")");
+ _nested=nested;
+ }
+
+ @Override
+ State fillInterested()
+ {
+ return new NestedState(_nested.fillInterested());
+ }
+
+ @Override
+ State onFillable()
+ {
+ return new NestedState(_nested.onFillable());
+ }
+
+ @Override
+ State onFilled()
+ {
+ return new NestedState(_nested.onFilled());
+ }
+ }
+
+
+ public class FillingInterestedCallback extends NestedState
+ {
+ private final Callback _callback;
+
+ FillingInterestedCallback(Callback callback,State nested)
+ {
+ super("FILLING_INTERESTED_CALLBACK",nested==FILLING?REFILLING:nested);
+ _callback=callback;
+ }
+
+ @Override
+ void onEnter(final AbstractConnection connection)
+ {
+ Callback callback=new Callback()
+ {
+ @Override
+ public void succeeded()
+ {
+ while(true)
+ {
+ State state = connection._state.get();
+ if (!(state instanceof NestedState))
+ break;
+ State nested=((NestedState)state)._nested;
+ if (connection.next(state,nested))
+ break;
+ }
+ _callback.succeeded();
+ }
+
+ @Override
+ public void failed(Throwable x)
+ {
+ while(true)
+ {
+ State state = connection._state.get();
+ if (!(state instanceof NestedState))
+ break;
+ State nested=((NestedState)state)._nested;
+ if (connection.next(state,nested))
+ break;
+ }
+ _callback.failed(x);
+ }
+ };
+
+ connection.getEndPoint().fillInterested(callback);
+ }
+ }
+
+ private final Runnable _runOnFillable = new Runnable()
{
@Override
public void run()
{
- if (_state.compareAndSet(State.INTERESTED,State.FILLING))
+ try
{
- try
+ onFillable();
+ }
+ finally
+ {
+ while(true)
{
- onFillable();
- }
- finally
- {
- loop:while(true)
- {
- switch(_state.get())
- {
- case IDLE:
- case INTERESTED:
- case BLOCKED:
- case BLOCKED_INTERESTED:
- LOG.warn(new IllegalStateException());
- return;
-
- case FILLING:
- if (_state.compareAndSet(State.FILLING,State.IDLE))
- break loop;
- break;
-
- case FILLING_BLOCKED:
- if (_state.compareAndSet(State.FILLING_BLOCKED,State.BLOCKED))
- break loop;
- break;
-
- case FILLING_BLOCKED_INTERESTED:
- if (_state.compareAndSet(State.FILLING_BLOCKED_INTERESTED,State.BLOCKED_INTERESTED))
- break loop;
- break;
-
- case FILLING_INTERESTED:
- if (_state.compareAndSet(State.FILLING_INTERESTED,State.INTERESTED))
- {
- getEndPoint().fillInterested(_readCallback);
- break loop;
- }
- break;
- }
- }
+ State state=_state.get();
+ if (next(state,state.onFilled()))
+ break;
}
}
- else
- LOG.warn(new IllegalStateException());
}
-
+ };
+
+
+ private class ReadCallback implements Callback
+ {
@Override
public void succeeded()
{
- if (_executeOnfillable)
- _executor.execute(this);
- else
- run();
+ while(true)
+ {
+ State state=_state.get();
+ if (next(state,state.onFillable()))
+ break;
+ }
}
@Override
public void failed(Throwable x)
{
+ while(true)
+ {
+ State state=_state.get();
+ if (next(state,state.onFailed()))
+ break;
+ }
onFillInterestedFailed(x);
}
@Override
public String toString()
{
- return String.format("AC.ExReadCB@%x", AbstractConnection.this.hashCode());
+ return String.format("AC.ReadCB@%x{%s}", AbstractConnection.this.hashCode(),AbstractConnection.this);
}
};
}
diff --git a/jetty-io/src/main/java/org/eclipse/jetty/io/AbstractEndPoint.java b/jetty-io/src/main/java/org/eclipse/jetty/io/AbstractEndPoint.java
index 4ec1d84..9ec9e60 100644
--- a/jetty-io/src/main/java/org/eclipse/jetty/io/AbstractEndPoint.java
+++ b/jetty-io/src/main/java/org/eclipse/jetty/io/AbstractEndPoint.java
@@ -152,17 +152,17 @@
@Override
public String toString()
{
- return String.format("%s@%x{%s<r-l>%s,o=%b,is=%b,os=%b,fi=%s,wf=%s,it=%d}{%s}",
+ return String.format("%s@%x{%s<->%d,%s,%s,%s,%s,%s,%d,%s}",
getClass().getSimpleName(),
hashCode(),
getRemoteAddress(),
- getLocalAddress(),
- isOpen(),
- isInputShutdown(),
- isOutputShutdown(),
- _fillInterest,
- _writeFlusher,
+ getLocalAddress().getPort(),
+ isOpen()?"Open":"CLOSED",
+ isInputShutdown()?"ISHUT":"in",
+ isOutputShutdown()?"OSHUT":"out",
+ _fillInterest.isInterested()?"R":"-",
+ _writeFlusher.isInProgress()?"W":"-",
getIdleTimeout(),
- getConnection());
+ getConnection()==null?null:getConnection().getClass().getSimpleName());
}
}
diff --git a/jetty-io/src/main/java/org/eclipse/jetty/io/FillInterest.java b/jetty-io/src/main/java/org/eclipse/jetty/io/FillInterest.java
index 6af94f5..5460974 100644
--- a/jetty-io/src/main/java/org/eclipse/jetty/io/FillInterest.java
+++ b/jetty-io/src/main/java/org/eclipse/jetty/io/FillInterest.java
@@ -25,6 +25,8 @@
import java.util.concurrent.atomic.AtomicReference;
import org.eclipse.jetty.util.Callback;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
/* ------------------------------------------------------------ */
@@ -35,6 +37,7 @@
*/
public abstract class FillInterest
{
+ private final static Logger LOG = Log.getLogger(FillInterest.class);
private final AtomicReference<Callback> _interested = new AtomicReference<>(null);
/* ------------------------------------------------------------ */
@@ -56,7 +59,10 @@
throw new IllegalArgumentException();
if (!_interested.compareAndSet(null,callback))
+ {
+ LOG.warn("Read pending for "+_interested.get()+" pervented "+callback);
throw new ReadPendingException();
+ }
try
{
if (needsFill())
diff --git a/jetty-io/src/main/java/org/eclipse/jetty/io/WriteFlusher.java b/jetty-io/src/main/java/org/eclipse/jetty/io/WriteFlusher.java
index 8b3def6..670217b 100644
--- a/jetty-io/src/main/java/org/eclipse/jetty/io/WriteFlusher.java
+++ b/jetty-io/src/main/java/org/eclipse/jetty/io/WriteFlusher.java
@@ -432,9 +432,6 @@
public void onFail(Throwable cause)
{
- if (DEBUG)
- LOG.debug("failed: {} {}", this, cause);
-
// Keep trying to handle the failure until we get to IDLE or FAILED state
while(true)
{
@@ -443,9 +440,14 @@
{
case IDLE:
case FAILED:
+ if (DEBUG)
+ LOG.debug("ignored: {} {}", this, cause);
return;
case PENDING:
+ if (DEBUG)
+ LOG.debug("failed: {} {}", this, cause);
+
PendingState pending = (PendingState)current;
if (updateState(pending,__IDLE))
{
@@ -455,6 +457,9 @@
break;
default:
+ if (DEBUG)
+ LOG.debug("failed: {} {}", this, cause);
+
if (updateState(current,new FailedState(cause)))
return;
break;
diff --git a/jetty-io/src/main/java/org/eclipse/jetty/io/ssl/SslConnection.java b/jetty-io/src/main/java/org/eclipse/jetty/io/ssl/SslConnection.java
index b972ce9..04815c6 100644
--- a/jetty-io/src/main/java/org/eclipse/jetty/io/ssl/SslConnection.java
+++ b/jetty-io/src/main/java/org/eclipse/jetty/io/ssl/SslConnection.java
@@ -189,7 +189,7 @@
// filling.
if (DEBUG)
- LOG.debug("onFillable enter {}", getEndPoint());
+ LOG.debug("onFillable enter {}", _decryptedEndPoint);
// We have received a close handshake, close the end point to send FIN.
if (_decryptedEndPoint.isInputShutdown())
@@ -210,7 +210,7 @@
}
if (DEBUG)
- LOG.debug("onFillable exit {}", getEndPoint());
+ LOG.debug("onFillable exit {}", _decryptedEndPoint);
}
@Override
diff --git a/jetty-jaas/pom.xml b/jetty-jaas/pom.xml
index d84da94..313e218 100644
--- a/jetty-jaas/pom.xml
+++ b/jetty-jaas/pom.xml
@@ -2,7 +2,7 @@
<parent>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-project</artifactId>
- <version>9.0.5-SNAPSHOT</version>
+ <version>9.1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>jetty-jaas</artifactId>
@@ -13,9 +13,6 @@
</properties>
<build>
<plugins>
-<!--
- COMMENTED OUT UNTIL CORRECT CONFIG IS FOUND FOR Export uses clauses
--->
<plugin>
<groupId>org.apache.felix</groupId>
<artifactId>maven-bundle-plugin</artifactId>
@@ -29,7 +26,7 @@
<instructions>
<_versionpolicy> </_versionpolicy>
<Import-Package>javax.sql.*,javax.security.*,javax.naming.*,
- javax.servlet.*;version="2.6.0",
+ javax.servlet.*;version="[2.6.0,3.2)",
*</Import-Package>
</instructions>
</configuration>
diff --git a/jetty-jaspi/pom.xml b/jetty-jaspi/pom.xml
index 414e03b..71be42c 100644
--- a/jetty-jaspi/pom.xml
+++ b/jetty-jaspi/pom.xml
@@ -2,7 +2,7 @@
<parent>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-project</artifactId>
- <version>9.0.5-SNAPSHOT</version>
+ <version>9.1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>jetty-jaspi</artifactId>
@@ -25,7 +25,7 @@
</goals>
<configuration>
<instructions>
- <Import-Package>javax.servlet.*;version="2.6.0",*</Import-Package>
+ <Import-Package>javax.servlet.*;version="[2.6.0,3.2)",*</Import-Package>
<Export-Package>org.eclipse.jetty.security.jaspi.*;version="${parsedVersion.osgiVersion}"</Export-Package>
</instructions>
</configuration>
diff --git a/jetty-jmx/pom.xml b/jetty-jmx/pom.xml
index d694928..f048e28 100644
--- a/jetty-jmx/pom.xml
+++ b/jetty-jmx/pom.xml
@@ -2,7 +2,7 @@
<parent>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-project</artifactId>
- <version>9.0.5-SNAPSHOT</version>
+ <version>9.1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>jetty-jmx</artifactId>
diff --git a/jetty-jndi/pom.xml b/jetty-jndi/pom.xml
index 3eb271a..dbd3fbc 100644
--- a/jetty-jndi/pom.xml
+++ b/jetty-jndi/pom.xml
@@ -2,7 +2,7 @@
<parent>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-project</artifactId>
- <version>9.0.5-SNAPSHOT</version>
+ <version>9.1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>jetty-jndi</artifactId>
@@ -67,20 +67,8 @@
<dependency>
<groupId>org.eclipse.jetty.orbit</groupId>
<artifactId>javax.mail.glassfish</artifactId>
+ <version>1.4.1.v201005082020</version>
+ <scope>provided</scope>
</dependency>
</dependencies>
- <profiles>
- <profile>
- <id>below-jdk1.6</id>
- <activation>
- <jdk>(,1.6)</jdk>
- </activation>
- <dependencies>
- <dependency>
- <groupId>org.eclipse.jetty.orbit</groupId>
- <artifactId>javax.activation</artifactId>
- </dependency>
- </dependencies>
- </profile>
- </profiles>
</project>
diff --git a/jetty-jsp/pom.xml b/jetty-jsp/pom.xml
index d6ab594..b8067b4 100644
--- a/jetty-jsp/pom.xml
+++ b/jetty-jsp/pom.xml
@@ -2,7 +2,7 @@
<parent>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-project</artifactId>
- <version>9.0.5-SNAPSHOT</version>
+ <version>9.1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>jetty-jsp</artifactId>
@@ -13,47 +13,59 @@
</build>
<dependencies>
+ <!-- Schemas -->
+ <dependency>
+ <groupId>org.eclipse.jetty.toolchain</groupId>
+ <artifactId>jetty-schemas</artifactId>
+ </dependency>
+
+ <!-- servlet api -->
+ <dependency>
+ <groupId>javax.servlet</groupId>
+ <artifactId>javax.servlet-api</artifactId>
+ </dependency>
+
<!-- JSP Api -->
<dependency>
- <groupId>org.eclipse.jetty.orbit</groupId>
- <artifactId>javax.servlet.jsp</artifactId>
- <version>2.2.0.v201112011158</version>
+ <groupId>javax.servlet.jsp</groupId>
+ <artifactId>javax.servlet.jsp-api</artifactId>
</dependency>
<!-- JSP Impl -->
<dependency>
- <groupId>org.eclipse.jetty.orbit</groupId>
- <artifactId>org.apache.jasper.glassfish</artifactId>
- <version>2.2.2.v201112011158</version>
+ <groupId>org.glassfish.web</groupId>
+ <artifactId>javax.servlet.jsp</artifactId>
</dependency>
+
<!-- JSTL Api -->
<dependency>
<groupId>org.eclipse.jetty.orbit</groupId>
<artifactId>javax.servlet.jsp.jstl</artifactId>
- <version>1.2.0.v201105211821</version>
</dependency>
<!-- JSTL Impl -->
<dependency>
<groupId>org.eclipse.jetty.orbit</groupId>
<artifactId>org.apache.taglibs.standard.glassfish</artifactId>
- <version>1.2.0.v201112081803</version>
</dependency>
+
<!-- EL Api -->
+ <!-- Not needed as glassfish impl jars contain also the api classes
<dependency>
- <groupId>org.eclipse.jetty.orbit</groupId>
- <artifactId>javax.el</artifactId>
- <version>2.2.0.v201303151357</version>
+ <groupId>javax.el</groupId>
+ <artifactId>javax.el-api</artifactId>
</dependency>
+ -->
+
<!-- EL Impl -->
<dependency>
- <groupId>org.eclipse.jetty.orbit</groupId>
- <artifactId>com.sun.el</artifactId>
- <version>2.2.0.v201303151357</version>
+ <groupId>org.glassfish</groupId>
+ <artifactId>javax.el</artifactId>
</dependency>
+
+
<!-- Eclipse Java Compiler (for JSP Compilation) -->
<dependency>
<groupId>org.eclipse.jetty.orbit</groupId>
<artifactId>org.eclipse.jdt.core</artifactId>
- <version>3.8.2.v20130121</version>
</dependency>
</dependencies>
</project>
diff --git a/jetty-jspc-maven-plugin/pom.xml b/jetty-jspc-maven-plugin/pom.xml
index 37d267d..26c5aa5 100644
--- a/jetty-jspc-maven-plugin/pom.xml
+++ b/jetty-jspc-maven-plugin/pom.xml
@@ -2,7 +2,7 @@
<parent>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-project</artifactId>
- <version>9.0.5-SNAPSHOT</version>
+ <version>9.1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>jetty-jspc-maven-plugin</artifactId>
diff --git a/jetty-maven-plugin/pom.xml b/jetty-maven-plugin/pom.xml
index 8b5f991..133832c 100644
--- a/jetty-maven-plugin/pom.xml
+++ b/jetty-maven-plugin/pom.xml
@@ -2,7 +2,7 @@
<parent>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-project</artifactId>
- <version>9.0.5-SNAPSHOT</version>
+ <version>9.1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>jetty-maven-plugin</artifactId>
@@ -120,11 +120,16 @@
<artifactId>jetty-jsp</artifactId>
<version>${project.version}</version>
</dependency>
- <dependency>
- <groupId>org.eclipse.jetty</groupId>
- <artifactId>jetty-continuation</artifactId>
- <version>${project.version}</version>
- </dependency>
+ <dependency>
+ <groupId>org.eclipse.jetty.orbit</groupId>
+ <artifactId>javax.activation</artifactId>
+ <scope>compile</scope>
+ </dependency>
+ <dependency>
+ <groupId>javax.transaction</groupId>
+ <artifactId>javax.transaction-api</artifactId>
+ <scope>compile</scope>
+ </dependency>
</dependencies>
<reporting>
<plugins>
diff --git a/jetty-monitor/pom.xml b/jetty-monitor/pom.xml
index 54c88cb..c95b52a 100644
--- a/jetty-monitor/pom.xml
+++ b/jetty-monitor/pom.xml
@@ -19,7 +19,7 @@
<parent>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-project</artifactId>
- <version>9.0.5-SNAPSHOT</version>
+ <version>9.1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>jetty-monitor</artifactId>
diff --git a/jetty-monitor/src/test/resources/jetty-logging.properties b/jetty-monitor/src/test/resources/jetty-logging.properties
index e94eed3..2071498 100644
--- a/jetty-monitor/src/test/resources/jetty-logging.properties
+++ b/jetty-monitor/src/test/resources/jetty-logging.properties
@@ -1,3 +1,3 @@
org.eclipse.jetty.util.log.class=org.eclipse.jetty.util.log.StdErrLog
#org.eclipse.jetty.LEVEL=DEBUG
-org.eclipse.jetty.monitor.LEVEL=DEBUG
+#org.eclipse.jetty.monitor.LEVEL=DEBUG
diff --git a/jetty-nosql/pom.xml b/jetty-nosql/pom.xml
index 720668e..e821e4f 100644
--- a/jetty-nosql/pom.xml
+++ b/jetty-nosql/pom.xml
@@ -2,7 +2,7 @@
<parent>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-project</artifactId>
- <version>9.0.5-SNAPSHOT</version>
+ <version>9.1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>jetty-nosql</artifactId>
@@ -29,7 +29,7 @@
<artifactId>maven-bundle-plugin</artifactId>
<configuration>
<instructions>
- <Import-Package>javax.servlet.*;version="2.6.0",org.eclipse.jetty.server.session.jmx;version="9.0.0";resolution:=optional,,org.eclipse.jetty.*;version="9.0",*</Import-Package>
+ <Import-Package>javax.servlet.*;version="[2.6.0,3.2)",org.eclipse.jetty.server.session.jmx;version="9.1";resolution:=optional,,org.eclipse.jetty.*;version="9.1",*</Import-Package>
</instructions>
</configuration>
<extensions>true</extensions>
diff --git a/jetty-nosql/src/main/java/org/eclipse/jetty/nosql/NoSqlSessionManager.java b/jetty-nosql/src/main/java/org/eclipse/jetty/nosql/NoSqlSessionManager.java
index f802795..d5ebdd2 100644
--- a/jetty-nosql/src/main/java/org/eclipse/jetty/nosql/NoSqlSessionManager.java
+++ b/jetty-nosql/src/main/java/org/eclipse/jetty/nosql/NoSqlSessionManager.java
@@ -356,6 +356,7 @@
__log.warn(e);
}
}
+ super.renewSessionId(oldClusterId, oldNodeId, newClusterId, newNodeId);
}
diff --git a/jetty-osgi/jetty-osgi-boot-jsp/pom.xml b/jetty-osgi/jetty-osgi-boot-jsp/pom.xml
index ef64922..6d80844 100644
--- a/jetty-osgi/jetty-osgi-boot-jsp/pom.xml
+++ b/jetty-osgi/jetty-osgi-boot-jsp/pom.xml
@@ -2,7 +2,7 @@
<parent>
<groupId>org.eclipse.jetty.osgi</groupId>
<artifactId>jetty-osgi-project</artifactId>
- <version>9.0.5-SNAPSHOT</version>
+ <version>9.1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>jetty-osgi-boot-jsp</artifactId>
@@ -33,8 +33,8 @@
</dependency>
<!-- Orbit Servlet Deps -->
<dependency>
- <groupId>org.eclipse.jetty.orbit</groupId>
- <artifactId>javax.servlet</artifactId>
+ <groupId>javax.servlet</groupId>
+ <artifactId>javax.servlet-api</artifactId>
</dependency>
<!-- Orbit JSP Deps -->
<dependency>
@@ -92,27 +92,27 @@
com.sun.el.lang;resolution:=optional,
com.sun.el.parser;resolution:=optional,
com.sun.el.util;resolution:=optional,
- javax.el;version="2.2.0";resolution:=optional,
- javax.servlet;version="2.6.0",
- javax.servlet.jsp;version="2.2.0",
- javax.servlet.jsp.el;version="2.2.0",
+ javax.el;version="[3.0,3.1)";resolution:=optional,
+ javax.servlet;version="[2.6.0,3.2)",
+ javax.servlet.jsp;version="2.3",
+ javax.servlet.jsp.el;version="2.3",
javax.servlet.jsp.jstl.core;version="1.2.0";resolution:=optional,
javax.servlet.jsp.jstl.fmt;version="1.2.0";resolution:=optional,
javax.servlet.jsp.jstl.sql;version="1.2.0";resolution:=optional,
javax.servlet.jsp.jstl.tlv;version="1.2.0";resolution:=optional,
- javax.servlet.jsp.resources;version="2.2.0",
- javax.servlet.jsp.tagext;version="2.2.0",
- javax.servlet.resources;version="2.6.0",
- org.apache.jasper;version="2.2.2";resolution:=optional,
- org.apache.jasper.compiler;version="2.2.2";resolution:=optional,
- org.apache.jasper.compiler.tagplugin;version="2.2.2";resolution:=optional,
- org.apache.jasper.runtime;version="2.2.2";resolution:=optional,
- org.apache.jasper.security;version="2.2.2";resolution:=optional,
- org.apache.jasper.servlet;version="2.2.2";resolution:=optional,
- org.apache.jasper.tagplugins.jstl;version="2.2.2";resolution:=optional,
- org.apache.jasper.util;version="2.2.2";resolution:=optional,
- org.apache.jasper.xmlparser;version="2.2.2";resolution:=optional,
- org.glassfish.jsp.api;version="2.2.2";resolution:=optional,
+ javax.servlet.jsp.resources;version="2.3",
+ javax.servlet.jsp.tagext;version="2.3",
+ javax.servlet.resources;version="(2.6.0,3.2)",
+ org.apache.jasper;version="2.3.2";resolution:=optional,
+ org.apache.jasper.compiler;version="2.3.2";resolution:=optional,
+ org.apache.jasper.compiler.tagplugin;version="2.3.2";resolution:=optional,
+ org.apache.jasper.runtime;version="2.3.2";resolution:=optional,
+ org.apache.jasper.security;version="2.3.2";resolution:=optional,
+ org.apache.jasper.servlet;version="2.3.2";resolution:=optional,
+ org.apache.jasper.tagplugins.jstl;version="2.3.2";resolution:=optional,
+ org.apache.jasper.util;version="2.3.2";resolution:=optional,
+ org.apache.jasper.xmlparser;version="2.3.2";resolution:=optional,
+ org.glassfish.jsp.api;version="2.3.2";resolution:=optional,
org.apache.taglibs.standard;version="1.2.0";resolution:=optional,
org.apache.taglibs.standard.extra.spath;version="1.2.0";resolution:=optional,
org.apache.taglibs.standard.functions;version="1.2.0";resolution:=optional,
@@ -145,7 +145,7 @@
javax.xml.parser;resolution:=optional
</Import-Package>
<_nouses>true</_nouses>
- <DynamicImport-Package>org.apache.jasper.*;version="2.2"</DynamicImport-Package>
+ <DynamicImport-Package>org.apache.jasper.*;version="2.3"</DynamicImport-Package>
</instructions>
</configuration>
</plugin>
diff --git a/jetty-osgi/jetty-osgi-boot-warurl/pom.xml b/jetty-osgi/jetty-osgi-boot-warurl/pom.xml
index 60eb08b..a2ad1f2 100644
--- a/jetty-osgi/jetty-osgi-boot-warurl/pom.xml
+++ b/jetty-osgi/jetty-osgi-boot-warurl/pom.xml
@@ -2,7 +2,7 @@
<parent>
<groupId>org.eclipse.jetty.osgi</groupId>
<artifactId>jetty-osgi-project</artifactId>
- <version>9.0.5-SNAPSHOT</version>
+ <version>9.1.0-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
diff --git a/jetty-osgi/jetty-osgi-boot/pom.xml b/jetty-osgi/jetty-osgi-boot/pom.xml
index be8b6f4..7e0608a 100644
--- a/jetty-osgi/jetty-osgi-boot/pom.xml
+++ b/jetty-osgi/jetty-osgi-boot/pom.xml
@@ -2,7 +2,7 @@
<parent>
<groupId>org.eclipse.jetty.osgi</groupId>
<artifactId>jetty-osgi-project</artifactId>
- <version>9.0.5-SNAPSHOT</version>
+ <version>9.1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>jetty-osgi-boot</artifactId>
@@ -108,8 +108,8 @@
javax.mail.internet;version="1.4.0";resolution:=optional,
javax.mail.search;version="1.4.0";resolution:=optional,
javax.mail.util;version="1.4.0";resolution:=optional,
- javax.servlet;version="2.6.0",
- javax.servlet.http;version="2.6.0",
+ javax.servlet;version="[2.6,3.2)",
+ javax.servlet.http;version="[2.6,3.2)",
javax.transaction;version="1.1.0";resolution:=optional,
javax.transaction.xa;version="1.1.0";resolution:=optional,
org.eclipse.jetty.annotations;version="9.0.0";resolution:=optional,
diff --git a/jetty-osgi/jetty-osgi-httpservice/pom.xml b/jetty-osgi/jetty-osgi-httpservice/pom.xml
index e7fcc75..b7ec57f 100644
--- a/jetty-osgi/jetty-osgi-httpservice/pom.xml
+++ b/jetty-osgi/jetty-osgi-httpservice/pom.xml
@@ -2,7 +2,7 @@
<parent>
<groupId>org.eclipse.jetty.osgi</groupId>
<artifactId>jetty-osgi-project</artifactId>
- <version>9.0.5-SNAPSHOT</version>
+ <version>9.1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>jetty-httpservice</artifactId>
@@ -30,8 +30,8 @@
<artifactId>org.eclipse.osgi</artifactId>
</dependency>
<dependency>
- <groupId>org.eclipse.jetty.orbit</groupId>
- <artifactId>javax.servlet</artifactId>
+ <groupId>javax.servlet</groupId>
+ <artifactId>javax.servlet-api</artifactId>
</dependency>
</dependencies>
diff --git a/jetty-osgi/jetty-osgi-npn/pom.xml b/jetty-osgi/jetty-osgi-npn/pom.xml
index de04a5c..dea1db4 100644
--- a/jetty-osgi/jetty-osgi-npn/pom.xml
+++ b/jetty-osgi/jetty-osgi-npn/pom.xml
@@ -2,7 +2,7 @@
<parent>
<groupId>org.eclipse.jetty.osgi</groupId>
<artifactId>jetty-osgi-project</artifactId>
- <version>9.0.5-SNAPSHOT</version>
+ <version>9.1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>jetty-osgi-npn</artifactId>
diff --git a/jetty-osgi/pom.xml b/jetty-osgi/pom.xml
index 29826d2..48675f8 100644
--- a/jetty-osgi/pom.xml
+++ b/jetty-osgi/pom.xml
@@ -3,7 +3,7 @@
<parent>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-project</artifactId>
- <version>9.0.5-SNAPSHOT</version>
+ <version>9.1.0-SNAPSHOT</version>
</parent>
<groupId>org.eclipse.jetty.osgi</groupId>
<artifactId>jetty-osgi-project</artifactId>
@@ -26,7 +26,9 @@
<module>jetty-osgi-httpservice</module>
<module>test-jetty-osgi-webapp</module>
<module>test-jetty-osgi-context</module>
+<!--
<module>test-jetty-osgi</module>
+-->
</modules>
<build>
<resources>
diff --git a/jetty-osgi/test-jetty-osgi-context/pom.xml b/jetty-osgi/test-jetty-osgi-context/pom.xml
index b180cb7..a904c2d 100644
--- a/jetty-osgi/test-jetty-osgi-context/pom.xml
+++ b/jetty-osgi/test-jetty-osgi-context/pom.xml
@@ -2,7 +2,7 @@
<parent>
<groupId>org.eclipse.jetty.osgi</groupId>
<artifactId>jetty-osgi-project</artifactId>
- <version>9.0.5-SNAPSHOT</version>
+ <version>9.1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>test-jetty-osgi-context</artifactId>
@@ -86,8 +86,8 @@
compilation time. -->
<_nouses>true</_nouses>
<Import-Package>
- javax.servlet;version="2.6.0",
- javax.servlet.resources;version="2.6.0",
+ javax.servlet;version="[2.6.0,3.2)",
+ javax.servlet.resources;version="[2.6.0,3.2)",
org.osgi.framework,
org.osgi.service.cm;version="1.2.0",
org.osgi.service.packageadmin,
diff --git a/jetty-osgi/test-jetty-osgi-webapp/pom.xml b/jetty-osgi/test-jetty-osgi-webapp/pom.xml
index 31a2c39..da2c45e 100644
--- a/jetty-osgi/test-jetty-osgi-webapp/pom.xml
+++ b/jetty-osgi/test-jetty-osgi-webapp/pom.xml
@@ -2,7 +2,7 @@
<parent>
<groupId>org.eclipse.jetty.osgi</groupId>
<artifactId>jetty-osgi-project</artifactId>
- <version>9.0.5-SNAPSHOT</version>
+ <version>9.1.0-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
diff --git a/jetty-osgi/test-jetty-osgi/pom.xml b/jetty-osgi/test-jetty-osgi/pom.xml
index 9120a43..db0ddc5 100644
--- a/jetty-osgi/test-jetty-osgi/pom.xml
+++ b/jetty-osgi/test-jetty-osgi/pom.xml
@@ -152,8 +152,8 @@
-->
<!-- Orbit Servlet Deps -->
<dependency>
- <groupId>org.eclipse.jetty.orbit</groupId>
- <artifactId>javax.servlet</artifactId>
+ <groupId>javax.servlet</groupId>
+ <artifactId>javax.servlet-api</artifactId>
<scope>test</scope>
</dependency>
<!-- Orbit JSP Deps -->
diff --git a/jetty-osgi/test-jetty-osgi/src/test/java/org/eclipse/jetty/osgi/test/TestJettyOSGiBootCore.java b/jetty-osgi/test-jetty-osgi/src/test/java/org/eclipse/jetty/osgi/test/TestJettyOSGiBootCore.java
index aaf9fd1..2784b20 100644
--- a/jetty-osgi/test-jetty-osgi/src/test/java/org/eclipse/jetty/osgi/test/TestJettyOSGiBootCore.java
+++ b/jetty-osgi/test-jetty-osgi/src/test/java/org/eclipse/jetty/osgi/test/TestJettyOSGiBootCore.java
@@ -78,7 +78,8 @@
res.add(mavenBundle().groupId( "org.eclipse.jetty.osgi" ).artifactId( "jetty-osgi-boot" ).versionAsInProject().start());
- res.add(mavenBundle().groupId( "org.eclipse.jetty.orbit" ).artifactId( "javax.servlet" ).versionAsInProject().noStart());
+ //res.add(mavenBundle().groupId( "org.eclipse.jetty.orbit" ).artifactId( "javax.servlet" ).versionAsInProject().noStart());
+ res.add(mavenBundle().groupId( "javax.servlet" ).artifactId( "javax.servlet-api" ).versionAsInProject().noStart());
res.add(mavenBundle().groupId( "org.eclipse.jetty" ).artifactId( "jetty-deploy" ).versionAsInProject().noStart());
res.add(mavenBundle().groupId( "org.eclipse.jetty" ).artifactId( "jetty-server" ).versionAsInProject().noStart());
res.add(mavenBundle().groupId( "org.eclipse.jetty" ).artifactId( "jetty-servlet" ).versionAsInProject().noStart());
diff --git a/jetty-overlay-deployer/pom.xml b/jetty-overlay-deployer/pom.xml
index 03abbaa..a531f46 100644
--- a/jetty-overlay-deployer/pom.xml
+++ b/jetty-overlay-deployer/pom.xml
@@ -2,7 +2,7 @@
<parent>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-project</artifactId>
- <version>9.0.5-SNAPSHOT</version>
+ <version>9.1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>jetty-overlay-deployer</artifactId>
@@ -49,8 +49,8 @@
<version>${project.version}</version>
</dependency>
<dependency>
- <groupId>org.eclipse.jetty.orbit</groupId>
- <artifactId>javax.transaction</artifactId>
+ <groupId>javax.transaction</groupId>
+ <artifactId>javax.transaction-api</artifactId>
<scope>test</scope>
</dependency>
<dependency>
diff --git a/jetty-plus/pom.xml b/jetty-plus/pom.xml
index e7e3372..bcd6f5c 100644
--- a/jetty-plus/pom.xml
+++ b/jetty-plus/pom.xml
@@ -2,7 +2,7 @@
<parent>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-project</artifactId>
- <version>9.0.5-SNAPSHOT</version>
+ <version>9.1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>jetty-plus</artifactId>
@@ -14,9 +14,6 @@
</properties>
<build>
<plugins>
-<!--
- COMMENTED OUT UNTIL CORRECT CONFIG IS FOUND FOR Export uses clauses
--->
<plugin>
<groupId>org.apache.felix</groupId>
<artifactId>maven-bundle-plugin</artifactId>
@@ -30,7 +27,7 @@
<instructions>
<_versionpolicy> </_versionpolicy>
<Import-Package>javax.sql.*,javax.security.*,javax.naming.*,
- javax.servlet.*;version="2.6.0",javax.transaction.*;version="[1.1,1.2)",
+ javax.servlet.*;version="[2.6.0,3.2)",javax.transaction.*;version="[1.1,1.3)",
*</Import-Package>
</instructions>
</configuration>
@@ -91,8 +88,8 @@
<scope>test</scope>
</dependency>
<dependency>
- <groupId>org.eclipse.jetty.orbit</groupId>
- <artifactId>javax.transaction</artifactId>
+ <groupId>javax.transaction</groupId>
+ <artifactId>javax.transaction-api</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
diff --git a/jetty-plus/src/main/java/org/eclipse/jetty/plus/annotation/ContainerInitializer.java b/jetty-plus/src/main/java/org/eclipse/jetty/plus/annotation/ContainerInitializer.java
index 9d93203..6ac2102 100644
--- a/jetty-plus/src/main/java/org/eclipse/jetty/plus/annotation/ContainerInitializer.java
+++ b/jetty-plus/src/main/java/org/eclipse/jetty/plus/annotation/ContainerInitializer.java
@@ -22,6 +22,7 @@
import java.util.Set;
import javax.servlet.ServletContainerInitializer;
+import javax.servlet.ServletContextListener;
import org.eclipse.jetty.util.Loader;
import org.eclipse.jetty.webapp.WebAppContext;
@@ -101,11 +102,12 @@
for (String s : _applicableTypeNames)
classes.add(Loader.loadClass(context.getClass(), s));
}
-
+ context.getServletContext().setExtendedListenerTypes(true);
_target.onStartup(classes, context.getServletContext());
}
finally
- {
+ {
+ context.getServletContext().setExtendedListenerTypes(false);
Thread.currentThread().setContextClassLoader(oldLoader);
}
}
diff --git a/jetty-plus/src/main/java/org/eclipse/jetty/plus/annotation/RunAs.java b/jetty-plus/src/main/java/org/eclipse/jetty/plus/annotation/RunAs.java
index de3cae7..ab0c954 100644
--- a/jetty-plus/src/main/java/org/eclipse/jetty/plus/annotation/RunAs.java
+++ b/jetty-plus/src/main/java/org/eclipse/jetty/plus/annotation/RunAs.java
@@ -18,8 +18,6 @@
package org.eclipse.jetty.plus.annotation;
-import javax.servlet.ServletException;
-
import org.eclipse.jetty.servlet.ServletHolder;
/**
@@ -57,8 +55,10 @@
}
+ /**
+ * @param holder
+ */
public void setRunAs (ServletHolder holder)
- throws ServletException
{
if (holder == null)
return;
diff --git a/jetty-plus/src/main/java/org/eclipse/jetty/plus/annotation/RunAsCollection.java b/jetty-plus/src/main/java/org/eclipse/jetty/plus/annotation/RunAsCollection.java
index b43ca86..b62cf43 100644
--- a/jetty-plus/src/main/java/org/eclipse/jetty/plus/annotation/RunAsCollection.java
+++ b/jetty-plus/src/main/java/org/eclipse/jetty/plus/annotation/RunAsCollection.java
@@ -52,7 +52,6 @@
}
public RunAs getRunAs (Object o)
- throws ServletException
{
if (o==null)
return null;
@@ -61,7 +60,6 @@
}
public void setRunAs(Object o)
- throws ServletException
{
if (o == null)
return;
diff --git a/jetty-plus/src/main/java/org/eclipse/jetty/plus/webapp/PlusDecorator.java b/jetty-plus/src/main/java/org/eclipse/jetty/plus/webapp/PlusDecorator.java
index 68f50bc..a61ff28 100644
--- a/jetty-plus/src/main/java/org/eclipse/jetty/plus/webapp/PlusDecorator.java
+++ b/jetty-plus/src/main/java/org/eclipse/jetty/plus/webapp/PlusDecorator.java
@@ -18,24 +18,18 @@
package org.eclipse.jetty.plus.webapp;
-import java.util.EventListener;
-
-import javax.servlet.Filter;
-import javax.servlet.Servlet;
import javax.servlet.ServletException;
import org.eclipse.jetty.plus.annotation.InjectionCollection;
import org.eclipse.jetty.plus.annotation.LifeCycleCallbackCollection;
import org.eclipse.jetty.plus.annotation.RunAsCollection;
-import org.eclipse.jetty.servlet.FilterHolder;
import org.eclipse.jetty.servlet.ServletContextHandler.Decorator;
-import org.eclipse.jetty.servlet.ServletHolder;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;
import org.eclipse.jetty.webapp.WebAppContext;
/**
- * WebAppDecorator
+ * PlusDecorator
*
*
*/
@@ -50,83 +44,7 @@
_context = context;
}
- /* ------------------------------------------------------------ */
- /**
- * @see org.eclipse.jetty.servlet.ServletContextHandler.Decorator#decorateFilterHolder(org.eclipse.jetty.servlet.FilterHolder)
- */
- public void decorateFilterHolder(FilterHolder filter) throws ServletException
- {
- }
-
- /* ------------------------------------------------------------ */
- /**
- * @see org.eclipse.jetty.servlet.ServletContextHandler.Decorator#decorateFilterInstance(javax.servlet.Filter)
- */
- public <T extends Filter> T decorateFilterInstance(T filter) throws ServletException
- {
- decorate(filter);
- return filter;
- }
-
- /* ------------------------------------------------------------ */
- /**
- * @see org.eclipse.jetty.servlet.ServletContextHandler.Decorator#decorateListenerInstance(java.util.EventListener)
- */
- public <T extends EventListener> T decorateListenerInstance(T listener) throws ServletException
- {
- decorate(listener);
- return listener;
- }
-
- /* ------------------------------------------------------------ */
- /**
- * @see org.eclipse.jetty.servlet.ServletContextHandler.Decorator#decorateServletHolder(org.eclipse.jetty.servlet.ServletHolder)
- */
- public void decorateServletHolder(ServletHolder holder) throws ServletException
- {
- decorate(holder);
- }
-
- /* ------------------------------------------------------------ */
- /**
- * @see org.eclipse.jetty.servlet.ServletContextHandler.Decorator#decorateServletInstance(javax.servlet.Servlet)
- */
- public <T extends Servlet> T decorateServletInstance(T servlet) throws ServletException
- {
- decorate(servlet);
- return servlet;
- }
-
- /* ------------------------------------------------------------ */
- /**
- * @see org.eclipse.jetty.servlet.ServletContextHandler.Decorator#destroyFilterInstance(javax.servlet.Filter)
- */
- public void destroyFilterInstance(Filter f)
- {
- destroy(f);
- }
-
-
- /* ------------------------------------------------------------ */
- /**
- * @see org.eclipse.jetty.servlet.ServletContextHandler.Decorator#destroyServletInstance(javax.servlet.Servlet)
- */
- public void destroyServletInstance(Servlet s)
- {
- destroy(s);
- }
-
- /**
- * @see org.eclipse.jetty.servlet.ServletContextHandler.Decorator#destroyListenerInstance(java.util.EventListener)
- */
- public void destroyListenerInstance(EventListener l)
- {
- destroy(l);
- }
-
-
- protected void decorate (Object o)
- throws ServletException
+ public Object decorate (Object o)
{
RunAsCollection runAses = (RunAsCollection)_context.getAttribute(RunAsCollection.RUNAS_COLLECTION);
@@ -146,12 +64,13 @@
}
catch (Exception e)
{
- throw new ServletException(e);
+ throw new RuntimeException(e);
}
}
+ return o;
}
- protected void destroy (Object o)
+ public void destroy (Object o)
{
LifeCycleCallbackCollection callbacks = (LifeCycleCallbackCollection)_context.getAttribute(LifeCycleCallbackCollection.LIFECYCLE_CALLBACK_COLLECTION);
if (callbacks != null)
diff --git a/jetty-proxy/pom.xml b/jetty-proxy/pom.xml
index f2a1d00..de26eee 100644
--- a/jetty-proxy/pom.xml
+++ b/jetty-proxy/pom.xml
@@ -2,7 +2,7 @@
<parent>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-project</artifactId>
- <version>9.0.5-SNAPSHOT</version>
+ <version>9.1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>jetty-proxy</artifactId>
@@ -25,7 +25,7 @@
</goals>
<configuration>
<instructions>
- <Import-Package>javax.servlet.*;version="2.6.0",*</Import-Package>
+ <Import-Package>javax.servlet.*;version="[2.6.0,3.2)",*</Import-Package>
</instructions>
</configuration>
</execution>
@@ -87,8 +87,13 @@
<version>${project.version}</version>
</dependency>
<dependency>
- <groupId>org.eclipse.jetty.orbit</groupId>
- <artifactId>javax.servlet</artifactId>
+ <groupId>org.eclipse.jetty</groupId>
+ <artifactId>jetty-util</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>javax.servlet</groupId>
+ <artifactId>javax.servlet-api</artifactId>
<scope>provided</scope>
</dependency>
</dependencies>
diff --git a/jetty-proxy/src/test/java/org/eclipse/jetty/proxy/ProxyTunnellingTest.java b/jetty-proxy/src/test/java/org/eclipse/jetty/proxy/ProxyTunnellingTest.java
index e4de3f3..01fa377 100644
--- a/jetty-proxy/src/test/java/org/eclipse/jetty/proxy/ProxyTunnellingTest.java
+++ b/jetty-proxy/src/test/java/org/eclipse/jetty/proxy/ProxyTunnellingTest.java
@@ -375,7 +375,7 @@
}
@Test
- @Ignore // to delicate to rely on external proxy.
+ @Ignore("External Proxy Server no longer stable enough for testing")
public void testExternalProxy() throws Exception
{
// Free proxy server obtained from http://hidemyass.com/proxy-list/
diff --git a/jetty-rewrite/pom.xml b/jetty-rewrite/pom.xml
index daa1fe1..e1b5f85 100644
--- a/jetty-rewrite/pom.xml
+++ b/jetty-rewrite/pom.xml
@@ -2,7 +2,7 @@
<parent>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-project</artifactId>
- <version>9.0.5-SNAPSHOT</version>
+ <version>9.1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>jetty-rewrite</artifactId>
@@ -25,7 +25,7 @@
</goals>
<configuration>
<instructions>
- <Import-Package>javax.servlet.*;version="2.6.0",*</Import-Package>
+ <Import-Package>javax.servlet.*;version="[2.6.0,3.2)",*</Import-Package>
</instructions>
</configuration>
</execution>
@@ -86,8 +86,8 @@
<version>${project.version}</version>
</dependency>
<dependency>
- <groupId>org.eclipse.jetty.orbit</groupId>
- <artifactId>javax.servlet</artifactId>
+ <groupId>javax.servlet</groupId>
+ <artifactId>javax.servlet-api</artifactId>
</dependency>
</dependencies>
</project>
diff --git a/jetty-rewrite/src/main/java/org/eclipse/jetty/rewrite/handler/CompactPathRule.java b/jetty-rewrite/src/main/java/org/eclipse/jetty/rewrite/handler/CompactPathRule.java
new file mode 100644
index 0000000..de041f0
--- /dev/null
+++ b/jetty-rewrite/src/main/java/org/eclipse/jetty/rewrite/handler/CompactPathRule.java
@@ -0,0 +1,55 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.rewrite.handler;
+
+import java.io.IOException;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.eclipse.jetty.http.HttpURI;
+import org.eclipse.jetty.http.PathMap;
+import org.eclipse.jetty.server.Request;
+import org.eclipse.jetty.util.URIUtil;
+
+/**
+ * Rewrite the URI by compacting to remove //
+ */
+public class CompactPathRule extends Rule implements Rule.ApplyURI
+{
+ public CompactPathRule()
+ {
+ _handling = false;
+ _terminating = false;
+ }
+
+ @Override
+ public void applyURI(Request request, String oldTarget, String newTarget) throws IOException
+ {
+ request.setRequestURI(newTarget);
+ }
+
+ @Override
+ public String matchAndApply(String target, HttpServletRequest request, HttpServletResponse response) throws IOException
+ {
+ if (target.startsWith("/"))
+ return URIUtil.compactPath(target);
+ return target;
+ }
+}
diff --git a/jetty-rhttp/jetty-rhttp-connector/pom.xml b/jetty-rhttp/jetty-rhttp-connector/pom.xml
index 95b8d9c..b248c48 100644
--- a/jetty-rhttp/jetty-rhttp-connector/pom.xml
+++ b/jetty-rhttp/jetty-rhttp-connector/pom.xml
@@ -88,7 +88,7 @@
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
- <artifactId>servlet-api</artifactId>
+ <artifactId>javax.servlet-api</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
diff --git a/jetty-rhttp/jetty-rhttp-gateway/pom.xml b/jetty-rhttp/jetty-rhttp-gateway/pom.xml
index 1243cd8..054bbd5 100644
--- a/jetty-rhttp/jetty-rhttp-gateway/pom.xml
+++ b/jetty-rhttp/jetty-rhttp-gateway/pom.xml
@@ -66,7 +66,7 @@
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
- <artifactId>servlet-api</artifactId>
+ <artifactId>javax.servlet-api</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
diff --git a/jetty-runner/pom.xml b/jetty-runner/pom.xml
index 1c4fe9d..96f41be 100644
--- a/jetty-runner/pom.xml
+++ b/jetty-runner/pom.xml
@@ -2,7 +2,7 @@
<parent>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-project</artifactId>
- <version>9.0.5-SNAPSHOT</version>
+ <version>9.1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<groupId>org.eclipse.jetty</groupId>
diff --git a/jetty-security/pom.xml b/jetty-security/pom.xml
index 7a4643e..b0f21bc 100644
--- a/jetty-security/pom.xml
+++ b/jetty-security/pom.xml
@@ -2,7 +2,7 @@
<parent>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-project</artifactId>
- <version>9.0.5-SNAPSHOT</version>
+ <version>9.1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>jetty-security</artifactId>
@@ -25,7 +25,7 @@
</goals>
<configuration>
<instructions>
- <Import-Package>javax.servlet.*;version="2.6.0",javax.security.cert,*</Import-Package>
+ <Import-Package>javax.servlet.*;version="[2.6.0,3.2)",javax.security.cert,*</Import-Package>
</instructions>
</configuration>
diff --git a/jetty-security/src/main/java/org/eclipse/jetty/security/AbstractUserAuthentication.java b/jetty-security/src/main/java/org/eclipse/jetty/security/AbstractUserAuthentication.java
new file mode 100644
index 0000000..ab20ba8
--- /dev/null
+++ b/jetty-security/src/main/java/org/eclipse/jetty/security/AbstractUserAuthentication.java
@@ -0,0 +1,95 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.security;
+
+import java.util.Set;
+
+import org.eclipse.jetty.server.Authentication.User;
+import org.eclipse.jetty.server.UserIdentity;
+import org.eclipse.jetty.server.UserIdentity.Scope;
+
+/**
+ * AbstractUserAuthentication
+ *
+ *
+ * Base class for representing an authenticated user.
+ */
+public abstract class AbstractUserAuthentication implements User
+{
+ protected String _method;
+ protected UserIdentity _userIdentity;
+
+
+ public AbstractUserAuthentication(String method, UserIdentity userIdentity)
+ {
+ _method = method;
+ _userIdentity = userIdentity;
+ }
+
+
+ @Override
+ public String getAuthMethod()
+ {
+ return _method;
+ }
+
+ @Override
+ public UserIdentity getUserIdentity()
+ {
+ return _userIdentity;
+ }
+
+ @Override
+ public boolean isUserInRole(Scope scope, String role)
+ {
+ String roleToTest = null;
+ if (scope!=null && scope.getRoleRefMap()!=null)
+ roleToTest=scope.getRoleRefMap().get(role);
+ if (roleToTest==null)
+ roleToTest=role;
+ //Servlet Spec 3.1 pg 125 if testing special role **
+ if ("**".equals(roleToTest.trim()))
+ {
+ //if ** is NOT a declared role name, the we return true
+ //as the user is authenticated. If ** HAS been declared as a
+ //role name, then we have to check if the user has that role
+ if (!declaredRolesContains("**"))
+ return true;
+ else
+ return _userIdentity.isUserInRole(role, scope);
+ }
+
+ return _userIdentity.isUserInRole(role, scope);
+ }
+
+ public boolean declaredRolesContains(String roleName)
+ {
+ SecurityHandler security=SecurityHandler.getCurrentSecurityHandler();
+ if (security==null)
+ return false;
+
+ if (security instanceof ConstraintAware)
+ {
+ Set<String> declaredRoles = ((ConstraintAware)security).getRoles();
+ return (declaredRoles != null) && declaredRoles.contains(roleName);
+ }
+
+ return false;
+ }
+}
diff --git a/jetty-security/src/main/java/org/eclipse/jetty/security/Authenticator.java b/jetty-security/src/main/java/org/eclipse/jetty/security/Authenticator.java
index 2e079db..c487319 100644
--- a/jetty-security/src/main/java/org/eclipse/jetty/security/Authenticator.java
+++ b/jetty-security/src/main/java/org/eclipse/jetty/security/Authenticator.java
@@ -52,9 +52,25 @@
* @return The name of the authentication method
*/
String getAuthMethod();
+
+
+ /* ------------------------------------------------------------ */
+ /**
+ * Called prior to validateRequest. The authenticator can
+ * manipulate the request to update it with information that
+ * can be inspected prior to validateRequest being called.
+ * The primary purpose of this method is to satisfy the Servlet
+ * Spec 3.1 section 13.6.3 on handling Form authentication
+ * where the http method of the original request causing authentication
+ * is not the same as the http method resulting from the redirect
+ * after authentication.
+ * @param request
+ */
+ void prepareRequest(ServletRequest request);
+
/* ------------------------------------------------------------ */
- /** Validate a response
+ /** Validate a request
* @param request The request
* @param response The response
* @param mandatory True if authentication is mandatory.
diff --git a/jetty-security/src/main/java/org/eclipse/jetty/security/ConstraintAware.java b/jetty-security/src/main/java/org/eclipse/jetty/security/ConstraintAware.java
index bec6764..d5c74a6 100644
--- a/jetty-security/src/main/java/org/eclipse/jetty/security/ConstraintAware.java
+++ b/jetty-security/src/main/java/org/eclipse/jetty/security/ConstraintAware.java
@@ -51,4 +51,20 @@
* @param role
*/
void addRole(String role);
+
+ /**
+ * See Servlet Spec 31, sec 13.8.4, pg 145
+ * When true, requests with http methods not explicitly covered either by inclusion or omissions
+ * in constraints, will have access denied.
+ * @param deny
+ */
+ void setDenyUncoveredHttpMethods(boolean deny);
+
+ boolean isDenyUncoveredHttpMethods();
+
+ /**
+ * See Servlet Spec 31, sec 13.8.4, pg 145
+ * Container must check if there are urls with uncovered http methods
+ */
+ boolean checkPathsWithUncoveredHttpMethods();
}
diff --git a/jetty-security/src/main/java/org/eclipse/jetty/security/ConstraintSecurityHandler.java b/jetty-security/src/main/java/org/eclipse/jetty/security/ConstraintSecurityHandler.java
index fd9a415..99dce3f 100644
--- a/jetty-security/src/main/java/org/eclipse/jetty/security/ConstraintSecurityHandler.java
+++ b/jetty-security/src/main/java/org/eclipse/jetty/security/ConstraintSecurityHandler.java
@@ -45,23 +45,30 @@
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.Response;
import org.eclipse.jetty.server.UserIdentity;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
import org.eclipse.jetty.util.security.Constraint;
/* ------------------------------------------------------------ */
/**
+ * ConstraintSecurityHandler
+ *
* Handler to enforce SecurityConstraints. This implementation is servlet spec
- * 3.0 compliant and pre-computes the constraint combinations for runtime
+ * 3.1 compliant and pre-computes the constraint combinations for runtime
* efficiency.
*
*/
public class ConstraintSecurityHandler extends SecurityHandler implements ConstraintAware
{
+ private static final Logger LOG = Log.getLogger(SecurityHandler.class); //use same as SecurityHandler
+
private static final String OMISSION_SUFFIX = ".omission";
private static final String ALL_METHODS = "*";
private final List<ConstraintMapping> _constraintMappings= new CopyOnWriteArrayList<>();
private final Set<String> _roles = new CopyOnWriteArraySet<>();
private final PathMap<Map<String, RoleInfo>> _constraintMap = new PathMap<>();
- private boolean _strict = true;
+ private boolean _denyUncoveredMethods = false;
+
/* ------------------------------------------------------------ */
/**
@@ -228,76 +235,56 @@
List<ConstraintMapping> mappings = new ArrayList<ConstraintMapping>();
//Create a constraint that will describe the default case (ie if not overridden by specific HttpMethodConstraints)
- Constraint constraint = ConstraintSecurityHandler.createConstraint(name, securityElement);
+ Constraint httpConstraint = null;
+ ConstraintMapping httpConstraintMapping = null;
+
+ if (securityElement.getEmptyRoleSemantic() != EmptyRoleSemantic.PERMIT ||
+ securityElement.getRolesAllowed().length != 0 ||
+ securityElement.getTransportGuarantee() != TransportGuarantee.NONE)
+ {
+ httpConstraint = ConstraintSecurityHandler.createConstraint(name, securityElement);
- //Create a mapping for the pathSpec for the default case
- ConstraintMapping defaultMapping = new ConstraintMapping();
- defaultMapping.setPathSpec(pathSpec);
- defaultMapping.setConstraint(constraint);
- mappings.add(defaultMapping);
-
+ //Create a mapping for the pathSpec for the default case
+ httpConstraintMapping = new ConstraintMapping();
+ httpConstraintMapping.setPathSpec(pathSpec);
+ httpConstraintMapping.setConstraint(httpConstraint);
+ mappings.add(httpConstraintMapping);
+ }
+
//See Spec 13.4.1.2 p127
List<String> methodOmissions = new ArrayList<String>();
//make constraint mappings for this url for each of the HttpMethodConstraintElements
- Collection<HttpMethodConstraintElement> methodConstraints = securityElement.getHttpMethodConstraints();
- if (methodConstraints != null)
+ Collection<HttpMethodConstraintElement> methodConstraintElements = securityElement.getHttpMethodConstraints();
+ if (methodConstraintElements != null)
{
- for (HttpMethodConstraintElement methodConstraint:methodConstraints)
+ for (HttpMethodConstraintElement methodConstraintElement:methodConstraintElements)
{
//Make a Constraint that captures the <auth-constraint> and <user-data-constraint> elements supplied for the HttpMethodConstraintElement
- Constraint mconstraint = ConstraintSecurityHandler.createConstraint(name, methodConstraint);
+ Constraint methodConstraint = ConstraintSecurityHandler.createConstraint(name, methodConstraintElement);
ConstraintMapping mapping = new ConstraintMapping();
- mapping.setConstraint(mconstraint);
+ mapping.setConstraint(methodConstraint);
mapping.setPathSpec(pathSpec);
- if (methodConstraint.getMethodName() != null)
+ if (methodConstraintElement.getMethodName() != null)
{
- mapping.setMethod(methodConstraint.getMethodName());
+ mapping.setMethod(methodConstraintElement.getMethodName());
//See spec 13.4.1.2 p127 - add an omission for every method name to the default constraint
- methodOmissions.add(methodConstraint.getMethodName());
+ methodOmissions.add(methodConstraintElement.getMethodName());
}
mappings.add(mapping);
}
}
//See spec 13.4.1.2 p127 - add an omission for every method name to the default constraint
- if (methodOmissions.size() > 0)
- defaultMapping.setMethodOmissions(methodOmissions.toArray(new String[methodOmissions.size()]));
+ //UNLESS the default constraint contains all default values. In that case, we won't add it. See Servlet Spec 3.1 pg 129
+ if (methodOmissions.size() > 0 && httpConstraintMapping != null)
+ httpConstraintMapping.setMethodOmissions(methodOmissions.toArray(new String[methodOmissions.size()]));
return mappings;
}
-
- /* ------------------------------------------------------------ */
- /** Get the strict mode.
- * @return true if the security handler is running in strict mode.
- */
- public boolean isStrict()
- {
- return _strict;
- }
- /* ------------------------------------------------------------ */
- /** Set the strict mode of the security handler.
- * <p>
- * When in strict mode (the default), the full servlet specification
- * will be implemented.
- * If not in strict mode, some additional flexibility in configuration
- * is allowed:<ul>
- * <li>All users do not need to have a role defined in the deployment descriptor
- * <li>The * role in a constraint applies to ANY role rather than all roles defined in
- * the deployment descriptor.
- * </ul>
- *
- * @param strict the strict to set
- * @see #setRoles(Set)
- * @see #setConstraintMappings(List, Set)
- */
- public void setStrict(boolean strict)
- {
- _strict = strict;
- }
/* ------------------------------------------------------------ */
/**
@@ -408,8 +395,16 @@
{
_constraintMappings.add(mapping);
if (mapping.getConstraint()!=null && mapping.getConstraint().getRoles()!=null)
+ {
+ //allow for lazy role naming: if a role is named in a security constraint, try and
+ //add it to the list of declared roles (ie as if it was declared with a security-role
for (String role : mapping.getConstraint().getRoles())
+ {
+ if ("*".equals(role) || "**".equals(role))
+ continue;
addRole(role);
+ }
+ }
if (isStarted())
{
@@ -424,8 +419,9 @@
@Override
public void addRole(String role)
{
+ //add to list of declared roles
boolean modified = _roles.add(role);
- if (isStarted() && modified && isStrict())
+ if (isStarted() && modified)
{
// Add the new role to currently defined any role role infos
for (Map<String,RoleInfo> map : _constraintMap.values())
@@ -454,9 +450,13 @@
processConstraintMapping(mapping);
}
}
+
+ //Servlet Spec 3.1 pg 147 sec 13.8.4.2 log paths for which there are uncovered http methods
+ checkPathsWithUncoveredHttpMethods();
+
super.doStart();
}
-
+
/* ------------------------------------------------------------ */
@Override
@@ -538,13 +538,13 @@
/* ------------------------------------------------------------ */
/** Constraints that name method omissions are dealt with differently.
- * We create an entry in the mappings with key "method.omission". This entry
+ * We create an entry in the mappings with key "<method>.omission". This entry
* is only ever combined with other omissions for the same method to produce a
* consolidated RoleInfo. Then, when we wish to find the relevant constraints for
* a given Request (in prepareConstraintInfo()), we consult 3 types of entries in
* the mappings: an entry that names the method of the Request specifically, an
* entry that names constraints that apply to all methods, entries of the form
- * method.omission, where the method of the Request is not named in the omission.
+ * <method>.omission, where the method of the Request is not named in the omission.
* @param mapping
* @param mappings
*/
@@ -559,7 +559,6 @@
sb.append(omissions[i]);
}
sb.append(OMISSION_SUFFIX);
-
RoleInfo ri = new RoleInfo();
mappings.put(sb.toString(), ri);
configureRoleInfo(ri, mapping);
@@ -573,7 +572,7 @@
* @param mapping
*/
protected void configureRoleInfo (RoleInfo ri, ConstraintMapping mapping)
- {
+ {
Constraint constraint = mapping.getConstraint();
boolean forbidden = constraint.isForbidden();
ri.setForbidden(forbidden);
@@ -582,7 +581,6 @@
//which we need in order to do combining of omissions in prepareConstraintInfo
UserDataConstraint userDataConstraint = UserDataConstraint.get(mapping.getConstraint().getDataConstraint());
ri.setUserDataConstraint(userDataConstraint);
-
//if forbidden, no point setting up roles
if (!ri.isForbidden())
@@ -590,26 +588,29 @@
//add in the roles
boolean checked = mapping.getConstraint().getAuthenticate();
ri.setChecked(checked);
+
if (ri.isChecked())
{
if (mapping.getConstraint().isAnyRole())
- {
- if (_strict)
- {
- // * means "all defined roles"
- for (String role : _roles)
- ri.addRole(role);
- }
- else
- // * means any role
- ri.setAnyRole(true);
- }
- else
- {
+ {
+ // * means matches any defined role
+ for (String role : _roles)
+ ri.addRole(role);
+ ri.setAnyRole(true);
+ }
+ else if (mapping.getConstraint().isAnyAuth())
+ {
+ //being authenticated is sufficient, not necessary to check roles
+ ri.setAnyAuth(true);
+ }
+ else
+ {
+ //user must be in one of the named roles
String[] newRoles = mapping.getConstraint().getRoles();
for (String role : newRoles)
{
- if (_strict &&!_roles.contains(role))
+ //check role has been defined
+ if (!_roles.contains(role))
throw new IllegalArgumentException("Attempt to use undeclared role: " + role + ", known roles: " + _roles);
ri.addRole(role);
}
@@ -626,7 +627,7 @@
* represents a merged set of user data constraints, roles etc -:
* <ol>
* <li>A mapping of an exact method name </li>
- * <li>A mapping will null key that matches every method name</li>
+ * <li>A mapping with key * that matches every method name</li>
* <li>Mappings with keys of the form "<method>.<method>.<method>.omission" that indicates it will match every method name EXCEPT those given</li>
* </ol>
*
@@ -660,7 +661,12 @@
applicableConstraints.add(entry.getValue());
}
- if (applicableConstraints.size() == 1)
+ if (applicableConstraints.size() == 0 && isDenyUncoveredHttpMethods())
+ {
+ roleInfo = new RoleInfo();
+ roleInfo.setForbidden(true);
+ }
+ else if (applicableConstraints.size() == 1)
roleInfo = applicableConstraints.get(0);
else
{
@@ -672,6 +678,7 @@
}
}
+
return roleInfo;
}
@@ -693,7 +700,6 @@
HttpConfiguration httpConfig = HttpChannel.getCurrentHttpChannel().getHttpConfiguration();
-
if (dataConstraint == UserDataConstraint.Confidential || dataConstraint == UserDataConstraint.Integral)
{
if (request.isSecure())
@@ -750,14 +756,35 @@
return true;
}
- if (roleInfo.isAnyRole() && request.getAuthType()!=null)
+ //handle ** role constraint
+ if (roleInfo.isAnyAuth() && request.getUserPrincipal() != null)
+ {
return true;
-
+ }
+
+ //check if user is any of the allowed roles
+ boolean isUserInRole = false;
for (String role : roleInfo.getRoles())
{
if (userIdentity.isUserInRole(role, null))
- return true;
+ {
+ isUserInRole = true;
+ break;
+ }
}
+
+ //handle * role constraint
+ if (roleInfo.isAnyRole() && request.getUserPrincipal() != null && isUserInRole)
+ {
+ return true;
+ }
+
+ //normal role check
+ if (isUserInRole)
+ {
+ return true;
+ }
+
return false;
}
@@ -773,5 +800,135 @@
Collections.singleton(_roles),
_constraintMap.entrySet());
}
+
+ /* ------------------------------------------------------------ */
+ /**
+ * @see org.eclipse.jetty.security.ConstraintAware#setDenyUncoveredHttpMethods(boolean)
+ */
+ @Override
+ public void setDenyUncoveredHttpMethods(boolean deny)
+ {
+ _denyUncoveredMethods = deny;
+ }
+
+ /* ------------------------------------------------------------ */
+ @Override
+ public boolean isDenyUncoveredHttpMethods()
+ {
+ return _denyUncoveredMethods;
+ }
+
+
+ /* ------------------------------------------------------------ */
+ /**
+ * Servlet spec 3.1 pg. 147.
+ */
+ @Override
+ public boolean checkPathsWithUncoveredHttpMethods()
+ {
+ Set<String> paths = getPathsWithUncoveredHttpMethods();
+ if (paths != null && !paths.isEmpty())
+ {
+ for (String p:paths)
+ LOG.warn("Path with uncovered http methods: {}",p);
+ return true;
+ }
+ return false;
+ }
+
+ /* ------------------------------------------------------------ */
+ /**
+ * Servlet spec 3.1 pg. 147.
+ * The container must check all the combined security constraint
+ * information and log any methods that are not protected and the
+ * urls at which they are not protected
+ *
+ * @return list of paths for which there are uncovered methods
+ */
+ public Set<String> getPathsWithUncoveredHttpMethods ()
+ {
+ //if automatically denying uncovered methods, there are no uncovered methods
+ if (_denyUncoveredMethods)
+ return Collections.emptySet();
+
+ Set<String> uncoveredPaths = new HashSet<String>();
+
+ for (String path:_constraintMap.keySet())
+ {
+ Map<String, RoleInfo> methodMappings = _constraintMap.get(path);
+ //Each key is either:
+ // : an exact method name
+ // : * which means that the constraint applies to every method
+ // : a name of the form <method>.<method>.<method>.omission, which means it applies to every method EXCEPT those named
+ if (methodMappings.get(ALL_METHODS) != null)
+ continue; //can't be any uncovered methods for this url path
+
+ boolean hasOmissions = omissionsExist(path, methodMappings);
+
+ for (String method:methodMappings.keySet())
+ {
+ if (method.endsWith(OMISSION_SUFFIX))
+ {
+ Set<String> omittedMethods = getOmittedMethods(method);
+ for (String m:omittedMethods)
+ {
+ if (!methodMappings.containsKey(m))
+ uncoveredPaths.add(path);
+ }
+ }
+ else
+ {
+ //an exact method name
+ if (!hasOmissions)
+ //a http-method does not have http-method-omission to cover the other method names
+ uncoveredPaths.add(path);
+ }
+
+ }
+ }
+ return uncoveredPaths;
+ }
+
+ /* ------------------------------------------------------------ */
+ /**
+ * Check if any http method omissions exist in the list of method
+ * to auth info mappings.
+ *
+ * @param methodNames
+ * @return
+ */
+ protected boolean omissionsExist (String path, Map<String, RoleInfo> methodMappings)
+ {
+ if (methodMappings == null)
+ return false;
+ boolean hasOmissions = false;
+ for (String m:methodMappings.keySet())
+ {
+ if (m.endsWith(OMISSION_SUFFIX))
+ hasOmissions = true;
+ }
+ return hasOmissions;
+ }
+
+
+ /* ------------------------------------------------------------ */
+ /**
+ * Given a string of the form <method>.<method>.omission
+ * split out the individual method names.
+ *
+ * @param omission
+ * @return
+ */
+ protected Set<String> getOmittedMethods (String omission)
+ {
+ if (omission == null || !omission.endsWith(OMISSION_SUFFIX))
+ return Collections.emptySet();
+
+ String[] strings = omission.split("\\.");
+ Set<String> methods = new HashSet<String>();
+ for (int i=0;i<strings.length-1;i++)
+ methods.add(strings[i]);
+ return methods;
+ }
}
diff --git a/jetty-security/src/main/java/org/eclipse/jetty/security/DefaultUserIdentity.java b/jetty-security/src/main/java/org/eclipse/jetty/security/DefaultUserIdentity.java
index dd12b1d..52e93ba 100644
--- a/jetty-security/src/main/java/org/eclipse/jetty/security/DefaultUserIdentity.java
+++ b/jetty-security/src/main/java/org/eclipse/jetty/security/DefaultUserIdentity.java
@@ -54,19 +54,22 @@
}
public boolean isUserInRole(String role, Scope scope)
- {
- if (scope!=null && scope.getRoleRefMap()!=null)
- {
- String mappedRole = scope.getRoleRefMap().get(role);
- if (mappedRole != null)
- role = mappedRole;
- }
+ {
+ //Servlet Spec 3.1, pg 125
+ if ("*".equals(role))
+ return false;
+ String roleToTest = null;
+ if (scope!=null && scope.getRoleRefMap()!=null)
+ roleToTest=scope.getRoleRefMap().get(role);
+
+ //Servlet Spec 3.1, pg 125
+ if (roleToTest == null)
+ roleToTest = role;
+
for (String r :_roles)
- {
- if (r.equals(role))
+ if (r.equals(roleToTest))
return true;
- }
return false;
}
diff --git a/jetty-security/src/main/java/org/eclipse/jetty/security/RoleInfo.java b/jetty-security/src/main/java/org/eclipse/jetty/security/RoleInfo.java
index 3168e78..5fcab95 100644
--- a/jetty-security/src/main/java/org/eclipse/jetty/security/RoleInfo.java
+++ b/jetty-security/src/main/java/org/eclipse/jetty/security/RoleInfo.java
@@ -22,6 +22,7 @@
import java.util.concurrent.CopyOnWriteArraySet;
/**
+ * RoleInfo
*
* Badly named class that holds the role and user data constraint info for a
* path/http method combination, extracted and combined from security
@@ -31,11 +32,15 @@
*/
public class RoleInfo
{
+ private boolean _isAnyAuth;
private boolean _isAnyRole;
private boolean _checked;
private boolean _forbidden;
private UserDataConstraint _userDataConstraint;
+ /**
+ * List of permitted roles
+ */
private final Set<String> _roles = new CopyOnWriteArraySet<String>();
public RoleInfo()
@@ -55,6 +60,7 @@
_forbidden=false;
_roles.clear();
_isAnyRole=false;
+ _isAnyAuth=false;
}
}
@@ -71,6 +77,7 @@
_checked = true;
_userDataConstraint = null;
_isAnyRole=false;
+ _isAnyAuth=false;
_roles.clear();
}
}
@@ -84,10 +91,19 @@
{
this._isAnyRole=anyRole;
if (anyRole)
- {
_checked = true;
- _roles.clear();
- }
+ }
+
+ public boolean isAnyAuth ()
+ {
+ return _isAnyAuth;
+ }
+
+ public void setAnyAuth(boolean anyAuth)
+ {
+ this._isAnyAuth=anyAuth;
+ if (anyAuth)
+ _checked = true;
}
public UserDataConstraint getUserDataConstraint()
@@ -100,6 +116,7 @@
if (userDataConstraint == null) throw new NullPointerException("Null UserDataConstraint");
if (this._userDataConstraint == null)
{
+
this._userDataConstraint = userDataConstraint;
}
else
@@ -126,6 +143,8 @@
setChecked(true);
else if (other._isAnyRole)
setAnyRole(true);
+ else if (other._isAnyAuth)
+ setAnyAuth(true);
else if (!_isAnyRole)
{
for (String r : other._roles)
@@ -138,6 +157,6 @@
@Override
public String toString()
{
- return "{RoleInfo"+(_forbidden?",F":"")+(_checked?",C":"")+(_isAnyRole?",*":_roles)+"}";
+ return "{RoleInfo"+(_forbidden?",F":"")+(_checked?",C":"")+(_isAnyRole?",*":_roles)+(_userDataConstraint!=null?","+_userDataConstraint:"")+"}";
}
}
diff --git a/jetty-security/src/main/java/org/eclipse/jetty/security/SecurityHandler.java b/jetty-security/src/main/java/org/eclipse/jetty/security/SecurityHandler.java
index edb5f91..fad49cc 100644
--- a/jetty-security/src/main/java/org/eclipse/jetty/security/SecurityHandler.java
+++ b/jetty-security/src/main/java/org/eclipse/jetty/security/SecurityHandler.java
@@ -462,6 +462,10 @@
if (checkSecurity(baseRequest))
{
+ //See Servlet Spec 3.1 sec 13.6.3
+ if (authenticator != null)
+ authenticator.prepareRequest(baseRequest);
+
RoleInfo roleInfo = prepareConstraintInfo(pathInContext, baseRequest);
// Check data constraints
diff --git a/jetty-security/src/main/java/org/eclipse/jetty/security/UserAuthentication.java b/jetty-security/src/main/java/org/eclipse/jetty/security/UserAuthentication.java
index 3d8dcba..58847bf 100644
--- a/jetty-security/src/main/java/org/eclipse/jetty/security/UserAuthentication.java
+++ b/jetty-security/src/main/java/org/eclipse/jetty/security/UserAuthentication.java
@@ -18,39 +18,20 @@
package org.eclipse.jetty.security;
-import org.eclipse.jetty.server.Authentication;
import org.eclipse.jetty.server.UserIdentity;
-import org.eclipse.jetty.server.UserIdentity.Scope;
/**
* @version $Rev: 4793 $ $Date: 2009-03-19 00:00:01 +0100 (Thu, 19 Mar 2009) $
*/
-public class UserAuthentication implements Authentication.User
+public class UserAuthentication extends AbstractUserAuthentication
{
- private final String _method;
- private final UserIdentity _userIdentity;
-
+
public UserAuthentication(String method, UserIdentity userIdentity)
{
- _method = method;
- _userIdentity = userIdentity;
- }
-
- public String getAuthMethod()
- {
- return _method;
+ super(method, userIdentity);
}
- public UserIdentity getUserIdentity()
- {
- return _userIdentity;
- }
-
- public boolean isUserInRole(Scope scope, String role)
- {
- return _userIdentity.isUserInRole(role, scope);
- }
@Override
public String toString()
diff --git a/jetty-security/src/main/java/org/eclipse/jetty/security/authentication/DeferredAuthentication.java b/jetty-security/src/main/java/org/eclipse/jetty/security/authentication/DeferredAuthentication.java
index d47a65d..1dedf0f 100644
--- a/jetty-security/src/main/java/org/eclipse/jetty/security/authentication/DeferredAuthentication.java
+++ b/jetty-security/src/main/java/org/eclipse/jetty/security/authentication/DeferredAuthentication.java
@@ -28,6 +28,7 @@
import javax.servlet.ServletOutputStream;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
+import javax.servlet.WriteListener;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletResponse;
@@ -310,6 +311,11 @@
public void setContentLength(int len)
{
}
+
+ public void setContentLengthLong(long len)
+ {
+
+ }
@Override
public void setContentType(String type)
@@ -345,6 +351,7 @@
return 0;
}
+
};
/* ------------------------------------------------------------ */
@@ -352,17 +359,33 @@
/* ------------------------------------------------------------ */
private static ServletOutputStream __nullOut = new ServletOutputStream()
{
+ @Override
public void write(int b) throws IOException
{
}
-
+
+ @Override
public void print(String s) throws IOException
{
}
-
+
+ @Override
public void println(String s) throws IOException
{
}
+
+
+ @Override
+ public void setWriteListener(WriteListener writeListener)
+ {
+
+ }
+
+ @Override
+ public boolean isReady()
+ {
+ return false;
+ }
};
diff --git a/jetty-security/src/main/java/org/eclipse/jetty/security/authentication/FormAuthenticator.java b/jetty-security/src/main/java/org/eclipse/jetty/security/authentication/FormAuthenticator.java
index 71bba4a..0de7288 100644
--- a/jetty-security/src/main/java/org/eclipse/jetty/security/authentication/FormAuthenticator.java
+++ b/jetty-security/src/main/java/org/eclipse/jetty/security/authentication/FormAuthenticator.java
@@ -36,6 +36,7 @@
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpHeaderValue;
import org.eclipse.jetty.http.HttpMethod;
+import org.eclipse.jetty.http.HttpVersion;
import org.eclipse.jetty.http.MimeTypes;
import org.eclipse.jetty.security.ServerAuthException;
import org.eclipse.jetty.security.UserAuthentication;
@@ -43,6 +44,7 @@
import org.eclipse.jetty.server.Authentication.User;
import org.eclipse.jetty.server.HttpChannel;
import org.eclipse.jetty.server.Request;
+import org.eclipse.jetty.server.Response;
import org.eclipse.jetty.server.UserIdentity;
import org.eclipse.jetty.util.MultiMap;
import org.eclipse.jetty.util.StringUtil;
@@ -75,6 +77,7 @@
public final static String __FORM_DISPATCH="org.eclipse.jetty.security.dispatch";
public final static String __J_URI = "org.eclipse.jetty.security.form_URI";
public final static String __J_POST = "org.eclipse.jetty.security.form_POST";
+ public final static String __J_METHOD = "org.eclipse.jetty.security.form_METHOD";
public final static String __J_SECURITY_CHECK = "/j_security_check";
public final static String __J_USERNAME = "j_username";
public final static String __J_PASSWORD = "j_password";
@@ -198,6 +201,45 @@
}
return user;
}
+
+
+ /* ------------------------------------------------------------ */
+ @Override
+ public void prepareRequest(ServletRequest request)
+ {
+ //if this is a request resulting from a redirect after auth is complete
+ //(ie its from a redirect to the original request uri) then due to
+ //browser handling of 302 redirects, the method may not be the same as
+ //that of the original request. Replace the method and original post
+ //params (if it was a post).
+ //
+ //See Servlet Spec 3.1 sec 13.6.3
+ HttpServletRequest httpRequest = (HttpServletRequest)request;
+ HttpSession session = httpRequest.getSession(false);
+ if (session == null || session.getAttribute(SessionAuthentication.__J_AUTHENTICATED) == null)
+ return; //not authenticated yet
+
+ String juri = (String)session.getAttribute(__J_URI);
+ if (juri == null || juri.length() == 0)
+ return; //no original uri saved
+
+ String method = (String)session.getAttribute(__J_METHOD);
+ if (method == null || method.length() == 0)
+ return; //didn't save original request method
+
+ StringBuffer buf = httpRequest.getRequestURL();
+ if (httpRequest.getQueryString() != null)
+ buf.append("?").append(httpRequest.getQueryString());
+
+ if (!juri.equals(buf.toString()))
+ return; //this request is not for the same url as the original
+
+ //restore the original request's method on this request
+ if (LOG.isDebugEnabled()) LOG.debug("Restoring original method {} for {} with method {}", method, juri,httpRequest.getMethod());
+ Request base_request = HttpChannel.getCurrentHttpChannel().getRequest();
+ HttpMethod m = HttpMethod.fromString(method);
+ base_request.setMethod(m,m.asString());
+ }
/* ------------------------------------------------------------ */
@Override
@@ -249,7 +291,10 @@
LOG.debug("authenticated {}->{}",form_auth,nuri);
response.setContentLength(0);
- response.sendRedirect(response.encodeRedirectURL(nuri));
+ Response base_response = HttpChannel.getCurrentHttpChannel().getResponse();
+ Request base_request = HttpChannel.getCurrentHttpChannel().getRequest();
+ int redirectCode = (base_request.getHttpVersion().getVersion() < HttpVersion.HTTP_1_1.getVersion() ? HttpServletResponse.SC_MOVED_TEMPORARILY : HttpServletResponse.SC_SEE_OTHER);
+ base_response.sendRedirect(redirectCode, response.encodeRedirectURL(nuri));
return form_auth;
}
@@ -273,7 +318,10 @@
else
{
LOG.debug("auth failed {}->{}",username,_formErrorPage);
- response.sendRedirect(response.encodeRedirectURL(URIUtil.addPaths(request.getContextPath(),_formErrorPage)));
+ Response base_response = HttpChannel.getCurrentHttpChannel().getResponse();
+ Request base_request = HttpChannel.getCurrentHttpChannel().getRequest();
+ int redirectCode = (base_request.getHttpVersion().getVersion() < HttpVersion.HTTP_1_1.getVersion() ? HttpServletResponse.SC_MOVED_TEMPORARILY : HttpServletResponse.SC_SEE_OTHER);
+ base_response.sendRedirect(redirectCode, response.encodeRedirectURL(URIUtil.addPaths(request.getContextPath(),_formErrorPage)));
}
return Authentication.SEND_FAILURE;
@@ -298,28 +346,26 @@
String j_uri=(String)session.getAttribute(__J_URI);
if (j_uri!=null)
{
+ //check if the request is for the same url as the original and restore
+ //params if it was a post
LOG.debug("auth retry {}->{}",authentication,j_uri);
- MultiMap<String> j_post = (MultiMap<String>)session.getAttribute(__J_POST);
- if (j_post!=null)
+ StringBuffer buf = request.getRequestURL();
+ if (request.getQueryString() != null)
+ buf.append("?").append(request.getQueryString());
+
+ if (j_uri.equals(buf.toString()))
{
- LOG.debug("auth rePOST {}->{}",authentication,j_uri);
- StringBuffer buf = request.getRequestURL();
- if (request.getQueryString() != null)
- buf.append("?").append(request.getQueryString());
-
- if (j_uri.equals(buf.toString()))
+ MultiMap<String> j_post = (MultiMap<String>)session.getAttribute(__J_POST);
+ if (j_post!=null)
{
- // This is a retry of an original POST request
- // so restore method and parameters
-
- session.removeAttribute(__J_POST);
+ LOG.debug("auth rePOST {}->{}",authentication,j_uri);
Request base_request = HttpChannel.getCurrentHttpChannel().getRequest();
- base_request.setMethod(HttpMethod.POST,HttpMethod.POST.asString());
base_request.setParameters(j_post);
}
- }
- else
session.removeAttribute(__J_URI);
+ session.removeAttribute(__J_METHOD);
+ session.removeAttribute(__J_POST);
+ }
}
}
LOG.debug("auth {}",authentication);
@@ -344,6 +390,7 @@
if (request.getQueryString() != null)
buf.append("?").append(request.getQueryString());
session.setAttribute(__J_URI, buf.toString());
+ session.setAttribute(__J_METHOD, request.getMethod());
if (MimeTypes.Type.FORM_ENCODED.is(req.getContentType()) && HttpMethod.POST.is(request.getMethod()))
{
@@ -366,7 +413,10 @@
else
{
LOG.debug("challenge {}->{}",session.getId(),_formLoginPage);
- response.sendRedirect(response.encodeRedirectURL(URIUtil.addPaths(request.getContextPath(),_formLoginPage)));
+ Response base_response = HttpChannel.getCurrentHttpChannel().getResponse();
+ Request base_request = HttpChannel.getCurrentHttpChannel().getRequest();
+ int redirectCode = (base_request.getHttpVersion().getVersion() < HttpVersion.HTTP_1_1.getVersion() ? HttpServletResponse.SC_MOVED_TEMPORARILY : HttpServletResponse.SC_SEE_OTHER);
+ base_response.sendRedirect(redirectCode, response.encodeRedirectURL(URIUtil.addPaths(request.getContextPath(),_formLoginPage)));
}
return Authentication.SEND_CONTINUE;
}
diff --git a/jetty-security/src/main/java/org/eclipse/jetty/security/authentication/LoginAuthenticator.java b/jetty-security/src/main/java/org/eclipse/jetty/security/authentication/LoginAuthenticator.java
index 51ad8e9..d34a438 100644
--- a/jetty-security/src/main/java/org/eclipse/jetty/security/authentication/LoginAuthenticator.java
+++ b/jetty-security/src/main/java/org/eclipse/jetty/security/authentication/LoginAuthenticator.java
@@ -40,11 +40,20 @@
protected LoginService _loginService;
protected IdentityService _identityService;
private boolean _renewSession;
-
+
+
+ /* ------------------------------------------------------------ */
protected LoginAuthenticator()
{
}
+ /* ------------------------------------------------------------ */
+ @Override
+ public void prepareRequest(ServletRequest request)
+ {
+ //empty implementation as the default
+ }
+
/* ------------------------------------------------------------ */
public UserIdentity login(String username, Object password, ServletRequest request)
@@ -58,7 +67,7 @@
return null;
}
-
+ /* ------------------------------------------------------------ */
@Override
public void setConfiguration(AuthConfiguration configuration)
{
@@ -70,12 +79,16 @@
throw new IllegalStateException("No IdentityService for "+this+" in "+configuration);
_renewSession=configuration.isSessionRenewedOnAuthentication();
}
-
+
+
+ /* ------------------------------------------------------------ */
public LoginService getLoginService()
{
return _loginService;
}
-
+
+
+ /* ------------------------------------------------------------ */
/** Change the session id.
* The session is changed to a new instance with a new ID if and only if:<ul>
* <li>A session exists.
diff --git a/jetty-security/src/main/java/org/eclipse/jetty/security/authentication/SessionAuthentication.java b/jetty-security/src/main/java/org/eclipse/jetty/security/authentication/SessionAuthentication.java
index ab0888e..dd4c31a 100644
--- a/jetty-security/src/main/java/org/eclipse/jetty/security/authentication/SessionAuthentication.java
+++ b/jetty-security/src/main/java/org/eclipse/jetty/security/authentication/SessionAuthentication.java
@@ -29,16 +29,15 @@
import javax.servlet.http.HttpSessionBindingListener;
import javax.servlet.http.HttpSessionEvent;
+import org.eclipse.jetty.security.AbstractUserAuthentication;
import org.eclipse.jetty.security.LoginService;
import org.eclipse.jetty.security.SecurityHandler;
-import org.eclipse.jetty.server.Authentication;
import org.eclipse.jetty.server.UserIdentity;
-import org.eclipse.jetty.server.UserIdentity.Scope;
import org.eclipse.jetty.server.session.AbstractSession;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;
-public class SessionAuthentication implements Authentication.User, Serializable, HttpSessionActivationListener, HttpSessionBindingListener
+public class SessionAuthentication extends AbstractUserAuthentication implements Serializable, HttpSessionActivationListener, HttpSessionBindingListener
{
private static final Logger LOG = Log.getLogger(SessionAuthentication.class);
@@ -48,35 +47,17 @@
public final static String __J_AUTHENTICATED="org.eclipse.jetty.security.UserIdentity";
- private final String _method;
private final String _name;
private final Object _credentials;
-
- private transient UserIdentity _userIdentity;
private transient HttpSession _session;
public SessionAuthentication(String method, UserIdentity userIdentity, Object credentials)
{
- _method = method;
- _userIdentity = userIdentity;
- _name=_userIdentity.getUserPrincipal().getName();
+ super(method, userIdentity);
+ _name=userIdentity.getUserPrincipal().getName();
_credentials=credentials;
}
- public String getAuthMethod()
- {
- return _method;
- }
-
- public UserIdentity getUserIdentity()
- {
- return _userIdentity;
- }
-
- public boolean isUserInRole(Scope scope, String role)
- {
- return _userIdentity.isUserInRole(role, scope);
- }
private void readObject(ObjectInputStream stream)
throws IOException, ClassNotFoundException
diff --git a/jetty-security/src/test/java/org/eclipse/jetty/security/ConstraintTest.java b/jetty-security/src/test/java/org/eclipse/jetty/security/ConstraintTest.java
index a1480b4..42ddacf 100644
--- a/jetty-security/src/test/java/org/eclipse/jetty/security/ConstraintTest.java
+++ b/jetty-security/src/test/java/org/eclipse/jetty/security/ConstraintTest.java
@@ -19,7 +19,9 @@
package org.eclipse.jetty.security;
import static org.hamcrest.Matchers.startsWith;
+import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
import static org.junit.matchers.JUnitMatchers.containsString;
@@ -28,6 +30,7 @@
import java.security.MessageDigest;
import java.util.ArrayList;
import java.util.Arrays;
+import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
@@ -37,7 +40,12 @@
import java.util.regex.Matcher;
import java.util.regex.Pattern;
+import javax.servlet.HttpConstraintElement;
+import javax.servlet.HttpMethodConstraintElement;
import javax.servlet.ServletException;
+import javax.servlet.ServletSecurityElement;
+import javax.servlet.annotation.ServletSecurity.EmptyRoleSemantic;
+import javax.servlet.annotation.ServletSecurity.TransportGuarantee;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@@ -88,7 +96,8 @@
SessionHandler _session = new SessionHandler();
HashLoginService _loginService = new HashLoginService(TEST_REALM);
- _loginService.putUser("user",new Password("password"));
+ _loginService.putUser("user0", new Password("password"), new String[]{});
+ _loginService.putUser("user",new Password("password"), new String[] {"user"});
_loginService.putUser("user2",new Password("password"), new String[] {"user"});
_loginService.putUser("admin",new Password("password"), new String[] {"user","administrator"});
_loginService.putUser("user3", new Password("password"), new String[] {"foo"});
@@ -180,7 +189,16 @@
mapping6.setPathSpec("/data/*");
mapping6.setConstraint(constraint6);
- return Arrays.asList(mapping0, mapping1, mapping2, mapping3, mapping4, mapping5, mapping6);
+ Constraint constraint7 = new Constraint();
+ constraint7.setAuthenticate(true);
+ constraint7.setName("** constraint");
+ constraint7.setRoles(new String[]{Constraint.ANY_AUTH,"user"}); //the "user" role is superfluous once ** has been defined
+ ConstraintMapping mapping7 = new ConstraintMapping();
+ mapping7.setPathSpec("/starstar/*");
+ mapping7.setConstraint(constraint7);
+
+
+ return Arrays.asList(mapping0, mapping1, mapping2, mapping3, mapping4, mapping5, mapping6, mapping7);
}
@Test
@@ -208,6 +226,224 @@
assertTrue (mappings.get(2).getConstraint().getAuthenticate());
assertFalse(mappings.get(3).getConstraint().getAuthenticate());
}
+
+
+ /**
+ * Equivalent of Servlet Spec 3.1 pg 132, sec 13.4.1.1, Example 13-1
+ * @ServletSecurity
+ * @throws Exception
+ */
+ @Test
+ public void testSecurityElementExample13_1() throws Exception
+ {
+ ServletSecurityElement element = new ServletSecurityElement();
+ List<ConstraintMapping> mappings = ConstraintSecurityHandler.createConstraintsWithMappingsForPath("foo", "/foo/*", element);
+ assertTrue(mappings.isEmpty());
+ }
+
+
+ /**
+ * Equivalent of Servlet Spec 3.1 pg 132, sec 13.4.1.1, Example 13-2
+ * @ServletSecurity(@HttpConstraint(transportGuarantee = TransportGuarantee.CONFIDENTIAL))
+ *
+ * @throws Exception
+ */
+ @Test
+ public void testSecurityElementExample13_2() throws Exception
+ {
+ HttpConstraintElement httpConstraintElement = new HttpConstraintElement(TransportGuarantee.CONFIDENTIAL, new String[]{});
+ ServletSecurityElement element = new ServletSecurityElement(httpConstraintElement);
+ List<ConstraintMapping> mappings = ConstraintSecurityHandler.createConstraintsWithMappingsForPath("foo", "/foo/*", element);
+ assertTrue(!mappings.isEmpty());
+ assertEquals(1, mappings.size());
+ ConstraintMapping mapping = mappings.get(0);
+ assertEquals(2, mapping.getConstraint().getDataConstraint());
+ }
+
+ /**
+ * Equivalent of Servlet Spec 3.1 pg 132, sec 13.4.1.1, Example 13-3
+ * @ServletSecurity(@HttpConstraint(EmptyRoleSemantic.DENY))
+ * @throws Exception
+ */
+ @Test
+ public void testSecurityElementExample13_3() throws Exception
+ {
+ HttpConstraintElement httpConstraintElement = new HttpConstraintElement(EmptyRoleSemantic.DENY);
+ ServletSecurityElement element = new ServletSecurityElement(httpConstraintElement);
+ List<ConstraintMapping> mappings = ConstraintSecurityHandler.createConstraintsWithMappingsForPath("foo", "/foo/*", element);
+ assertTrue(!mappings.isEmpty());
+ assertEquals(1, mappings.size());
+ ConstraintMapping mapping = mappings.get(0);
+ assertTrue(mapping.getConstraint().isForbidden());
+ }
+
+ /**
+ * Equivalent of Servlet Spec 3.1 pg 132, sec 13.4.1.1, Example 13-4
+ * @ServletSecurity(@HttpConstraint(rolesAllowed = "R1"))
+ * @throws Exception
+ */
+ @Test
+ public void testSecurityElementExample13_4() throws Exception
+ {
+ HttpConstraintElement httpConstraintElement = new HttpConstraintElement(TransportGuarantee.NONE, "R1");
+ ServletSecurityElement element = new ServletSecurityElement(httpConstraintElement);
+ List<ConstraintMapping> mappings = ConstraintSecurityHandler.createConstraintsWithMappingsForPath("foo", "/foo/*", element);
+ assertTrue(!mappings.isEmpty());
+ assertEquals(1, mappings.size());
+ ConstraintMapping mapping = mappings.get(0);
+ assertTrue(mapping.getConstraint().getAuthenticate());
+ assertTrue(mapping.getConstraint().getRoles() != null);
+ assertEquals(1, mapping.getConstraint().getRoles().length);
+ assertEquals("R1", mapping.getConstraint().getRoles()[0]);
+ assertEquals(0, mapping.getConstraint().getDataConstraint());
+ }
+
+ /**
+ * Equivalent of Servlet Spec 3.1 pg 132, sec 13.4.1.1, Example 13-5
+ * @ServletSecurity((httpMethodConstraints = {
+ * @HttpMethodConstraint(value = "GET", rolesAllowed = "R1"),
+ * @HttpMethodConstraint(value = "POST", rolesAllowed = "R1",
+ * transportGuarantee = TransportGuarantee.CONFIDENTIAL)})
+ * @throws Exception
+ */
+ @Test
+ public void testSecurityElementExample13_5() throws Exception
+ {
+ List<HttpMethodConstraintElement> methodElements = new ArrayList<HttpMethodConstraintElement>();
+ methodElements.add(new HttpMethodConstraintElement("GET", new HttpConstraintElement(TransportGuarantee.NONE, "R1")));
+ methodElements.add(new HttpMethodConstraintElement("POST", new HttpConstraintElement(TransportGuarantee.CONFIDENTIAL, "R1")));
+ ServletSecurityElement element = new ServletSecurityElement(methodElements);
+ List<ConstraintMapping> mappings = ConstraintSecurityHandler.createConstraintsWithMappingsForPath("foo", "/foo/*", element);
+ assertTrue(!mappings.isEmpty());
+ assertEquals(2, mappings.size());
+ assertEquals("GET", mappings.get(0).getMethod());
+ assertEquals("R1", mappings.get(0).getConstraint().getRoles()[0]);
+ assertTrue(mappings.get(0).getMethodOmissions() == null);
+ assertEquals(0, mappings.get(0).getConstraint().getDataConstraint());
+ assertEquals("POST", mappings.get(1).getMethod());
+ assertEquals("R1", mappings.get(1).getConstraint().getRoles()[0]);
+ assertEquals(2, mappings.get(1).getConstraint().getDataConstraint());
+ assertTrue(mappings.get(1).getMethodOmissions() == null);
+ }
+
+ /**
+ * Equivalent of Servlet Spec 3.1 pg 132, sec 13.4.1.1, Example 13-6
+ * @ServletSecurity(value = @HttpConstraint(rolesAllowed = "R1"), httpMethodConstraints = @HttpMethodConstraint("GET"))
+ * @throws Exception
+ */
+ @Test
+ public void testSecurityElementExample13_6 () throws Exception
+ {
+ List<HttpMethodConstraintElement> methodElements = new ArrayList<HttpMethodConstraintElement>();
+ methodElements.add(new HttpMethodConstraintElement("GET"));
+ ServletSecurityElement element = new ServletSecurityElement(new HttpConstraintElement(TransportGuarantee.NONE, "R1"), methodElements);
+ List<ConstraintMapping> mappings = ConstraintSecurityHandler.createConstraintsWithMappingsForPath("foo", "/foo/*", element);
+ assertTrue(!mappings.isEmpty());
+ assertEquals(2, mappings.size());
+ assertTrue(mappings.get(0).getMethodOmissions() != null);
+ assertEquals("GET", mappings.get(0).getMethodOmissions()[0]);
+ assertTrue(mappings.get(0).getConstraint().getAuthenticate());
+ assertEquals("R1", mappings.get(0).getConstraint().getRoles()[0]);
+ assertEquals("GET", mappings.get(1).getMethod());
+ assertTrue(mappings.get(1).getMethodOmissions() == null);
+ assertEquals(0, mappings.get(1).getConstraint().getDataConstraint());
+ assertFalse(mappings.get(1).getConstraint().getAuthenticate());
+ }
+
+ /**
+ * Equivalent of Servlet Spec 3.1 pg 132, sec 13.4.1.1, Example 13-7
+ * @ServletSecurity(value = @HttpConstraint(rolesAllowed = "R1"),
+ * httpMethodConstraints = @HttpMethodConstraint(value="TRACE",
+ * emptyRoleSemantic = EmptyRoleSemantic.DENY))
+ * @throws Exception
+ */
+ @Test
+ public void testSecurityElementExample13_7() throws Exception
+ {
+ List<HttpMethodConstraintElement> methodElements = new ArrayList<HttpMethodConstraintElement>();
+ methodElements.add(new HttpMethodConstraintElement("TRACE", new HttpConstraintElement(EmptyRoleSemantic.DENY)));
+ ServletSecurityElement element = new ServletSecurityElement(new HttpConstraintElement(TransportGuarantee.NONE, "R1"), methodElements);
+ List<ConstraintMapping> mappings = ConstraintSecurityHandler.createConstraintsWithMappingsForPath("foo", "/foo/*", element);
+ assertTrue(!mappings.isEmpty());
+ assertEquals(2, mappings.size());
+ assertTrue(mappings.get(0).getMethodOmissions() != null);
+ assertEquals("TRACE", mappings.get(0).getMethodOmissions()[0]);
+ assertTrue(mappings.get(0).getConstraint().getAuthenticate());
+ assertEquals("R1", mappings.get(0).getConstraint().getRoles()[0]);
+ assertEquals("TRACE", mappings.get(1).getMethod());
+ assertTrue(mappings.get(1).getMethodOmissions() == null);
+ assertEquals(0, mappings.get(1).getConstraint().getDataConstraint());
+ assertTrue(mappings.get(1).getConstraint().isForbidden());
+ }
+
+ @Test
+ public void testUncoveredHttpMethodDetection() throws Exception
+ {
+ //Test no methods named
+ Constraint constraint1 = new Constraint();
+ constraint1.setAuthenticate(true);
+ constraint1.setName("** constraint");
+ constraint1.setRoles(new String[]{Constraint.ANY_AUTH,"user"}); //No methods named, no uncovered methods
+ ConstraintMapping mapping1 = new ConstraintMapping();
+ mapping1.setPathSpec("/starstar/*");
+ mapping1.setConstraint(constraint1);
+
+ _security.setConstraintMappings(Collections.singletonList(mapping1));
+ _security.setAuthenticator(new BasicAuthenticator());
+ _server.start();
+
+ Set<String> uncoveredPaths = _security.getPathsWithUncoveredHttpMethods();
+ assertTrue(uncoveredPaths.isEmpty()); //no uncovered methods
+
+ //Test only an explicitly named method, no omissions to cover other methods
+ Constraint constraint2 = new Constraint();
+ constraint2.setAuthenticate(true);
+ constraint2.setName("user constraint");
+ constraint2.setRoles(new String[]{"user"});
+ ConstraintMapping mapping2 = new ConstraintMapping();
+ mapping2.setPathSpec("/user/*");
+ mapping2.setMethod("GET");
+ mapping2.setConstraint(constraint2);
+
+ _security.addConstraintMapping(mapping2);
+ uncoveredPaths = _security.getPathsWithUncoveredHttpMethods();
+ assertNotNull(uncoveredPaths);
+ assertEquals(1, uncoveredPaths.size());
+ assertTrue(uncoveredPaths.contains("/user/*"));
+
+ //Test an explicitly named method with a http-method-omission to cover all other methods
+ Constraint constraint2a = new Constraint();
+ constraint2a.setAuthenticate(true);
+ constraint2a.setName("forbid constraint");
+ ConstraintMapping mapping2a = new ConstraintMapping();
+ mapping2a.setPathSpec("/user/*");
+ mapping2a.setMethodOmissions(new String[]{"GET"});
+ mapping2a.setConstraint(constraint2a);
+
+ _security.addConstraintMapping(mapping2a);
+ uncoveredPaths = _security.getPathsWithUncoveredHttpMethods();
+ assertNotNull(uncoveredPaths);
+ assertEquals(0, uncoveredPaths.size());
+
+ //Test a http-method-omission only
+ Constraint constraint3 = new Constraint();
+ constraint3.setAuthenticate(true);
+ constraint3.setName("omit constraint");
+ ConstraintMapping mapping3 = new ConstraintMapping();
+ mapping3.setPathSpec("/omit/*");
+ mapping3.setMethodOmissions(new String[]{"GET", "POST"});
+ mapping3.setConstraint(constraint3);
+
+ _security.addConstraintMapping(mapping3);
+ uncoveredPaths = _security.getPathsWithUncoveredHttpMethods();
+ assertNotNull(uncoveredPaths);
+ assertTrue(uncoveredPaths.contains("/omit/*"));
+
+ _security.setDenyUncoveredHttpMethods(true);
+ uncoveredPaths = _security.getPathsWithUncoveredHttpMethods();
+ assertNotNull(uncoveredPaths);
+ assertEquals(0, uncoveredPaths.size());
+ }
@Test
public void testBasic() throws Exception
@@ -252,7 +488,6 @@
_security.setAuthenticator(new BasicAuthenticator());
- _security.setStrict(false);
_server.start();
String response;
@@ -377,7 +612,6 @@
DigestAuthenticator authenticator = new DigestAuthenticator();
authenticator.setMaxNonceCount(5);
_security.setAuthenticator(authenticator);
- _security.setStrict(false);
_server.start();
String response;
@@ -464,7 +698,6 @@
public void testFormDispatch() throws Exception
{
_security.setAuthenticator(new FormAuthenticator("/testLoginPage","/testErrorPage",true));
- _security.setStrict(false);
_server.start();
String response;
@@ -519,7 +752,6 @@
public void testFormRedirect() throws Exception
{
_security.setAuthenticator(new FormAuthenticator("/testLoginPage","/testErrorPage",false));
- _security.setStrict(false);
_server.start();
String response;
@@ -576,7 +808,6 @@
public void testFormPostRedirect() throws Exception
{
_security.setAuthenticator(new FormAuthenticator("/testLoginPage","/testErrorPage",false));
- _security.setStrict(false);
_server.start();
String response;
@@ -608,6 +839,7 @@
"Content-Length: 31\r\n" +
"\r\n" +
"j_username=user&j_password=wrong\r\n");
+
assertThat(response,containsString("Location"));
response = _connector.getResponses("POST /ctx/j_security_check HTTP/1.0\r\n" +
@@ -646,7 +878,6 @@
public void testFormNoCookies() throws Exception
{
_security.setAuthenticator(new FormAuthenticator("/testLoginPage","/testErrorPage",false));
- _security.setStrict(false);
_server.start();
String response;
@@ -719,7 +950,7 @@
assertThat(response,containsString("WWW-Authenticate: basic realm=\"TestRealm\""));
response = _connector.getResponses("GET /ctx/auth/info HTTP/1.0\r\n" +
- "Authorization: Basic " + B64Code.encode("user:password") + "\r\n" +
+ "Authorization: Basic " + B64Code.encode("user3:password") + "\r\n" +
"\r\n");
assertThat(response,startsWith("HTTP/1.1 403"));
@@ -793,9 +1024,9 @@
response = _connector.getResponses("POST /ctx/j_security_check HTTP/1.0\r\n" +
"Cookie: JSESSIONID=" + session + "\r\n" +
"Content-Type: application/x-www-form-urlencoded\r\n" +
- "Content-Length: 35\r\n" +
+ "Content-Length: 36\r\n" +
"\r\n" +
- "j_username=user&j_password=password\r\n");
+ "j_username=user0&j_password=password\r\n");
assertThat(response,startsWith("HTTP/1.1 302 "));
assertThat(response,containsString("Location"));
assertThat(response,containsString("/ctx/auth/info"));
@@ -904,9 +1135,9 @@
response = _connector.getResponses("POST /ctx/j_security_check HTTP/1.0\r\n" +
"Cookie: JSESSIONID=" + session + "\r\n" +
"Content-Type: application/x-www-form-urlencoded\r\n" +
- "Content-Length: 35\r\n" +
+ "Content-Length: 36\r\n" +
"\r\n" +
- "j_username=user&j_password=password\r\n");
+ "j_username=user3&j_password=password\r\n");
assertThat(response,startsWith("HTTP/1.1 302 "));
assertThat(response,containsString("Location"));
assertThat(response,containsString("/ctx/auth/info"));
@@ -949,12 +1180,35 @@
"\r\n");
assertThat(response,startsWith("HTTP/1.1 200 OK"));
+ //check user2 does not have right role to access /admin/*
response = _connector.getResponses("GET /ctx/admin/info HTTP/1.0\r\n" +
"Cookie: JSESSIONID=" + session + "\r\n" +
"\r\n");
assertThat(response,startsWith("HTTP/1.1 403"));
assertThat(response,containsString("!role"));
+
+ //log in as user3, who doesn't have a valid role, but we are checking a constraint
+ //of ** which just means they have to be authenticated
+ response = _connector.getResponses("GET /ctx/starstar/info HTTP/1.0\r\n\r\n");
+ assertThat(response,startsWith("HTTP/1.1 302 "));
+ assertThat(response,containsString("testLoginPage"));
+ session = response.substring(response.indexOf("JSESSIONID=") + 11, response.indexOf(";Path=/ctx"));
+ response = _connector.getResponses("POST /ctx/j_security_check HTTP/1.0\r\n" +
+ "Cookie: JSESSIONID=" + session + "\r\n" +
+ "Content-Type: application/x-www-form-urlencoded\r\n" +
+ "Content-Length: 36\r\n" +
+ "\r\n" +
+ "j_username=user3&j_password=password\r\n");
+ assertThat(response,startsWith("HTTP/1.1 302 "));
+ assertThat(response,containsString("Location"));
+ assertThat(response,containsString("/ctx/starstar/info"));
+ session = response.substring(response.indexOf("JSESSIONID=") + 11, response.indexOf(";Path=/ctx"));
+
+ response = _connector.getResponses("GET /ctx/starstar/info HTTP/1.0\r\n" +
+ "Cookie: JSESSIONID=" + session + "\r\n" +
+ "\r\n");
+ assertThat(response,startsWith("HTTP/1.1 200 OK"));
// log in again as admin
@@ -1033,7 +1287,6 @@
RoleCheckHandler check=new RoleCheckHandler();
_security.setHandler(check);
_security.setAuthenticator(new BasicAuthenticator());
- _security.setStrict(false);
_server.start();
@@ -1065,7 +1318,6 @@
public void testDeferredBasic() throws Exception
{
_security.setAuthenticator(new BasicAuthenticator());
- _security.setStrict(false);
_server.start();
String response;
@@ -1092,7 +1344,6 @@
public void testRelaxedMethod() throws Exception
{
_security.setAuthenticator(new BasicAuthenticator());
- _security.setStrict(false);
_server.start();
String response;
diff --git a/jetty-security/src/test/java/org/eclipse/jetty/security/SpecExampleConstraintTest.java b/jetty-security/src/test/java/org/eclipse/jetty/security/SpecExampleConstraintTest.java
index 1d656ae..ff20051 100644
--- a/jetty-security/src/test/java/org/eclipse/jetty/security/SpecExampleConstraintTest.java
+++ b/jetty-security/src/test/java/org/eclipse/jetty/security/SpecExampleConstraintTest.java
@@ -19,6 +19,7 @@
package org.eclipse.jetty.security;
import static org.hamcrest.Matchers.startsWith;
+import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
@@ -232,6 +233,45 @@
}
}
+ @Test
+ public void testUncoveredHttpMethodDetection() throws Exception
+ {
+ _security.setAuthenticator(new BasicAuthenticator());
+ _server.start();
+
+ Set<String> paths = _security.getPathsWithUncoveredHttpMethods();
+ assertEquals(1, paths.size());
+ assertEquals("/*", paths.iterator().next());
+ }
+
+ @Test
+ public void testUncoveredHttpMethodsDenied() throws Exception
+ {
+ try
+ {
+ _security.setDenyUncoveredHttpMethods(false);
+ _security.setAuthenticator(new BasicAuthenticator());
+ _server.start();
+
+ //There are uncovered methods for GET/POST at url /*
+ //without deny-uncovered-http-methods they should be accessible
+ String response;
+ response = _connector.getResponses("GET /ctx/index.html HTTP/1.0\r\n\r\n");
+ assertThat(response,startsWith("HTTP/1.1 200 OK"));
+
+ //set deny-uncovered-http-methods true
+ _security.setDenyUncoveredHttpMethods(true);
+
+ //check they cannot be accessed
+ response = _connector.getResponses("GET /ctx/index.html HTTP/1.0\r\n\r\n");
+ assertTrue(response.startsWith("HTTP/1.1 403 Forbidden"));
+ }
+ finally
+ {
+ _security.setDenyUncoveredHttpMethods(false);
+ }
+
+ }
@Test
@@ -239,7 +279,6 @@
{
_security.setAuthenticator(new BasicAuthenticator());
- _security.setStrict(false);
_server.start();
String response;
diff --git a/jetty-server/pom.xml b/jetty-server/pom.xml
index 70f2af6..e3dcd9e 100644
--- a/jetty-server/pom.xml
+++ b/jetty-server/pom.xml
@@ -2,7 +2,7 @@
<parent>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-project</artifactId>
- <version>9.0.5-SNAPSHOT</version>
+ <version>9.1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>jetty-server</artifactId>
@@ -26,7 +26,7 @@
</goals>
<configuration>
<instructions>
- <Import-Package>javax.servlet.*;version="2.6.0",org.eclipse.jetty.jmx.*;version="9.0";resolution:=optional,*</Import-Package>
+ <Import-Package>javax.servlet.*;version="[2.6.0,3.2)",org.eclipse.jetty.jmx.*;version="9.1";resolution:=optional,*</Import-Package>
<_nouses>true</_nouses>
</instructions>
</configuration>
@@ -89,8 +89,12 @@
<scope>test</scope>
</dependency>
<dependency>
+ <groupId>javax.servlet</groupId>
+ <artifactId>javax.servlet-api</artifactId>
+<!--
<groupId>org.eclipse.jetty.orbit</groupId>
<artifactId>javax.servlet</artifactId>
+-->
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/AbstractConnector.java b/jetty-server/src/main/java/org/eclipse/jetty/server/AbstractConnector.java
index 28fea90..0747b01 100644
--- a/jetty-server/src/main/java/org/eclipse/jetty/server/AbstractConnector.java
+++ b/jetty-server/src/main/java/org/eclipse/jetty/server/AbstractConnector.java
@@ -268,10 +268,13 @@
protected void interruptAcceptors()
{
- for (Thread thread : _acceptors)
+ synchronized (this)
{
- if (thread != null)
- thread.interrupt();
+ for (Thread thread : _acceptors)
+ {
+ if (thread != null)
+ thread.interrupt();
+ }
}
}
@@ -306,9 +309,12 @@
public void join(long timeout) throws InterruptedException
{
- for (Thread thread : _acceptors)
- if (thread != null)
- thread.join(timeout);
+ synchronized (this)
+ {
+ for (Thread thread : _acceptors)
+ if (thread != null)
+ thread.join(timeout);
+ }
}
protected abstract void accept(int acceptorID) throws IOException, InterruptedException;
@@ -464,7 +470,7 @@
if (isAccepting())
LOG.warn(e);
else
- LOG.debug(e);
+ LOG.ignore(e);
}
}
}
diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/AsyncContextState.java b/jetty-server/src/main/java/org/eclipse/jetty/server/AsyncContextState.java
index 876e7ae..2538f41 100644
--- a/jetty-server/src/main/java/org/eclipse/jetty/server/AsyncContextState.java
+++ b/jetty-server/src/main/java/org/eclipse/jetty/server/AsyncContextState.java
@@ -28,6 +28,8 @@
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
+import org.eclipse.jetty.server.handler.ContextHandler;
+
public class AsyncContextState implements AsyncContext
{
@@ -92,17 +94,20 @@
@Override
public <T extends AsyncListener> T createListener(Class<T> clazz) throws ServletException
- {
+ {
+ ContextHandler contextHandler = state().getContextHandler();
+ if (contextHandler != null)
+ return contextHandler.getServletContext().createListener(clazz);
try
{
return clazz.newInstance();
}
- catch(Exception e)
+ catch (Exception e)
{
throw new ServletException(e);
}
}
-
+
@Override
public void dispatch()
{
diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/ByteBufferHttpInput.java b/jetty-server/src/main/java/org/eclipse/jetty/server/ByteBufferHttpInput.java
deleted file mode 100644
index 90d81d9..0000000
--- a/jetty-server/src/main/java/org/eclipse/jetty/server/ByteBufferHttpInput.java
+++ /dev/null
@@ -1,46 +0,0 @@
-//
-// ========================================================================
-// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
-// ------------------------------------------------------------------------
-// All rights reserved. This program and the accompanying materials
-// are made available under the terms of the Eclipse Public License v1.0
-// and Apache License v2.0 which accompanies this distribution.
-//
-// The Eclipse Public License is available at
-// http://www.eclipse.org/legal/epl-v10.html
-//
-// The Apache License v2.0 is available at
-// http://www.opensource.org/licenses/apache2.0.php
-//
-// You may elect to redistribute this code under either of these licenses.
-// ========================================================================
-//
-
-package org.eclipse.jetty.server;
-
-import java.nio.ByteBuffer;
-
-/**
- * <p>An implementation of HttpInput using {@link ByteBuffer} as items.</p>
- */
-public class ByteBufferHttpInput extends HttpInput<ByteBuffer>
-{
- @Override
- protected int remaining(ByteBuffer item)
- {
- return item.remaining();
- }
-
- @Override
- protected int get(ByteBuffer item, byte[] buffer, int offset, int length)
- {
- int l = Math.min(item.remaining(), length);
- item.get(buffer, offset, l);
- return l;
- }
-
- @Override
- protected void onContentConsumed(ByteBuffer item)
- {
- }
-}
diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/ByteBufferQueuedHttpInput.java b/jetty-server/src/main/java/org/eclipse/jetty/server/ByteBufferQueuedHttpInput.java
new file mode 100644
index 0000000..db5e841
--- /dev/null
+++ b/jetty-server/src/main/java/org/eclipse/jetty/server/ByteBufferQueuedHttpInput.java
@@ -0,0 +1,55 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server;
+
+import java.nio.ByteBuffer;
+
+import javax.servlet.ReadListener;
+
+/**
+ * <p>An implementation of HttpInput using {@link ByteBuffer} as items.</p>
+ */
+public class ByteBufferQueuedHttpInput extends QueuedHttpInput<ByteBuffer>
+{
+ @Override
+ protected int remaining(ByteBuffer item)
+ {
+ return item.remaining();
+ }
+
+ @Override
+ protected int get(ByteBuffer item, byte[] buffer, int offset, int length)
+ {
+ int l = Math.min(item.remaining(), length);
+ item.get(buffer, offset, l);
+ return l;
+ }
+
+ @Override
+ protected void consume(ByteBuffer item, int length)
+ {
+ item.position(item.position()+length);
+ }
+
+ @Override
+ protected void onContentConsumed(ByteBuffer item)
+ {
+ }
+
+}
diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/EncodingHttpWriter.java b/jetty-server/src/main/java/org/eclipse/jetty/server/EncodingHttpWriter.java
index 7725a34..fd8c1c5 100644
--- a/jetty-server/src/main/java/org/eclipse/jetty/server/EncodingHttpWriter.java
+++ b/jetty-server/src/main/java/org/eclipse/jetty/server/EncodingHttpWriter.java
@@ -49,10 +49,10 @@
public void write (char[] s,int offset, int length) throws IOException
{
HttpOutput out = _out;
- if (length==0)
+ if (length==0 && out.isAllContentWritten())
{
- if (_out.isAllContentWritten())
- close();
+ out.close();
+ return;
}
while (length > 0)
diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannel.java b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannel.java
index ae28048..e4d7259 100644
--- a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannel.java
+++ b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannel.java
@@ -25,6 +25,7 @@
import java.util.concurrent.atomic.AtomicInteger;
import javax.servlet.DispatcherType;
import javax.servlet.RequestDispatcher;
+import javax.servlet.WriteListener;
import org.eclipse.jetty.http.HttpField;
import org.eclipse.jetty.http.HttpFields;
@@ -42,7 +43,8 @@
import org.eclipse.jetty.io.ChannelEndPoint;
import org.eclipse.jetty.io.EndPoint;
import org.eclipse.jetty.io.EofException;
-import org.eclipse.jetty.server.HttpChannelState.Next;
+import org.eclipse.jetty.server.HttpChannelState.Action;
+import org.eclipse.jetty.server.handler.ContextHandler;
import org.eclipse.jetty.server.handler.ErrorHandler;
import org.eclipse.jetty.util.BlockingCallback;
import org.eclipse.jetty.util.Callback;
@@ -104,6 +106,7 @@
_uri = new HttpURI(URIUtil.__CHARSET);
_state = new HttpChannelState(this);
+ input.init(_state);
_request = new Request(this, input);
_response = new Response(this, new HttpOutput(this));
}
@@ -249,36 +252,69 @@
// The loop is controlled by the call to async.unhandle in the
// finally block below. Unhandle will return false only if an async dispatch has
// already happened when unhandle is called.
- HttpChannelState.Next next = _state.handling();
- while (next==Next.CONTINUE && getServer().isRunning())
+ HttpChannelState.Action action = _state.handling();
+ loop: while (action.ordinal()<HttpChannelState.Action.WAIT.ordinal() && getServer().isRunning())
{
try
{
- _request.setHandled(false);
- _response.getHttpOutput().reopen();
-
- if (_state.isInitial())
+ LOG.debug("{} action {}",this,action);
+
+ switch(action)
{
- _request.setTimeStamp(System.currentTimeMillis());
- _request.setDispatcherType(DispatcherType.REQUEST);
+ case REQUEST_DISPATCH:
+ _request.setHandled(false);
+ _response.getHttpOutput().reopen();
+ _request.setTimeStamp(System.currentTimeMillis());
+ _request.setDispatcherType(DispatcherType.REQUEST);
- for (HttpConfiguration.Customizer customizer : _configuration.getCustomizers())
- customizer.customize(getConnector(),_configuration,_request);
- getServer().handle(this);
- }
- else
- {
- if (_request.getHttpChannelState().isExpired())
- {
+ for (HttpConfiguration.Customizer customizer : _configuration.getCustomizers())
+ customizer.customize(getConnector(),_configuration,_request);
+ getServer().handle(this);
+ break;
+
+ case ASYNC_DISPATCH:
+ _request.setHandled(false);
+ _response.getHttpOutput().reopen();
+ _request.setDispatcherType(DispatcherType.ASYNC);
+ getServer().handleAsync(this);
+ break;
+
+ case ASYNC_EXPIRED:
+ _request.setHandled(false);
+ _response.getHttpOutput().reopen();
_request.setDispatcherType(DispatcherType.ERROR);
_request.setAttribute(RequestDispatcher.ERROR_STATUS_CODE,new Integer(500));
_request.setAttribute(RequestDispatcher.ERROR_MESSAGE,"Async Timeout");
_request.setAttribute(RequestDispatcher.ERROR_REQUEST_URI,_request.getRequestURI());
_response.setStatusWithReason(500,"Async Timeout");
+
+ getServer().handleAsync(this);
+ break;
+
+ case READ_CALLBACK:
+ {
+ ContextHandler handler=_state.getContextHandler();
+ if (handler!=null)
+ handler.handle(_request.getHttpInput());
+ else
+ _request.getHttpInput().run();
+ break;
}
- else
- _request.setDispatcherType(DispatcherType.ASYNC);
- getServer().handleAsync(this);
+
+ case WRITE_CALLBACK:
+ {
+ ContextHandler handler=_state.getContextHandler();
+
+ if (handler!=null)
+ handler.handle(_response.getHttpOutput());
+ else
+ _response.getHttpOutput().run();
+ break;
+ }
+
+ default:
+ break loop;
+
}
}
catch (Error e)
@@ -300,7 +336,7 @@
}
finally
{
- next = _state.unhandle();
+ action = _state.unhandle();
}
}
@@ -308,7 +344,7 @@
Thread.currentThread().setName(threadName);
setCurrentHttpChannel(null);
- if (next==Next.COMPLETE)
+ if (action==Action.COMPLETE)
{
try
{
@@ -326,7 +362,7 @@
}
catch(Exception e)
{
- LOG.warn(e);
+ LOG.warn("handle complete",e);
}
finally
{
@@ -335,9 +371,9 @@
}
}
- LOG.debug("{} handle exit, result {}", this, next);
+ LOG.debug("{} handle exit, result {}", this, action);
- return next!=Next.WAIT;
+ return action!=Action.WAIT;
}
/**
@@ -575,7 +611,8 @@
@Override
public boolean messageComplete()
{
- _request.getHttpInput().shutdown();
+ LOG.debug("{} messageComplete", this);
+ _request.getHttpInput().messageComplete();
return true;
}
@@ -593,17 +630,19 @@
try
{
- if (_state.handling()==Next.CONTINUE)
+ if (_state.handling()==Action.REQUEST_DISPATCH)
sendResponse(new ResponseInfo(HttpVersion.HTTP_1_1,new HttpFields(),0,status,reason,false),null,true);
}
catch (IOException e)
{
- LOG.warn(e);
+ LOG.debug(e);
}
finally
{
- if (_state.unhandle()==Next.COMPLETE)
+ if (_state.unhandle()==Action.COMPLETE)
_state.completed();
+ else
+ throw new IllegalStateException();
}
}
@@ -726,7 +765,7 @@
}
else
{
- LOG.warn(x);
+ LOG.warn("Commit failed",x);
_transport.send(HttpGenerator.RESPONSE_500_INFO,null,true,new Callback()
{
@Override
diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannelState.java b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannelState.java
index 10ee7bc..a57d7ed 100644
--- a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannelState.java
+++ b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannelState.java
@@ -21,7 +21,6 @@
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
-
import javax.servlet.AsyncEvent;
import javax.servlet.AsyncListener;
import javax.servlet.RequestDispatcher;
@@ -62,32 +61,41 @@
{
IDLE, // Idle request
DISPATCHED, // Request dispatched to filter/servlet
- ASYNCSTARTED, // Suspend called, but not yet returned to container
- REDISPATCHING, // resumed while dispatched
ASYNCWAIT, // Suspended and parked
- REDISPATCH, // Has been scheduled
- REDISPATCHED, // Request redispatched to filter/servlet
- COMPLETECALLED,// complete called
+ ASYNCIO, // Has been dispatched for async IO
COMPLETING, // Request is completable
COMPLETED // Request is complete
}
- public enum Next
+ public enum Action
{
- CONTINUE, // Continue handling the channel
- WAIT, // Wait for further events
- COMPLETE // Complete the channel
+ REQUEST_DISPATCH, // handle a normal request dispatch
+ ASYNC_DISPATCH, // handle an async request dispatch
+ ASYNC_EXPIRED, // handle an async timeout
+ WRITE_CALLBACK, // handle an IO write callback
+ READ_CALLBACK, // handle an IO read callback
+ WAIT, // Wait for further events
+ COMPLETE // Complete the channel
+ }
+
+ public enum Async
+ {
+ STARTED,
+ DISPATCH,
+ COMPLETE,
+ EXPIRING,
+ EXPIRED
}
+ private final boolean DEBUG=LOG.isDebugEnabled();
private final HttpChannel<?> _channel;
- private List<AsyncListener> _lastAsyncListeners;
private List<AsyncListener> _asyncListeners;
private State _state;
+ private Async _async;
private boolean _initial;
- private boolean _dispatched;
- private boolean _expired;
- private volatile boolean _responseWrapped;
+ private boolean _asyncRead;
+ private boolean _asyncWrite;
private long _timeoutMs=DEFAULT_TIMEOUT;
private AsyncContextEvent _event;
@@ -95,6 +103,7 @@
{
_channel=channel;
_state=State.IDLE;
+ _async=null;
_initial=true;
}
@@ -145,7 +154,7 @@
{
synchronized (this)
{
- return super.toString()+"@"+getStatusString();
+ return String.format("%s@%x{s=%s i=%b a=%s}",getClass().getSimpleName(),hashCode(),_state,_initial,_async);
}
}
@@ -153,97 +162,102 @@
{
synchronized (this)
{
- return _state+
- (_initial?",initial":"")+
- (_dispatched?",resumed":"")+
- (_expired?",expired":"");
+ return String.format("s=%s i=%b a=%s",_state,_initial,_async);
}
}
/**
* @return Next handling of the request should proceed
*/
- protected Next handling()
+ protected Action handling()
{
synchronized (this)
{
+ if(DEBUG)
+ LOG.debug("{} handling {}",this,_state);
switch(_state)
{
case IDLE:
_initial=true;
_state=State.DISPATCHED;
- if (_lastAsyncListeners!=null)
- _lastAsyncListeners.clear();
- if (_asyncListeners!=null)
- _asyncListeners.clear();
- else
- {
- _asyncListeners=_lastAsyncListeners;
- _lastAsyncListeners=null;
- }
- break;
-
- case COMPLETECALLED:
- _state=State.COMPLETING;
- return Next.COMPLETE;
+ return Action.REQUEST_DISPATCH;
case COMPLETING:
- return Next.COMPLETE;
-
- case ASYNCWAIT:
- return Next.WAIT;
+ return Action.COMPLETE;
case COMPLETED:
- return Next.WAIT;
+ return Action.WAIT;
- case REDISPATCH:
- _state=State.REDISPATCHED;
- break;
+ case ASYNCWAIT:
+ if (_asyncRead)
+ {
+ _state=State.ASYNCIO;
+ _asyncRead=false;
+ return Action.READ_CALLBACK;
+ }
+ if (_asyncWrite)
+ {
+ _state=State.ASYNCIO;
+ _asyncWrite=false;
+ return Action.WRITE_CALLBACK;
+ }
+
+ if (_async!=null)
+ {
+ Async async=_async;
+ switch(async)
+ {
+ case COMPLETE:
+ _state=State.COMPLETING;
+ return Action.COMPLETE;
+ case DISPATCH:
+ _state=State.DISPATCHED;
+ _async=null;
+ return Action.ASYNC_DISPATCH;
+ case EXPIRING:
+ break;
+ case EXPIRED:
+ _state=State.DISPATCHED;
+ _async=null;
+ return Action.ASYNC_EXPIRED;
+ case STARTED:
+ if (DEBUG)
+ LOG.debug("TODO Fix this double dispatch",new IllegalStateException(this
+ .getStatusString()));
+ return Action.WAIT;
+ }
+ }
+
+ return Action.WAIT;
default:
throw new IllegalStateException(this.getStatusString());
}
-
- _responseWrapped=false;
- return Next.CONTINUE;
-
}
}
-
public void startAsync(AsyncContextEvent event)
{
+ final List<AsyncListener> lastAsyncListeners;
+
synchronized (this)
{
- switch(_state)
- {
- case DISPATCHED:
- case REDISPATCHED:
- _dispatched=false;
- _expired=false;
- _responseWrapped=event.getSuppliedResponse()!=_channel.getResponse();
- _responseWrapped=false;
- _event=event;
- _state=State.ASYNCSTARTED;
- List<AsyncListener> listeners=_lastAsyncListeners;
- _lastAsyncListeners=_asyncListeners;
- if (listeners!=null)
- listeners.clear();
- _asyncListeners=listeners;
- break;
-
- default:
- throw new IllegalStateException(this.getStatusString());
- }
+ if (_state!=State.DISPATCHED || _async!=null)
+ throw new IllegalStateException(this.getStatusString());
+
+ _async=Async.STARTED;
+ _event=event;
+ lastAsyncListeners=_asyncListeners;
+ _asyncListeners=null;
}
- if (_lastAsyncListeners!=null)
+ if (lastAsyncListeners!=null)
{
- for (AsyncListener listener : _lastAsyncListeners)
+ for (AsyncListener listener : lastAsyncListeners)
{
try
{
- listener.onStartAsync(_event);
+ listener.onStartAsync(event);
}
catch(Exception e)
{
@@ -269,39 +283,63 @@
* @return next actions
* be handled again (eg because of a resume that happened before unhandle was called)
*/
- protected Next unhandle()
+ protected Action unhandle()
{
synchronized (this)
{
+ if(DEBUG)
+ LOG.debug("{} unhandle {}",this,_state);
+
switch(_state)
{
- case REDISPATCHED:
case DISPATCHED:
- _state=State.COMPLETING;
- return Next.COMPLETE;
-
- case IDLE:
- throw new IllegalStateException(this.getStatusString());
-
- case ASYNCSTARTED:
- _initial=false;
- _state=State.ASYNCWAIT;
- scheduleTimeout();
- return Next.WAIT;
-
- case REDISPATCHING:
- _initial=false;
- _state=State.REDISPATCHED;
- return Next.CONTINUE;
-
- case COMPLETECALLED:
- _initial=false;
- _state=State.COMPLETING;
- return Next.COMPLETE;
-
+ case ASYNCIO:
+ break;
default:
throw new IllegalStateException(this.getStatusString());
}
+
+ if (_asyncRead)
+ {
+ _state=State.ASYNCIO;
+ _asyncRead=false;
+ return Action.READ_CALLBACK;
+ }
+
+ if (_asyncWrite)
+ {
+ _asyncWrite=false;
+ _state=State.ASYNCIO;
+ return Action.WRITE_CALLBACK;
+ }
+
+ if (_async!=null)
+ {
+ _initial=false;
+ switch(_async)
+ {
+ case COMPLETE:
+ _state=State.COMPLETING;
+ _async=null;
+ return Action.COMPLETE;
+ case DISPATCH:
+ _state=State.DISPATCHED;
+ _async=null;
+ return Action.ASYNC_DISPATCH;
+ case EXPIRED:
+ _state=State.DISPATCHED;
+ _async=null;
+ return Action.ASYNC_EXPIRED;
+ case EXPIRING:
+ case STARTED:
+ scheduleTimeout();
+ _state=State.ASYNCWAIT;
+ return Action.WAIT;
+ }
+ }
+
+ _state=State.COMPLETING;
+ return Action.COMPLETE;
}
}
@@ -310,39 +348,26 @@
boolean dispatch;
synchronized (this)
{
+ if (_async!=Async.STARTED && _async!=Async.EXPIRING)
+ throw new IllegalStateException("AsyncContext#dispath "+this.getStatusString());
+ _async=Async.DISPATCH;
+ _event.setDispatchTarget(context,path);
+
switch(_state)
{
- case ASYNCSTARTED:
- _state=State.REDISPATCHING;
- _event.setDispatchTarget(context,path);
- _dispatched=true;
- return;
-
- case ASYNCWAIT:
- dispatch=!_expired;
- _state=State.REDISPATCH;
- _event.setDispatchTarget(context,path);
- _dispatched=true;
+ case DISPATCHED:
+ case ASYNCIO:
+ dispatch=false;
break;
-
default:
- throw new IllegalStateException(this.getStatusString());
+ dispatch=true;
+ break;
}
}
+ cancelTimeout();
if (dispatch)
- {
- cancelTimeout();
scheduleDispatch();
- }
- }
-
- public boolean isDispatched()
- {
- synchronized (this)
- {
- return _dispatched;
- }
}
protected void expired()
@@ -351,17 +376,11 @@
AsyncEvent event;
synchronized (this)
{
- switch(_state)
- {
- case ASYNCSTARTED:
- case ASYNCWAIT:
- _expired=true;
- event=_event;
- aListeners=_asyncListeners;
- break;
- default:
- return;
- }
+ if (_async!=Async.STARTED)
+ return;
+ _async=Async.EXPIRING;
+ event=_event;
+ aListeners=_asyncListeners;
}
if (aListeners!=null)
@@ -378,22 +397,20 @@
}
}
}
-
+
+ boolean dispatch=false;
synchronized (this)
{
- switch(_state)
+ if (_async==Async.EXPIRING)
{
- case ASYNCSTARTED:
- case ASYNCWAIT:
- _state=State.REDISPATCH;
- break;
- default:
- _expired=false;
- break;
+ _async=Async.EXPIRED;
+ if (_state==State.ASYNCWAIT)
+ dispatch=true;
}
}
- scheduleDispatch();
+ if (dispatch)
+ scheduleDispatch();
}
public void complete()
@@ -402,30 +419,15 @@
boolean handle;
synchronized (this)
{
- switch(_state)
- {
- case DISPATCHED:
- case REDISPATCHED:
- throw new IllegalStateException(this.getStatusString());
-
- case IDLE:
- case ASYNCSTARTED:
- _state=State.COMPLETECALLED;
- return;
-
- case ASYNCWAIT:
- _state=State.COMPLETECALLED;
- handle=!_expired;
- break;
-
- default:
- throw new IllegalStateException(this.getStatusString());
- }
+ if (_async!=Async.STARTED && _async!=Async.EXPIRING)
+ throw new IllegalStateException(this.getStatusString());
+ _async=Async.COMPLETE;
+ handle=_state==State.ASYNCWAIT;
}
+ cancelTimeout();
if (handle)
{
- cancelTimeout();
ContextHandler handler=getContextHandler();
if (handler!=null)
handler.handle(_channel);
@@ -453,30 +455,34 @@
}
}
- if (aListeners!=null)
+ if (event!=null)
{
- for (AsyncListener listener : aListeners)
+ if (aListeners!=null)
{
- try
+ if (event.getThrowable()!=null)
{
- if (event!=null && event.getThrowable()!=null)
- {
- event.getSuppliedRequest().setAttribute(RequestDispatcher.ERROR_EXCEPTION,event.getThrowable());
- event.getSuppliedRequest().setAttribute(RequestDispatcher.ERROR_MESSAGE,event.getThrowable().getMessage());
- listener.onError(event);
- }
- else
- listener.onComplete(event);
+ event.getSuppliedRequest().setAttribute(RequestDispatcher.ERROR_EXCEPTION,event.getThrowable());
+ event.getSuppliedRequest().setAttribute(RequestDispatcher.ERROR_MESSAGE,event.getThrowable().getMessage());
}
- catch(Exception e)
+
+ for (AsyncListener listener : aListeners)
{
- LOG.warn(e);
+ try
+ {
+ if (event.getThrowable()!=null)
+ listener.onError(event);
+ else
+ listener.onComplete(event);
+ }
+ catch(Exception e)
+ {
+ LOG.warn(e);
+ }
}
}
- }
- if (event!=null)
event.completed();
+ }
}
protected void recycle()
@@ -486,18 +492,18 @@
switch(_state)
{
case DISPATCHED:
- case REDISPATCHED:
+ case ASYNCIO:
throw new IllegalStateException(getStatusString());
default:
- _state=State.IDLE;
+ break;
}
+ _state=State.IDLE;
+ _async=null;
_initial = true;
- _dispatched=false;
- _expired=false;
- _responseWrapped=false;
cancelTimeout();
_timeoutMs=DEFAULT_TIMEOUT;
_event=null;
+ _asyncWrite=false;
}
}
@@ -515,7 +521,11 @@
protected void cancelTimeout()
{
- AsyncContextEvent event=_event;
+ final AsyncContextEvent event;
+ synchronized (this)
+ {
+ event=_event;
+ }
if (event!=null)
event.cancelTimeoutTask();
}
@@ -524,7 +534,7 @@
{
synchronized (this)
{
- return _expired;
+ return _async==Async.EXPIRED;
}
}
@@ -540,17 +550,7 @@
{
synchronized(this)
{
- switch(_state)
- {
- case ASYNCSTARTED:
- case REDISPATCHING:
- case COMPLETECALLED:
- case ASYNCWAIT:
- return true;
-
- default:
- return false;
- }
+ return _state==State.ASYNCWAIT || _state==State.DISPATCHED && _async==Async.STARTED;
}
}
@@ -573,16 +573,8 @@
public boolean isAsyncStarted()
{
synchronized (this)
- {
- switch(_state)
- {
- case ASYNCSTARTED:
- case ASYNCWAIT:
- return true;
-
- default:
- return false;
- }
+ {
+ return _async==Async.STARTED || _async==Async.EXPIRING;
}
}
@@ -590,19 +582,7 @@
{
synchronized (this)
{
- switch(_state)
- {
- case ASYNCSTARTED:
- case REDISPATCHING:
- case ASYNCWAIT:
- case REDISPATCHED:
- case REDISPATCH:
- case COMPLETECALLED:
- return true;
-
- default:
- return false;
- }
+ return !_initial || _async!=null;
}
}
@@ -618,7 +598,12 @@
public ContextHandler getContextHandler()
{
- final AsyncContextEvent event=_event;
+ final AsyncContextEvent event;
+ synchronized (this)
+ {
+ event=_event;
+ }
+
if (event!=null)
{
Context context=((Context)event.getServletContext());
@@ -630,8 +615,13 @@
public ServletResponse getServletResponse()
{
- if (_responseWrapped && _event!=null && _event.getSuppliedResponse()!=null)
- return _event.getSuppliedResponse();
+ final AsyncContextEvent event;
+ synchronized (this)
+ {
+ event=_event;
+ }
+ if (event!=null && event.getSuppliedResponse()!=null)
+ return event.getSuppliedResponse();
return _channel.getResponse();
}
@@ -650,6 +640,34 @@
_channel.getRequest().setAttribute(name,attribute);
}
+ public void onReadPossible()
+ {
+ boolean handle;
+
+ synchronized (this)
+ {
+ _asyncRead=true;
+ handle=_state==State.ASYNCWAIT;
+ }
+
+ if (handle)
+ _channel.execute(_channel);
+ }
+
+ public void onWritePossible()
+ {
+ boolean handle;
+
+ synchronized (this)
+ {
+ _asyncWrite=true;
+ handle=_state==State.ASYNCWAIT;
+ }
+
+ if (handle)
+ _channel.execute(_channel);
+ }
+
public class AsyncTimeout implements Runnable
{
@Override
diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpConnection.java b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpConnection.java
index d56fdd3..faca50e 100644
--- a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpConnection.java
+++ b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpConnection.java
@@ -23,6 +23,8 @@
import java.nio.channels.ClosedChannelException;
import java.util.concurrent.RejectedExecutionException;
+import javax.servlet.ReadListener;
+
import org.eclipse.jetty.http.HttpGenerator;
import org.eclipse.jetty.http.HttpGenerator.ResponseInfo;
import org.eclipse.jetty.http.HttpHeader;
@@ -39,7 +41,7 @@
import org.eclipse.jetty.util.BlockingCallback;
import org.eclipse.jetty.util.BufferUtil;
import org.eclipse.jetty.util.Callback;
-import org.eclipse.jetty.util.IteratingCallback;
+import org.eclipse.jetty.util.IteratingNestedCallback;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;
@@ -63,7 +65,6 @@
private final HttpParser _parser;
private volatile ByteBuffer _requestBuffer = null;
private volatile ByteBuffer _chunk = null;
- private BlockingCallback _readBlocker = new BlockingCallback();
private BlockingCallback _writeBlocker = new BlockingCallback();
@@ -92,9 +93,8 @@
_connector = connector;
_bufferPool = _connector.getByteBufferPool();
_generator = new HttpGenerator(_config.getSendServerVersion(),_config.getSendXPoweredBy());
- _channel = new HttpChannelOverHttp(connector, config, endPoint, this, new Input());
+ _channel = new HttpChannelOverHttp(connector, config, endPoint, this, new HttpInputOverHTTP(this));
_parser = newHttpParser();
-
LOG.debug("New HTTP Connection {}", this);
}
@@ -123,40 +123,11 @@
return _channel;
}
- public void reset()
+ public HttpParser getParser()
{
- // If we are still expecting
- if (_channel.isExpecting100Continue())
- {
- // reset to avoid seeking remaining content
- _parser.reset();
- // close to seek EOF
- _parser.close();
- if (getEndPoint().isOpen())
- fillInterested();
- }
- // else if we are persistent
- else if (_generator.isPersistent())
- // reset to seek next request
- _parser.reset();
- else
- {
- // else seek EOF
- _parser.close();
- if (getEndPoint().isOpen())
- fillInterested();
- }
-
- _generator.reset();
- _channel.reset();
-
- releaseRequestBuffer();
- if (_chunk!=null)
- {
- _bufferPool.release(_chunk);
- _chunk=null;
- }
+ return _parser;
}
+
@Override
@@ -171,16 +142,7 @@
return getHttpChannel().getRequests();
}
- @Override
- public String toString()
- {
- return String.format("%s,g=%s,p=%s",
- super.toString(),
- _generator,
- _parser);
- }
-
- private void releaseRequestBuffer()
+ void releaseRequestBuffer()
{
if (_requestBuffer != null && !_requestBuffer.hasRemaining())
{
@@ -189,6 +151,13 @@
_bufferPool.release(buffer);
}
}
+
+ public ByteBuffer getRequestBuffer()
+ {
+ if (_requestBuffer == null)
+ _requestBuffer = _bufferPool.acquire(getInputBufferSize(), REQUEST_BUFFER_DIRECT);
+ return _requestBuffer;
+ }
/**
* <p>Parses and handles HTTP messages.</p>
@@ -204,77 +173,53 @@
LOG.debug("{} onFillable {}", this, _channel.getState());
setCurrentConnection(this);
+ int filled=Integer.MAX_VALUE;
+ boolean suspended=false;
try
{
- while (true)
+ // while not suspended and not upgraded
+ while (!suspended && getEndPoint().getConnection()==this)
{
- // Can the parser progress (even with an empty buffer)
- boolean call_channel=_parser.parseNext(_requestBuffer==null?BufferUtil.EMPTY_BUFFER:_requestBuffer);
-
- // Parse the buffer
- if (call_channel)
+ // Do we need some data to parse
+ if (BufferUtil.isEmpty(_requestBuffer))
{
- // Parse as much content as there is available before calling the channel
- // this is both efficient (may queue many chunks), will correctly set available for 100 continues
- // and will drive the parser to completion if all content is available.
- while (_parser.inContentState())
+ // If the previous iteration filled 0 bytes or saw a close, then break here
+ if (filled<=0)
+ break;
+
+ // Can we fill?
+ if(getEndPoint().isInputShutdown())
{
- if (!_parser.parseNext(_requestBuffer==null?BufferUtil.EMPTY_BUFFER:_requestBuffer))
- break;
+ // No pretend we read -1
+ filled=-1;
+ _parser.atEOF();
}
+ else
+ {
+ // Get a buffer
+ if (_requestBuffer == null)
+ _requestBuffer = _bufferPool.acquire(getInputBufferSize(), REQUEST_BUFFER_DIRECT);
+ // fill
+ filled = getEndPoint().fill(_requestBuffer);
+ if (filled==0) // Do a retry on fill 0 (optimization for SSL connections)
+ filled = getEndPoint().fill(_requestBuffer);
+
+ // tell parser
+ if (filled < 0)
+ _parser.atEOF();
+ }
+ }
+
+ // Parse the buffer
+ if (_parser.parseNext(_requestBuffer==null?BufferUtil.EMPTY_BUFFER:_requestBuffer))
+ {
// The parser returned true, which indicates the channel is ready to handle a request.
// Call the channel and this will either handle the request/response to completion OR,
// if the request suspends, the request/response will be incomplete so the outer loop will exit.
- boolean handle=_channel.handle();
-
- // Return if suspended or upgraded
- if (!handle || getEndPoint().getConnection()!=this)
- return;
+ suspended = !_channel.handle();
}
- else if (BufferUtil.isEmpty(_requestBuffer))
- {
- if (_requestBuffer == null)
- _requestBuffer = _bufferPool.acquire(getInputBufferSize(), REQUEST_BUFFER_DIRECT);
-
- int filled = getEndPoint().fill(_requestBuffer);
- if (filled==0) // Do a retry on fill 0 (optimisation for SSL connections)
- filled = getEndPoint().fill(_requestBuffer);
-
- LOG.debug("{} filled {}", this, filled);
-
- // If we failed to fill
- if (filled == 0)
- {
- // Somebody wanted to read, we didn't so schedule another attempt
- releaseRequestBuffer();
- fillInterested();
- return;
- }
- else if (filled < 0)
- {
- _parser.shutdownInput();
- // We were only filling if fully consumed, so if we have
- // read -1 then we have nothing to parse and thus nothing that
- // will generate a response. If we had a suspended request pending
- // a response or a request waiting in the buffer, we would not be here.
- if (getEndPoint().isOutputShutdown())
- getEndPoint().close();
- else
- getEndPoint().shutdownOutput();
- // buffer must be empty and the channel must be idle, so we can release.
- releaseRequestBuffer();
- return;
- }
- }
- else
- {
- // TODO work out how we can get here and a better way to handle it
- LOG.warn("Unexpected state: "+this+ " "+_channel+" "+_channel.getRequest());
- if (!_channel.getState().isSuspended())
- getEndPoint().close();
- return;
- }
+
}
}
catch (EofException e)
@@ -290,10 +235,22 @@
close();
}
finally
- {
+ {
setCurrentConnection(null);
+ if (!suspended && getEndPoint().isOpen() && getEndPoint().getConnection()==this)
+ {
+ fillInterested();
+ }
}
}
+
+
+ @Override
+ protected void onFillInterestedFailed(Throwable cause)
+ {
+ _parser.close();
+ super.onFillInterestedFailed(cause);
+ }
@Override
public void onOpen()
@@ -359,11 +316,6 @@
@Override
public void completed()
{
- // Finish consuming the request
- if (_parser.isInContent() && _generator.isPersistent() && !_channel.isExpecting100Continue())
- // Complete reading the request
- _channel.getRequest().getHttpInput().consumeAll();
-
// Handle connection upgrades
if (_channel.getResponse().getStatus() == HttpStatus.SWITCHING_PROTOCOLS_101)
{
@@ -374,34 +326,58 @@
onClose();
getEndPoint().setConnection(connection);
connection.onOpen();
- reset();
+ _channel.reset();
+ _parser.reset();
+ _generator.reset();
+ releaseRequestBuffer();
return;
}
}
+
+ // Finish consuming the request
+ // If we are still expecting
+ if (_channel.isExpecting100Continue())
+ // close to seek EOF
+ _parser.close();
+ else if (_parser.inContentState() && _generator.isPersistent())
+ // Complete reading the request
+ _channel.getRequest().getHttpInput().consumeAll();
- reset();
+ // Reset the channel, parsers and generator
+ _channel.reset();
+ if (_generator.isPersistent() && !_parser.isClosed())
+ _parser.reset();
+ else
+ _parser.close();
+ releaseRequestBuffer();
+ if (_chunk!=null)
+ _bufferPool.release(_chunk);
+ _chunk=null;
+ _generator.reset();
// if we are not called from the onfillable thread, schedule completion
if (getCurrentConnection()!=this)
{
+ // If we are looking for the next request
if (_parser.isStart())
{
- // it wants to eat more
+ // if the buffer is empty
if (_requestBuffer == null)
{
+ // look for more data
fillInterested();
}
- else if (getConnector().isStarted())
+ // else if we are still running
+ else if (getConnector().isRunning())
{
- LOG.debug("{} pipelined", this);
-
+ // Dispatched to handle a pipelined request
try
{
getExecutor().execute(this);
}
catch (RejectedExecutionException e)
{
- if (getConnector().isStarted())
+ if (getConnector().isRunning())
LOG.warn(e);
else
LOG.ignore(e);
@@ -413,131 +389,9 @@
getEndPoint().close();
}
}
- }
- }
-
- public ByteBuffer getRequestBuffer()
- {
- return _requestBuffer;
- }
-
- private class Input extends ByteBufferHttpInput
- {
- @Override
- protected void blockForContent() throws IOException
- {
- /* We extend the blockForContent method to replace the
- default implementation of a blocking queue with an implementation
- that uses the calling thread to block on a readable callback and
- then to do the parsing before before attempting the read.
- */
- while (!_parser.isComplete())
- {
- // Can the parser progress (even with an empty buffer)
- boolean event=_parser.parseNext(_requestBuffer==null?BufferUtil.EMPTY_BUFFER:_requestBuffer);
-
- // If there is more content to parse, loop so we can queue all content from this buffer now without the
- // need to call blockForContent again
- while (!event && BufferUtil.hasContent(_requestBuffer) && _parser.inContentState())
- event=_parser.parseNext(_requestBuffer);
-
- // If we have content, return
- if (_parser.isComplete() || available()>0)
- return;
-
- // Do we have content ready to parse?
- if (BufferUtil.isEmpty(_requestBuffer))
- {
- // If no more input
- if (getEndPoint().isInputShutdown())
- {
- _parser.shutdownInput();
- shutdown();
- return;
- }
-
- // Wait until we can read
- block(_readBlocker);
- LOG.debug("{} block readable on {}",this,_readBlocker);
- _readBlocker.block();
-
- // We will need a buffer to read into
- if (_requestBuffer==null)
- {
- long content_length=_channel.getRequest().getContentLength();
- int size=getInputBufferSize();
- if (size<content_length)
- size=size*4; // TODO tune this
- _requestBuffer=_bufferPool.acquire(size,REQUEST_BUFFER_DIRECT);
- }
-
- // read some data
- int filled=getEndPoint().fill(_requestBuffer);
- LOG.debug("{} block filled {}",this,filled);
- if (filled<0)
- {
- _parser.shutdownInput();
- return;
- }
- }
- }
- }
-
- @Override
- protected void onContentQueued(ByteBuffer ref)
- {
- /* This callback could be used to tell the connection
- * that the request did contain content and thus the request
- * buffer needs to be held until a call to #onAllContentConsumed
- *
- * However it turns out that nothing is needed here because either a
- * request will have content, in which case the request buffer will be
- * released by a call to onAllContentConsumed; or it will not have content.
- * If it does not have content, either it will complete quickly and the
- * buffers will be released in completed() or it will be suspended and
- * onReadable() contains explicit handling to release if it is suspended.
- *
- * We extend this method anyway, to turn off the notify done by the
- * default implementation as this is not needed by our implementation
- * of blockForContent
- */
- }
-
- @Override
- public void earlyEOF()
- {
- synchronized (lock())
- {
- _inputEOF=true;
- _earlyEOF = true;
- LOG.debug("{} early EOF", this);
- }
- }
-
- @Override
- public void shutdown()
- {
- synchronized (lock())
- {
- _inputEOF=true;
- LOG.debug("{} shutdown", this);
- }
- }
-
- @Override
- protected void onAllContentConsumed()
- {
- /* This callback tells the connection that all content that has
- * been parsed has been consumed. Thus the request buffer may be
- * released if it is empty.
- */
- releaseRequestBuffer();
- }
-
- @Override
- public String toString()
- {
- return super.toString()+"{"+_channel+","+HttpConnection.this+"}";
+ // else the parser must be closed, so seek the EOF if we are still open
+ else if (getEndPoint().isOpen())
+ fillInterested();
}
}
@@ -547,6 +401,23 @@
{
super(connector,config,endPoint,transport,input);
}
+
+ @Override
+ public void earlyEOF()
+ {
+ // If we have no request yet, just close
+ if (getRequest().getMethod()==null)
+ close();
+ else
+ super.earlyEOF();
+ }
+
+ @Override
+ public boolean content(ByteBuffer item)
+ {
+ super.content(item);
+ return true;
+ }
@Override
public void badMessage(int status, String reason)
@@ -612,7 +483,7 @@
}
}
- private class CommitCallback extends IteratingCallback
+ private class CommitCallback extends IteratingNestedCallback
{
final ByteBuffer _content;
final boolean _lastContent;
@@ -733,7 +604,7 @@
}
}
- private class ContentCallback extends IteratingCallback
+ private class ContentCallback extends IteratingNestedCallback
{
final ByteBuffer _content;
final boolean _lastContent;
@@ -814,4 +685,5 @@
}
}
}
+
}
diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpInput.java b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpInput.java
index 7b3808d..15bdcf4 100644
--- a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpInput.java
+++ b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpInput.java
@@ -20,12 +20,15 @@
import java.io.IOException;
import java.io.InterruptedIOException;
+import java.nio.ByteBuffer;
import javax.servlet.ServletInputStream;
+import javax.servlet.ReadListener;
import org.eclipse.jetty.io.EofException;
import org.eclipse.jetty.io.RuntimeIOException;
import org.eclipse.jetty.util.ArrayQueue;
+import org.eclipse.jetty.util.BufferUtil;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;
@@ -39,37 +42,84 @@
* {@link #onContentConsumed(T)} and {@link #onAllContentConsumed()} that can be implemented so that the
* caller will know when buffers are queued and consumed.</p>
*/
-public abstract class HttpInput<T> extends ServletInputStream
+/**
+ * @author gregw
+ *
+ * @param <T>
+ */
+/**
+ * @author gregw
+ *
+ * @param <T>
+ */
+public abstract class HttpInput<T> extends ServletInputStream implements Runnable
{
private final static Logger LOG = Log.getLogger(HttpInput.class);
- private final ArrayQueue<T> _inputQ = new ArrayQueue<>();
- protected boolean _earlyEOF;
- protected boolean _inputEOF;
+
+ private HttpChannelState _channelState;
+ private Throwable _onError;
+ private ReadListener _listener;
+ private boolean _notReady;
+
+ protected State _state = BLOCKING;
+ private State _eof=null;
+ private final Object _lock;
- public Object lock()
+ protected HttpInput()
{
- return _inputQ.lock();
+ this(null);
+ }
+
+ protected HttpInput(Object lock)
+ {
+ _lock=lock==null?this:lock;
+ }
+
+ public final Object lock()
+ {
+ return _lock;
}
public void recycle()
{
synchronized (lock())
{
- T item = _inputQ.peekUnsafe();
- while (item != null)
- {
- _inputQ.pollUnsafe();
- onContentConsumed(item);
-
- item = _inputQ.peekUnsafe();
- if (item == null)
- onAllContentConsumed();
- }
- _inputEOF = false;
- _earlyEOF = false;
+ _state = BLOCKING;
+ _eof=null;
+ _onError=null;
}
}
+ /**
+ * Access the next content to be consumed from. Returning the next item does not consume it
+ * and it may be returned multiple times until it is consumed. Calls to {@link #get(Object, byte[], int, int)}
+ * or {@link #consume(Object, int)} are required to consume data from the content.
+ * @return Content or null if none available.
+ * @throws IOException
+ */
+ protected abstract T nextContent() throws IOException;
+
+ /**
+ * A convenience method to call nextContent and to check the return value, which if null then the
+ * a check is made for EOF and the state changed accordingly.
+ * @see #nextContent()
+ * @return Content or null if none available.
+ * @throws IOException
+ */
+ protected T getNextContent() throws IOException
+ {
+ T content=nextContent();
+
+ if (content==null && _eof!=null)
+ {
+ LOG.debug("{} eof {}",this,_eof);
+ _state=_eof;
+ _eof=null;
+ }
+
+ return content;
+ }
+
@Override
public int read() throws IOException
{
@@ -81,12 +131,17 @@
@Override
public int available()
{
- synchronized (lock())
+ try
{
- T item = _inputQ.peekUnsafe();
- if (item == null)
- return 0;
- return remaining(item);
+ synchronized (lock())
+ {
+ T item = getNextContent();
+ return item==null?0:remaining(item);
+ }
+ }
+ catch (IOException e)
+ {
+ throw new RuntimeIOException(e);
}
}
@@ -96,122 +151,45 @@
T item = null;
synchronized (lock())
{
- // Get the current head of the input Q
- item = _inputQ.peekUnsafe();
-
- // Skip empty items at the head of the queue
- while (item != null && remaining(item) == 0)
- {
- _inputQ.pollUnsafe();
- onContentConsumed(item);
- LOG.debug("{} consumed {}", this, item);
- item = _inputQ.peekUnsafe();
-
- // If that was the last item then notify
- if (item==null)
- onAllContentConsumed();
- }
+ // System.err.printf("read s=%s q=%d e=%s%n",_state,_inputQ.size(),_eof);
+ // Get the current head of the input Q
+ item = getNextContent();
+
// If we have no item
if (item == null)
{
- // Was it unexpectedly EOF'd?
- if (isEarlyEOF())
- throw new EofException();
-
- // check for EOF
- if (isShutdown())
- {
- onEOF();
- return -1;
- }
-
- // OK then block for some more content
- blockForContent();
-
- // If still not content, we must be closed
- item = _inputQ.peekUnsafe();
+ _state.waitForContent(this);
+ item=getNextContent();
if (item==null)
- {
- if (isEarlyEOF())
- throw new EofException();
-
- // blockForContent will only return with no
- // content if it is closed.
- if (!isShutdown())
- LOG.warn("Unexpected !EOF: "+this);
-
- onEOF();
- return -1;
- }
+ return _state.noContent();
}
}
+
return get(item, b, off, len);
}
protected abstract int remaining(T item);
protected abstract int get(T item, byte[] buffer, int offset, int length);
+
+ protected abstract void consume(T item, int length);
- protected abstract void onContentConsumed(T item);
-
- protected void blockForContent() throws IOException
+ protected abstract void blockForContent() throws IOException;
+
+ protected boolean onAsyncRead()
{
- synchronized (lock())
- {
- while (_inputQ.isEmpty() && !isShutdown() && !isEarlyEOF())
- {
- try
- {
- LOG.debug("{} waiting for content", this);
- lock().wait();
- }
- catch (InterruptedException e)
- {
- throw (IOException)new InterruptedIOException().initCause(e);
- }
- }
- }
- }
-
- /* ------------------------------------------------------------ */
- /** Called by this HttpInput to signal new content has been queued
- * @param item
- */
- protected void onContentQueued(T item)
- {
- lock().notify();
- }
-
- /* ------------------------------------------------------------ */
- /** Called by this HttpInput to signal all available content has been consumed
- */
- protected void onAllContentConsumed()
- {
- }
-
- /* ------------------------------------------------------------ */
- /** Called by this HttpInput to signal it has reached EOF
- */
- protected void onEOF()
- {
+ if (_listener==null)
+ return false;
+ _channelState.onReadPossible();
+ return true;
}
/* ------------------------------------------------------------ */
/** Add some content to the input stream
* @param item
*/
- public void content(T item)
- {
- synchronized (lock())
- {
- // The buffer is not copied here. This relies on the caller not recycling the buffer
- // until the it is consumed. The onAllContentConsumed() callback is the signal to the
- // caller that the buffers can be recycled.
- _inputQ.add(item);
- onContentQueued(item);
- LOG.debug("{} queued {}", this, item);
- }
- }
+ public abstract void content(T item);
+
/* ------------------------------------------------------------ */
/** This method should be called to signal to the HttpInput
@@ -223,39 +201,28 @@
{
synchronized (lock())
{
- _earlyEOF = true;
- _inputEOF = true;
- lock().notify();
- LOG.debug("{} early EOF", this);
+ if (_eof==null || !_eof.isEOF())
+ {
+ LOG.debug("{} early EOF", this);
+ _eof=EARLY_EOF;
+ if (_listener!=null)
+ _channelState.onReadPossible();
+ }
}
}
/* ------------------------------------------------------------ */
- public boolean isEarlyEOF()
+ public void messageComplete()
{
synchronized (lock())
{
- return _earlyEOF;
- }
- }
-
- /* ------------------------------------------------------------ */
- public void shutdown()
- {
- synchronized (lock())
- {
- _inputEOF = true;
- lock().notify();
- LOG.debug("{} shutdown", this);
- }
- }
-
- /* ------------------------------------------------------------ */
- public boolean isShutdown()
- {
- synchronized (lock())
- {
- return _inputEOF;
+ if (_eof==null || !_eof.isEOF())
+ {
+ LOG.debug("{} EOF", this);
+ _eof=EOF;
+ if (_listener!=null)
+ _channelState.onReadPossible();
+ }
}
}
@@ -264,31 +231,219 @@
{
synchronized (lock())
{
- T item = _inputQ.peekUnsafe();
- while (!isShutdown() && !isEarlyEOF())
+ try
{
- while (item != null)
+ while (!isFinished())
{
- _inputQ.pollUnsafe();
- onContentConsumed(item);
-
- item = _inputQ.peekUnsafe();
- if (item == null)
- onAllContentConsumed();
- }
-
- try
- {
- blockForContent();
- item = _inputQ.peekUnsafe();
+ T item = getNextContent();
if (item==null)
- break;
+ _state.waitForContent(this);
+ else
+ consume(item,remaining(item));
}
- catch (IOException e)
- {
- throw new RuntimeIOException(e);
- }
+ }
+ catch (IOException e)
+ {
+ throw new RuntimeIOException(e);
}
}
}
+
+ @Override
+ public boolean isFinished()
+ {
+ synchronized (lock())
+ {
+ return _state.isEOF();
+ }
+ }
+
+ @Override
+ public boolean isReady()
+ {
+ synchronized (lock())
+ {
+ if (_listener==null)
+ return true;
+ int available = available();
+ if (available>0)
+ return true;
+ if (!_notReady)
+ {
+ _notReady=true;
+ if (_state.isEOF())
+ _channelState.onReadPossible();
+ else
+ unready();
+ }
+ return false;
+ }
+ }
+
+ protected void unready()
+ {
+ }
+
+ @Override
+ public void setReadListener(ReadListener readListener)
+ {
+ if (readListener==null)
+ throw new NullPointerException("readListener==null");
+ synchronized (lock())
+ {
+ if (_state!=BLOCKING)
+ throw new IllegalStateException("state="+_state);
+ _state=ASYNC;
+ _listener=readListener;
+ _notReady=true;
+
+ _channelState.onReadPossible();
+ }
+ }
+
+ public void failed(Throwable x)
+ {
+ synchronized (lock())
+ {
+ if (_onError==null)
+ LOG.warn(x);
+ else
+ _onError=x;
+ }
+ }
+
+ @Override
+ public void run()
+ {
+ final boolean available;
+ final boolean eof;
+ final Throwable x;
+
+ synchronized (lock())
+ {
+ if (!_notReady || _listener==null)
+ return;
+
+ x=_onError;
+ T item;
+ try
+ {
+ item = getNextContent();
+ }
+ catch(Exception e)
+ {
+ item=null;
+ failed(e);
+ }
+ available= item!=null && remaining(item)>0;
+
+ eof = !available && _state.isEOF();
+ _notReady=!available&&!eof;
+ }
+
+ try
+ {
+ if (x!=null)
+ _listener.onError(x);
+ else if (available)
+ _listener.onDataAvailable();
+ else if (eof)
+ _listener.onAllDataRead();
+ else
+ unready();
+ }
+ catch(Exception e)
+ {
+ LOG.warn(e.toString());
+ LOG.debug(e);
+ _listener.onError(e);
+ }
+ }
+
+
+ protected static class State
+ {
+ public void waitForContent(HttpInput<?> in) throws IOException
+ {
+ }
+
+ public int noContent() throws IOException
+ {
+ return -1;
+ }
+
+ public boolean isEOF()
+ {
+ return false;
+ }
+ }
+
+ protected static final State BLOCKING= new State()
+ {
+ @Override
+ public void waitForContent(HttpInput<?> in) throws IOException
+ {
+ in.blockForContent();
+ }
+ public String toString()
+ {
+ return "OPEN";
+ }
+ };
+
+ protected static final State ASYNC= new State()
+ {
+ @Override
+ public int noContent() throws IOException
+ {
+ return 0;
+ }
+ @Override
+ public String toString()
+ {
+ return "ASYNC";
+ }
+ };
+
+ protected static final State EARLY_EOF= new State()
+ {
+ @Override
+ public int noContent() throws IOException
+ {
+ throw new EofException();
+ }
+ @Override
+ public boolean isEOF()
+ {
+ return true;
+ }
+ public String toString()
+ {
+ return "EARLY_EOF";
+ }
+ };
+
+ protected static final State EOF= new State()
+ {
+ @Override
+ public boolean isEOF()
+ {
+ return true;
+ }
+
+ public String toString()
+ {
+ return "EOF";
+ }
+ };
+
+
+ public void init(HttpChannelState state)
+ {
+ synchronized (lock())
+ {
+ _channelState=state;
+ }
+ }
+
}
diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpInputOverHTTP.java b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpInputOverHTTP.java
new file mode 100644
index 0000000..8f29462
--- /dev/null
+++ b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpInputOverHTTP.java
@@ -0,0 +1,168 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+
+import org.eclipse.jetty.http.HttpParser;
+import org.eclipse.jetty.util.BlockingCallback;
+import org.eclipse.jetty.util.BufferUtil;
+import org.eclipse.jetty.util.Callback;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+
+class HttpInputOverHTTP extends HttpInput<ByteBuffer> implements Callback
+{
+ private static final Logger LOG = Log.getLogger(HttpInputOverHTTP.class);
+ private final BlockingCallback _readBlocker = new BlockingCallback();
+ private final HttpConnection _httpConnection;
+ private ByteBuffer _content;
+
+ /**
+ * @param httpConnection
+ */
+ HttpInputOverHTTP(HttpConnection httpConnection)
+ {
+ _httpConnection = httpConnection;
+ }
+
+ @Override
+ public void recycle()
+ {
+ synchronized (lock())
+ {
+ super.recycle();
+ _content=null;
+ }
+ }
+
+ @Override
+ protected void blockForContent() throws IOException
+ {
+ while(true)
+ {
+ _httpConnection.fillInterested(_readBlocker);
+ LOG.debug("{} block readable on {}",this,_readBlocker);
+ _readBlocker.block();
+
+ Object content=getNextContent();
+ if (content!=null || isFinished())
+ break;
+ }
+ }
+
+ @Override
+ public String toString()
+ {
+ return String.format("%s@%x",getClass().getSimpleName(),hashCode());
+ }
+
+ @Override
+ protected ByteBuffer nextContent() throws IOException
+ {
+ // If we have some content available, return it
+ if (BufferUtil.hasContent(_content))
+ return _content;
+
+ // No - then we are going to need to parse some more content
+ _content=null;
+ ByteBuffer requestBuffer = _httpConnection.getRequestBuffer();
+
+ while (!_httpConnection.getParser().isComplete())
+ {
+ // Can the parser progress (even with an empty buffer)
+ _httpConnection.getParser().parseNext(requestBuffer==null?BufferUtil.EMPTY_BUFFER:requestBuffer);
+
+ // If we got some content, that will do for now!
+ if (BufferUtil.hasContent(_content))
+ return _content;
+
+ // No, we can we try reading some content?
+ if (BufferUtil.isEmpty(requestBuffer) && _httpConnection.getEndPoint().isInputShutdown())
+ {
+ _httpConnection.getParser().atEOF();
+ continue;
+ }
+
+ // OK lets read some data
+ int filled=_httpConnection.getEndPoint().fill(requestBuffer);
+ LOG.debug("{} filled {}",this,filled);
+ if (filled<=0)
+ {
+ if (filled<0)
+ {
+ _httpConnection.getParser().atEOF();
+ continue;
+ }
+ return null;
+ }
+ }
+
+ return null;
+
+ }
+
+ @Override
+ protected int remaining(ByteBuffer item)
+ {
+ return item.remaining();
+ }
+
+ @Override
+ protected int get(ByteBuffer item, byte[] buffer, int offset, int length)
+ {
+ int l = Math.min(item.remaining(), length);
+ item.get(buffer, offset, l);
+ return l;
+ }
+
+ @Override
+ protected void consume(ByteBuffer item, int length)
+ {
+ item.position(item.position()+length);
+ }
+
+ @Override
+ public void content(ByteBuffer item)
+ {
+ if (BufferUtil.hasContent(_content))
+ throw new IllegalStateException();
+ _content=item;
+ }
+
+ @Override
+ protected void unready()
+ {
+ _httpConnection.fillInterested(this);
+ }
+
+ @Override
+ public void succeeded()
+ {
+ _httpConnection.getHttpChannel().getState().onReadPossible();
+ }
+
+ @Override
+ public void failed(Throwable x)
+ {
+ super.failed(x);
+ _httpConnection.getHttpChannel().getState().onReadPossible();
+ }
+}
diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpOutput.java b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpOutput.java
index f52d92b..5f14281 100644
--- a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpOutput.java
+++ b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpOutput.java
@@ -18,26 +18,28 @@
package org.eclipse.jetty.server;
-import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.nio.channels.ReadableByteChannel;
+import java.nio.channels.WritePendingException;
+import java.util.concurrent.atomic.AtomicReference;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletOutputStream;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
+import javax.servlet.WriteListener;
import org.eclipse.jetty.http.HttpContent;
-import org.eclipse.jetty.http.HttpHeader;
+import org.eclipse.jetty.io.EofException;
import org.eclipse.jetty.io.EofException;
import org.eclipse.jetty.util.BlockingCallback;
import org.eclipse.jetty.util.BufferUtil;
import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.util.IteratingCallback;
+import org.eclipse.jetty.util.IteratingNestedCallback;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;
-import org.eclipse.jetty.util.resource.Resource;
/**
* <p>{@link HttpOutput} implements {@link ServletOutputStream}
@@ -49,14 +51,30 @@
* via {@link RequestDispatcher#include(ServletRequest, ServletResponse)} to
* close the stream, to be reopened after the inclusion ends.</p>
*/
-public class HttpOutput extends ServletOutputStream
+public class HttpOutput extends ServletOutputStream implements Runnable
{
private static Logger LOG = Log.getLogger(HttpOutput.class);
private final HttpChannel<?> _channel;
- private boolean _closed;
private long _written;
private ByteBuffer _aggregate;
private int _bufferSize;
+ private WriteListener _writeListener;
+ private volatile Throwable _onError;
+
+ /*
+ACTION OPEN ASYNC READY PENDING UNREADY
+-------------------------------------------------------------------------------
+setWriteListener() READY->owp ise ise ise ise
+write() OPEN ise PENDING wpe wpe
+flush() OPEN ise PENDING wpe wpe
+isReady() OPEN:true READY:true READY:true UNREADY:false UNREADY:false
+write completed - - - ASYNC READY->owp
+
+ */
+ enum State { OPEN, ASYNC, READY, PENDING, UNREADY, CLOSED }
+ private final AtomicReference<State> _state=new AtomicReference<>(State.OPEN);
+
+
public HttpOutput(HttpChannel<?> channel)
{
@@ -82,49 +100,63 @@
public void reopen()
{
- _closed = false;
+ _state.set(State.OPEN);
}
- /** Called by the HttpChannel if the output was closed
- * externally (eg by a 500 exception handling).
- */
- void closed()
+ public boolean isAllContentWritten()
{
- if (!_closed)
- {
- _closed = true;
- try
- {
- _channel.getResponse().closeOutput();
- }
- catch(IOException e)
- {
- _channel.failed();
- LOG.ignore(e);
- }
- releaseBuffer();
- }
+ return _channel.getResponse().isAllContentWritten(_written);
}
-
+
@Override
public void close()
{
- if (!isClosed())
+ State state=_state.get();
+ while(state!=State.CLOSED)
{
- try
+ if (_state.compareAndSet(state,State.CLOSED))
{
- if (BufferUtil.hasContent(_aggregate))
- _channel.write(_aggregate, !_channel.getResponse().isIncluding());
- else
- _channel.write(BufferUtil.EMPTY_BUFFER, !_channel.getResponse().isIncluding());
+ try
+ {
+ if (BufferUtil.hasContent(_aggregate))
+ _channel.write(_aggregate, !_channel.getResponse().isIncluding());
+ else
+ _channel.write(BufferUtil.EMPTY_BUFFER, !_channel.getResponse().isIncluding());
+ }
+ catch(IOException e)
+ {
+ LOG.debug(e);
+ _channel.failed();
+ }
+ releaseBuffer();
+ return;
}
- catch(IOException e)
- {
- _channel.failed();
- LOG.ignore(e);
- }
+ state=_state.get();
}
- closed();
+ }
+
+ /* Called to indicated that the output is already closed and the state needs to be updated to match */
+ void closed()
+ {
+ State state=_state.get();
+ while(state!=State.CLOSED)
+ {
+ if (_state.compareAndSet(state,State.CLOSED))
+ {
+ try
+ {
+ _channel.getResponse().closeOutput();
+ }
+ catch(IOException e)
+ {
+ LOG.debug(e);
+ _channel.failed();
+ }
+ releaseBuffer();
+ return;
+ }
+ state=_state.get();
+ }
}
private void releaseBuffer()
@@ -138,76 +170,197 @@
public boolean isClosed()
{
- return _closed;
+ return _state.get()==State.CLOSED;
}
@Override
public void flush() throws IOException
{
- if (isClosed())
- return;
-
- if (BufferUtil.hasContent(_aggregate))
- _channel.write(_aggregate, false);
- else
- _channel.write(BufferUtil.EMPTY_BUFFER, false);
+ while(true)
+ {
+ switch(_state.get())
+ {
+ case OPEN:
+ if (BufferUtil.hasContent(_aggregate))
+ _channel.write(_aggregate, false);
+ else
+ _channel.write(BufferUtil.EMPTY_BUFFER, false);
+ return;
+
+ case ASYNC:
+ throw new IllegalStateException("isReady() not called");
+
+ case READY:
+ if (!_state.compareAndSet(State.READY, State.PENDING))
+ continue;
+ new AsyncFlush().process();
+ return;
+
+ case PENDING:
+ case UNREADY:
+ throw new WritePendingException();
+
+ case CLOSED:
+ return;
+ }
+ break;
+ }
}
- public boolean isAllContentWritten()
- {
- Response response=_channel.getResponse();
- return response.isAllContentWritten(_written);
- }
- public void closeOutput() throws IOException
- {
- _channel.getResponse().closeOutput();
- }
-
@Override
public void write(byte[] b, int off, int len) throws IOException
{
- if (isClosed())
- throw new EofException("Closed");
+ _written+=len;
+ boolean complete=_channel.getResponse().isAllContentWritten(_written);
- _written+=len;
+ // Async or Blocking ?
+ while(true)
+ {
+ switch(_state.get())
+ {
+ case OPEN:
+ // process blocking below
+ break;
+
+ case ASYNC:
+ throw new IllegalStateException("isReady() not called");
+
+ case READY:
+ if (!_state.compareAndSet(State.READY, State.PENDING))
+ continue;
+
+ // Should we aggregate?
+ int capacity = getBufferSize();
+ if (!complete && len<=capacity/4)
+ {
+ if (_aggregate == null)
+ _aggregate = _channel.getByteBufferPool().acquire(capacity, false);
+
+ // YES - fill the aggregate with content from the buffer
+ int filled = BufferUtil.fill(_aggregate, b, off, len);
+
+ // return if we are not complete, not full and filled all the content
+ if (!complete && filled==len && !BufferUtil.isFull(_aggregate))
+ {
+ if (!_state.compareAndSet(State.PENDING, State.ASYNC))
+ throw new IllegalStateException();
+ return;
+ }
+
+ // adjust offset/length
+ off+=filled;
+ len-=filled;
+ }
+
+ // Do the asynchronous writing from the callback
+ new AsyncWrite(b,off,len,complete).process();
+ return;
+
+ case PENDING:
+ case UNREADY:
+ throw new WritePendingException();
+
+ case CLOSED:
+ throw new EofException("Closed");
+ }
+ break;
+ }
+
+
+ // handle blocking write
+
+ // Should we aggregate?
+ int capacity = getBufferSize();
+ if (!complete && len<=capacity/4)
+ {
+ if (_aggregate == null)
+ _aggregate = _channel.getByteBufferPool().acquire(capacity, false);
+
+ // YES - fill the aggregate with content from the buffer
+ int filled = BufferUtil.fill(_aggregate, b, off, len);
+
+ // return if we are not complete, not full and filled all the content
+ if (!complete && filled==len && !BufferUtil.isFull(_aggregate))
+ return;
+
+ // adjust offset/length
+ off+=filled;
+ len-=filled;
+ }
+
+ // flush any content from the aggregate
+ if (BufferUtil.hasContent(_aggregate))
+ {
+ _channel.write(_aggregate, complete && len==0);
+
+ // should we fill aggregate again from the buffer?
+ if (len>0 && !complete && len<=_aggregate.capacity()/4)
+ {
+ BufferUtil.append(_aggregate, b, off, len);
+ return;
+ }
+ }
+
+ // write any remaining content in the buffer directly
+ if (len>0)
+ _channel.write(ByteBuffer.wrap(b, off, len), complete);
+ else if (complete)
+ _channel.write(BufferUtil.EMPTY_BUFFER,complete);
+
+ if (complete)
+ {
+ closed();
+ }
+
+ }
+
+ public void write(ByteBuffer buffer) throws IOException
+ {
+ _written+=buffer.remaining();
boolean complete=_channel.getResponse().isAllContentWritten(_written);
- int capacity = getBufferSize();
- // Should we aggregate?
- if (!complete && len<=capacity/4)
+ // Async or Blocking ?
+ while(true)
{
- if (_aggregate == null)
- _aggregate = _channel.getByteBufferPool().acquire(capacity, false);
+ switch(_state.get())
+ {
+ case OPEN:
+ // process blocking below
+ break;
+
+ case ASYNC:
+ throw new IllegalStateException("isReady() not called");
- // YES - fill the aggregate with content from the buffer
- int filled = BufferUtil.fill(_aggregate, b, off, len);
+ case READY:
+ if (!_state.compareAndSet(State.READY, State.PENDING))
+ continue;
- // return if we are not complete, not full and filled all the content
- if (!complete && filled == len && !BufferUtil.isFull(_aggregate))
- return;
+ // Do the asynchronous writing from the callback
+ new AsyncWrite(buffer,complete).process();
+ return;
- // adjust offset/length
- off += filled;
- len -= filled;
+ case PENDING:
+ case UNREADY:
+ throw new WritePendingException();
+
+ case CLOSED:
+ throw new EofException("Closed");
+ }
+ break;
}
+
+ // handle blocking write
+ int len=BufferUtil.length(buffer);
+
// flush any content from the aggregate
if (BufferUtil.hasContent(_aggregate))
- {
_channel.write(_aggregate, complete && len==0);
- // should we fill aggregate again from the buffer?
- if (len>0 && !complete && len<=_aggregate.capacity()/4)
- {
- BufferUtil.append(_aggregate, b, off, len);
- return;
- }
- }
-
// write any remaining content in the buffer directly
if (len>0)
- _channel.write(ByteBuffer.wrap(b, off, len), complete);
+ _channel.write(buffer, complete);
else if (complete)
_channel.write(BufferUtil.EMPTY_BUFFER,complete);
@@ -215,34 +368,69 @@
closed();
}
-
@Override
public void write(int b) throws IOException
{
- if (isClosed())
- throw new EOFException("Closed");
-
- // Allocate an aggregate buffer.
- // Never direct as it is slow to do little writes to a direct buffer.
- if (_aggregate == null)
- _aggregate = _channel.getByteBufferPool().acquire(getBufferSize(), false);
-
- BufferUtil.append(_aggregate, (byte)b);
- _written++;
-
+ _written+=1;
boolean complete=_channel.getResponse().isAllContentWritten(_written);
-
- // Check if all written or full
- if (complete || BufferUtil.isFull(_aggregate))
+
+ // Async or Blocking ?
+ while(true)
{
- BlockingCallback callback = _channel.getWriteBlockingCallback();
- _channel.write(_aggregate, false, callback);
- callback.block();
- if (complete)
- closed();
+ switch(_state.get())
+ {
+ case OPEN:
+ if (_aggregate == null)
+ _aggregate = _channel.getByteBufferPool().acquire(getBufferSize(), false);
+ BufferUtil.append(_aggregate, (byte)b);
+
+ // Check if all written or full
+ if (complete || BufferUtil.isFull(_aggregate))
+ {
+ BlockingCallback callback = _channel.getWriteBlockingCallback();
+ _channel.write(_aggregate, complete, callback);
+ callback.block();
+ if (complete)
+ closed();
+ }
+ break;
+
+ case ASYNC:
+ throw new IllegalStateException("isReady() not called");
+
+ case READY:
+ if (!_state.compareAndSet(State.READY, State.PENDING))
+ continue;
+
+ if (_aggregate == null)
+ _aggregate = _channel.getByteBufferPool().acquire(getBufferSize(), false);
+ BufferUtil.append(_aggregate, (byte)b);
+
+ // Check if all written or full
+ if (!complete && !BufferUtil.isFull(_aggregate))
+ {
+ if (!_state.compareAndSet(State.PENDING, State.ASYNC))
+ throw new IllegalStateException();
+ return;
+ }
+
+ // Do the asynchronous writing from the callback
+ new AsyncFlush().process();
+ return;
+
+ case PENDING:
+ case UNREADY:
+ throw new WritePendingException();
+
+ case CLOSED:
+ throw new EofException("Closed");
+ }
+ break;
}
}
+
+
@Override
public void print(String s) throws IOException
{
@@ -253,51 +441,6 @@
}
/* ------------------------------------------------------------ */
- /** Set headers and send content.
- * @deprecated Use {@link Response#setHeaders(HttpContent)} and {@link #sendContent(HttpContent)} instead.
- * @param content
- * @throws IOException
- */
- @Deprecated
- public void sendContent(Object content) throws IOException
- {
- final BlockingCallback callback =_channel.getWriteBlockingCallback();
-
- if (content instanceof HttpContent)
- {
- _channel.getResponse().setHeaders((HttpContent)content);
- sendContent((HttpContent)content,callback);
- }
- else if (content instanceof Resource)
- {
- Resource resource = (Resource)content;
- _channel.getResponse().getHttpFields().putDateField(HttpHeader.LAST_MODIFIED, resource.lastModified());
-
- ReadableByteChannel in=((Resource)content).getReadableByteChannel();
- if (in!=null)
- sendContent(in,callback);
- else
- sendContent(resource.getInputStream(),callback);
- }
- else if (content instanceof ByteBuffer)
- {
- sendContent((ByteBuffer)content,callback);
- }
- else if (content instanceof ReadableByteChannel)
- {
- sendContent((ReadableByteChannel)content,callback);
- }
- else if (content instanceof InputStream)
- {
- sendContent((InputStream)content,callback);
- }
- else
- callback.failed(new IllegalArgumentException("unknown content type "+content.getClass()));
-
- callback.block();
- }
-
- /* ------------------------------------------------------------ */
/** Blocking send of content.
* @param content The content to send
* @throws IOException
@@ -332,7 +475,7 @@
new ReadableByteChannelWritingCB(in,callback).iterate();
callback.block();
}
-
+
/* ------------------------------------------------------------ */
/** Blocking send of content.
@@ -345,7 +488,7 @@
sendContent(content,callback);
callback.block();
}
-
+
/* ------------------------------------------------------------ */
/** Asynchronous send of content.
@@ -398,30 +541,43 @@
*/
public void sendContent(HttpContent httpContent, Callback callback) throws IOException
{
- if (isClosed())
- throw new IOException("Closed");
if (BufferUtil.hasContent(_aggregate))
throw new IOException("written");
if (_channel.isCommitted())
throw new IOException("committed");
-
+
+ while (true)
+ {
+ switch(_state.get())
+ {
+ case OPEN:
+ if (!_state.compareAndSet(State.OPEN, State.PENDING))
+ continue;
+ break;
+ case CLOSED:
+ throw new EofException("Closed");
+ default:
+ throw new IllegalStateException();
+ }
+ break;
+ }
ByteBuffer buffer= _channel.useDirectBuffers()?httpContent.getDirectBuffer():null;
if (buffer == null)
buffer = httpContent.getIndirectBuffer();
-
+
if (buffer!=null)
{
sendContent(buffer,callback);
return;
}
-
+
ReadableByteChannel rbc=httpContent.getReadableByteChannel();
if (rbc!=null)
{
sendContent(rbc,callback);
return;
}
-
+
InputStream in = httpContent.getInputStream();
if ( in!=null )
{
@@ -447,8 +603,209 @@
if (BufferUtil.hasContent(_aggregate))
BufferUtil.clear(_aggregate);
}
-
-
+
+ @Override
+ public void setWriteListener(WriteListener writeListener)
+ {
+ if (!_channel.getState().isAsync())
+ throw new IllegalStateException("!ASYNC");
+
+ if (_state.compareAndSet(State.OPEN, State.READY))
+ {
+ _writeListener = writeListener;
+ _channel.getState().onWritePossible();
+ }
+ else
+ throw new IllegalStateException();
+ }
+
+ /**
+ * @see javax.servlet.ServletOutputStream#isReady()
+ */
+ @Override
+ public boolean isReady()
+ {
+ while (true)
+ {
+ switch(_state.get())
+ {
+ case OPEN:
+ return true;
+ case ASYNC:
+ if (!_state.compareAndSet(State.ASYNC, State.READY))
+ continue;
+ return true;
+ case READY:
+ return true;
+ case PENDING:
+ if (!_state.compareAndSet(State.PENDING, State.UNREADY))
+ continue;
+ return false;
+ case UNREADY:
+ return false;
+ case CLOSED:
+ return false;
+ }
+ }
+ }
+
+ @Override
+ public void run()
+ {
+ if(_onError!=null)
+ {
+ Throwable th=_onError;
+ _onError=null;
+ _writeListener.onError(th);
+ close();
+ }
+ if (_state.get()==State.READY)
+ {
+ try
+ {
+ _writeListener.onWritePossible();
+ }
+ catch (IOException e)
+ {
+ _writeListener.onError(e);
+ close();
+ }
+ }
+ }
+
+ private class AsyncWrite extends AsyncFlush
+ {
+ private final ByteBuffer _buffer;
+ private final boolean _complete;
+ private final int _len;
+
+ public AsyncWrite(byte[] b, int off, int len, boolean complete)
+ {
+ _buffer=ByteBuffer.wrap(b, off, len);
+ _complete=complete;
+ _len=len;
+ }
+
+ public AsyncWrite(ByteBuffer buffer, boolean complete)
+ {
+ _buffer=buffer;
+ _complete=complete;
+ _len=buffer.remaining();
+ }
+
+ @Override
+ protected boolean process()
+ {
+ // flush any content from the aggregate
+ if (BufferUtil.hasContent(_aggregate))
+ {
+ _channel.write(_aggregate, _complete && _len==0, this);
+ return false;
+ }
+
+ // TODO write comments
+ if (!_complete && _len<BufferUtil.space(_aggregate) && _len<_aggregate.capacity()/4)
+ {
+ BufferUtil.put(_buffer,_aggregate);
+ }
+ // TODO write comments
+ else if (_len>0 && !_flushed)
+ {
+ _flushed=true;
+ _channel.write(_buffer, _complete, this);
+ return false;
+ }
+ else if (_len==0 && !_flushed)
+ {
+ _flushed=true;
+ _channel.write(BufferUtil.EMPTY_BUFFER, _complete, this);
+ return false;
+ }
+
+ if (_complete)
+ closed();
+ return true;
+ }
+ }
+
+ private class AsyncFlush extends IteratingCallback
+ {
+ protected boolean _flushed;
+
+ public AsyncFlush()
+ {
+ }
+
+ @Override
+ protected boolean process()
+ {
+ if (BufferUtil.hasContent(_aggregate))
+ {
+ _flushed=true;
+ _channel.write(_aggregate, false, this);
+ return false;
+ }
+
+ if (!_flushed)
+ {
+ _flushed=true;
+ _channel.write(BufferUtil.EMPTY_BUFFER,false,this);
+ return false;
+ }
+
+ return true;
+ }
+
+ @Override
+ protected void completed()
+ {
+ try
+ {
+ loop: while(true)
+ {
+ State last=_state.get();
+ switch(last)
+ {
+ case PENDING:
+ if (!_state.compareAndSet(State.PENDING, State.ASYNC))
+ continue;
+ break;
+
+ case UNREADY:
+ if (!_state.compareAndSet(State.UNREADY, State.READY))
+ continue;
+ _channel.getState().onWritePossible();
+ break;
+
+ case CLOSED:
+ _onError=new EofException("Closed");
+ break;
+
+ default:
+ throw new IllegalStateException();
+ }
+
+ break loop;
+ }
+ }
+ catch (Exception e)
+ {
+ _onError=e;
+ _channel.getState().onWritePossible();
+ }
+ }
+
+ @Override
+ public void failed(Throwable e)
+ {
+ _onError=e;
+ _channel.getState().onWritePossible();
+ }
+
+
+ }
+
+
/* ------------------------------------------------------------ */
/** An iterating callback that will take content from an
* InputStream and write it to the associated {@link HttpChannel}.
@@ -457,11 +814,11 @@
* be notified as each buffer is written and only once all the input is consumed will the
* wrapped {@link Callback#succeeded()} method be called.
*/
- private class InputStreamWritingCB extends IteratingCallback
+ private class InputStreamWritingCB extends IteratingNestedCallback
{
final InputStream _in;
final ByteBuffer _buffer;
-
+
public InputStreamWritingCB(InputStream in, Callback callback)
{
super(callback);
@@ -512,7 +869,7 @@
super.failed(x);
_channel.getByteBufferPool().release(_buffer);
}
-
+
}
/* ------------------------------------------------------------ */
@@ -524,11 +881,11 @@
* be notified as each buffer is written and only once all the input is consumed will the
* wrapped {@link Callback#succeeded()} method be called.
*/
- private class ReadableByteChannelWritingCB extends IteratingCallback
+ private class ReadableByteChannelWritingCB extends IteratingNestedCallback
{
final ReadableByteChannel _in;
final ByteBuffer _buffer;
-
+
public ReadableByteChannelWritingCB(ReadableByteChannel in, Callback callback)
{
super(callback);
@@ -580,4 +937,5 @@
_channel.getByteBufferPool().release(_buffer);
}
}
+
}
diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/Iso88591HttpWriter.java b/jetty-server/src/main/java/org/eclipse/jetty/server/Iso88591HttpWriter.java
index 83335c4..0f4abd9 100644
--- a/jetty-server/src/main/java/org/eclipse/jetty/server/Iso88591HttpWriter.java
+++ b/jetty-server/src/main/java/org/eclipse/jetty/server/Iso88591HttpWriter.java
@@ -35,10 +35,10 @@
public void write (char[] s,int offset, int length) throws IOException
{
HttpOutput out = _out;
- if (length==0)
+ if (length==0 && out.isAllContentWritten())
{
- if (_out.isAllContentWritten())
- close();
+ close();
+ return;
}
if (length==1)
diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/LocalConnector.java b/jetty-server/src/main/java/org/eclipse/jetty/server/LocalConnector.java
index 6d161de..acf1cf0 100644
--- a/jetty-server/src/main/java/org/eclipse/jetty/server/LocalConnector.java
+++ b/jetty-server/src/main/java/org/eclipse/jetty/server/LocalConnector.java
@@ -172,7 +172,6 @@
Connection connection = getDefaultConnectionFactory().newConnection(this, endPoint);
endPoint.setConnection(connection);
-// connectionOpened(connection);
connection.onOpen();
}
diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/QueuedHttpInput.java b/jetty-server/src/main/java/org/eclipse/jetty/server/QueuedHttpInput.java
new file mode 100644
index 0000000..560b728
--- /dev/null
+++ b/jetty-server/src/main/java/org/eclipse/jetty/server/QueuedHttpInput.java
@@ -0,0 +1,160 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server;
+
+import java.io.IOException;
+import java.io.InterruptedIOException;
+import javax.servlet.ServletInputStream;
+
+import org.eclipse.jetty.util.ArrayQueue;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+
+/**
+ * <p>{@link QueuedHttpInput} provides an implementation of {@link ServletInputStream} for {@link HttpChannel}.</p>
+ * <p>{@link QueuedHttpInput} holds a queue of items passed to it by calls to {@link #content(T)}.</p>
+ * <p>{@link QueuedHttpInput} stores the items directly; if the items contain byte buffers, it does not copy them
+ * but simply holds references to the item, thus the caller must organize for those buffers to valid while
+ * held by this class.</p>
+ * <p>To assist the caller, subclasses may override methods {@link #onAsyncRead()},
+ * {@link #onContentConsumed(T)} and {@link #onAllContentConsumed()} that can be implemented so that the
+ * caller will know when buffers are queued and consumed.</p>
+ */
+public abstract class QueuedHttpInput<T> extends HttpInput<T>
+{
+ private final static Logger LOG = Log.getLogger(QueuedHttpInput.class);
+
+ private final ArrayQueue<T> _inputQ = new ArrayQueue<>(lock());
+
+ public QueuedHttpInput()
+ {}
+
+ public void recycle()
+ {
+ synchronized (lock())
+ {
+ T item = _inputQ.peekUnsafe();
+ while (item != null)
+ {
+ _inputQ.pollUnsafe();
+ onContentConsumed(item);
+
+ item = _inputQ.peekUnsafe();
+ if (item == null)
+ onAllContentConsumed();
+ }
+ super.recycle();
+ }
+ }
+
+ @Override
+ protected T nextContent()
+ {
+ T item = _inputQ.peekUnsafe();
+
+ // Skip empty items at the head of the queue
+ while (item != null && remaining(item) == 0)
+ {
+ _inputQ.pollUnsafe();
+ onContentConsumed(item);
+ LOG.debug("{} consumed {}", this, item);
+ item = _inputQ.peekUnsafe();
+
+ // If that was the last item then notify
+ if (item==null)
+ onAllContentConsumed();
+ }
+ return item;
+ }
+
+ protected abstract void onContentConsumed(T item);
+
+ protected void blockForContent() throws IOException
+ {
+ synchronized (lock())
+ {
+ while (_inputQ.isEmpty() && !_state.isEOF())
+ {
+ try
+ {
+ LOG.debug("{} waiting for content", this);
+ lock().wait();
+ }
+ catch (InterruptedException e)
+ {
+ throw (IOException)new InterruptedIOException().initCause(e);
+ }
+ }
+ }
+ }
+
+
+ /* ------------------------------------------------------------ */
+ /** Called by this HttpInput to signal all available content has been consumed
+ */
+ protected void onAllContentConsumed()
+ {
+ }
+
+ /* ------------------------------------------------------------ */
+ /** Add some content to the input stream
+ * @param item
+ */
+ public void content(T item)
+ {
+ // The buffer is not copied here. This relies on the caller not recycling the buffer
+ // until the it is consumed. The onContentConsumed and onAllContentConsumed() callbacks are
+ // the signals to the caller that the buffers can be recycled.
+
+ synchronized (lock())
+ {
+ boolean empty=_inputQ.isEmpty();
+
+ _inputQ.add(item);
+
+ if (empty)
+ {
+ if (!onAsyncRead())
+ lock().notify();
+ }
+
+ LOG.debug("{} queued {}", this, item);
+ }
+ }
+
+
+ public void earlyEOF()
+ {
+ synchronized (lock())
+ {
+ super.earlyEOF();
+ lock().notify();
+ }
+ }
+
+ public void messageComplete()
+ {
+ synchronized (lock())
+ {
+ super.messageComplete();
+ lock().notify();
+ }
+ }
+
+}
diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/Request.java b/jetty-server/src/main/java/org/eclipse/jetty/server/Request.java
index ffe98f1..a37000f 100644
--- a/jetty-server/src/main/java/org/eclipse/jetty/server/Request.java
+++ b/jetty-server/src/main/java/org/eclipse/jetty/server/Request.java
@@ -57,6 +57,7 @@
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
+import javax.servlet.http.HttpUpgradeHandler;
import javax.servlet.http.Part;
import org.eclipse.jetty.http.HttpCookie;
@@ -495,6 +496,16 @@
{
return (int)_fields.getLongField(HttpHeader.CONTENT_LENGTH.toString());
}
+
+ /* ------------------------------------------------------------ */
+ /*
+ * @see javax.servlet.ServletRequest.getContentLengthLong()
+ */
+ @Override
+ public long getContentLengthLong()
+ {
+ return _fields.getLongField(HttpHeader.CONTENT_LENGTH.toString());
+ }
/* ------------------------------------------------------------ */
/*
@@ -1608,9 +1619,7 @@
/* ------------------------------------------------------------ */
/*
* Set a request attribute. if the attribute name is "org.eclipse.jetty.server.server.Request.queryEncoding" then the value is also passed in a call to
- * {@link #setQueryEncoding}. <p> if the attribute name is "org.eclipse.jetty.server.server.ResponseBuffer", then the response buffer is flushed with @{link
- * #flushResponseBuffer} <p> if the attribute name is "org.eclipse.jetty.io.EndPoint.maxIdleTime", then the value is passed to the associated {@link
- * EndPoint#setIdleTimeout}.
+ * {@link #setQueryEncoding}.
*
* @see javax.servlet.ServletRequest#setAttribute(java.lang.String, java.lang.Object)
*/
@@ -1619,35 +1628,11 @@
{
Object old_value = _attributes == null?null:_attributes.getAttribute(name);
- if (name.startsWith("org.eclipse.jetty."))
- {
- if ("org.eclipse.jetty.server.Request.queryEncoding".equals(name))
- setQueryEncoding(value == null?null:value.toString());
- else if ("org.eclipse.jetty.server.sendContent".equals(name))
- {
- try
- {
- ((HttpOutput)getServletResponse().getOutputStream()).sendContent(value);
- }
- catch (IOException e)
- {
- throw new RuntimeException(e);
- }
- }
- else if ("org.eclipse.jetty.server.ResponseBuffer".equals(name))
- {
- try
- {
- throw new IOException("not implemented");
- //((HttpChannel.Output)getServletResponse().getOutputStream()).sendResponse(byteBuffer);
- }
- catch (IOException e)
- {
- throw new RuntimeException(e);
- }
- }
- }
-
+ if ("org.eclipse.jetty.server.Request.queryEncoding".equals(name))
+ setQueryEncoding(value == null?null:value.toString());
+ else if ("org.eclipse.jetty.server.sendContent".equals(name))
+ LOG.warn("Deprecated: org.eclipse.jetty.server.sendContent");
+
if (_attributes == null)
_attributes = new AttributesMap();
_attributes.setAttribute(name,value);
@@ -2186,4 +2171,32 @@
setParameters(parameters);
setQueryString(query);
}
+
+
+
+ /**
+ * @see javax.servlet.http.HttpServletRequest#upgrade(java.lang.Class)
+ */
+ @Override
+ public <T extends HttpUpgradeHandler> T upgrade(Class<T> handlerClass) throws IOException, ServletException
+ {
+ if (getContext() == null)
+ throw new ServletException ("Unable to instantiate "+handlerClass);
+
+ try
+ {
+ //Instantiate an instance and inject it
+ T h = getContext().createInstance(handlerClass);
+
+ //TODO handle the rest of the upgrade process
+
+ return h;
+ }
+ catch (Exception e)
+ {
+ if (e instanceof ServletException)
+ throw (ServletException)e;
+ throw new ServletException(e);
+ }
+ }
}
diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/ResourceCache.java b/jetty-server/src/main/java/org/eclipse/jetty/server/ResourceCache.java
index c41ee2b..0d67712 100644
--- a/jetty-server/src/main/java/org/eclipse/jetty/server/ResourceCache.java
+++ b/jetty-server/src/main/java/org/eclipse/jetty/server/ResourceCache.java
@@ -284,26 +284,9 @@
{
try
{
- int len=(int)resource.length();
- if (len<0)
- {
- LOG.warn("invalid resource: "+String.valueOf(resource)+" "+len);
- return null;
- }
- ByteBuffer buffer = BufferUtil.allocate(len);
- int pos=BufferUtil.flipToFill(buffer);
- if (resource.getFile()!=null)
- BufferUtil.readFrom(resource.getFile(),buffer);
- else
- {
- InputStream is = resource.getInputStream();
- BufferUtil.readFrom(is,len,buffer);
- is.close();
- }
- BufferUtil.flipToFlush(buffer,pos);
- return buffer;
+ return BufferUtil.toBuffer(resource,true);
}
- catch(IOException e)
+ catch(IOException|IllegalArgumentException e)
{
LOG.warn(e);
return null;
@@ -316,30 +299,11 @@
try
{
if (_useFileMappedBuffer && resource.getFile()!=null)
- return BufferUtil.toBuffer(resource.getFile());
+ return BufferUtil.toMappedBuffer(resource.getFile());
- int len=(int)resource.length();
- if (len<0)
- {
- LOG.warn("invalid resource: "+String.valueOf(resource)+" "+len);
- return null;
- }
- ByteBuffer buffer = BufferUtil.allocateDirect(len);
-
- int pos=BufferUtil.flipToFill(buffer);
- if (resource.getFile()!=null)
- BufferUtil.readFrom(resource.getFile(),buffer);
- else
- {
- InputStream is = resource.getInputStream();
- BufferUtil.readFrom(is,len,buffer);
- is.close();
- }
- BufferUtil.flipToFlush(buffer,pos);
-
- return buffer;
+ return BufferUtil.toBuffer(resource,true);
}
- catch(IOException e)
+ catch(IOException|IllegalArgumentException e)
{
LOG.warn(e);
return null;
diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/Response.java b/jetty-server/src/main/java/org/eclipse/jetty/server/Response.java
index 6bf81cc..1dab467 100644
--- a/jetty-server/src/main/java/org/eclipse/jetty/server/Response.java
+++ b/jetty-server/src/main/java/org/eclipse/jetty/server/Response.java
@@ -99,6 +99,7 @@
private Locale _locale;
private MimeTypes.Type _mimeType;
private String _characterEncoding;
+ private boolean _explicitEncoding;
private String _contentType;
private OutputType _outputType = OutputType.NONE;
private ResponseWriter _writer;
@@ -128,6 +129,7 @@
_contentLength = -1;
_out.reset();
_fields.clear();
+ _explicitEncoding=false;
}
public void setHeaders(HttpContent httpContent)
@@ -454,10 +456,18 @@
_channel.sendResponse(HttpGenerator.PROGRESS_102_INFO, null, true);
}
}
-
- @Override
- public void sendRedirect(String location) throws IOException
+
+ /**
+ * Sends a response with one of the 300 series redirection codes.
+ * @param code
+ * @param location
+ * @throws IOException
+ */
+ public void sendRedirect(int code, String location) throws IOException
{
+ if ((code < HttpServletResponse.SC_MULTIPLE_CHOICES) || (code >= HttpServletResponse.SC_BAD_REQUEST))
+ throw new IllegalArgumentException("Not a 3xx redirect code");
+
if (isIncluding())
return;
@@ -467,7 +477,15 @@
if (!URIUtil.hasScheme(location))
{
StringBuilder buf = _channel.getRequest().getRootURL();
- if (location.startsWith("/"))
+
+ if (location.startsWith("//"))
+ {
+ buf.delete(0, buf.length());
+ buf.append(_channel.getRequest().getScheme());
+ buf.append(":");
+ buf.append(location);
+ }
+ else if (location.startsWith("/"))
buf.append(location);
else
{
@@ -515,11 +533,17 @@
resetBuffer();
setHeader(HttpHeader.LOCATION, location);
- setStatus(HttpServletResponse.SC_MOVED_TEMPORARILY);
+ setStatus(code);
closeOutput();
}
@Override
+ public void sendRedirect(String location) throws IOException
+ {
+ sendRedirect(HttpServletResponse.SC_MOVED_TEMPORARILY, location);
+ }
+
+ @Override
public void setDateHeader(String name, long date)
{
if (!isIncluding())
@@ -723,7 +747,7 @@
encoding = MimeTypes.inferCharsetFromContentType(_contentType);
if (encoding == null)
encoding = StringUtil.__ISO_8859_1;
- setCharacterEncoding(encoding);
+ setCharacterEncoding(encoding,false);
}
if (_writer != null && _writer.isFor(encoding))
@@ -811,10 +835,21 @@
_contentLength = len;
_fields.putLongField(HttpHeader.CONTENT_LENGTH.toString(), len);
}
+
+ @Override
+ public void setContentLengthLong(long length)
+ {
+ setLongContentLength(length);
+ }
@Override
public void setCharacterEncoding(String encoding)
{
+ setCharacterEncoding(encoding,true);
+ }
+
+ private void setCharacterEncoding(String encoding, boolean explicit)
+ {
if (isIncluding())
return;
@@ -822,6 +857,8 @@
{
if (encoding == null)
{
+ _explicitEncoding=false;
+
// Clear any encoding.
if (_characterEncoding != null)
{
@@ -840,6 +877,7 @@
else
{
// No, so just add this one to the mimetype
+ _explicitEncoding=explicit;
_characterEncoding = StringUtil.normalizeCharset(encoding);
if (_contentType != null)
{
@@ -900,6 +938,7 @@
else
{
_characterEncoding = charset;
+ _explicitEncoding = true;
}
HttpField field = HttpField.CONTENT_TYPE.get(_contentType);
@@ -1040,8 +1079,8 @@
String charset = _channel.getRequest().getContext().getContextHandler().getLocaleEncoding(locale);
- if (charset != null && charset.length() > 0 && _characterEncoding == null)
- setCharacterEncoding(charset);
+ if (charset != null && charset.length() > 0 && !_explicitEncoding)
+ setCharacterEncoding(charset,false);
}
@Override
diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/ServletRequestHttpWrapper.java b/jetty-server/src/main/java/org/eclipse/jetty/server/ServletRequestHttpWrapper.java
index b131448..b0c6270 100644
--- a/jetty-server/src/main/java/org/eclipse/jetty/server/ServletRequestHttpWrapper.java
+++ b/jetty-server/src/main/java/org/eclipse/jetty/server/ServletRequestHttpWrapper.java
@@ -31,10 +31,15 @@
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
+import javax.servlet.http.HttpUpgradeHandler;
import javax.servlet.http.Part;
+
/* ------------------------------------------------------------ */
-/** Class to tunnel a ServletRequest via a HttpServletRequest
+/**
+ * ServletRequestHttpWrapper
+ *
+ * Class to tunnel a ServletRequest via a HttpServletRequest
*/
public class ServletRequestHttpWrapper extends ServletRequestWrapper implements HttpServletRequest
{
@@ -209,4 +214,25 @@
}
+ /**
+ * @see javax.servlet.http.HttpServletRequest#changeSessionId()
+ */
+ @Override
+ public String changeSessionId()
+ {
+ // TODO 3.1 Auto-generated method stub
+ return null;
+ }
+
+ /**
+ * @see javax.servlet.http.HttpServletRequest#upgrade(java.lang.Class)
+ */
+ @Override
+ public <T extends HttpUpgradeHandler> T upgrade(Class<T> handlerClass) throws IOException, ServletException
+ {
+ // TODO 3.1 Auto-generated method stub
+ return null;
+ }
+
+
}
diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/ServletResponseHttpWrapper.java b/jetty-server/src/main/java/org/eclipse/jetty/server/ServletResponseHttpWrapper.java
index 1f1ffb4..4c62b9e 100644
--- a/jetty-server/src/main/java/org/eclipse/jetty/server/ServletResponseHttpWrapper.java
+++ b/jetty-server/src/main/java/org/eclipse/jetty/server/ServletResponseHttpWrapper.java
@@ -28,7 +28,10 @@
/* ------------------------------------------------------------ */
-/** Wrapper to tunnel a ServletResponse via a HttpServletResponse
+/**
+ * ServletResponseHttpWrapper
+ *
+ * Wrapper to tunnel a ServletResponse via a HttpServletResponse
*/
public class ServletResponseHttpWrapper extends ServletResponseWrapper implements HttpServletResponse
{
diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/Utf8HttpWriter.java b/jetty-server/src/main/java/org/eclipse/jetty/server/Utf8HttpWriter.java
index 251e3d0..6028109 100644
--- a/jetty-server/src/main/java/org/eclipse/jetty/server/Utf8HttpWriter.java
+++ b/jetty-server/src/main/java/org/eclipse/jetty/server/Utf8HttpWriter.java
@@ -43,10 +43,10 @@
public void write (char[] s,int offset, int length) throws IOException
{
HttpOutput out = _out;
- if (length==0)
+ if (length==0 && out.isAllContentWritten())
{
- if (_out.isAllContentWritten())
- close();
+ close();
+ return;
}
while (length > 0)
diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ContextHandler.java b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ContextHandler.java
index b30c94e..38e9b69 100644
--- a/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ContextHandler.java
+++ b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ContextHandler.java
@@ -40,6 +40,7 @@
import java.util.Set;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.Future;
+
import javax.servlet.DispatcherType;
import javax.servlet.Filter;
import javax.servlet.FilterRegistration;
@@ -61,6 +62,9 @@
import javax.servlet.descriptor.JspConfigDescriptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
+import javax.servlet.http.HttpSessionAttributeListener;
+import javax.servlet.http.HttpSessionIdListener;
+import javax.servlet.http.HttpSessionListener;
import org.eclipse.jetty.http.MimeTypes;
import org.eclipse.jetty.server.ClassLoaderDump;
@@ -103,11 +107,17 @@
{
public static int SERVLET_MAJOR_VERSION=3;
public static int SERVLET_MINOR_VERSION=0;
+ public static final Class[] SERVLET_LISTENER_TYPES = new Class[] {ServletContextListener.class,
+ ServletContextAttributeListener.class,
+ ServletRequestListener.class,
+ ServletRequestAttributeListener.class};
+
+ public static final int DEFAULT_LISTENER_TYPE_INDEX = 1;
+ public static final int EXTENDED_LISTENER_TYPE_INDEX = 0;
+
final private static String __unimplmented="Unimplemented - use org.eclipse.jetty.servlet.ServletContextHandler";
-
-
private static final Logger LOG = Log.getLogger(ContextHandler.class);
private static final ThreadLocal<Context> __context = new ThreadLocal<Context>();
@@ -967,7 +977,9 @@
if (old_context != _scontext)
{
// check the target.
- if (DispatcherType.REQUEST.equals(dispatch) || DispatcherType.ASYNC.equals(dispatch) || (DispatcherType.ERROR.equals(dispatch) && baseRequest.getHttpChannelState().isExpired()))
+ if (DispatcherType.REQUEST.equals(dispatch) ||
+ DispatcherType.ASYNC.equals(dispatch) ||
+ DispatcherType.ERROR.equals(dispatch) && baseRequest.getHttpChannelState().isAsync())
{
if (_compactPath)
target = URIUtil.compactPath(target);
@@ -1719,6 +1731,8 @@
public class Context extends NoContext
{
protected boolean _enabled = true; //whether or not the dynamic API is enabled for callers
+ protected boolean _extendedListenerTypes = false;
+
/* ------------------------------------------------------------ */
protected Context()
@@ -2123,6 +2137,7 @@
try
{
Class<? extends EventListener> clazz = _classLoader==null?Loader.loadClass(ContextHandler.class,className):_classLoader.loadClass(className);
+ checkListener(clazz);
addListener(clazz);
}
catch (ClassNotFoundException e)
@@ -2136,6 +2151,9 @@
{
if (!_enabled)
throw new UnsupportedOperationException();
+
+ checkListener(t.getClass());
+
ContextHandler.this.addEventListener(t);
ContextHandler.this.addProgrammaticListener(t);
}
@@ -2146,6 +2164,8 @@
if (!_enabled)
throw new UnsupportedOperationException();
+ checkListener(listenerClass);
+
try
{
EventListener e = createListener(listenerClass);
@@ -2163,18 +2183,42 @@
{
try
{
- return clazz.newInstance();
+ return createInstance(clazz);
}
- catch (InstantiationException e)
- {
- throw new ServletException(e);
- }
- catch (IllegalAccessException e)
+ catch (Exception e)
{
throw new ServletException(e);
}
}
+
+ public void checkListener (Class<? extends EventListener> listener) throws IllegalStateException
+ {
+ boolean ok = false;
+ int startIndex = (isExtendedListenerTypes()?EXTENDED_LISTENER_TYPE_INDEX:DEFAULT_LISTENER_TYPE_INDEX);
+ for (int i=startIndex;i<SERVLET_LISTENER_TYPES.length;i++)
+ {
+ if (SERVLET_LISTENER_TYPES[i].isAssignableFrom(listener))
+ {
+ ok = true;
+ break;
+ }
+ }
+ if (!ok)
+ throw new IllegalArgumentException("Inappropriate listener class "+listener.getName());
+ }
+
+ public void setExtendedListenerTypes (boolean extended)
+ {
+ _extendedListenerTypes = extended;
+ }
+
+ public boolean isExtendedListenerTypes()
+ {
+ return _extendedListenerTypes;
+ }
+
+
@Override
public ClassLoader getClassLoader()
{
@@ -2212,6 +2256,14 @@
{
return _enabled;
}
+
+
+
+ public <T> T createInstance (Class<T> clazz) throws Exception
+ {
+ T o = clazz.newInstance();
+ return o;
+ }
}
@@ -2552,6 +2604,16 @@
{
LOG.warn(__unimplmented);
}
+
+ /**
+ * @see javax.servlet.ServletContext#getVirtualServerName()
+ */
+ @Override
+ public String getVirtualServerName()
+ {
+ // TODO 3.1 Auto-generated method stub
+ return null;
+ }
}
diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ResourceHandler.java b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ResourceHandler.java
index 21e4bc8..1a499c2 100644
--- a/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ResourceHandler.java
+++ b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ResourceHandler.java
@@ -531,7 +531,7 @@
resource.length()>_minMemoryMappedContentLength &&
resource instanceof FileResource)
{
- ByteBuffer buffer = BufferUtil.toBuffer(resource.getFile());
+ ByteBuffer buffer = BufferUtil.toMappedBuffer(resource.getFile());
((HttpOutput)out).sendContent(buffer,callback);
}
else // Do a blocking write of a channel (if available) or input stream
@@ -550,7 +550,7 @@
resource.length()>_minMemoryMappedContentLength &&
resource instanceof FileResource)
{
- ByteBuffer buffer = BufferUtil.toBuffer(resource.getFile());
+ ByteBuffer buffer = BufferUtil.toMappedBuffer(resource.getFile());
((HttpOutput)out).sendContent(buffer);
}
else // Do a blocking write of a channel (if available) or input stream
diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/handler/StatisticsHandler.java b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/StatisticsHandler.java
index 78e8eb7..26a0d8d 100644
--- a/jetty-server/src/main/java/org/eclipse/jetty/server/handler/StatisticsHandler.java
+++ b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/StatisticsHandler.java
@@ -81,7 +81,6 @@
@Override
public void onComplete(AsyncEvent event) throws IOException
{
-
HttpChannelState state = ((AsyncContextEvent)event).getHttpChannelState();
Request request = state.getBaseRequest();
@@ -92,8 +91,7 @@
updateResponse(request);
- if (!state.isDispatched())
- _asyncWaitStats.decrement();
+ _asyncWaitStats.decrement();
}
};
@@ -139,9 +137,7 @@
{
// resumed request
start = System.currentTimeMillis();
- _asyncWaitStats.decrement();
- if (state.isDispatched())
- _asyncDispatches.incrementAndGet();
+ _asyncDispatches.incrementAndGet();
}
try
@@ -159,8 +155,10 @@
if (state.isSuspended())
{
if (state.isInitial())
+ {
state.addListener(_onCompletion);
- _asyncWaitStats.increment();
+ _asyncWaitStats.increment();
+ }
}
else if (state.isInitial())
{
diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/session/AbstractSession.java b/jetty-server/src/main/java/org/eclipse/jetty/server/session/AbstractSession.java
index db6b465..96db015 100644
--- a/jetty-server/src/main/java/org/eclipse/jetty/server/session/AbstractSession.java
+++ b/jetty-server/src/main/java/org/eclipse/jetty/server/session/AbstractSession.java
@@ -51,8 +51,8 @@
{
final static Logger LOG = SessionHandler.LOG;
public final static String SESSION_KNOWN_ONLY_TO_AUTHENTICATED="org.eclipse.jetty.security.sessionKnownOnlytoAuthenticated";
- private String _clusterId; // ID unique within cluster
- private String _nodeId; // ID unique within node
+ private String _clusterId; // ID without any node (ie "worker") id appended
+ private String _nodeId; // ID of session with node(ie "worker") id appended
private final AbstractSessionManager _manager;
private final Map<String,Object> _attributes=new HashMap<String, Object>();
private boolean _idChanged;
@@ -185,6 +185,7 @@
@Override
public long getCreationTime() throws IllegalStateException
{
+ checkValid();
return _created;
}
@@ -365,6 +366,7 @@
@Override
public void invalidate() throws IllegalStateException
{
+ checkValid();
// remove session from context and invalidate other sessions with same ID.
_manager.removeSession(this,true);
doInvalidate();
diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/session/AbstractSessionManager.java b/jetty-server/src/main/java/org/eclipse/jetty/server/session/AbstractSessionManager.java
index 58741c2..31b5b29 100644
--- a/jetty-server/src/main/java/org/eclipse/jetty/server/session/AbstractSessionManager.java
+++ b/jetty-server/src/main/java/org/eclipse/jetty/server/session/AbstractSessionManager.java
@@ -38,6 +38,7 @@
import javax.servlet.http.HttpSessionBindingEvent;
import javax.servlet.http.HttpSessionContext;
import javax.servlet.http.HttpSessionEvent;
+import javax.servlet.http.HttpSessionIdListener;
import javax.servlet.http.HttpSessionListener;
import org.eclipse.jetty.http.HttpCookie;
@@ -107,6 +108,7 @@
protected final List<HttpSessionAttributeListener> _sessionAttributeListeners = new CopyOnWriteArrayList<HttpSessionAttributeListener>();
protected final List<HttpSessionListener> _sessionListeners= new CopyOnWriteArrayList<HttpSessionListener>();
+ protected final List<HttpSessionIdListener> _sessionIdListeners = new CopyOnWriteArrayList<HttpSessionIdListener>();
protected ClassLoader _loader;
protected ContextHandler.Context _context;
@@ -191,6 +193,8 @@
_sessionAttributeListeners.add((HttpSessionAttributeListener)listener);
if (listener instanceof HttpSessionListener)
_sessionListeners.add((HttpSessionListener)listener);
+ if (listener instanceof HttpSessionIdListener)
+ _sessionIdListeners.add((HttpSessionIdListener)listener);
}
/* ------------------------------------------------------------ */
@@ -198,6 +202,7 @@
{
_sessionAttributeListeners.clear();
_sessionListeners.clear();
+ _sessionIdListeners.clear();
}
/* ------------------------------------------------------------ */
@@ -1004,6 +1009,29 @@
{
_checkingRemoteSessionIdEncoding=remote;
}
+
+
+ /* ------------------------------------------------------------ */
+ /**
+ * Tell the HttpSessionIdListeners the id changed.
+ * NOTE: this method must be called LAST in subclass overrides, after the session has been updated
+ * with the new id.
+ * @see org.eclipse.jetty.server.SessionManager#renewSessionId(java.lang.String, java.lang.String, java.lang.String, java.lang.String)
+ */
+ @Override
+ public void renewSessionId(String oldClusterId, String oldNodeId, String newClusterId, String newNodeId)
+ {
+ if (!_sessionIdListeners.isEmpty())
+ {
+ AbstractSession session = getSession(newClusterId);
+ HttpSessionEvent event = new HttpSessionEvent(session);
+ for (HttpSessionIdListener l:_sessionIdListeners)
+ {
+ l.sessionIdChanged(event, oldClusterId);
+ }
+ }
+
+ }
/* ------------------------------------------------------------ */
/* ------------------------------------------------------------ */
diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/session/HashSessionManager.java b/jetty-server/src/main/java/org/eclipse/jetty/server/session/HashSessionManager.java
index 2f7e1b4..1c6676a 100644
--- a/jetty-server/src/main/java/org/eclipse/jetty/server/session/HashSessionManager.java
+++ b/jetty-server/src/main/java/org/eclipse/jetty/server/session/HashSessionManager.java
@@ -440,6 +440,8 @@
session.setNodeId(newNodeId);
session.save(); //save updated session: TODO consider only saving file if idled
sessions.put(newClusterId, session);
+
+ super.renewSessionId(oldClusterId, oldNodeId, newClusterId, newNodeId);
}
catch (Exception e)
{
diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/session/JDBCSessionManager.java b/jetty-server/src/main/java/org/eclipse/jetty/server/session/JDBCSessionManager.java
index bf595d1..9cdae2c 100644
--- a/jetty-server/src/main/java/org/eclipse/jetty/server/session/JDBCSessionManager.java
+++ b/jetty-server/src/main/java/org/eclipse/jetty/server/session/JDBCSessionManager.java
@@ -647,6 +647,8 @@
LOG.warn(e);
}
}
+
+ super.renewSessionId(oldClusterId, oldNodeId, newClusterId, newNodeId);
}
diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/session/SessionHandler.java b/jetty-server/src/main/java/org/eclipse/jetty/server/session/SessionHandler.java
index 0c8df26..44de981 100644
--- a/jetty-server/src/main/java/org/eclipse/jetty/server/session/SessionHandler.java
+++ b/jetty-server/src/main/java/org/eclipse/jetty/server/session/SessionHandler.java
@@ -29,6 +29,9 @@
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
+import javax.servlet.http.HttpSessionAttributeListener;
+import javax.servlet.http.HttpSessionIdListener;
+import javax.servlet.http.HttpSessionListener;
import org.eclipse.jetty.http.HttpCookie;
import org.eclipse.jetty.server.Request;
@@ -46,6 +49,11 @@
final static Logger LOG = Log.getLogger("org.eclipse.jetty.server.session");
public final static EnumSet<SessionTrackingMode> DEFAULT_TRACKING = EnumSet.of(SessionTrackingMode.COOKIE,SessionTrackingMode.URL);
+
+ public static final Class[] SESSION_LISTENER_TYPES = new Class[] {HttpSessionAttributeListener.class,
+ HttpSessionIdListener.class,
+ HttpSessionListener.class};
+
/* -------------------------------------------------------------- */
diff --git a/jetty-server/src/test/java/org/eclipse/jetty/server/AbstractHttpTest.java b/jetty-server/src/test/java/org/eclipse/jetty/server/AbstractHttpTest.java
index 323e16a..ced9dca 100644
--- a/jetty-server/src/test/java/org/eclipse/jetty/server/AbstractHttpTest.java
+++ b/jetty-server/src/test/java/org/eclipse/jetty/server/AbstractHttpTest.java
@@ -57,8 +57,9 @@
public void setUp() throws Exception
{
server = new Server();
- connector = new ServerConnector(server);
+ connector = new ServerConnector(server,1,1);
connector.setIdleTimeout(10000);
+
server.addConnector(connector);
httpParser = new SimpleHttpParser();
((StdErrLog)Log.getLogger(HttpChannel.class)).setHideStacks(true);
diff --git a/jetty-server/src/test/java/org/eclipse/jetty/server/ConnectionOpenCloseTest.java b/jetty-server/src/test/java/org/eclipse/jetty/server/ConnectionOpenCloseTest.java
index 51d1326..87e6de1 100644
--- a/jetty-server/src/test/java/org/eclipse/jetty/server/ConnectionOpenCloseTest.java
+++ b/jetty-server/src/test/java/org/eclipse/jetty/server/ConnectionOpenCloseTest.java
@@ -38,8 +38,10 @@
import org.eclipse.jetty.toolchain.test.MavenTestingUtils;
import org.eclipse.jetty.toolchain.test.annotation.Slow;
import org.eclipse.jetty.toolchain.test.http.SimpleHttpResponse;
+import org.eclipse.jetty.util.IO;
import org.eclipse.jetty.util.resource.Resource;
import org.eclipse.jetty.util.ssl.SslContextFactory;
+import org.hamcrest.Matchers;
import org.junit.Assert;
import org.junit.Test;
@@ -49,7 +51,7 @@
{
super(HttpVersion.HTTP_1_1.asString());
}
-
+
@Slow
@Test
public void testOpenClose() throws Exception
@@ -59,6 +61,56 @@
@Override
public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
{
+ throw new IllegalStateException();
+ }
+ });
+ server.start();
+
+ final AtomicInteger callbacks = new AtomicInteger();
+ final CountDownLatch openLatch = new CountDownLatch(1);
+ final CountDownLatch closeLatch = new CountDownLatch(1);
+ connector.addBean(new Connection.Listener.Empty()
+ {
+ @Override
+ public void onOpened(Connection connection)
+ {
+ callbacks.incrementAndGet();
+ openLatch.countDown();
+ }
+
+ @Override
+ public void onClosed(Connection connection)
+ {
+ callbacks.incrementAndGet();
+ closeLatch.countDown();
+ }
+ });
+
+ try (Socket socket = new Socket("localhost", connector.getLocalPort());)
+ {
+ socket.setSoTimeout((int)connector.getIdleTimeout());
+
+ Assert.assertTrue(openLatch.await(5, TimeUnit.SECONDS));
+ socket.shutdownOutput();
+ Assert.assertTrue(closeLatch.await(5, TimeUnit.SECONDS));
+ String response=IO.toString(socket.getInputStream());
+ Assert.assertEquals(0,response.length());
+
+ // Wait some time to see if the callbacks are called too many times
+ TimeUnit.MILLISECONDS.sleep(200);
+ Assert.assertEquals(2, callbacks.get());
+ }
+ }
+
+ @Slow
+ @Test
+ public void testOpenRequestClose() throws Exception
+ {
+ server.setHandler(new AbstractHandler()
+ {
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
+ {
baseRequest.setHandled(true);
}
});
@@ -87,7 +139,7 @@
Socket socket = new Socket("localhost", connector.getLocalPort());
socket.setSoTimeout((int)connector.getIdleTimeout());
OutputStream output = socket.getOutputStream();
- output.write(("" +
+ output.write((
"GET / HTTP/1.1\r\n" +
"Host: localhost:" + connector.getLocalPort() + "\r\n" +
"Connection: close\r\n" +
@@ -112,7 +164,7 @@
@Slow
@Test
- public void testSSLOpenClose() throws Exception
+ public void testSSLOpenRequestClose() throws Exception
{
SslContextFactory sslContextFactory = new SslContextFactory();
File keystore = MavenTestingUtils.getTestResourceFile("keystore");
diff --git a/jetty-server/src/test/java/org/eclipse/jetty/server/DumpHandler.java b/jetty-server/src/test/java/org/eclipse/jetty/server/DumpHandler.java
index 4e590fb..8d68725 100644
--- a/jetty-server/src/test/java/org/eclipse/jetty/server/DumpHandler.java
+++ b/jetty-server/src/test/java/org/eclipse/jetty/server/DumpHandler.java
@@ -175,7 +175,7 @@
}
writer.write("</pre>\n<h3>Attributes:</h3>\n<pre>");
- Enumeration attributes=request.getAttributeNames();
+ Enumeration<String> attributes=request.getAttributeNames();
if (attributes!=null && attributes.hasMoreElements())
{
while(attributes.hasMoreElements())
diff --git a/jetty-server/src/test/java/org/eclipse/jetty/server/HalfCloseRaceTest.java b/jetty-server/src/test/java/org/eclipse/jetty/server/HalfCloseRaceTest.java
deleted file mode 100644
index 4d3714b..0000000
--- a/jetty-server/src/test/java/org/eclipse/jetty/server/HalfCloseRaceTest.java
+++ /dev/null
@@ -1,85 +0,0 @@
-//
-// ========================================================================
-// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
-// ------------------------------------------------------------------------
-// All rights reserved. This program and the accompanying materials
-// are made available under the terms of the Eclipse Public License v1.0
-// and Apache License v2.0 which accompanies this distribution.
-//
-// The Eclipse Public License is available at
-// http://www.eclipse.org/legal/epl-v10.html
-//
-// The Apache License v2.0 is available at
-// http://www.opensource.org/licenses/apache2.0.php
-//
-// You may elect to redistribute this code under either of these licenses.
-// ========================================================================
-//
-
-package org.eclipse.jetty.server;
-
-import static org.junit.Assert.assertEquals;
-
-import java.io.IOException;
-import java.net.Socket;
-
-import javax.servlet.ServletException;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-
-import org.eclipse.jetty.server.Request;
-import org.eclipse.jetty.server.Server;
-import org.eclipse.jetty.server.handler.AbstractHandler;
-import org.junit.Test;
-
-public class HalfCloseRaceTest
-{
- @Test
- public void testHalfCloseRace() throws Exception
- {
- Server server = new Server();
- ServerConnector connector = new ServerConnector(server);
- connector.setPort(0);
- connector.setIdleTimeout(500);
- server.addConnector(connector);
- TestHandler handler = new TestHandler();
- server.setHandler(handler);
-
- server.start();
-
- Socket client = new Socket("localhost",connector.getLocalPort());
-
- int in = client.getInputStream().read();
- assertEquals(-1,in);
-
- client.getOutputStream().write("GET / HTTP/1.0\r\n\r\n".getBytes());
-
- Thread.sleep(200);
- assertEquals(0,handler.getHandled());
-
- }
-
- public static class TestHandler extends AbstractHandler
- {
- transient int handled;
-
- public TestHandler()
- {
- }
-
- @Override
- public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
- {
- baseRequest.setHandled(true);
- handled++;
- response.setContentType("text/html;charset=utf-8");
- response.setStatus(HttpServletResponse.SC_OK);
- response.getWriter().println("<h1>Test</h1>");
- }
-
- public int getHandled()
- {
- return handled;
- }
- }
-}
diff --git a/jetty-server/src/test/java/org/eclipse/jetty/server/HalfCloseTest.java b/jetty-server/src/test/java/org/eclipse/jetty/server/HalfCloseTest.java
new file mode 100644
index 0000000..a9652f0
--- /dev/null
+++ b/jetty-server/src/test/java/org/eclipse/jetty/server/HalfCloseTest.java
@@ -0,0 +1,217 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server;
+
+import static org.junit.Assert.assertEquals;
+
+import java.io.IOException;
+import java.net.Socket;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import javax.servlet.AsyncContext;
+import javax.servlet.ServletException;
+import javax.servlet.http.Cookie;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.eclipse.jetty.io.Connection;
+import org.eclipse.jetty.server.Request;
+import org.eclipse.jetty.server.Server;
+import org.eclipse.jetty.server.handler.AbstractHandler;
+import org.eclipse.jetty.util.IO;
+import org.junit.Test;
+
+public class HalfCloseTest
+{
+ @Test
+ public void testHalfCloseRace() throws Exception
+ {
+ Server server = new Server();
+ ServerConnector connector = new ServerConnector(server,1,1);
+ connector.setPort(0);
+ connector.setIdleTimeout(500);
+ server.addConnector(connector);
+ TestHandler handler = new TestHandler();
+ server.setHandler(handler);
+
+ server.start();
+
+ try(Socket client = new Socket("localhost",connector.getLocalPort());)
+ {
+ int in = client.getInputStream().read();
+ assertEquals(-1,in);
+
+ client.getOutputStream().write("GET / HTTP/1.0\r\n\r\n".getBytes());
+
+ Thread.sleep(200);
+ assertEquals(0,handler.getHandled());
+ }
+
+ }
+
+ @Test
+ public void testCompleteClose() throws Exception
+ {
+ Server server = new Server();
+ ServerConnector connector = new ServerConnector(server,1,1);
+ connector.setPort(0);
+ connector.setIdleTimeout(5000);
+ final AtomicInteger opened = new AtomicInteger(0);
+ final CountDownLatch closed = new CountDownLatch(1);
+ connector.addBean(new Connection.Listener()
+ {
+ @Override
+ public void onOpened(Connection connection)
+ {
+ opened.incrementAndGet();
+ }
+
+ @Override
+ public void onClosed(Connection connection)
+ {
+ closed.countDown();
+ }
+
+ });
+ server.addConnector(connector);
+ TestHandler handler = new TestHandler();
+ server.setHandler(handler);
+
+ server.start();
+
+ try(Socket client = new Socket("localhost",connector.getLocalPort());)
+ {
+ client.getOutputStream().write("GET / HTTP/1.0\r\n\r\n".getBytes());
+ IO.toString(client.getInputStream());
+ assertEquals(1,handler.getHandled());
+ assertEquals(1,opened.get());
+ }
+ assertEquals(true,closed.await(1,TimeUnit.SECONDS));
+ }
+
+ @Test
+ public void testAsyncClose() throws Exception
+ {
+ Server server = new Server();
+ ServerConnector connector = new ServerConnector(server,1,1);
+ connector.setPort(0);
+ connector.setIdleTimeout(5000);
+ final AtomicInteger opened = new AtomicInteger(0);
+ final CountDownLatch closed = new CountDownLatch(1);
+ connector.addBean(new Connection.Listener()
+ {
+ @Override
+ public void onOpened(Connection connection)
+ {
+ opened.incrementAndGet();
+ }
+
+ @Override
+ public void onClosed(Connection connection)
+ {
+ closed.countDown();
+ }
+
+ });
+ server.addConnector(connector);
+ AsyncHandler handler = new AsyncHandler();
+ server.setHandler(handler);
+
+ server.start();
+
+ try(Socket client = new Socket("localhost",connector.getLocalPort());)
+ {
+ client.getOutputStream().write("GET / HTTP/1.0\r\n\r\n".getBytes());
+ IO.toString(client.getInputStream());
+ assertEquals(1,handler.getHandled());
+ assertEquals(1,opened.get());
+ }
+ assertEquals(true,closed.await(1,TimeUnit.SECONDS));
+ }
+
+ public static class TestHandler extends AbstractHandler
+ {
+ transient int handled;
+
+ public TestHandler()
+ {
+ }
+
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
+ {
+ baseRequest.setHandled(true);
+ handled++;
+ response.setContentType("text/html;charset=utf-8");
+ response.setStatus(HttpServletResponse.SC_OK);
+ response.getWriter().println("<h1>Test</h1>");
+ }
+
+ public int getHandled()
+ {
+ return handled;
+ }
+ }
+
+ public static class AsyncHandler extends AbstractHandler
+ {
+ transient int handled;
+
+ public AsyncHandler()
+ {
+ }
+
+ @Override
+ public void handle(String target, Request baseRequest, final HttpServletRequest request, final HttpServletResponse response) throws IOException, ServletException
+ {
+ baseRequest.setHandled(true);
+ handled++;
+
+ final AsyncContext async = request.startAsync();
+ new Thread()
+ {
+ @Override
+ public void run()
+ {
+ try
+ {
+ response.setContentType("text/html;charset=utf-8");
+ response.setStatus(HttpServletResponse.SC_OK);
+ response.getWriter().println("<h1>Test</h1>");
+ }
+ catch (Exception ex)
+ {
+ System.err.println(ex);
+ }
+ finally
+ {
+ async.complete();
+ }
+ }
+ }.start();
+ }
+
+ public int getHandled()
+ {
+ return handled;
+ }
+ }
+}
diff --git a/jetty-server/src/test/java/org/eclipse/jetty/server/HttpConnectionTest.java b/jetty-server/src/test/java/org/eclipse/jetty/server/HttpConnectionTest.java
index 6e7a6d5..031832f 100644
--- a/jetty-server/src/test/java/org/eclipse/jetty/server/HttpConnectionTest.java
+++ b/jetty-server/src/test/java/org/eclipse/jetty/server/HttpConnectionTest.java
@@ -200,7 +200,20 @@
}
@Test
- public void testEmpty() throws Exception
+ public void testSimple() throws Exception
+ {
+ String response=connector.getResponses("GET /R1 HTTP/1.1\n"+
+ "Host: localhost\n"+
+ "Connection: close\n"+
+ "\n");
+
+ int offset=0;
+ offset = checkContains(response,offset,"HTTP/1.1 200");
+ checkContains(response,offset,"/R1");
+ }
+
+ @Test
+ public void testEmptyChunk() throws Exception
{
String response=connector.getResponses("GET /R1 HTTP/1.1\n"+
"Host: localhost\n"+
@@ -372,7 +385,7 @@
}
@Test
- public void testUnconsumedError() throws Exception
+ public void testUnconsumedErrorRead() throws Exception
{
String response=null;
String requests=null;
@@ -380,7 +393,7 @@
offset=0;
requests=
- "GET /R1?read=1&error=500 HTTP/1.1\n"+
+ "GET /R1?read=1&error=599 HTTP/1.1\n"+
"Host: localhost\n"+
"Transfer-Encoding: chunked\n"+
"Content-Type: text/plain; charset=utf-8\n"+
@@ -400,7 +413,43 @@
response=connector.getResponses(requests);
- offset = checkContains(response,offset,"HTTP/1.1 500");
+ offset = checkContains(response,offset,"HTTP/1.1 599");
+ offset = checkContains(response,offset,"HTTP/1.1 200");
+ offset = checkContains(response,offset,"/R2");
+ offset = checkContains(response,offset,"encoding=UTF-8");
+ offset = checkContains(response,offset,"abcdefghij");
+ }
+
+ @Test
+ public void testUnconsumedErrorStream() throws Exception
+ {
+ String response=null;
+ String requests=null;
+ int offset=0;
+
+ offset=0;
+ requests=
+ "GET /R1?error=599 HTTP/1.1\n"+
+ "Host: localhost\n"+
+ "Transfer-Encoding: chunked\n"+
+ "Content-Type: application/data; charset=utf-8\n"+
+ "\015\012"+
+ "5;\015\012"+
+ "12345\015\012"+
+ "5;\015\012"+
+ "67890\015\012"+
+ "0;\015\012\015\012"+
+ "GET /R2 HTTP/1.1\n"+
+ "Host: localhost\n"+
+ "Content-Type: text/plain; charset=utf-8\n"+
+ "Content-Length: 10\n"+
+ "Connection: close\n"+
+ "\n"+
+ "abcdefghij\n";
+
+ response=connector.getResponses(requests);
+
+ offset = checkContains(response,offset,"HTTP/1.1 599");
offset = checkContains(response,offset,"HTTP/1.1 200");
offset = checkContains(response,offset,"/R2");
offset = checkContains(response,offset,"encoding=UTF-8");
diff --git a/jetty-server/src/test/java/org/eclipse/jetty/server/HttpOutputTest.java b/jetty-server/src/test/java/org/eclipse/jetty/server/HttpOutputTest.java
index c64387b..ce1441f 100644
--- a/jetty-server/src/test/java/org/eclipse/jetty/server/HttpOutputTest.java
+++ b/jetty-server/src/test/java/org/eclipse/jetty/server/HttpOutputTest.java
@@ -27,11 +27,14 @@
import java.nio.ByteBuffer;
import java.nio.channels.ReadableByteChannel;
+import javax.servlet.AsyncContext;
import javax.servlet.ServletException;
+import javax.servlet.WriteListener;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.eclipse.jetty.server.handler.AbstractHandler;
+import org.eclipse.jetty.util.BufferUtil;
import org.eclipse.jetty.util.resource.Resource;
import org.hamcrest.Matchers;
import org.junit.After;
@@ -119,9 +122,9 @@
assertThat(response,containsString("HTTP/1.1 200 OK"));
assertThat(response,containsString("Transfer-Encoding: chunked"));
+ assertThat(response,containsString("400\tThis is a big file"));
assertThat(response,containsString("\r\n0\r\n"));
}
-
@Test
public void testSendChannelSimple() throws Exception
@@ -141,9 +144,33 @@
String response=_connector.getResponses("GET / HTTP/1.0\nHost: localhost:80\n\n");
assertThat(response,containsString("HTTP/1.1 200 OK"));
assertThat(response,Matchers.not(containsString("Content-Length")));
+ assertThat(response,containsString("400\tThis is a big file"));
}
@Test
+ public void testSendBigDirect() throws Exception
+ {
+ Resource big = Resource.newClassPathResource("simple/big.txt");
+ _handler._content=BufferUtil.toBuffer(big,true);
+ String response=_connector.getResponses("GET / HTTP/1.0\nHost: localhost:80\n\n");
+ assertThat(response,containsString("HTTP/1.1 200 OK"));
+ assertThat(response,containsString("Content-Length"));
+ assertThat(response,containsString("400\tThis is a big file"));
+ }
+
+ @Test
+ public void testSendBigInDirect() throws Exception
+ {
+ Resource big = Resource.newClassPathResource("simple/big.txt");
+ _handler._content=BufferUtil.toBuffer(big,false);
+ String response=_connector.getResponses("GET / HTTP/1.0\nHost: localhost:80\n\n");
+ assertThat(response,containsString("HTTP/1.1 200 OK"));
+ assertThat(response,containsString("Content-Length"));
+ assertThat(response,containsString("400\tThis is a big file"));
+ }
+
+
+ @Test
public void testSendChannelBigChunked() throws Exception
{
Resource big = Resource.newClassPathResource("simple/big.txt");
@@ -191,8 +218,175 @@
assertThat(response,containsString("\r\n0\r\n"));
}
+
+ @Test
+ public void testWriteSmall() throws Exception
+ {
+ final Resource big = Resource.newClassPathResource("simple/big.txt");
+ _handler._content=BufferUtil.toBuffer(big,false);
+ _handler._bytes=new byte[8];
+
+ String response=_connector.getResponses("GET / HTTP/1.0\nHost: localhost:80\n\n");
+ assertThat(response,containsString("HTTP/1.1 200 OK"));
+ assertThat(response,Matchers.not(containsString("Content-Length")));
+ assertThat(response,containsString("400\tThis is a big file"));
+ }
+
+ @Test
+ public void testWriteMed() throws Exception
+ {
+ final Resource big = Resource.newClassPathResource("simple/big.txt");
+ _handler._content=BufferUtil.toBuffer(big,false);
+ _handler._bytes=new byte[4000];
+
+ String response=_connector.getResponses("GET / HTTP/1.0\nHost: localhost:80\n\n");
+ assertThat(response,containsString("HTTP/1.1 200 OK"));
+ assertThat(response,Matchers.not(containsString("Content-Length")));
+ assertThat(response,containsString("400\tThis is a big file"));
+ }
+
+ @Test
+ public void testWriteLarge() throws Exception
+ {
+ final Resource big = Resource.newClassPathResource("simple/big.txt");
+ _handler._content=BufferUtil.toBuffer(big,false);
+ _handler._bytes=new byte[8192];
+
+ String response=_connector.getResponses("GET / HTTP/1.0\nHost: localhost:80\n\n");
+ assertThat(response,containsString("HTTP/1.1 200 OK"));
+ assertThat(response,Matchers.not(containsString("Content-Length")));
+ assertThat(response,containsString("400\tThis is a big file"));
+ }
+
+ @Test
+ public void testWriteBufferSmall() throws Exception
+ {
+ final Resource big = Resource.newClassPathResource("simple/big.txt");
+ _handler._content=BufferUtil.toBuffer(big,false);
+ _handler._buffer=BufferUtil.allocate(8);
+
+ String response=_connector.getResponses("GET / HTTP/1.0\nHost: localhost:80\n\n");
+ assertThat(response,containsString("HTTP/1.1 200 OK"));
+ assertThat(response,Matchers.not(containsString("Content-Length")));
+ assertThat(response,containsString("400\tThis is a big file"));
+ }
+
+ @Test
+ public void testWriteBufferMed() throws Exception
+ {
+ final Resource big = Resource.newClassPathResource("simple/big.txt");
+ _handler._content=BufferUtil.toBuffer(big,false);
+ _handler._buffer=BufferUtil.allocate(4000);
+
+ String response=_connector.getResponses("GET / HTTP/1.0\nHost: localhost:80\n\n");
+ assertThat(response,containsString("HTTP/1.1 200 OK"));
+ assertThat(response,Matchers.not(containsString("Content-Length")));
+ assertThat(response,containsString("400\tThis is a big file"));
+ }
+
+ @Test
+ public void testWriteBufferLarge() throws Exception
+ {
+ final Resource big = Resource.newClassPathResource("simple/big.txt");
+ _handler._content=BufferUtil.toBuffer(big,false);
+ _handler._buffer=BufferUtil.allocate(8192);
+
+ String response=_connector.getResponses("GET / HTTP/1.0\nHost: localhost:80\n\n");
+ assertThat(response,containsString("HTTP/1.1 200 OK"));
+ assertThat(response,Matchers.not(containsString("Content-Length")));
+ assertThat(response,containsString("400\tThis is a big file"));
+ }
+
+ @Test
+ public void testAsyncWriteSmall() throws Exception
+ {
+ final Resource big = Resource.newClassPathResource("simple/big.txt");
+ _handler._content=BufferUtil.toBuffer(big,false);
+ _handler._bytes=new byte[8];
+ _handler._async=true;
+
+ String response=_connector.getResponses("GET / HTTP/1.0\nHost: localhost:80\n\n");
+ assertThat(response,containsString("HTTP/1.1 200 OK"));
+ assertThat(response,Matchers.not(containsString("Content-Length")));
+ assertThat(response,containsString("400\tThis is a big file"));
+ }
+
+ @Test
+ public void testAsyncWriteMed() throws Exception
+ {
+ final Resource big = Resource.newClassPathResource("simple/big.txt");
+ _handler._content=BufferUtil.toBuffer(big,false);
+ _handler._bytes=new byte[4000];
+ _handler._async=true;
+
+ String response=_connector.getResponses("GET / HTTP/1.0\nHost: localhost:80\n\n");
+ assertThat(response,containsString("HTTP/1.1 200 OK"));
+ assertThat(response,Matchers.not(containsString("Content-Length")));
+ assertThat(response,containsString("400\tThis is a big file"));
+ }
+
+ @Test
+ public void testAsyncWriteLarge() throws Exception
+ {
+ final Resource big = Resource.newClassPathResource("simple/big.txt");
+ _handler._content=BufferUtil.toBuffer(big,false);
+ _handler._bytes=new byte[8192];
+ _handler._async=true;
+
+ String response=_connector.getResponses("GET / HTTP/1.0\nHost: localhost:80\n\n");
+ assertThat(response,containsString("HTTP/1.1 200 OK"));
+ assertThat(response,Matchers.not(containsString("Content-Length")));
+ assertThat(response,containsString("400\tThis is a big file"));
+ }
+
+ @Test
+ public void testAsyncWriteBufferSmall() throws Exception
+ {
+ final Resource big = Resource.newClassPathResource("simple/big.txt");
+ _handler._content=BufferUtil.toBuffer(big,false);
+ _handler._buffer=BufferUtil.allocate(8);
+ _handler._async=true;
+
+ String response=_connector.getResponses("GET / HTTP/1.0\nHost: localhost:80\n\n");
+ assertThat(response,containsString("HTTP/1.1 200 OK"));
+ assertThat(response,Matchers.not(containsString("Content-Length")));
+ assertThat(response,containsString("400\tThis is a big file"));
+ }
+
+ @Test
+ public void testAsyncWriteBufferMed() throws Exception
+ {
+ final Resource big = Resource.newClassPathResource("simple/big.txt");
+ _handler._content=BufferUtil.toBuffer(big,false);
+ _handler._buffer=BufferUtil.allocate(4000);
+ _handler._async=true;
+
+ String response=_connector.getResponses("GET / HTTP/1.0\nHost: localhost:80\n\n");
+ assertThat(response,containsString("HTTP/1.1 200 OK"));
+ assertThat(response,Matchers.not(containsString("Content-Length")));
+ assertThat(response,containsString("400\tThis is a big file"));
+ }
+
+ @Test
+ public void testAsyncWriteBufferLarge() throws Exception
+ {
+ final Resource big = Resource.newClassPathResource("simple/big.txt");
+ _handler._content=BufferUtil.toBuffer(big,false);
+ _handler._buffer=BufferUtil.allocate(8192);
+ _handler._async=true;
+
+ String response=_connector.getResponses("GET / HTTP/1.0\nHost: localhost:80\n\n");
+ assertThat(response,containsString("HTTP/1.1 200 OK"));
+ assertThat(response,Matchers.not(containsString("Content-Length")));
+ assertThat(response,containsString("400\tThis is a big file"));
+ }
+
static class ContentHandler extends AbstractHandler
{
+ boolean _async;
+ ByteBuffer _buffer;
+ byte[] _bytes;
+ ByteBuffer _content;
InputStream _contentInputStream;
ReadableByteChannel _contentChannel;
@@ -202,7 +396,7 @@
baseRequest.setHandled(true);
response.setContentType("text/plain");
- HttpOutput out = (HttpOutput) response.getOutputStream();
+ final HttpOutput out = (HttpOutput) response.getOutputStream();
if (_contentInputStream!=null)
{
@@ -218,6 +412,113 @@
return;
}
+ if (_bytes!=null)
+ {
+ if (_async)
+ {
+ final AsyncContext async = request.startAsync();
+ out.setWriteListener(new WriteListener()
+ {
+ @Override
+ public void onWritePossible() throws IOException
+ {
+ while (out.isReady())
+ {
+ int len=_content.remaining();
+ if (len>_bytes.length)
+ len=_bytes.length;
+ if (len==0)
+ {
+ async.complete();
+ break;
+ }
+
+ _content.get(_bytes,0,len);
+ out.write(_bytes,0,len);
+ }
+ }
+
+ @Override
+ public void onError(Throwable t)
+ {
+ t.printStackTrace();
+ async.complete();
+ }
+ });
+
+ return;
+ }
+
+
+ while(BufferUtil.hasContent(_content))
+ {
+ int len=_content.remaining();
+ if (len>_bytes.length)
+ len=_bytes.length;
+ _content.get(_bytes,0,len);
+ out.write(_bytes,0,len);
+ }
+
+ return;
+ }
+
+ if (_buffer!=null)
+ {
+ if (_async)
+ {
+ final AsyncContext async = request.startAsync();
+ out.setWriteListener(new WriteListener()
+ {
+ @Override
+ public void onWritePossible() throws IOException
+ {
+ while (out.isReady())
+ {
+ if(BufferUtil.isEmpty(_content))
+ {
+ async.complete();
+ break;
+ }
+
+ BufferUtil.clearToFill(_buffer);
+ BufferUtil.put(_content,_buffer);
+ BufferUtil.flipToFlush(_buffer,0);
+ out.write(_buffer);
+ }
+ }
+
+ @Override
+ public void onError(Throwable t)
+ {
+ t.printStackTrace();
+ async.complete();
+ }
+ });
+
+ return;
+ }
+
+
+ while(BufferUtil.hasContent(_content))
+ {
+ BufferUtil.clearToFill(_buffer);
+ BufferUtil.put(_content,_buffer);
+ BufferUtil.flipToFlush(_buffer,0);
+ out.write(_buffer);
+ }
+
+ return;
+ }
+
+
+ if (_content!=null)
+ {
+ out.sendContent(_content);
+ _content=null;
+ return;
+ }
+
+
}
}
diff --git a/jetty-server/src/test/java/org/eclipse/jetty/server/HttpServerTestBase.java b/jetty-server/src/test/java/org/eclipse/jetty/server/HttpServerTestBase.java
index 5a5f594..a8e511b 100644
--- a/jetty-server/src/test/java/org/eclipse/jetty/server/HttpServerTestBase.java
+++ b/jetty-server/src/test/java/org/eclipse/jetty/server/HttpServerTestBase.java
@@ -18,8 +18,10 @@
package org.eclipse.jetty.server;
+import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.greaterThanOrEqualTo;
import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.not;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertThat;
@@ -48,12 +50,14 @@
import org.eclipse.jetty.io.EndPoint;
import org.eclipse.jetty.io.EofException;
import org.eclipse.jetty.server.handler.AbstractHandler;
+import org.eclipse.jetty.toolchain.test.annotation.Slow;
import org.eclipse.jetty.util.IO;
import org.eclipse.jetty.util.StringUtil;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.StdErrLog;
import org.hamcrest.Matchers;
import org.junit.Assert;
+import org.junit.Ignore;
import org.junit.Test;
import org.junit.matchers.JUnitMatchers;
/**
@@ -348,17 +352,19 @@
"Connection: close\015\012" +
"\015\012").getBytes());
os.flush();
- Thread.sleep(PAUSE);
- os.write(("5\015\012").getBytes());
+ Thread.sleep(1000);
+ os.write(("5").getBytes());
+ Thread.sleep(1000);
+ os.write(("\015\012").getBytes());
os.flush();
- Thread.sleep(PAUSE);
+ Thread.sleep(1000);
os.write(("ABCDE\015\012" +
"0;\015\012\015\012").getBytes());
os.flush();
// Read the response.
String response = readResponse(client);
- assertTrue(response.indexOf("200") > 0);
+ assertThat(response,containsString("200"));
}
}
@@ -448,59 +454,53 @@
}
}
+
@Test
- public void testRequest2Fragments() throws Exception
+ @Slow
+ public void testRequest2Sliced2() throws Exception
{
configureServer(new EchoHandler());
byte[] bytes = REQUEST2.getBytes();
- final int pointCount = 2;
- // TODO random unit tests suck!
- Random random = new Random(System.currentTimeMillis());
- for (int i = 0; i < LOOPS; i++)
- {
- int[] points = new int[pointCount];
- StringBuilder message = new StringBuilder();
-
- message.append("iteration #").append(i + 1);
-
- // Pick fragment points at random
- for (int j = 0; j < points.length; ++j)
- points[j] = random.nextInt(bytes.length);
-
- // Sort the list
- Arrays.sort(points);
-
- try (Socket client = newSocket(_serverURI.getHost(), _serverURI.getPort()))
- {
- OutputStream os = client.getOutputStream();
-
- writeFragments(bytes, points, message, os);
-
- // Read the response
- String response = readResponse(client);
-
- // Check the response
- assertEquals("response for " + i + " " + message.toString(), RESPONSE2, response);
- }
- }
- }
-
- @Test
- public void testRequest2Iterate() throws Exception
- {
- configureServer(new EchoHandler());
-
- byte[] bytes = REQUEST2.getBytes();
- for (int i = 0; i < bytes.length; i += 3)
+ int splits = bytes.length-REQUEST2_CONTENT.length()+5;
+ for (int i = 0; i < splits; i += 1)
{
int[] points = new int[]{i};
StringBuilder message = new StringBuilder();
message.append("iteration #").append(i + 1);
- // Sort the list
- Arrays.sort(points);
+ try (Socket client = newSocket(_serverURI.getHost(), _serverURI.getPort()))
+ {
+ OutputStream os = client.getOutputStream();
+
+ writeFragments(bytes, points, message, os);
+
+ // Read the response
+ String response = readResponse(client);
+
+ // Check the response
+ assertEquals("response for " + i + " " + message.toString(), RESPONSE2, response);
+
+ Thread.sleep(100);
+ }
+ }
+ }
+
+ @Test
+ @Slow
+ public void testRequest2Sliced3() throws Exception
+ {
+ configureServer(new EchoHandler());
+
+ byte[] bytes = REQUEST2.getBytes();
+ int splits = bytes.length-REQUEST2_CONTENT.length()+5;
+ for (int i = 0; i < splits; i += 1)
+ {
+ int[] points = new int[]{i,i+1};
+ StringBuilder message = new StringBuilder();
+
+ message.append("iteration #").append(i + 1);
try (Socket client = newSocket(_serverURI.getHost(), _serverURI.getPort()))
{
@@ -513,9 +513,14 @@
// Check the response
assertEquals("response for " + i + " " + message.toString(), RESPONSE2, response);
+
+ Thread.sleep(100);
}
}
}
+
+
+
@Test
public void testFlush() throws Exception
@@ -938,33 +943,28 @@
"\015\012" +
"123456789\n" +
- "HEAD /R1 HTTP/1.1\015\012" +
+ "HEAD /R2 HTTP/1.1\015\012" +
"Host: " + _serverURI.getHost() + ":" + _serverURI.getPort() + "\015\012" +
"content-type: text/plain; charset=utf-8\r\n" +
"content-length: 10\r\n" +
"\015\012" +
- "123456789\n" +
+ "ABCDEFGHI\n" +
- "POST /R1 HTTP/1.1\015\012" +
+ "POST /R3 HTTP/1.1\015\012" +
"Host: " + _serverURI.getHost() + ":" + _serverURI.getPort() + "\015\012" +
"content-type: text/plain; charset=utf-8\r\n" +
"content-length: 10\r\n" +
"Connection: close\015\012" +
"\015\012" +
- "123456789\n"
+ "abcdefghi\n"
).getBytes("iso-8859-1"));
String in = IO.toString(is);
- // System.err.println(in);
-
- int index = in.indexOf("123456789");
- assertTrue(index > 0);
- index = in.indexOf("123456789", index + 1);
- assertTrue(index > 0);
- index = in.indexOf("123456789", index + 1);
- assertTrue(index == -1);
+ Assert.assertThat(in,containsString("123456789"));
+ Assert.assertThat(in,not(containsString("ABCDEFGHI")));
+ Assert.assertThat(in,containsString("abcdefghi"));
}
}
@@ -1304,7 +1304,7 @@
}
}
- private void writeFragments(byte[] bytes, int[] points, StringBuilder message, OutputStream os) throws IOException, InterruptedException
+ protected void writeFragments(byte[] bytes, int[] points, StringBuilder message, OutputStream os) throws IOException, InterruptedException
{
int last = 0;
diff --git a/jetty-server/src/test/java/org/eclipse/jetty/server/HttpServerTestFixture.java b/jetty-server/src/test/java/org/eclipse/jetty/server/HttpServerTestFixture.java
index ca1c4ee..27b1463 100644
--- a/jetty-server/src/test/java/org/eclipse/jetty/server/HttpServerTestFixture.java
+++ b/jetty-server/src/test/java/org/eclipse/jetty/server/HttpServerTestFixture.java
@@ -93,14 +93,14 @@
protected static class EchoHandler extends AbstractHandler
{
- boolean musthavecontent=true;
+ boolean _musthavecontent=true;
public EchoHandler()
{}
public EchoHandler(boolean content)
{
- musthavecontent=false;
+ _musthavecontent=false;
}
@Override
@@ -134,7 +134,7 @@
if (count==0)
{
- if (musthavecontent)
+ if (_musthavecontent)
throw new IllegalStateException("no input recieved");
writer.println("No content");
diff --git a/jetty-server/src/test/java/org/eclipse/jetty/server/HttpWriterTest.java b/jetty-server/src/test/java/org/eclipse/jetty/server/HttpWriterTest.java
index 1b30841..71aa3cf 100644
--- a/jetty-server/src/test/java/org/eclipse/jetty/server/HttpWriterTest.java
+++ b/jetty-server/src/test/java/org/eclipse/jetty/server/HttpWriterTest.java
@@ -43,7 +43,7 @@
_bytes = BufferUtil.allocate(2048);
final ByteBufferPool bufferPool = new MappedByteBufferPool();
- HttpChannel<?> channel = new HttpChannel<ByteBuffer>(null,new HttpConfiguration(),null,null,null)
+ HttpChannel<?> channel = new HttpChannel<ByteBuffer>(null,new HttpConfiguration(),null,null,new ByteBufferQueuedHttpInput())
{
@Override
public ByteBufferPool getByteBufferPool()
diff --git a/jetty-server/src/test/java/org/eclipse/jetty/server/LowResourcesMonitorTest.java b/jetty-server/src/test/java/org/eclipse/jetty/server/LowResourcesMonitorTest.java
index eac1057..b4f38a1 100644
--- a/jetty-server/src/test/java/org/eclipse/jetty/server/LowResourcesMonitorTest.java
+++ b/jetty-server/src/test/java/org/eclipse/jetty/server/LowResourcesMonitorTest.java
@@ -192,6 +192,6 @@
Thread.sleep(1200);
Assert.assertTrue(_lowResourcesMonitor.isLowOnResources());
Assert.assertEquals(-1,socket1.getInputStream().read());
-
+
}
}
diff --git a/jetty-server/src/test/java/org/eclipse/jetty/server/RequestTest.java b/jetty-server/src/test/java/org/eclipse/jetty/server/RequestTest.java
index 370c8e2..3368841 100644
--- a/jetty-server/src/test/java/org/eclipse/jetty/server/RequestTest.java
+++ b/jetty-server/src/test/java/org/eclipse/jetty/server/RequestTest.java
@@ -37,9 +37,11 @@
import java.util.Enumeration;
import java.util.Map;
import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
import javax.servlet.MultipartConfigElement;
import javax.servlet.ServletException;
+import javax.servlet.ServletInputStream;
import javax.servlet.ServletRequestEvent;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
@@ -478,15 +480,25 @@
@Test
public void testContent() throws Exception
{
- final int[] length=new int[1];
+ final AtomicInteger length=new AtomicInteger();
_handler._checker = new RequestTester()
{
@Override
- public boolean check(HttpServletRequest request,HttpServletResponse response)
+ public boolean check(HttpServletRequest request,HttpServletResponse response) throws IOException
{
- //assertEquals(request.getContentLength(), ((Request)request).getContentRead());
- length[0]=request.getContentLength();
+ int len=request.getContentLength();
+ ServletInputStream in = request.getInputStream();
+ for (int i=0;i<len;i++)
+ {
+ int b=in.read();
+ if (b<0)
+ return false;
+ }
+ if (in.read()>0)
+ return false;
+
+ length.set(len);
return true;
}
};
@@ -494,11 +506,11 @@
String content="";
- for (int l=0;l<1025;l++)
+ for (int l=0;l<1024;l++)
{
String request="POST / HTTP/1.1\r\n"+
"Host: whatever\r\n"+
- "Content-Type: text/test\r\n"+
+ "Content-Type: multipart/form-data-test\r\n"+
"Content-Length: "+l+"\r\n"+
"Connection: close\r\n"+
"\r\n"+
@@ -506,9 +518,8 @@
Log.getRootLogger().debug("test l={}",l);
String response = _connector.getResponses(request);
Log.getRootLogger().debug(response);
- assertEquals(l,length[0]);
- if (l>0)
- assertEquals(l,_handler._content.length());
+ assertThat(response,Matchers.containsString(" 200 OK"));
+ assertEquals(l,length.get());
content+="x";
}
}
diff --git a/jetty-server/src/test/java/org/eclipse/jetty/server/ResponseTest.java b/jetty-server/src/test/java/org/eclipse/jetty/server/ResponseTest.java
index 84967af..c768aef 100644
--- a/jetty-server/src/test/java/org/eclipse/jetty/server/ResponseTest.java
+++ b/jetty-server/src/test/java/org/eclipse/jetty/server/ResponseTest.java
@@ -81,8 +81,8 @@
_server.start();
AbstractEndPoint endp = new ByteArrayEndPoint(_scheduler, 5000);
- ByteBufferHttpInput input = new ByteBufferHttpInput();
- _channel = new HttpChannel<>(connector, new HttpConfiguration(), endp, new HttpTransport()
+ ByteBufferQueuedHttpInput input = new ByteBufferQueuedHttpInput();
+ _channel = new HttpChannel<ByteBuffer>(connector, new HttpConfiguration(), endp, new HttpTransport()
{
@Override
public void send(HttpGenerator.ResponseInfo info, ByteBuffer content, boolean lastContent) throws IOException
diff --git a/jetty-servlet/pom.xml b/jetty-servlet/pom.xml
index 9a6785a..c93342f 100644
--- a/jetty-servlet/pom.xml
+++ b/jetty-servlet/pom.xml
@@ -3,7 +3,7 @@
<parent>
<artifactId>jetty-project</artifactId>
<groupId>org.eclipse.jetty</groupId>
- <version>9.0.5-SNAPSHOT</version>
+ <version>9.1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>jetty-servlet</artifactId>
@@ -26,7 +26,7 @@
</goals>
<configuration>
<instructions>
- <Import-Package>javax.servlet.*;version="2.6.0",org.eclipse.jetty.jmx.*;version="9.0";resolution:=optional,*</Import-Package>
+ <Import-Package>javax.servlet.*;version="[2.6.0,3.2)",org.eclipse.jetty.jmx.*;version="9.1";resolution:=optional,*</Import-Package>
<_nouses>true</_nouses>
</instructions>
</configuration>
diff --git a/jetty-servlet/src/main/java/org/eclipse/jetty/servlet/FilterHolder.java b/jetty-servlet/src/main/java/org/eclipse/jetty/servlet/FilterHolder.java
index b63e40a..a4e2683 100644
--- a/jetty-servlet/src/main/java/org/eclipse/jetty/servlet/FilterHolder.java
+++ b/jetty-servlet/src/main/java/org/eclipse/jetty/servlet/FilterHolder.java
@@ -18,6 +18,7 @@
package org.eclipse.jetty.servlet;
+import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
@@ -32,6 +33,9 @@
import javax.servlet.ServletException;
import org.eclipse.jetty.util.TypeUtil;
+import org.eclipse.jetty.util.component.AbstractLifeCycle;
+import org.eclipse.jetty.util.component.ContainerLifeCycle;
+import org.eclipse.jetty.util.component.Dumpable;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;
@@ -193,6 +197,16 @@
{
return getName();
}
+
+ /* ------------------------------------------------------------ */
+ @Override
+ public void dump(Appendable out, String indent) throws IOException
+ {
+ super.dump(out, indent);
+ if(_filter instanceof Dumpable) {
+ ((Dumpable) _filter).dump(out, indent);
+ }
+ }
/* ------------------------------------------------------------ */
public FilterRegistration.Dynamic getRegistration()
diff --git a/jetty-servlet/src/main/java/org/eclipse/jetty/servlet/ServletContextHandler.java b/jetty-servlet/src/main/java/org/eclipse/jetty/servlet/ServletContextHandler.java
index 991ff84..9e259ad 100644
--- a/jetty-servlet/src/main/java/org/eclipse/jetty/servlet/ServletContextHandler.java
+++ b/jetty-servlet/src/main/java/org/eclipse/jetty/servlet/ServletContextHandler.java
@@ -46,6 +46,7 @@
import javax.servlet.descriptor.JspConfigDescriptor;
import javax.servlet.descriptor.JspPropertyGroupDescriptor;
import javax.servlet.descriptor.TaglibDescriptor;
+import javax.servlet.http.HttpUpgradeHandler;
import org.eclipse.jetty.security.ConstraintAware;
import org.eclipse.jetty.security.ConstraintMapping;
@@ -274,10 +275,10 @@
Decorator decorator = _decorators.get(i);
if (_servletHandler.getFilters()!=null)
for (FilterHolder holder:_servletHandler.getFilters())
- decorator.decorateFilterHolder(holder);
+ decorator.decorate(holder);
if(_servletHandler.getServlets()!=null)
for (ServletHolder holder:_servletHandler.getServlets())
- decorator.decorateServletHolder(holder);
+ decorator.decorate(holder);
}
}
@@ -574,14 +575,14 @@
void destroyServlet(Servlet servlet)
{
for (Decorator decorator : _decorators)
- decorator.destroyServletInstance(servlet);
+ decorator.destroy(servlet);
}
/* ------------------------------------------------------------ */
void destroyFilter(Filter filter)
{
for (Decorator decorator : _decorators)
- decorator.destroyFilterInstance(filter);
+ decorator.destroy(filter);
}
/* ------------------------------------------------------------ */
@@ -896,6 +897,9 @@
{
if (isStarted())
throw new IllegalStateException();
+
+ if (filterName == null || "".equals(filterName.trim()))
+ throw new IllegalStateException("Missing filter name");
if (!_enabled)
throw new UnsupportedOperationException();
@@ -930,6 +934,9 @@
{
if (isStarted())
throw new IllegalStateException();
+
+ if (filterName == null || "".equals(filterName.trim()))
+ throw new IllegalStateException("Missing filter name");
if (!_enabled)
throw new UnsupportedOperationException();
@@ -966,6 +973,9 @@
if (isStarted())
throw new IllegalStateException();
+ if (filterName == null || "".equals(filterName.trim()))
+ throw new IllegalStateException("Missing filter name");
+
if (!_enabled)
throw new UnsupportedOperationException();
@@ -1001,6 +1011,9 @@
if (!isStarting())
throw new IllegalStateException();
+ if (servletName == null || "".equals(servletName.trim()))
+ throw new IllegalStateException("Missing servlet name");
+
if (!_enabled)
throw new UnsupportedOperationException();
@@ -1036,6 +1049,9 @@
if (!isStarting())
throw new IllegalStateException();
+ if (servletName == null || "".equals(servletName.trim()))
+ throw new IllegalStateException("Missing servlet name");
+
if (!_enabled)
throw new UnsupportedOperationException();
@@ -1071,7 +1087,10 @@
{
if (!isStarting())
throw new IllegalStateException();
-
+
+ if (servletName == null || "".equals(servletName.trim()))
+ throw new IllegalStateException("Missing servlet name");
+
if (!_enabled)
throw new UnsupportedOperationException();
@@ -1115,19 +1134,10 @@
{
try
{
- T f = c.newInstance();
- for (int i=_decorators.size()-1; i>=0; i--)
- {
- Decorator decorator = _decorators.get(i);
- f=decorator.decorateFilterInstance(f);
- }
+ T f = createInstance(c);
return f;
}
- catch (InstantiationException e)
- {
- throw new ServletException(e);
- }
- catch (IllegalAccessException e)
+ catch (Exception e)
{
throw new ServletException(e);
}
@@ -1139,23 +1149,29 @@
{
try
{
- T s = c.newInstance();
- for (int i=_decorators.size()-1; i>=0; i--)
- {
- Decorator decorator = _decorators.get(i);
- s=decorator.decorateServletInstance(s);
- }
+ T s = createInstance(c);
return s;
}
- catch (InstantiationException e)
- {
- throw new ServletException(e);
- }
- catch (IllegalAccessException e)
+ catch (Exception e)
{
throw new ServletException(e);
}
}
+
+
+
+ public <T> T createInstance (Class<T> c) throws Exception
+ {
+ T o = super.createInstance(c);
+ for (int i=_decorators.size()-1; i>=0; i--)
+ {
+ Decorator decorator = _decorators.get(i);
+ o=decorator.decorate(o);
+ }
+ return o;
+ }
+
+
@Override
public Set<SessionTrackingMode> getDefaultSessionTrackingModes()
@@ -1286,20 +1302,10 @@
{
try
{
- T l = super.createListener(clazz);
-
- for (int i=_decorators.size()-1; i>=0; i--)
- {
- Decorator decorator = _decorators.get(i);
- l=decorator.decorateListenerInstance(l);
- }
+ T l = createInstance(clazz);
return l;
- }
- catch(ServletException e)
- {
- throw e;
- }
- catch(Exception e)
+ }
+ catch (Exception e)
{
throw new ServletException(e);
}
@@ -1340,15 +1346,7 @@
*/
public interface Decorator
{
- <T extends Filter> T decorateFilterInstance(T filter) throws ServletException;
- <T extends Servlet> T decorateServletInstance(T servlet) throws ServletException;
- <T extends EventListener> T decorateListenerInstance(T listener) throws ServletException;
-
- void decorateFilterHolder(FilterHolder filter) throws ServletException;
- void decorateServletHolder(ServletHolder servlet) throws ServletException;
-
- void destroyServletInstance(Servlet s);
- void destroyFilterInstance(Filter f);
- void destroyListenerInstance(EventListener f);
+ <T> T decorate (T o);
+ void destroy (Object o);
}
}
diff --git a/jetty-servlet/src/main/java/org/eclipse/jetty/servlet/ServletHandler.java b/jetty-servlet/src/main/java/org/eclipse/jetty/servlet/ServletHandler.java
index 013d6ed..7004602 100644
--- a/jetty-servlet/src/main/java/org/eclipse/jetty/servlet/ServletHandler.java
+++ b/jetty-servlet/src/main/java/org/eclipse/jetty/servlet/ServletHandler.java
@@ -25,6 +25,7 @@
import java.util.Collections;
import java.util.EnumSet;
import java.util.HashMap;
+import java.util.HashSet;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
@@ -109,6 +110,7 @@
private ServletHolder[] _servlets=new ServletHolder[0];
private ServletMapping[] _servletMappings;
+ private Map<String,ServletMapping> _servletPathMappings = new HashMap<String,ServletMapping>();
private final Map<String,FilterHolder> _filterNameMap= new HashMap<>();
private List<FilterMapping> _filterPathMappings;
@@ -264,6 +266,7 @@
_filterPathMappings=null;
_filterNameMappings=null;
_servletPathMap=null;
+ _servletPathMappings=null;
}
/* ------------------------------------------------------------ */
@@ -329,31 +332,26 @@
return _servletMappings;
}
+
+
/* ------------------------------------------------------------ */
/**
- * @return Returns the servletMappings.
+ * Get the ServletMapping matching the path
+ *
+ * @param pathSpec
+ * @return
*/
- public ServletMapping getServletMapping(String pattern)
+ public ServletMapping getServletMapping(String pathSpec)
{
- ServletMapping theMapping = null;
- if (_servletMappings!=null)
- {
- for (ServletMapping m:_servletMappings)
- {
- String[] paths=m.getPathSpecs();
- if (paths!=null)
- {
- for (String path:paths)
- {
- if (pattern.equals(path))
- theMapping = m;
- }
- }
- }
- }
- return theMapping;
+ if (pathSpec == null || _servletPathMappings == null)
+ return null;
+
+ return _servletPathMappings.get(pathSpec);
}
+
+
+
/* ------------------------------------------------------------ */
/** Get Servlets.
* @return Array of defined servlets
@@ -906,9 +904,12 @@
{
setServletMappings(ArrayUtil.addToArray(getServletMappings(), mapping, ServletMapping.class));
}
-
- public Set<String> setServletSecurity(ServletRegistration.Dynamic registration, ServletSecurityElement servletSecurityElement) {
- if (_contextHandler != null) {
+
+ /* ------------------------------------------------------------ */
+ public Set<String> setServletSecurity(ServletRegistration.Dynamic registration, ServletSecurityElement servletSecurityElement)
+ {
+ if (_contextHandler != null)
+ {
return _contextHandler.setServletSecurity(registration, servletSecurityElement);
}
return Collections.emptySet();
@@ -1316,23 +1317,75 @@
else
{
PathMap<ServletHolder> pm = new PathMap<>();
-
- // update the maps
- for (ServletMapping servletmapping : _servletMappings)
+ Map<String,ServletMapping> servletPathMappings = new HashMap<String,ServletMapping>();
+
+ //create a map of paths to set of ServletMappings that define that mapping
+ HashMap<String, Set<ServletMapping>> sms = new HashMap<String, Set<ServletMapping>>();
+ for (ServletMapping servletMapping : _servletMappings)
{
- ServletHolder servlet_holder = _servletNameMap.get(servletmapping.getServletName());
- if (servlet_holder == null)
- throw new IllegalStateException("No such servlet: " + servletmapping.getServletName());
- else if (servlet_holder.isEnabled() && servletmapping.getPathSpecs() != null)
+ String[] pathSpecs = servletMapping.getPathSpecs();
+ if (pathSpecs != null)
{
- String[] pathSpecs = servletmapping.getPathSpecs();
for (String pathSpec : pathSpecs)
- if (pathSpec != null)
- pm.put(pathSpec, servlet_holder);
+ {
+ Set<ServletMapping> mappings = sms.get(pathSpec);
+ if (mappings == null)
+ {
+ mappings = new HashSet<ServletMapping>();
+ sms.put(pathSpec, mappings);
+ }
+ mappings.add(servletMapping);
+ }
}
}
+
+ //evaluate path to servlet map based on servlet mappings
+ for (String pathSpec : sms.keySet())
+ {
+ //for each path, look at the mappings where it is referenced
+ //if a mapping is for a servlet that is not enabled, skip it
+ Set<ServletMapping> mappings = sms.get(pathSpec);
+
+
+
+ ServletMapping finalMapping = null;
+ for (ServletMapping mapping : mappings)
+ {
+ //Get servlet associated with the mapping and check it is enabled
+ ServletHolder servlet_holder = _servletNameMap.get(mapping.getServletName());
+ if (servlet_holder == null)
+ throw new IllegalStateException("No such servlet: " + mapping.getServletName());
+ //if the servlet related to the mapping is not enabled, skip it from consideration
+ if (!servlet_holder.isEnabled())
+ continue;
+ //only accept a default mapping if we don't have any other
+ if (finalMapping == null)
+ finalMapping = mapping;
+ else
+ {
+ //already have a candidate - only accept another one if the candidate is a default
+ if (finalMapping.isDefault())
+ finalMapping = mapping;
+ else
+ {
+ //existing candidate isn't a default, if the one we're looking at isn't a default either, then its an error
+ if (!mapping.isDefault())
+ throw new IllegalStateException("Multiple servlets map to path: "+pathSpec);
+ }
+ }
+ }
+ if (finalMapping == null)
+ throw new IllegalStateException ("No acceptable servlet mappings for "+pathSpec);
+
+ if (LOG.isDebugEnabled()) LOG.debug("Chose path={} mapped to servlet={} from default={}", pathSpec, finalMapping.getServletName(), finalMapping.isDefault());
+
+ servletPathMappings.put(pathSpec, finalMapping);
+ pm.put(pathSpec,_servletNameMap.get(finalMapping.getServletName()));
+ }
+
_servletPathMap=pm;
+ _servletPathMappings=servletPathMappings;
}
// flush filter chain cache
diff --git a/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/AsyncServletIOTest.java b/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/AsyncServletIOTest.java
new file mode 100644
index 0000000..c6cd0a3
--- /dev/null
+++ b/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/AsyncServletIOTest.java
@@ -0,0 +1,377 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.servlet;
+
+import static org.junit.Assert.assertEquals;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.net.Socket;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import javax.servlet.AsyncContext;
+import javax.servlet.ReadListener;
+import javax.servlet.ServletException;
+import javax.servlet.ServletInputStream;
+import javax.servlet.ServletOutputStream;
+import javax.servlet.WriteListener;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.eclipse.jetty.server.Connector;
+import org.eclipse.jetty.server.HttpConfiguration;
+import org.eclipse.jetty.server.HttpConnectionFactory;
+import org.eclipse.jetty.server.Server;
+import org.eclipse.jetty.server.ServerConnector;
+import org.hamcrest.Matchers;
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+
+// TODO need these on SPDY as well!
+public class AsyncServletIOTest
+{
+ protected AsyncIOServlet _servlet=new AsyncIOServlet();
+ protected int _port;
+
+ protected Server _server = new Server();
+ protected ServletHandler _servletHandler;
+ protected ServerConnector _connector;
+
+ @Before
+ public void setUp() throws Exception
+ {
+ HttpConfiguration http_config = new HttpConfiguration();
+ http_config.setOutputBufferSize(4096);
+ _connector = new ServerConnector(_server,new HttpConnectionFactory(http_config));
+
+ _server.setConnectors(new Connector[]{ _connector });
+ ServletContextHandler context = new ServletContextHandler(ServletContextHandler.NO_SECURITY|ServletContextHandler.NO_SESSIONS);
+ context.setContextPath("/ctx");
+ _server.setHandler(context);
+ _servletHandler=context.getServletHandler();
+ ServletHolder holder=new ServletHolder(_servlet);
+ holder.setAsyncSupported(true);
+ _servletHandler.addServletWithMapping(holder,"/path/*");
+ _server.start();
+ _port=_connector.getLocalPort();
+
+ _owp.set(0);
+ _oda.set(0);
+ _read.set(0);
+ }
+
+ @After
+ public void tearDown() throws Exception
+ {
+ _server.stop();
+ }
+
+ @Test
+ public void testEmpty() throws Exception
+ {
+ process();
+ }
+
+ @Test
+ public void testWrite() throws Exception
+ {
+ process(10);
+ }
+
+ @Test
+ public void testWrites() throws Exception
+ {
+ process(10,1,20,10);
+ }
+
+ @Test
+ public void testWritesFlushWrites() throws Exception
+ {
+ process(10,1,0,20,10);
+ }
+
+ @Test
+ public void testBigWrite() throws Exception
+ {
+ process(102400);
+ }
+
+ @Test
+ public void testBigWrites() throws Exception
+ {
+ process(102400,102400,102400,102400,102400,102400,102400,102400,102400,102400,102400,102400,102400,102400,102400,102400,102400,102400,102400,102400);
+ Assert.assertThat(_owp.get(),Matchers.greaterThan(1));
+ }
+
+
+
+ @Test
+ public void testRead() throws Exception
+ {
+ process("Hello!!!\r\n");
+ }
+
+ @Test
+ public void testBigRead() throws Exception
+ {
+ process("Now is the time for all good men to come to the aid of the party. How now Brown Cow. The quick brown fox jumped over the lazy dog. The moon is blue to a fish in love.\r\n");
+ }
+
+ @Test
+ public void testReadWrite() throws Exception
+ {
+ process("Hello!!!\r\n",10);
+ }
+
+
+ protected void assertContains(String content,String response)
+ {
+ Assert.assertThat(response,Matchers.containsString(content));
+ }
+
+ protected void assertNotContains(String content,String response)
+ {
+ Assert.assertThat(response,Matchers.not(Matchers.containsString(content)));
+ }
+
+ public synchronized List<String> process(String content,int... writes) throws Exception
+ {
+ return process(content.getBytes("ISO-8859-1"),writes);
+ }
+
+ public synchronized List<String> process(int... writes) throws Exception
+ {
+ return process((byte[])null,writes);
+ }
+
+ public synchronized List<String> process(byte[] content, int... writes) throws Exception
+ {
+ StringBuilder request = new StringBuilder(512);
+ request.append("GET /ctx/path/info");
+ char s='?';
+ for (int w: writes)
+ {
+ request.append(s).append("w=").append(w);
+ s='&';
+ }
+
+ request.append(" HTTP/1.1\r\n")
+ .append("Host: localhost\r\n")
+ .append("Connection: close\r\n");
+
+ if (content!=null)
+ request.append("Content-Length: "+content.length+"\r\n")
+ .append("Content-Type: text/plain\r\n");
+
+ request.append("\r\n");
+
+ int port=_port;
+ List<String> list = new ArrayList<>();
+ try (Socket socket = new Socket("localhost",port);)
+ {
+ socket.setSoTimeout(1000000);
+ OutputStream out = socket.getOutputStream();
+ out.write(request.toString().getBytes("ISO-8859-1"));
+
+ if (content!=null && content.length>0)
+ {
+ Thread.sleep(100);
+ out.write(content[0]);
+ Thread.sleep(100);
+ int half=(content.length-1)/2;
+ out.write(content,1,half);
+ Thread.sleep(100);
+ out.write(content,1+half,content.length-half-1);
+ }
+
+
+ BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()),102400);
+
+ // response line
+ String line = in.readLine();
+ //System.err.println("line: "+line);
+ Assert.assertThat(line,Matchers.startsWith("HTTP/1.1 200 OK"));
+
+ // Skip headers
+ while (line!=null)
+ {
+ line = in.readLine();
+ //System.err.println("line: "+line);
+ if (line.length()==0)
+ break;
+ }
+
+ // Get body slowly
+ while (true)
+ {
+ line = in.readLine();
+ if (line==null)
+ break;
+ //System.err.println("line: "+line.length()+"\t"+(line.length()>40?(line.substring(0,40)+"..."):line));
+ list.add(line);
+ }
+ }
+
+ // check lines
+ int w=0;
+ for (String line : list)
+ {
+ if ("-".equals(line))
+ continue;
+ assertEquals(writes[w],line.length());
+ assertEquals(line.charAt(0),'0'+(w%10));
+
+ w++;
+ if (w<writes.length && writes[w]<=0)
+ w++;
+ }
+
+ if (content!=null)
+ Assert.assertEquals(content.length,_read.get());
+
+ return list;
+ }
+
+ static AtomicInteger _owp = new AtomicInteger();
+ static AtomicInteger _oda = new AtomicInteger();
+ static AtomicInteger _read = new AtomicInteger();
+
+ private static class AsyncIOServlet extends HttpServlet
+ {
+ private static final long serialVersionUID = -8161977157098646562L;
+
+ public AsyncIOServlet()
+ {}
+
+ /* ------------------------------------------------------------ */
+ @Override
+ public void doGet(final HttpServletRequest request, final HttpServletResponse response) throws ServletException, IOException
+ {
+ final AsyncContext async = request.startAsync();
+ final AtomicInteger complete = new AtomicInteger(2);
+ final AtomicBoolean onDataAvailable = new AtomicBoolean(false);
+
+ // Asynchronous Read
+ if (request.getContentLength()>0)
+ {
+ // System.err.println("reading "+request.getContentLength());
+ final ServletInputStream in=request.getInputStream();
+ in.setReadListener(new ReadListener()
+ {
+ byte[] _buf=new byte[32];
+ @Override
+ public void onError(Throwable t)
+ {
+ if (complete.decrementAndGet()==0)
+ async.complete();
+ }
+
+ @Override
+ public void onDataAvailable() throws IOException
+ {
+ if (!onDataAvailable.compareAndSet(false,true))
+ throw new IllegalStateException();
+
+ // System.err.println("ODA");
+ while (in.isReady())
+ {
+ _oda.incrementAndGet();
+ int len=in.read(_buf);
+ // System.err.println("read "+len);
+ if (len>0)
+ _read.addAndGet(len);
+ }
+
+ if (!onDataAvailable.compareAndSet(true,false))
+ throw new IllegalStateException();
+
+ }
+
+ @Override
+ public void onAllDataRead() throws IOException
+ {
+ if (onDataAvailable.get())
+ {
+ System.err.println("OADR too early!");
+ _read.set(-1);
+ }
+
+ // System.err.println("OADR");
+ if (complete.decrementAndGet()==0)
+ async.complete();
+ }
+ });
+ }
+ else
+ complete.decrementAndGet();
+
+
+ // Asynchronous Write
+ final String[] writes = request.getParameterValues("w");
+ final ServletOutputStream out = response.getOutputStream();
+ out.setWriteListener(new WriteListener()
+ {
+ int _w=0;
+
+ @Override
+ public void onWritePossible() throws IOException
+ {
+ //System.err.println("OWP");
+ _owp.incrementAndGet();
+
+ while (writes!=null && _w< writes.length)
+ {
+ int write=Integer.valueOf(writes[_w++]);
+
+ if (write==0)
+ out.flush();
+ else
+ {
+ byte[] buf=new byte[write+1];
+ Arrays.fill(buf,(byte)('0'+((_w-1)%10)));
+ buf[write]='\n';
+ out.write(buf);
+ }
+
+ if (!out.isReady())
+ return;
+ }
+
+ if (complete.decrementAndGet()==0)
+ async.complete();
+ }
+
+ @Override
+ public void onError(Throwable t)
+ {
+ async.complete();
+ }
+ });
+ }
+ }
+}
diff --git a/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/AsyncServletTest.java b/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/AsyncServletTest.java
index 8d2e453..d34fd46 100644
--- a/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/AsyncServletTest.java
+++ b/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/AsyncServletTest.java
@@ -25,6 +25,7 @@
import java.util.List;
import java.util.Timer;
import java.util.TimerTask;
+
import javax.servlet.AsyncContext;
import javax.servlet.AsyncEvent;
import javax.servlet.AsyncListener;
@@ -41,6 +42,7 @@
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.server.handler.RequestLogHandler;
+import org.eclipse.jetty.toolchain.test.AdvancedRunner;
import org.eclipse.jetty.util.IO;
import org.eclipse.jetty.util.component.AbstractLifeCycle;
import org.hamcrest.Matchers;
@@ -48,10 +50,12 @@
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
+import org.junit.runner.RunWith;
import static org.junit.Assert.assertEquals;
+@RunWith(AdvancedRunner.class)
public class AsyncServletTest
{
protected AsyncServlet _servlet=new AsyncServlet();
diff --git a/jetty-servlet/src/test/resources/jetty-logging.properties b/jetty-servlet/src/test/resources/jetty-logging.properties
index 50680ef..3e0d7b7 100644
--- a/jetty-servlet/src/test/resources/jetty-logging.properties
+++ b/jetty-servlet/src/test/resources/jetty-logging.properties
@@ -1,3 +1,4 @@
org.eclipse.jetty.util.log.class=org.eclipse.jetty.util.log.StdErrLog
#org.eclipse.jetty.LEVEL=DEBUG
+#org.eclipse.jetty.server.LEVEL=DEBUG
#org.eclipse.jetty.servlet.LEVEL=DEBUG
diff --git a/jetty-servlets/pom.xml b/jetty-servlets/pom.xml
index 14c1ec2..16aaa7e 100644
--- a/jetty-servlets/pom.xml
+++ b/jetty-servlets/pom.xml
@@ -3,7 +3,7 @@
<parent>
<artifactId>jetty-project</artifactId>
<groupId>org.eclipse.jetty</groupId>
- <version>9.0.5-SNAPSHOT</version>
+ <version>9.1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>jetty-servlets</artifactId>
@@ -26,7 +26,7 @@
</goals>
<configuration>
<instructions>
- <Import-Package>javax.servlet.*;version="2.6.0",*</Import-Package>
+ <Import-Package>javax.servlet.*;version="[2.6.0,3.2)",*</Import-Package>
</instructions>
</configuration>
</execution>
@@ -76,16 +76,16 @@
<version>${project.version}</version>
</dependency>
<dependency>
+ <groupId>javax.servlet</groupId>
+ <artifactId>javax.servlet-api</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-io</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
- <groupId>org.eclipse.jetty.orbit</groupId>
- <artifactId>javax.servlet</artifactId>
- <scope>provided</scope>
- </dependency>
- <dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-jmx</artifactId>
<version>${project.version}</version>
diff --git a/jetty-servlets/src/main/java/org/eclipse/jetty/servlets/DataRateLimitedServlet.java b/jetty-servlets/src/main/java/org/eclipse/jetty/servlets/DataRateLimitedServlet.java
new file mode 100644
index 0000000..94873b9
--- /dev/null
+++ b/jetty-servlets/src/main/java/org/eclipse/jetty/servlets/DataRateLimitedServlet.java
@@ -0,0 +1,315 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.servlets;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.RandomAccessFile;
+import java.nio.ByteBuffer;
+import java.nio.channels.FileChannel.MapMode;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ScheduledThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+
+import javax.servlet.AsyncContext;
+import javax.servlet.ServletException;
+import javax.servlet.ServletOutputStream;
+import javax.servlet.WriteListener;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.eclipse.jetty.server.HttpOutput;
+
+/**
+ * A servlet that uses the Servlet 3.1 asynchronous IO API to server
+ * static content at a limited data rate.
+ * <p>
+ * Two implementations are supported: <ul>
+ * <li>the {@link StandardDataStream} impl uses only standard
+ * APIs, but produces more garbage due to the byte[] nature of the API.
+ * <li>the {@link JettyDataStream} impl uses a Jetty API to write a ByteBuffer
+ * and thus allow the efficient use of file mapped buffers without any
+ * temporary buffer copies (I did tell the JSR that this was a good idea to
+ * have in the standard!).
+ * </ul>
+ * <p>
+ * The data rate is controlled by setting init parameters:
+ * <dl>
+ * <dt>buffersize</dt><dd>The amount of data in bytes written per write</dd>
+ * <dt>pause</dt><dd>The period in ms to wait after a write before attempting another</dd>
+ * <dt>pool</dt><dd>The size of the thread pool used to service the writes (defaults to available processors)</dd>
+ * </dl>
+ * Thus if buffersize = 1024 and pause = 100, the data rate will be limited to 10KB per second.
+ */
+public class DataRateLimitedServlet extends HttpServlet
+{
+ private static final long serialVersionUID = -4771757707068097025L;
+ private int buffersize=8192;
+ private int pause=100;
+ ScheduledThreadPoolExecutor scheduler;
+ private final ConcurrentHashMap<String, ByteBuffer> cache=new ConcurrentHashMap<>();
+
+ @Override
+ public void init() throws ServletException
+ {
+ // read the init params
+ String tmp = getInitParameter("buffersize");
+ if (tmp!=null)
+ buffersize=Integer.parseInt(tmp);
+ tmp = getInitParameter("pause");
+ if (tmp!=null)
+ pause=Integer.parseInt(tmp);
+ tmp = getInitParameter("pool");
+ int pool=tmp==null?Runtime.getRuntime().availableProcessors():Integer.parseInt(tmp);
+
+ // Create and start a shared scheduler.
+ scheduler=new ScheduledThreadPoolExecutor(pool);
+ }
+
+ @Override
+ public void destroy()
+ {
+ scheduler.shutdown();
+ }
+
+ @Override
+ protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException
+ {
+ // Get the path of the static resource to serve.
+ String info=request.getPathInfo();
+
+ // We don't handle directories
+ if (info.endsWith("/"))
+ {
+ response.sendError(503,"directories not supported");
+ return;
+ }
+
+ // Set the mime type of the response
+ String content_type=getServletContext().getMimeType(info);
+ response.setContentType(content_type==null?"application/x-data":content_type);
+
+ // Look for a matching file path
+ String path = request.getPathTranslated();
+
+ // If we have a file path and this is a jetty response, we can use the JettyStream impl
+ ServletOutputStream out = response.getOutputStream();
+ if (path != null && out instanceof HttpOutput)
+ {
+ // If the file exists
+ File file = new File(path);
+ if (file.exists() && file.canRead())
+ {
+ // Set the content length
+ response.setContentLengthLong(file.length());
+
+ // Look for a file mapped buffer in the cache
+ ByteBuffer mapped=cache.get(path);
+
+ // Handle cache miss
+ if (mapped==null)
+ {
+ // TODO implement LRU cache flush
+ try (RandomAccessFile raf = new RandomAccessFile(file, "r"))
+ {
+ ByteBuffer buf = raf.getChannel().map(MapMode.READ_ONLY,0,raf.length());
+ mapped=cache.putIfAbsent(path,buf);
+ if (mapped==null)
+ mapped=buf;
+ }
+ }
+
+ // start async request handling
+ AsyncContext async=request.startAsync();
+
+ // Set a JettyStream as the write listener to write the content asynchronously.
+ out.setWriteListener(new JettyDataStream(mapped,async,out));
+ return;
+ }
+ }
+
+ // Jetty API was not used, so lets try the standards approach
+
+ // Can we find the content as an input stream
+ InputStream content = getServletContext().getResourceAsStream(info);
+ if (content==null)
+ {
+ response.sendError(404);
+ return;
+ }
+
+ // Set a StandardStream as he write listener to write the content asynchronously
+ out.setWriteListener(new StandardDataStream(content,request.startAsync(),out));
+ }
+
+ /**
+ * A standard API Stream writer
+ */
+ private final class StandardDataStream implements WriteListener, Runnable
+ {
+ private final InputStream content;
+ private final AsyncContext async;
+ private final ServletOutputStream out;
+
+ private StandardDataStream(InputStream content, AsyncContext async, ServletOutputStream out)
+ {
+ this.content = content;
+ this.async = async;
+ this.out = out;
+ }
+
+ @Override
+ public void onWritePossible() throws IOException
+ {
+ // If we are able to write
+ if(out.isReady())
+ {
+ // Allocated a copy buffer for each write, so as to not hold while paused
+ // TODO put these buffers into a pool
+ byte[] buffer = new byte[buffersize];
+
+ // read some content into the copy buffer
+ int len=content.read(buffer);
+
+ // If we are at EOF
+ if (len<0)
+ {
+ // complete the async lifecycle
+ async.complete();
+ return;
+ }
+
+ // write out the copy buffer. This will be an asynchronous write
+ // and will always return immediately without blocking. If a subsequent
+ // call to out.isReady() returns false, then this onWritePossible method
+ // will be called back when a write is possible.
+ out.write(buffer,0,len);
+
+ // Schedule a timer callback to pause writing. Because isReady() is not called,
+ // a onWritePossible callback is no scheduled.
+ scheduler.schedule(this,pause,TimeUnit.MILLISECONDS);
+ }
+ }
+
+ @Override
+ public void run()
+ {
+ try
+ {
+ // When the pause timer wakes up, call onWritePossible. Either isReady() will return
+ // true and another chunk of content will be written, or it will return false and the
+ // onWritePossible() callback will be scheduled when a write is next possible.
+ onWritePossible();
+ }
+ catch(Exception e)
+ {
+ onError(e);
+ }
+ }
+
+ @Override
+ public void onError(Throwable t)
+ {
+ getServletContext().log("Async Error",t);
+ async.complete();
+ }
+ }
+
+
+ /**
+ * A Jetty API DataStream
+ *
+ */
+ private final class JettyDataStream implements WriteListener, Runnable
+ {
+ private final ByteBuffer content;
+ private final int limit;
+ private final AsyncContext async;
+ private final HttpOutput out;
+
+ private JettyDataStream(ByteBuffer content, AsyncContext async, ServletOutputStream out)
+ {
+ // Make a readonly copy of the passed buffer. This uses the same underlying content
+ // without a copy, but gives this instance its own position and limit.
+ this.content = content.asReadOnlyBuffer();
+ // remember the ultimate limit.
+ this.limit=this.content.limit();
+ this.async = async;
+ this.out = (HttpOutput)out;
+ }
+
+ @Override
+ public void onWritePossible() throws IOException
+ {
+ // If we are able to write
+ if(out.isReady())
+ {
+ // Position our buffers limit to allow only buffersize bytes to be written
+ int l=content.position()+buffersize;
+ // respect the ultimate limit
+ if (l>limit)
+ l=limit;
+ content.limit(l);
+
+ // if all content has been written
+ if (!content.hasRemaining())
+ {
+ // complete the async lifecycle
+ async.complete();
+ return;
+ }
+
+ // write our limited buffer. This will be an asynchronous write
+ // and will always return immediately without blocking. If a subsequent
+ // call to out.isReady() returns false, then this onWritePossible method
+ // will be called back when a write is possible.
+ out.write(content);
+
+ // Schedule a timer callback to pause writing. Because isReady() is not called,
+ // a onWritePossible callback is no scheduled.
+ scheduler.schedule(this,pause,TimeUnit.MILLISECONDS);
+ }
+ }
+
+ @Override
+ public void run()
+ {
+ try
+ {
+ // When the pause timer wakes up, call onWritePossible. Either isReady() will return
+ // true and another chunk of content will be written, or it will return false and the
+ // onWritePossible() callback will be scheduled when a write is next possible.
+ onWritePossible();
+ }
+ catch(Exception e)
+ {
+ onError(e);
+ }
+ }
+
+ @Override
+ public void onError(Throwable t)
+ {
+ getServletContext().log("Async Error",t);
+ async.complete();
+ }
+ }
+}
diff --git a/jetty-servlets/src/main/java/org/eclipse/jetty/servlets/gzip/AbstractCompressedStream.java b/jetty-servlets/src/main/java/org/eclipse/jetty/servlets/gzip/AbstractCompressedStream.java
index c27a272..3f30f53 100644
--- a/jetty-servlets/src/main/java/org/eclipse/jetty/servlets/gzip/AbstractCompressedStream.java
+++ b/jetty-servlets/src/main/java/org/eclipse/jetty/servlets/gzip/AbstractCompressedStream.java
@@ -26,6 +26,7 @@
import java.util.zip.DeflaterOutputStream;
import javax.servlet.ServletOutputStream;
+import javax.servlet.WriteListener;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@@ -376,6 +377,21 @@
_response.setHeader(name, value);
}
+ @Override
+ public void setWriteListener(WriteListener writeListener)
+ {
+ // TODO 3.1 Auto-generated method stub
+
+ }
+
+
+ @Override
+ public boolean isReady()
+ {
+ // TODO 3.1 Auto-generated method stub
+ return false;
+ }
+
/**
* Create the stream fitting to the underlying compression type.
*
diff --git a/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/AbstractDoSFilterTest.java b/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/AbstractDoSFilterTest.java
index b81a1e4..63c6711 100644
--- a/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/AbstractDoSFilterTest.java
+++ b/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/AbstractDoSFilterTest.java
@@ -24,6 +24,8 @@
import static org.junit.Assert.assertTrue;
import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
import java.net.Socket;
import java.util.EnumSet;
@@ -112,35 +114,37 @@
private String doRequests(String loopRequests, int loops, long pauseBetweenLoops, long pauseBeforeLast, String lastRequest) throws Exception
{
- Socket socket = new Socket(_host, _port);
- socket.setSoTimeout(30000);
-
- for (int i=loops;i-->0;)
+ try (Socket socket = new Socket(_host,_port))
{
- socket.getOutputStream().write(loopRequests.getBytes("UTF-8"));
- socket.getOutputStream().flush();
- if (i>0 && pauseBetweenLoops>0)
- Thread.sleep(pauseBetweenLoops);
- }
- if (pauseBeforeLast>0)
- Thread.sleep(pauseBeforeLast);
- socket.getOutputStream().write(lastRequest.getBytes("UTF-8"));
- socket.getOutputStream().flush();
+ socket.setSoTimeout(30000);
+ OutputStream out = socket.getOutputStream();
- String response;
- if (loopRequests.contains("/unresponsive"))
- {
- // don't read in anything, forcing the request to time out
- Thread.sleep(_requestMaxTime * 2);
- response = IO.toString(socket.getInputStream(),"UTF-8");
+ for (int i = loops; i-- > 0;)
+ {
+ out.write(loopRequests.getBytes("UTF-8"));
+ out.flush();
+ if (i > 0 && pauseBetweenLoops > 0)
+ {
+ Thread.sleep(pauseBetweenLoops);
+ }
+ }
+ if (pauseBeforeLast > 0)
+ {
+ Thread.sleep(pauseBeforeLast);
+ }
+ out.write(lastRequest.getBytes("UTF-8"));
+ out.flush();
+
+ InputStream in = socket.getInputStream();
+ if (loopRequests.contains("/unresponsive"))
+ {
+ // don't read in anything, forcing the request to time out
+ Thread.sleep(_requestMaxTime * 2);
+ }
+ String response = IO.toString(in,"UTF-8");
+ return response;
}
- else
- {
- response = IO.toString(socket.getInputStream(),"UTF-8");
- }
- socket.close();
- return response;
}
private int count(String responses,String substring)
diff --git a/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/DataRateLimitedServletTest.java b/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/DataRateLimitedServletTest.java
new file mode 100644
index 0000000..c3d7d24
--- /dev/null
+++ b/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/DataRateLimitedServletTest.java
@@ -0,0 +1,109 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.servlets;
+
+import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.Matchers.greaterThan;
+import static org.junit.Assert.assertThat;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.OutputStream;
+import java.util.Arrays;
+
+import org.eclipse.jetty.server.HttpConfiguration;
+import org.eclipse.jetty.server.LocalConnector;
+import org.eclipse.jetty.server.Server;
+import org.eclipse.jetty.servlet.ServletContextHandler;
+import org.eclipse.jetty.servlet.ServletHolder;
+import org.eclipse.jetty.toolchain.test.TestingDir;
+import org.eclipse.jetty.util.resource.Resource;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+
+public class DataRateLimitedServletTest
+{
+ public static final int BUFFER=8192;
+ public static final int PAUSE=10;
+
+ @Rule
+ public TestingDir testdir = new TestingDir();
+
+ private Server server;
+ private LocalConnector connector;
+ private ServletContextHandler context;
+
+ @Before
+ public void init() throws Exception
+ {
+ server = new Server();
+
+ connector = new LocalConnector(server);
+ connector.getConnectionFactory(HttpConfiguration.ConnectionFactory.class).getHttpConfiguration().setSendServerVersion(false);
+
+ context = new ServletContextHandler();
+
+ context.setContextPath("/context");
+ context.setWelcomeFiles(new String[]{"index.html", "index.jsp", "index.htm"});
+ context.setBaseResource(Resource.newResource(testdir.getEmptyDir()));
+
+ ServletHolder holder =context.addServlet(DataRateLimitedServlet.class,"/stream/*");
+ holder.setInitParameter("buffersize",""+BUFFER);
+ holder.setInitParameter("pause",""+PAUSE);
+ server.setHandler(context);
+ server.addConnector(connector);
+
+ server.start();
+ }
+
+ @After
+ public void destroy() throws Exception
+ {
+ server.stop();
+ server.join();
+ }
+
+ @Test
+ public void testStream() throws Exception
+ {
+ File content = testdir.getFile("content.txt");
+ try(OutputStream out = new FileOutputStream(content);)
+ {
+ byte[] b= new byte[1024];
+
+ for (int i=1024;i-->0;)
+ {
+ Arrays.fill(b,(byte)('0'+(i%10)));
+ out.write(b);
+ out.write('\n');
+ }
+ }
+
+ long start=System.currentTimeMillis();
+ String response = connector.getResponses("GET /context/stream/content.txt HTTP/1.0\r\n\r\n");
+ long duration=System.currentTimeMillis()-start;
+
+ assertThat(response.length(),greaterThan(1024*1024));
+ assertThat(response,containsString("200 OK"));
+ assertThat(duration,greaterThan(PAUSE*1024L*1024/BUFFER));
+
+ }
+}
diff --git a/jetty-spdy/pom.xml b/jetty-spdy/pom.xml
index d351bed..c2d150c 100644
--- a/jetty-spdy/pom.xml
+++ b/jetty-spdy/pom.xml
@@ -3,7 +3,7 @@
<parent>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-project</artifactId>
- <version>9.0.5-SNAPSHOT</version>
+ <version>9.1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
@@ -119,7 +119,9 @@
<module>spdy-core</module>
<module>spdy-client</module>
<module>spdy-server</module>
+ <module>spdy-http-common</module>
<module>spdy-http-server</module>
+ <module>spdy-http-client-transport</module>
<module>spdy-example-webapp</module>
</modules>
@@ -176,7 +178,7 @@
</goals>
<configuration>
<instructions>
- <Export-Package>org.eclipse.jetty.spdy.*;version="9.0"</Export-Package>
+ <Export-Package>org.eclipse.jetty.spdy.*;version="9.1"</Export-Package>
<Import-Package>org.eclipse.jetty.*;version="[9.0,10.0)",*</Import-Package>
<_nouses>true</_nouses>
</instructions>
diff --git a/jetty-spdy/spdy-client/pom.xml b/jetty-spdy/spdy-client/pom.xml
index 7e49234..3f9e878 100644
--- a/jetty-spdy/spdy-client/pom.xml
+++ b/jetty-spdy/spdy-client/pom.xml
@@ -3,12 +3,12 @@
<parent>
<groupId>org.eclipse.jetty.spdy</groupId>
<artifactId>spdy-parent</artifactId>
- <version>9.0.5-SNAPSHOT</version>
+ <version>9.1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>spdy-client</artifactId>
- <name>Jetty :: SPDY :: Jetty Client Binding</name>
+ <name>Jetty :: SPDY :: Client Binding</name>
<properties>
<bundle-symbolic-name>${project.groupId}.client</bundle-symbolic-name>
@@ -58,7 +58,7 @@
</goals>
<configuration>
<instructions>
- <Export-Package>org.eclipse.jetty.spdy.client;version="9.0"</Export-Package>
+ <Export-Package>org.eclipse.jetty.spdy.client;version="9.1"</Export-Package>
<Import-Package>!org.eclipse.jetty.npn,org.eclipse.jetty.*;version="[9.0,10.0)",*</Import-Package>
</instructions>
</configuration>
diff --git a/jetty-spdy/spdy-client/src/main/java/org/eclipse/jetty/spdy/client/SPDYClient.java b/jetty-spdy/spdy-client/src/main/java/org/eclipse/jetty/spdy/client/SPDYClient.java
index c4360a2..9b062d8 100644
--- a/jetty-spdy/spdy-client/src/main/java/org/eclipse/jetty/spdy/client/SPDYClient.java
+++ b/jetty-spdy/spdy-client/src/main/java/org/eclipse/jetty/spdy/client/SPDYClient.java
@@ -19,7 +19,6 @@
package org.eclipse.jetty.spdy.client;
import java.io.IOException;
-import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.nio.channels.SelectionKey;
import java.nio.channels.SocketChannel;
@@ -28,8 +27,8 @@
import java.util.List;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
+import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executor;
-import java.util.concurrent.Future;
import javax.net.ssl.SSLEngine;
import org.eclipse.jetty.io.ByteBufferPool;
@@ -46,12 +45,29 @@
import org.eclipse.jetty.spdy.api.SessionFrameListener;
import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.util.FuturePromise;
+import org.eclipse.jetty.util.Promise;
import org.eclipse.jetty.util.component.ContainerLifeCycle;
import org.eclipse.jetty.util.ssl.SslContextFactory;
import org.eclipse.jetty.util.thread.QueuedThreadPool;
import org.eclipse.jetty.util.thread.ScheduledExecutorScheduler;
import org.eclipse.jetty.util.thread.Scheduler;
+/**
+ * A {@link SPDYClient} allows applications to connect to one or more SPDY servers,
+ * obtaining {@link Session} objects that can be used to send/receive SPDY frames.
+ * <p />
+ * {@link SPDYClient} instances are created through a {@link Factory}:
+ * <pre>
+ * SPDYClient.Factory factory = new SPDYClient.Factory();
+ * SPDYClient client = factory.newSPDYClient(SPDY.V3);
+ * </pre>
+ * and then used to connect to the server:
+ * <pre>
+ * FuturePromise<Session> promise = new FuturePromise<>();
+ * client.connect("server.com", null, promise);
+ * Session session = promise.get();
+ * </pre>
+ */
public class SPDYClient
{
private final SPDYClientConnectionFactory connectionFactory = new SPDYClientConnectionFactory();
@@ -87,23 +103,57 @@
this.bindAddress = bindAddress;
}
- public Future<Session> connect(InetSocketAddress address, SessionFrameListener listener) throws IOException
+ /**
+ * Equivalent to:
+ * <pre>
+ * Future<Session> promise = new FuturePromise<>();
+ * connect(address, listener, promise);
+ * </pre>
+ *
+ * @param address the address to connect to
+ * @param listener the session listener that will be notified of session events
+ * @return a {@link Session} when connected
+ */
+ public Session connect(SocketAddress address, SessionFrameListener listener) throws ExecutionException, InterruptedException
+ {
+ FuturePromise<Session> promise = new FuturePromise<>();
+ connect(address, listener, promise);
+ return promise.get();
+ }
+
+ /**
+ * Connects to the given {@code address}, binding the given {@code listener} to session events,
+ * and notified the given {@code promise} of the connect result.
+ * <p />
+ * If the connect operation is successful, the {@code promise} will be invoked with the {@link Session}
+ * object that applications can use to perform SPDY requests.
+ *
+ * @param address the address to connect to
+ * @param listener the session listener that will be notified of session events
+ * @param promise the promise notified of connection success/failure
+ */
+ public void connect(SocketAddress address, SessionFrameListener listener, Promise<Session> promise)
{
if (!factory.isStarted())
throw new IllegalStateException(Factory.class.getSimpleName() + " is not started");
- SocketChannel channel = SocketChannel.open();
- if (bindAddress != null)
- channel.bind(bindAddress);
- channel.socket().setTcpNoDelay(true);
- channel.configureBlocking(false);
+ try
+ {
+ SocketChannel channel = SocketChannel.open();
+ if (bindAddress != null)
+ channel.bind(bindAddress);
+ channel.socket().setTcpNoDelay(true);
+ channel.configureBlocking(false);
- SessionPromise result = new SessionPromise(channel, this, listener);
+ SessionPromise result = new SessionPromise(promise, channel, this, listener);
- channel.connect(address);
- factory.selector.connect(channel, result);
-
- return result;
+ channel.connect(address);
+ factory.selector.connect(channel, result);
+ }
+ catch (IOException x)
+ {
+ promise.failed(x);
+ }
}
public long getIdleTimeout()
@@ -340,14 +390,31 @@
static class SessionPromise extends FuturePromise<Session>
{
private final SocketChannel channel;
+ private final Promise wrappedPromise;
final SPDYClient client;
final SessionFrameListener listener;
- private SessionPromise(SocketChannel channel, SPDYClient client, SessionFrameListener listener)
+ private SessionPromise(Promise<Session> promise, SocketChannel channel, SPDYClient client,
+ SessionFrameListener listener)
{
this.channel = channel;
this.client = client;
this.listener = listener;
+ this.wrappedPromise = promise;
+ }
+
+ @Override
+ public void succeeded(Session result)
+ {
+ wrappedPromise.succeeded(result);
+ super.succeeded(result);
+ }
+
+ @Override
+ public void failed(Throwable cause)
+ {
+ wrappedPromise.failed(cause);
+ super.failed(cause);
}
@Override
diff --git a/jetty-spdy/spdy-core/pom.xml b/jetty-spdy/spdy-core/pom.xml
index 60df8ab..38154ac 100644
--- a/jetty-spdy/spdy-core/pom.xml
+++ b/jetty-spdy/spdy-core/pom.xml
@@ -3,7 +3,7 @@
<parent>
<groupId>org.eclipse.jetty.spdy</groupId>
<artifactId>spdy-parent</artifactId>
- <version>9.0.5-SNAPSHOT</version>
+ <version>9.1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
@@ -14,7 +14,6 @@
<bundle-symbolic-name>${project.groupId}.core</bundle-symbolic-name>
</properties>
- <url>http://www.eclipse.org/jetty</url>
<dependencies>
<dependency>
<groupId>org.eclipse.jetty</groupId>
diff --git a/jetty-spdy/spdy-example-webapp/pom.xml b/jetty-spdy/spdy-example-webapp/pom.xml
index d2df0aa..d5db877 100644
--- a/jetty-spdy/spdy-example-webapp/pom.xml
+++ b/jetty-spdy/spdy-example-webapp/pom.xml
@@ -3,13 +3,13 @@
<parent>
<groupId>org.eclipse.jetty.spdy</groupId>
<artifactId>spdy-parent</artifactId>
- <version>9.0.5-SNAPSHOT</version>
+ <version>9.1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>spdy-example-webapp</artifactId>
<packaging>war</packaging>
- <name>Jetty :: SPDY :: Jetty HTTP Web Application</name>
- <url>http://www.eclipse.org/jetty</url>
+ <name>Jetty :: SPDY :: HTTP Web Application</name>
+
<build>
<plugins>
<plugin>
diff --git a/jetty-spdy/spdy-http-client-transport/pom.xml b/jetty-spdy/spdy-http-client-transport/pom.xml
new file mode 100644
index 0000000..7c14d20
--- /dev/null
+++ b/jetty-spdy/spdy-http-client-transport/pom.xml
@@ -0,0 +1,101 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <parent>
+ <groupId>org.eclipse.jetty.spdy</groupId>
+ <artifactId>spdy-parent</artifactId>
+ <version>9.1.0-SNAPSHOT</version>
+ </parent>
+
+ <modelVersion>4.0.0</modelVersion>
+ <artifactId>spdy-http-client-transport</artifactId>
+ <name>Jetty :: SPDY :: HTTP Client Transport</name>
+
+ <properties>
+ <bundle-symbolic-name>${project.groupId}.client.http</bundle-symbolic-name>
+ </properties>
+
+ <build>
+ <plugins>
+ <plugin>
+ <artifactId>maven-dependency-plugin</artifactId>
+ <executions>
+ <execution>
+ <id>copy</id>
+ <phase>generate-resources</phase>
+ <goals>
+ <goal>copy</goal>
+ </goals>
+ <configuration>
+ <artifactItems>
+ <artifactItem>
+ <groupId>org.mortbay.jetty.npn</groupId>
+ <artifactId>npn-boot</artifactId>
+ <version>${npn.version}</version>
+ <type>jar</type>
+ <overWrite>false</overWrite>
+ <outputDirectory>${project.build.directory}/npn</outputDirectory>
+ </artifactItem>
+ </artifactItems>
+ </configuration>
+ </execution>
+ </executions>
+ </plugin>
+ <plugin>
+ <artifactId>maven-surefire-plugin</artifactId>
+ <configuration>
+ <argLine>-Xbootclasspath/p:${project.build.directory}/npn/npn-boot-${npn.version}.jar</argLine>
+ </configuration>
+ </plugin>
+ <plugin>
+ <groupId>org.apache.felix</groupId>
+ <artifactId>maven-bundle-plugin</artifactId>
+ <extensions>true</extensions>
+ <executions>
+ <execution>
+ <goals>
+ <goal>manifest</goal>
+ </goals>
+ <configuration>
+ <instructions>
+ <Export-Package>org.eclipse.jetty.spdy.client.http;version="9.1"</Export-Package>
+ <Import-Package>!org.eclipse.jetty.npn,org.eclipse.jetty.*;version="[9.0,10.0)",*</Import-Package>
+ </instructions>
+ </configuration>
+ </execution>
+ </executions>
+ </plugin>
+ </plugins>
+ </build>
+
+ <dependencies>
+ <dependency>
+ <groupId>org.eclipse.jetty</groupId>
+ <artifactId>jetty-client</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.eclipse.jetty.spdy</groupId>
+ <artifactId>spdy-client</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.eclipse.jetty.spdy</groupId>
+ <artifactId>spdy-http-common</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+
+ <dependency>
+ <groupId>org.eclipse.jetty</groupId>
+ <artifactId>jetty-server</artifactId>
+ <version>${project.version}</version>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.eclipse.jetty.spdy</groupId>
+ <artifactId>spdy-http-server</artifactId>
+ <version>${project.version}</version>
+ <scope>test</scope>
+ </dependency>
+ </dependencies>
+
+</project>
diff --git a/jetty-spdy/spdy-http-client-transport/src/main/java/org/eclipse/jetty/spdy/client/http/HttpChannelOverSPDY.java b/jetty-spdy/spdy-http-client-transport/src/main/java/org/eclipse/jetty/spdy/client/http/HttpChannelOverSPDY.java
new file mode 100644
index 0000000..992f2a5
--- /dev/null
+++ b/jetty-spdy/spdy-http-client-transport/src/main/java/org/eclipse/jetty/spdy/client/http/HttpChannelOverSPDY.java
@@ -0,0 +1,75 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.spdy.client.http;
+
+import org.eclipse.jetty.client.HttpChannel;
+import org.eclipse.jetty.client.HttpDestination;
+import org.eclipse.jetty.client.HttpExchange;
+import org.eclipse.jetty.spdy.api.Session;
+
+public class HttpChannelOverSPDY extends HttpChannel
+{
+ private final Session session;
+ private final HttpSenderOverSPDY sender;
+ private final HttpReceiverOverSPDY receiver;
+
+ public HttpChannelOverSPDY(HttpDestination destination, Session session)
+ {
+ super(destination);
+ this.session = session;
+ this.sender = new HttpSenderOverSPDY(this);
+ this.receiver = new HttpReceiverOverSPDY(this);
+ }
+
+ public Session getSession()
+ {
+ return session;
+ }
+
+ public HttpSenderOverSPDY getHttpSender()
+ {
+ return sender;
+ }
+
+ public HttpReceiverOverSPDY getHttpReceiver()
+ {
+ return receiver;
+ }
+
+ @Override
+ public void send()
+ {
+ HttpExchange exchange = getHttpExchange();
+ if (exchange != null)
+ sender.send(exchange);
+ }
+
+ @Override
+ public void proceed(HttpExchange exchange, boolean proceed)
+ {
+ sender.proceed(exchange, proceed);
+ }
+
+ @Override
+ public boolean abort(Throwable cause)
+ {
+ sender.abort(cause);
+ return receiver.abort(cause);
+ }
+}
diff --git a/jetty-spdy/spdy-http-client-transport/src/main/java/org/eclipse/jetty/spdy/client/http/HttpClientTransportOverSPDY.java b/jetty-spdy/spdy-http-client-transport/src/main/java/org/eclipse/jetty/spdy/client/http/HttpClientTransportOverSPDY.java
new file mode 100644
index 0000000..2533b86
--- /dev/null
+++ b/jetty-spdy/spdy-http-client-transport/src/main/java/org/eclipse/jetty/spdy/client/http/HttpClientTransportOverSPDY.java
@@ -0,0 +1,96 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.spdy.client.http;
+
+import java.net.SocketAddress;
+
+import org.eclipse.jetty.client.HttpClient;
+import org.eclipse.jetty.client.HttpClientTransport;
+import org.eclipse.jetty.client.HttpDestination;
+import org.eclipse.jetty.client.api.Connection;
+import org.eclipse.jetty.spdy.api.Session;
+import org.eclipse.jetty.spdy.api.SessionFrameListener;
+import org.eclipse.jetty.spdy.client.SPDYClient;
+import org.eclipse.jetty.util.Promise;
+
+public class HttpClientTransportOverSPDY implements HttpClientTransport
+{
+ private final SPDYClient client;
+ private volatile HttpClient httpClient;
+
+ public HttpClientTransportOverSPDY(SPDYClient client)
+ {
+ this.client = client;
+ }
+
+ @Override
+ public void setHttpClient(HttpClient client)
+ {
+ httpClient = client;
+ }
+
+ @Override
+ public HttpDestination newHttpDestination(HttpClient httpClient, String scheme, String host, int port)
+ {
+ return new HttpDestinationOverSPDY(httpClient, scheme, host, port);
+ }
+
+ @Override
+ public void connect(final HttpDestination destination, SocketAddress address, final Promise<Connection> promise)
+ {
+ SessionFrameListener.Adapter listener = new SessionFrameListener.Adapter()
+ {
+ @Override
+ public void onException(Throwable x)
+ {
+ // TODO: is this correct ?
+ // TODO: if I get a stream error (e.g. invalid response headers)
+ // TODO: I must abort the *current* exchange, while below I will abort
+ // TODO: the queued exchanges only.
+ // TODO: The problem is that a single destination/connection multiplexes
+ // TODO: several exchanges, so I would need to cancel them all,
+ // TODO: or only the one that failed ?
+ destination.abort(x);
+ }
+ };
+
+ client.connect(address, listener, new Promise<Session>()
+ {
+ @Override
+ public void succeeded(Session session)
+ {
+ Connection result = new HttpConnectionOverSPDY(httpClient, destination, session);
+ promise.succeeded(result);
+ }
+
+ @Override
+ public void failed(Throwable x)
+ {
+ promise.failed(x);
+ }
+ }
+ );
+ }
+
+ @Override
+ public Connection tunnel(Connection connection)
+ {
+ throw new UnsupportedOperationException();
+ }
+}
diff --git a/jetty-spdy/spdy-http-client-transport/src/main/java/org/eclipse/jetty/spdy/client/http/HttpConnectionOverSPDY.java b/jetty-spdy/spdy-http-client-transport/src/main/java/org/eclipse/jetty/spdy/client/http/HttpConnectionOverSPDY.java
new file mode 100644
index 0000000..ffe0251
--- /dev/null
+++ b/jetty-spdy/spdy-http-client-transport/src/main/java/org/eclipse/jetty/spdy/client/http/HttpConnectionOverSPDY.java
@@ -0,0 +1,55 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.spdy.client.http;
+
+import org.eclipse.jetty.client.HttpChannel;
+import org.eclipse.jetty.client.HttpClient;
+import org.eclipse.jetty.client.HttpConnection;
+import org.eclipse.jetty.client.HttpDestination;
+import org.eclipse.jetty.client.HttpExchange;
+import org.eclipse.jetty.spdy.api.GoAwayInfo;
+import org.eclipse.jetty.spdy.api.Session;
+import org.eclipse.jetty.util.Callback;
+
+public class HttpConnectionOverSPDY extends HttpConnection
+{
+ private final Session session;
+
+ public HttpConnectionOverSPDY(HttpClient client, HttpDestination destination, Session session)
+ {
+ super(client, destination);
+ this.session = session;
+ }
+
+ @Override
+ protected void send(HttpExchange exchange)
+ {
+ normalizeRequest(exchange.getRequest());
+ // One connection maps to N channels, so for each exchange we create a new channel
+ HttpChannel channel = new HttpChannelOverSPDY(getHttpDestination(), session);
+ channel.associate(exchange);
+ channel.send();
+ }
+
+ @Override
+ public void close()
+ {
+ session.goAway(new GoAwayInfo(), new Callback.Adapter());
+ }
+}
diff --git a/jetty-spdy/spdy-http-client-transport/src/main/java/org/eclipse/jetty/spdy/client/http/HttpDestinationOverSPDY.java b/jetty-spdy/spdy-http-client-transport/src/main/java/org/eclipse/jetty/spdy/client/http/HttpDestinationOverSPDY.java
new file mode 100644
index 0000000..24b2acf
--- /dev/null
+++ b/jetty-spdy/spdy-http-client-transport/src/main/java/org/eclipse/jetty/spdy/client/http/HttpDestinationOverSPDY.java
@@ -0,0 +1,135 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.spdy.client.http;
+
+import java.util.concurrent.atomic.AtomicReference;
+
+import org.eclipse.jetty.client.HttpClient;
+import org.eclipse.jetty.client.HttpDestination;
+import org.eclipse.jetty.client.HttpExchange;
+import org.eclipse.jetty.client.api.Connection;
+import org.eclipse.jetty.client.api.Request;
+import org.eclipse.jetty.util.Promise;
+
+public class HttpDestinationOverSPDY extends HttpDestination implements Promise<Connection>
+{
+ private final AtomicReference<ConnectState> connect = new AtomicReference<>(ConnectState.DISCONNECTED);
+ private HttpConnectionOverSPDY connection;
+
+ public HttpDestinationOverSPDY(HttpClient client, String scheme, String host, int port)
+ {
+ super(client, scheme, host, port);
+ }
+
+ @Override
+ protected void send()
+ {
+ while (true)
+ {
+ ConnectState current = connect.get();
+ switch (current)
+ {
+ case DISCONNECTED:
+ {
+ if (!connect.compareAndSet(current, ConnectState.CONNECTING))
+ break;
+ newConnection(this);
+ return;
+ }
+ case CONNECTING:
+ {
+ // Waiting to connect, just return
+ return;
+ }
+ case CONNECTED:
+ {
+ if (process(connection, false))
+ break;
+ return;
+ }
+ default:
+ {
+ throw new IllegalStateException();
+ }
+ }
+ }
+ }
+
+ @Override
+ public void succeeded(Connection result)
+ {
+ HttpConnectionOverSPDY connection = this.connection = (HttpConnectionOverSPDY)result;
+ if (connect.compareAndSet(ConnectState.CONNECTING, ConnectState.CONNECTED))
+ {
+ process(connection, true);
+ }
+ else
+ {
+ connection.close();
+ failed(new IllegalStateException());
+ }
+ }
+
+ @Override
+ public void failed(Throwable x)
+ {
+ connect.set(ConnectState.DISCONNECTED);
+ }
+
+ private boolean process(final HttpConnectionOverSPDY connection, boolean dispatch)
+ {
+ HttpClient client = getHttpClient();
+ final HttpExchange exchange = getHttpExchanges().poll();
+ LOG.debug("Processing exchange {} on connection {}", exchange, connection);
+ if (exchange == null)
+ return false;
+
+ final Request request = exchange.getRequest();
+ Throwable cause = request.getAbortCause();
+ if (cause != null)
+ {
+ LOG.debug("Abort before processing {}: {}", exchange, cause);
+ abort(exchange, cause);
+ }
+ else
+ {
+ if (dispatch)
+ {
+ client.getExecutor().execute(new Runnable()
+ {
+ @Override
+ public void run()
+ {
+ connection.send(exchange);
+ }
+ });
+ }
+ else
+ {
+ connection.send(exchange);
+ }
+ }
+ return true;
+ }
+
+ private enum ConnectState
+ {
+ DISCONNECTED, CONNECTING, CONNECTED
+ }
+}
diff --git a/jetty-spdy/spdy-http-client-transport/src/main/java/org/eclipse/jetty/spdy/client/http/HttpReceiverOverSPDY.java b/jetty-spdy/spdy-http-client-transport/src/main/java/org/eclipse/jetty/spdy/client/http/HttpReceiverOverSPDY.java
new file mode 100644
index 0000000..a870b5e
--- /dev/null
+++ b/jetty-spdy/spdy-http-client-transport/src/main/java/org/eclipse/jetty/spdy/client/http/HttpReceiverOverSPDY.java
@@ -0,0 +1,142 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.spdy.client.http;
+
+import org.eclipse.jetty.client.HttpExchange;
+import org.eclipse.jetty.client.HttpReceiver;
+import org.eclipse.jetty.client.HttpResponse;
+import org.eclipse.jetty.http.HttpField;
+import org.eclipse.jetty.http.HttpStatus;
+import org.eclipse.jetty.http.HttpVersion;
+import org.eclipse.jetty.spdy.api.DataInfo;
+import org.eclipse.jetty.spdy.api.HeadersInfo;
+import org.eclipse.jetty.spdy.api.PushInfo;
+import org.eclipse.jetty.spdy.api.ReplyInfo;
+import org.eclipse.jetty.spdy.api.RstInfo;
+import org.eclipse.jetty.spdy.api.Stream;
+import org.eclipse.jetty.spdy.api.StreamFrameListener;
+import org.eclipse.jetty.spdy.api.StreamStatus;
+import org.eclipse.jetty.spdy.http.HTTPSPDYHeader;
+import org.eclipse.jetty.util.Callback;
+import org.eclipse.jetty.util.Fields;
+
+public class HttpReceiverOverSPDY extends HttpReceiver implements StreamFrameListener
+{
+ public HttpReceiverOverSPDY(HttpChannelOverSPDY channel)
+ {
+ super(channel);
+ }
+
+ @Override
+ public HttpChannelOverSPDY getHttpChannel()
+ {
+ return (HttpChannelOverSPDY)super.getHttpChannel();
+ }
+
+ @Override
+ public void onReply(Stream stream, ReplyInfo replyInfo)
+ {
+ HttpExchange exchange = getHttpExchange();
+ if (exchange == null)
+ return;
+
+ try
+ {
+ HttpResponse response = exchange.getResponse();
+
+ Fields fields = replyInfo.getHeaders();
+ short spdy = stream.getSession().getVersion();
+ HttpVersion version = HttpVersion.fromString(fields.get(HTTPSPDYHeader.VERSION.name(spdy)).value());
+ response.version(version);
+ String[] status = fields.get(HTTPSPDYHeader.STATUS.name(spdy)).value().split(" ", 2);
+
+ Integer code = Integer.parseInt(status[0]);
+ response.status(code);
+ String reason = status.length < 2 ? HttpStatus.getMessage(code) : status[1];
+ response.reason(reason);
+
+ if (responseBegin(exchange))
+ {
+ for (Fields.Field field : fields)
+ {
+ String name = field.name();
+ if (HTTPSPDYHeader.from(spdy, name) != null)
+ continue;
+ // TODO: handle multiple values properly
+ HttpField httpField = new HttpField(name, field.value());
+ responseHeader(exchange, httpField);
+ }
+
+ if (responseHeaders(exchange))
+ {
+ if (replyInfo.isClose())
+ {
+ responseSuccess(exchange);
+ }
+ }
+ }
+ }
+ catch (Exception x)
+ {
+ responseFailure(x);
+ }
+ }
+
+ @Override
+ public StreamFrameListener onPush(Stream stream, PushInfo pushInfo)
+ {
+ // SPDY push not supported
+ getHttpChannel().getSession().rst(new RstInfo(stream.getId(), StreamStatus.REFUSED_STREAM), new Callback.Adapter());
+ return null;
+ }
+
+ @Override
+ public void onHeaders(Stream stream, HeadersInfo headersInfo)
+ {
+ // TODO: see above handling of headers
+ }
+
+ @Override
+ public void onData(Stream stream, DataInfo dataInfo)
+ {
+ HttpExchange exchange = getHttpExchange();
+ if (exchange == null)
+ return;
+
+ try
+ {
+ int length = dataInfo.length();
+ // TODO: avoid data copy here
+ boolean process = responseContent(exchange, dataInfo.asByteBuffer(false));
+ dataInfo.consume(length);
+
+ if (process)
+ {
+ if (dataInfo.isClose())
+ {
+ responseSuccess(exchange);
+ }
+ }
+ }
+ catch (Exception x)
+ {
+ responseFailure(x);
+ }
+ }
+}
diff --git a/jetty-spdy/spdy-http-client-transport/src/main/java/org/eclipse/jetty/spdy/client/http/HttpSenderOverSPDY.java b/jetty-spdy/spdy-http-client-transport/src/main/java/org/eclipse/jetty/spdy/client/http/HttpSenderOverSPDY.java
new file mode 100644
index 0000000..3a48371
--- /dev/null
+++ b/jetty-spdy/spdy-http-client-transport/src/main/java/org/eclipse/jetty/spdy/client/http/HttpSenderOverSPDY.java
@@ -0,0 +1,117 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.spdy.client.http;
+
+import org.eclipse.jetty.client.HttpContent;
+import org.eclipse.jetty.client.HttpExchange;
+import org.eclipse.jetty.client.HttpSender;
+import org.eclipse.jetty.client.api.Request;
+import org.eclipse.jetty.http.HttpField;
+import org.eclipse.jetty.spdy.api.ByteBufferDataInfo;
+import org.eclipse.jetty.spdy.api.Stream;
+import org.eclipse.jetty.spdy.api.SynInfo;
+import org.eclipse.jetty.spdy.http.HTTPSPDYHeader;
+import org.eclipse.jetty.util.Callback;
+import org.eclipse.jetty.util.Fields;
+import org.eclipse.jetty.util.Promise;
+
+public class HttpSenderOverSPDY extends HttpSender
+{
+ private volatile Stream stream;
+
+ public HttpSenderOverSPDY(HttpChannelOverSPDY channel)
+ {
+ super(channel);
+ }
+
+ @Override
+ public HttpChannelOverSPDY getHttpChannel()
+ {
+ return (HttpChannelOverSPDY)super.getHttpChannel();
+ }
+
+ @Override
+ protected void sendHeaders(HttpExchange exchange, final HttpContent content, final Callback callback)
+ {
+ final Request request = exchange.getRequest();
+
+ short spdyVersion = getHttpChannel().getSession().getVersion();
+ Fields fields = new Fields();
+ HttpField hostHeader = null;
+ for (HttpField header : request.getHeaders())
+ {
+ String name = header.getName();
+ // The host header needs a special treatment
+ if (HTTPSPDYHeader.from(spdyVersion, name) != HTTPSPDYHeader.HOST)
+ fields.add(name, header.getValue());
+ else
+ hostHeader = header;
+ }
+
+ // Add special SPDY headers
+ fields.put(HTTPSPDYHeader.METHOD.name(spdyVersion), request.getMethod());
+ String path = request.getPath();
+ String query = request.getQuery();
+ if (query != null)
+ path += "?" + query;
+ fields.put(HTTPSPDYHeader.URI.name(spdyVersion), path);
+ fields.put(HTTPSPDYHeader.VERSION.name(spdyVersion), request.getVersion().asString());
+ if (hostHeader != null)
+ fields.put(HTTPSPDYHeader.HOST.name(spdyVersion), hostHeader.getValue());
+
+ SynInfo synInfo = new SynInfo(fields, !content.hasContent());
+ getHttpChannel().getSession().syn(synInfo, getHttpChannel().getHttpReceiver(), new Promise<Stream>()
+ {
+ @Override
+ public void succeeded(Stream stream)
+ {
+ if (content.hasContent())
+ HttpSenderOverSPDY.this.stream = stream;
+ callback.succeeded();
+ }
+
+ @Override
+ public void failed(Throwable failure)
+ {
+ callback.failed(failure);
+ }
+ });
+ }
+
+ @Override
+ protected void sendContent(HttpExchange exchange, HttpContent content, Callback callback)
+ {
+ if (content.isConsumed())
+ {
+ callback.succeeded();
+ }
+ else
+ {
+ ByteBufferDataInfo dataInfo = new ByteBufferDataInfo(content.getByteBuffer(), content.isLast());
+ stream.data(dataInfo, callback);
+ }
+ }
+
+ @Override
+ protected void reset()
+ {
+ super.reset();
+ stream = null;
+ }
+}
diff --git a/jetty-spdy/spdy-http-client-transport/src/test/java/org/eclipse/jetty/spdy/client/http/AbstractHttpClientServerTest.java b/jetty-spdy/spdy-http-client-transport/src/test/java/org/eclipse/jetty/spdy/client/http/AbstractHttpClientServerTest.java
new file mode 100644
index 0000000..644067f
--- /dev/null
+++ b/jetty-spdy/spdy-http-client-transport/src/test/java/org/eclipse/jetty/spdy/client/http/AbstractHttpClientServerTest.java
@@ -0,0 +1,82 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.spdy.client.http;
+
+import org.eclipse.jetty.client.HttpClient;
+import org.eclipse.jetty.http.HttpScheme;
+import org.eclipse.jetty.server.Handler;
+import org.eclipse.jetty.server.HttpConfiguration;
+import org.eclipse.jetty.server.NetworkConnector;
+import org.eclipse.jetty.server.Server;
+import org.eclipse.jetty.server.ServerConnector;
+import org.eclipse.jetty.spdy.api.SPDY;
+import org.eclipse.jetty.spdy.client.SPDYClient;
+import org.eclipse.jetty.spdy.server.http.HTTPSPDYServerConnectionFactory;
+import org.eclipse.jetty.spdy.server.http.PushStrategy;
+import org.eclipse.jetty.toolchain.test.TestTracker;
+import org.eclipse.jetty.util.thread.QueuedThreadPool;
+import org.junit.After;
+import org.junit.Rule;
+
+public abstract class AbstractHttpClientServerTest
+{
+ @Rule
+ public final TestTracker tracker = new TestTracker();
+
+ protected Server server;
+ protected NetworkConnector connector;
+ protected SPDYClient.Factory factory;
+ protected HttpClient client;
+ protected String scheme = HttpScheme.HTTP.asString();
+
+ public void start(Handler handler) throws Exception
+ {
+ server = new Server();
+
+ short version = SPDY.V3;
+
+ HTTPSPDYServerConnectionFactory spdyConnectionFactory = new HTTPSPDYServerConnectionFactory(version, new HttpConfiguration(), new PushStrategy.None());
+ connector = new ServerConnector(server, spdyConnectionFactory);
+
+ server.addConnector(connector);
+ server.setHandler(handler);
+ server.start();
+
+ QueuedThreadPool executor = new QueuedThreadPool();
+ executor.setName(executor.getName() + "-client");
+
+ factory = new SPDYClient.Factory(executor);
+ factory.start();
+
+ client = new HttpClient(new HttpClientTransportOverSPDY(factory.newSPDYClient(version)), null);
+ client.setExecutor(executor);
+ client.start();
+ }
+
+ @After
+ public void dispose() throws Exception
+ {
+ if (client != null)
+ client.stop();
+ if (factory != null)
+ factory.stop();
+ if (server != null)
+ server.stop();
+ }
+}
diff --git a/jetty-spdy/spdy-http-client-transport/src/test/java/org/eclipse/jetty/spdy/client/http/EmptyServerHandler.java b/jetty-spdy/spdy-http-client-transport/src/test/java/org/eclipse/jetty/spdy/client/http/EmptyServerHandler.java
new file mode 100644
index 0000000..dfa5b95
--- /dev/null
+++ b/jetty-spdy/spdy-http-client-transport/src/test/java/org/eclipse/jetty/spdy/client/http/EmptyServerHandler.java
@@ -0,0 +1,36 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.spdy.client.http;
+
+import java.io.IOException;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.eclipse.jetty.server.Request;
+import org.eclipse.jetty.server.handler.AbstractHandler;
+
+public class EmptyServerHandler extends AbstractHandler
+{
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
+ {
+ baseRequest.setHandled(true);
+ }
+}
diff --git a/jetty-spdy/spdy-http-client-transport/src/test/java/org/eclipse/jetty/spdy/client/http/HttpClientTest.java b/jetty-spdy/spdy-http-client-transport/src/test/java/org/eclipse/jetty/spdy/client/http/HttpClientTest.java
new file mode 100644
index 0000000..a7b8e68
--- /dev/null
+++ b/jetty-spdy/spdy-http-client-transport/src/test/java/org/eclipse/jetty/spdy/client/http/HttpClientTest.java
@@ -0,0 +1,424 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.spdy.client.http;
+
+import java.io.IOException;
+import java.net.URI;
+import java.net.URLEncoder;
+import java.nio.ByteBuffer;
+import java.util.Arrays;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.zip.GZIPOutputStream;
+import javax.servlet.ServletException;
+import javax.servlet.ServletOutputStream;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.eclipse.jetty.client.api.ContentResponse;
+import org.eclipse.jetty.client.api.Request;
+import org.eclipse.jetty.client.api.Response;
+import org.eclipse.jetty.client.util.BytesContentProvider;
+import org.eclipse.jetty.http.HttpMethod;
+import org.eclipse.jetty.server.handler.AbstractHandler;
+import org.eclipse.jetty.toolchain.test.annotation.Slow;
+import org.junit.Assert;
+import org.junit.Ignore;
+import org.junit.Test;
+
+public class HttpClientTest extends AbstractHttpClientServerTest
+{
+ @Test
+ public void test_GET_ResponseWithoutContent() throws Exception
+ {
+ start(new EmptyServerHandler());
+
+ Response response = client.GET(scheme + "://localhost:" + connector.getLocalPort());
+
+ Assert.assertNotNull(response);
+ Assert.assertEquals(200, response.getStatus());
+ }
+
+ @Test
+ public void test_GET_ResponseWithContent() throws Exception
+ {
+ final byte[] data = new byte[]{0, 1, 2, 3, 4, 5, 6, 7};
+ start(new AbstractHandler()
+ {
+ @Override
+ public void handle(String target, org.eclipse.jetty.server.Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
+ {
+ response.getOutputStream().write(data);
+ baseRequest.setHandled(true);
+ }
+ });
+
+ ContentResponse response = client.GET(scheme + "://localhost:" + connector.getLocalPort());
+
+ Assert.assertNotNull(response);
+ Assert.assertEquals(200, response.getStatus());
+ byte[] content = response.getContent();
+ Assert.assertArrayEquals(data, content);
+ }
+
+ @Test
+ public void test_GET_WithParameters_ResponseWithContent() throws Exception
+ {
+ final String paramName1 = "a";
+ final String paramName2 = "b";
+ start(new AbstractHandler()
+ {
+ @Override
+ public void handle(String target, org.eclipse.jetty.server.Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
+ {
+ response.setCharacterEncoding("UTF-8");
+ ServletOutputStream output = response.getOutputStream();
+ String paramValue1 = request.getParameter(paramName1);
+ output.write(paramValue1.getBytes("UTF-8"));
+ String paramValue2 = request.getParameter(paramName2);
+ Assert.assertEquals("", paramValue2);
+ output.write("empty".getBytes("UTF-8"));
+ baseRequest.setHandled(true);
+ }
+ });
+
+ String value1 = "\u20AC";
+ String paramValue1 = URLEncoder.encode(value1, "UTF-8");
+ String query = paramName1 + "=" + paramValue1 + "&" + paramName2;
+ ContentResponse response = client.GET(scheme + "://localhost:" + connector.getLocalPort() + "/?" + query);
+
+ Assert.assertNotNull(response);
+ Assert.assertEquals(200, response.getStatus());
+ String content = new String(response.getContent(), "UTF-8");
+ Assert.assertEquals(value1 + "empty", content);
+ }
+
+ @Test
+ public void test_GET_WithParametersMultiValued_ResponseWithContent() throws Exception
+ {
+ final String paramName1 = "a";
+ final String paramName2 = "b";
+ start(new AbstractHandler()
+ {
+ @Override
+ public void handle(String target, org.eclipse.jetty.server.Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
+ {
+ response.setCharacterEncoding("UTF-8");
+ ServletOutputStream output = response.getOutputStream();
+ String[] paramValues1 = request.getParameterValues(paramName1);
+ for (String paramValue : paramValues1)
+ output.write(paramValue.getBytes("UTF-8"));
+ String paramValue2 = request.getParameter(paramName2);
+ output.write(paramValue2.getBytes("UTF-8"));
+ baseRequest.setHandled(true);
+ }
+ });
+
+ String value11 = "\u20AC";
+ String value12 = "\u20AA";
+ String value2 = "&";
+ String paramValue11 = URLEncoder.encode(value11, "UTF-8");
+ String paramValue12 = URLEncoder.encode(value12, "UTF-8");
+ String paramValue2 = URLEncoder.encode(value2, "UTF-8");
+ String query = paramName1 + "=" + paramValue11 + "&" + paramName1 + "=" + paramValue12 + "&" + paramName2 + "=" + paramValue2;
+ ContentResponse response = client.GET(scheme + "://localhost:" + connector.getLocalPort() + "/?" + query);
+
+ Assert.assertNotNull(response);
+ Assert.assertEquals(200, response.getStatus());
+ String content = new String(response.getContent(), "UTF-8");
+ Assert.assertEquals(value11 + value12 + value2, content);
+ }
+
+ @Test
+ public void test_POST_WithParameters() throws Exception
+ {
+ final String paramName = "a";
+ final String paramValue = "\u20AC";
+ start(new AbstractHandler()
+ {
+ @Override
+ public void handle(String target, org.eclipse.jetty.server.Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
+ {
+ baseRequest.setHandled(true);
+ String value = request.getParameter(paramName);
+ if (paramValue.equals(value))
+ {
+ response.setCharacterEncoding("UTF-8");
+ response.setContentType("text/plain");
+ response.getOutputStream().print(value);
+ }
+ }
+ });
+
+ ContentResponse response = client.POST(scheme + "://localhost:" + connector.getLocalPort())
+ .param(paramName, paramValue)
+ .timeout(5, TimeUnit.SECONDS)
+ .send();
+
+ Assert.assertNotNull(response);
+ Assert.assertEquals(200, response.getStatus());
+ Assert.assertEquals(paramValue, new String(response.getContent(), "UTF-8"));
+ }
+
+ @Test
+ public void test_PUT_WithParameters() throws Exception
+ {
+ final String paramName = "a";
+ final String paramValue = "\u20AC";
+ start(new AbstractHandler()
+ {
+ @Override
+ public void handle(String target, org.eclipse.jetty.server.Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
+ {
+ baseRequest.setHandled(true);
+ String value = request.getParameter(paramName);
+ if (paramValue.equals(value))
+ {
+ response.setCharacterEncoding("UTF-8");
+ response.setContentType("text/plain");
+ response.getOutputStream().print(value);
+ }
+ }
+ });
+
+ URI uri = URI.create(scheme + "://localhost:" + connector.getLocalPort() + "/path?" + paramName + "=" + paramValue);
+ ContentResponse response = client.newRequest(uri)
+ .method(HttpMethod.PUT)
+ .timeout(5, TimeUnit.SECONDS)
+ .send();
+
+ Assert.assertNotNull(response);
+ Assert.assertEquals(200, response.getStatus());
+ Assert.assertEquals(paramValue, new String(response.getContent(), "UTF-8"));
+ }
+
+ @Test
+ public void test_POST_WithParameters_WithContent() throws Exception
+ {
+ final byte[] content = {0, 1, 2, 3};
+ final String paramName = "a";
+ final String paramValue = "\u20AC";
+ start(new AbstractHandler()
+ {
+ @Override
+ public void handle(String target, org.eclipse.jetty.server.Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
+ {
+ baseRequest.setHandled(true);
+ String value = request.getParameter(paramName);
+ if (paramValue.equals(value))
+ {
+ response.setCharacterEncoding("UTF-8");
+ response.setContentType("application/octet-stream");
+ response.getOutputStream().write(content);
+ }
+ }
+ });
+
+ ContentResponse response = client.POST(scheme + "://localhost:" + connector.getLocalPort() + "/?b=1")
+ .param(paramName, paramValue)
+ .content(new BytesContentProvider(content))
+ .timeout(555, TimeUnit.SECONDS)
+ .send();
+
+ Assert.assertNotNull(response);
+ Assert.assertEquals(200, response.getStatus());
+ Assert.assertArrayEquals(content, response.getContent());
+ }
+
+ @Test
+ public void test_POST_WithContent_NotifiesRequestContentListener() throws Exception
+ {
+ final byte[] content = {0, 1, 2, 3};
+ start(new EmptyServerHandler());
+
+ ContentResponse response = client.POST(scheme + "://localhost:" + connector.getLocalPort())
+ .onRequestContent(new Request.ContentListener()
+ {
+ @Override
+ public void onContent(Request request, ByteBuffer buffer)
+ {
+ byte[] bytes = new byte[buffer.remaining()];
+ buffer.get(bytes);
+ if (!Arrays.equals(content, bytes))
+ request.abort(new Exception());
+ }
+ })
+ .content(new BytesContentProvider(content))
+ .timeout(5, TimeUnit.SECONDS)
+ .send();
+
+ Assert.assertNotNull(response);
+ Assert.assertEquals(200, response.getStatus());
+ }
+
+ @Test
+ public void test_POST_WithContent_TracksProgress() throws Exception
+ {
+ start(new EmptyServerHandler());
+
+ final AtomicInteger progress = new AtomicInteger();
+ ContentResponse response = client.POST(scheme + "://localhost:" + connector.getLocalPort())
+ .onRequestContent(new Request.ContentListener()
+ {
+ @Override
+ public void onContent(Request request, ByteBuffer buffer)
+ {
+ byte[] bytes = new byte[buffer.remaining()];
+ Assert.assertEquals(1, bytes.length);
+ buffer.get(bytes);
+ Assert.assertEquals(bytes[0], progress.getAndIncrement());
+ }
+ })
+ .content(new BytesContentProvider(new byte[]{0}, new byte[]{1}, new byte[]{2}, new byte[]{3}, new byte[]{4}))
+ .timeout(5, TimeUnit.SECONDS)
+ .send();
+
+ Assert.assertNotNull(response);
+ Assert.assertEquals(200, response.getStatus());
+ Assert.assertEquals(5, progress.get());
+ }
+
+ @Test
+ public void test_GZIP_ContentEncoding() throws Exception
+ {
+ final byte[] data = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
+ start(new AbstractHandler()
+ {
+ @Override
+ public void handle(String target, org.eclipse.jetty.server.Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
+ {
+ baseRequest.setHandled(true);
+ response.setHeader("Content-Encoding", "gzip");
+ GZIPOutputStream gzipOutput = new GZIPOutputStream(response.getOutputStream());
+ gzipOutput.write(data);
+ gzipOutput.finish();
+ }
+ });
+
+ ContentResponse response = client.newRequest("localhost", connector.getLocalPort())
+ .scheme(scheme)
+ .timeout(5, TimeUnit.SECONDS)
+ .send();
+
+ Assert.assertEquals(200, response.getStatus());
+ Assert.assertArrayEquals(data, response.getContent());
+ }
+
+ @Ignore("idle timeout not yet implemented properly")
+ @Slow
+ @Test
+ public void test_Request_IdleTimeout() throws Exception
+ {
+ final long idleTimeout = 1000;
+ start(new AbstractHandler()
+ {
+ @Override
+ public void handle(String target, org.eclipse.jetty.server.Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
+ {
+ try
+ {
+ baseRequest.setHandled(true);
+ TimeUnit.MILLISECONDS.sleep(2 * idleTimeout);
+ }
+ catch (InterruptedException x)
+ {
+ throw new ServletException(x);
+ }
+ }
+ });
+
+ final String host = "localhost";
+ final int port = connector.getLocalPort();
+ try
+ {
+ client.newRequest(host, port)
+ .scheme(scheme)
+ .idleTimeout(idleTimeout, TimeUnit.MILLISECONDS)
+ .timeout(3 * idleTimeout, TimeUnit.MILLISECONDS)
+ .send();
+ Assert.fail();
+ }
+ catch (ExecutionException expected)
+ {
+ Assert.assertTrue(expected.getCause() instanceof TimeoutException);
+ }
+
+ // Make another request without specifying the idle timeout, should not fail
+ ContentResponse response = client.newRequest(host, port)
+ .scheme(scheme)
+ .timeout(3 * idleTimeout, TimeUnit.MILLISECONDS)
+ .send();
+
+ Assert.assertNotNull(response);
+ Assert.assertEquals(200, response.getStatus());
+ }
+
+ @Test
+ public void testSendToIPv6Address() throws Exception
+ {
+ start(new EmptyServerHandler());
+
+ ContentResponse response = client.newRequest("[::1]", connector.getLocalPort())
+ .scheme(scheme)
+ .timeout(5, TimeUnit.SECONDS)
+ .send();
+
+ Assert.assertNotNull(response);
+ Assert.assertEquals(200, response.getStatus());
+ }
+
+ @Test
+ public void test_HEAD_With_ResponseContentLength() throws Exception
+ {
+ final int length = 1024;
+ start(new AbstractHandler()
+ {
+ @Override
+ public void handle(String target, org.eclipse.jetty.server.Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
+ {
+ baseRequest.setHandled(true);
+ response.getOutputStream().write(new byte[length]);
+ }
+ });
+
+ // HEAD requests receive a Content-Length header, but do not
+ // receive the content so they must handle this case properly
+ ContentResponse response = client.newRequest("localhost", connector.getLocalPort())
+ .scheme(scheme)
+ .method(HttpMethod.HEAD)
+ .timeout(5, TimeUnit.SECONDS)
+ .send();
+
+ Assert.assertNotNull(response);
+ Assert.assertEquals(200, response.getStatus());
+ Assert.assertEquals(0, response.getContent().length);
+
+ // Perform a normal GET request to be sure the content is now read
+ response = client.newRequest("localhost", connector.getLocalPort())
+ .scheme(scheme)
+ .timeout(5, TimeUnit.SECONDS)
+ .send();
+
+ Assert.assertNotNull(response);
+ Assert.assertEquals(200, response.getStatus());
+ Assert.assertEquals(length, response.getContent().length);
+ }
+}
diff --git a/jetty-spdy/spdy-http-client-transport/src/test/resources/jetty-logging.properties b/jetty-spdy/spdy-http-client-transport/src/test/resources/jetty-logging.properties
new file mode 100644
index 0000000..8163013
--- /dev/null
+++ b/jetty-spdy/spdy-http-client-transport/src/test/resources/jetty-logging.properties
@@ -0,0 +1,4 @@
+org.eclipse.jetty.util.log.class=org.eclipse.jetty.util.log.StdErrLog
+#org.eclipse.jetty.LEVEL=DEBUG
+#org.eclipse.jetty.client.LEVEL=DEBUG
+#org.eclipse.jetty.spdy.LEVEL=DEBUG
diff --git a/jetty-spdy/spdy-http-client-transport/src/test/resources/keystore.jks b/jetty-spdy/spdy-http-client-transport/src/test/resources/keystore.jks
new file mode 100644
index 0000000..428ba54
--- /dev/null
+++ b/jetty-spdy/spdy-http-client-transport/src/test/resources/keystore.jks
Binary files differ
diff --git a/jetty-spdy/spdy-http-client-transport/src/test/resources/truststore.jks b/jetty-spdy/spdy-http-client-transport/src/test/resources/truststore.jks
new file mode 100644
index 0000000..839cb8c
--- /dev/null
+++ b/jetty-spdy/spdy-http-client-transport/src/test/resources/truststore.jks
Binary files differ
diff --git a/jetty-spdy/spdy-http-common/pom.xml b/jetty-spdy/spdy-http-common/pom.xml
new file mode 100644
index 0000000..a807c8a
--- /dev/null
+++ b/jetty-spdy/spdy-http-common/pom.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <parent>
+ <groupId>org.eclipse.jetty.spdy</groupId>
+ <artifactId>spdy-parent</artifactId>
+ <version>9.1.0-SNAPSHOT</version>
+ </parent>
+
+ <modelVersion>4.0.0</modelVersion>
+ <artifactId>spdy-http-common</artifactId>
+ <name>Jetty :: SPDY :: HTTP Common</name>
+
+ <properties>
+ <bundle-symbolic-name>${project.groupId}.http.common</bundle-symbolic-name>
+ </properties>
+
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.felix</groupId>
+ <artifactId>maven-bundle-plugin</artifactId>
+ <extensions>true</extensions>
+ <executions>
+ <execution>
+ <goals>
+ <goal>manifest</goal>
+ </goals>
+ <configuration>
+ <instructions>
+ <Export-Package>org.eclipse.jetty.spdy.http;version="9.1"</Export-Package>
+ <Import-Package>!org.eclipse.jetty.npn,org.eclipse.jetty.*;version="[9.0,10.0)",*</Import-Package>
+ </instructions>
+ </configuration>
+ </execution>
+ </executions>
+ </plugin>
+ </plugins>
+ </build>
+
+ <dependencies>
+ <dependency>
+ <groupId>org.eclipse.jetty.spdy</groupId>
+ <artifactId>spdy-core</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+ </dependencies>
+
+</project>
diff --git a/jetty-spdy/spdy-http-common/src/main/java/org/eclipse/jetty/spdy/http/HTTPSPDYHeader.java b/jetty-spdy/spdy-http-common/src/main/java/org/eclipse/jetty/spdy/http/HTTPSPDYHeader.java
new file mode 100644
index 0000000..18634d0
--- /dev/null
+++ b/jetty-spdy/spdy-http-common/src/main/java/org/eclipse/jetty/spdy/http/HTTPSPDYHeader.java
@@ -0,0 +1,82 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.spdy.http;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.eclipse.jetty.spdy.api.SPDY;
+
+/**
+ * <p>{@link HTTPSPDYHeader} defines the SPDY headers that are not also HTTP headers,
+ * such as <tt>method</tt>, <tt>version</tt>, etc. or that are treated differently
+ * by the SPDY protocol, such as <tt>host</tt>.</p>
+ */
+public enum HTTPSPDYHeader
+{
+ METHOD("method", ":method"),
+ URI("url", ":path"),
+ VERSION("version", ":version"),
+ SCHEME("scheme", ":scheme"),
+ HOST("host", ":host"),
+ STATUS("status", ":status");
+
+ public static HTTPSPDYHeader from(short version, String name)
+ {
+ switch (version)
+ {
+ case SPDY.V2:
+ return Names.v2Names.get(name);
+ case SPDY.V3:
+ return Names.v3Names.get(name);
+ default:
+ throw new IllegalStateException();
+ }
+ }
+
+ private final String v2Name;
+ private final String v3Name;
+
+ private HTTPSPDYHeader(String v2Name, String v3Name)
+ {
+ this.v2Name = v2Name;
+ Names.v2Names.put(v2Name, this);
+ this.v3Name = v3Name;
+ Names.v3Names.put(v3Name, this);
+ }
+
+ public String name(short version)
+ {
+ switch (version)
+ {
+ case SPDY.V2:
+ return v2Name;
+ case SPDY.V3:
+ return v3Name;
+ default:
+ throw new IllegalStateException();
+ }
+ }
+
+ private static class Names
+ {
+ private static final Map<String, HTTPSPDYHeader> v2Names = new HashMap<>();
+ private static final Map<String, HTTPSPDYHeader> v3Names = new HashMap<>();
+ }
+}
diff --git a/jetty-spdy/spdy-http-server/pom.xml b/jetty-spdy/spdy-http-server/pom.xml
index af9bb4d..30b4f79 100644
--- a/jetty-spdy/spdy-http-server/pom.xml
+++ b/jetty-spdy/spdy-http-server/pom.xml
@@ -1,13 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
-<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<groupId>org.eclipse.jetty.spdy</groupId>
<artifactId>spdy-parent</artifactId>
- <version>9.0.5-SNAPSHOT</version>
+ <version>9.1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>spdy-http-server</artifactId>
- <name>Jetty :: SPDY :: Jetty Server HTTP Layer</name>
+ <name>Jetty :: SPDY :: HTTP Server</name>
<properties>
<bundle-symbolic-name>${project.groupId}.http.server</bundle-symbolic-name>
@@ -73,8 +74,8 @@
</goals>
<configuration>
<instructions>
- <Export-Package>org.eclipse.jetty.spdy.server.http;version="9.0",
- org.eclipse.jetty.spdy.server.proxy;version="9.0"
+ <Export-Package>org.eclipse.jetty.spdy.server.http;version="9.1",
+ org.eclipse.jetty.spdy.server.proxy;version="9.1"
</Export-Package>
<Import-Package>!org.eclipse.jetty.npn,org.eclipse.jetty.*;version="[9.0,10.0)",*
</Import-Package>
@@ -90,6 +91,11 @@
<dependencies>
<dependency>
<groupId>org.eclipse.jetty.spdy</groupId>
+ <artifactId>spdy-http-common</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.eclipse.jetty.spdy</groupId>
<artifactId>spdy-server</artifactId>
<version>${project.version}</version>
</dependency>
diff --git a/jetty-spdy/spdy-http-server/src/main/java/org/eclipse/jetty/spdy/server/http/HTTPSPDYHeader.java b/jetty-spdy/spdy-http-server/src/main/java/org/eclipse/jetty/spdy/server/http/HTTPSPDYHeader.java
deleted file mode 100644
index 8954fe2..0000000
--- a/jetty-spdy/spdy-http-server/src/main/java/org/eclipse/jetty/spdy/server/http/HTTPSPDYHeader.java
+++ /dev/null
@@ -1,82 +0,0 @@
-//
-// ========================================================================
-// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
-// ------------------------------------------------------------------------
-// All rights reserved. This program and the accompanying materials
-// are made available under the terms of the Eclipse Public License v1.0
-// and Apache License v2.0 which accompanies this distribution.
-//
-// The Eclipse Public License is available at
-// http://www.eclipse.org/legal/epl-v10.html
-//
-// The Apache License v2.0 is available at
-// http://www.opensource.org/licenses/apache2.0.php
-//
-// You may elect to redistribute this code under either of these licenses.
-// ========================================================================
-//
-
-package org.eclipse.jetty.spdy.server.http;
-
-import java.util.HashMap;
-import java.util.Map;
-
-import org.eclipse.jetty.spdy.api.SPDY;
-
-/**
- * <p>{@link HTTPSPDYHeader} defines the SPDY headers that are not also HTTP headers,
- * such as <tt>method</tt>, <tt>version</tt>, etc. or that are treated differently
- * by the SPDY protocol, such as <tt>host</tt>.</p>
- */
-public enum HTTPSPDYHeader
-{
- METHOD("method", ":method"),
- URI("url", ":path"),
- VERSION("version", ":version"),
- SCHEME("scheme", ":scheme"),
- HOST("host", ":host"),
- STATUS("status", ":status");
-
- public static HTTPSPDYHeader from(short version, String name)
- {
- switch (version)
- {
- case SPDY.V2:
- return Names.v2Names.get(name);
- case SPDY.V3:
- return Names.v3Names.get(name);
- default:
- throw new IllegalStateException();
- }
- }
-
- private final String v2Name;
- private final String v3Name;
-
- private HTTPSPDYHeader(String v2Name, String v3Name)
- {
- this.v2Name = v2Name;
- Names.v2Names.put(v2Name, this);
- this.v3Name = v3Name;
- Names.v3Names.put(v3Name, this);
- }
-
- public String name(short version)
- {
- switch (version)
- {
- case SPDY.V2:
- return v2Name;
- case SPDY.V3:
- return v3Name;
- default:
- throw new IllegalStateException();
- }
- }
-
- private static class Names
- {
- private static final Map<String, HTTPSPDYHeader> v2Names = new HashMap<>();
- private static final Map<String, HTTPSPDYHeader> v3Names = new HashMap<>();
- }
-}
diff --git a/jetty-spdy/spdy-http-server/src/main/java/org/eclipse/jetty/spdy/server/http/HttpChannelOverSPDY.java b/jetty-spdy/spdy-http-server/src/main/java/org/eclipse/jetty/spdy/server/http/HttpChannelOverSPDY.java
index fe2b9ac..ab7f8fc 100644
--- a/jetty-spdy/spdy-http-server/src/main/java/org/eclipse/jetty/spdy/server/http/HttpChannelOverSPDY.java
+++ b/jetty-spdy/spdy-http-server/src/main/java/org/eclipse/jetty/spdy/server/http/HttpChannelOverSPDY.java
@@ -33,6 +33,7 @@
import org.eclipse.jetty.spdy.api.ByteBufferDataInfo;
import org.eclipse.jetty.spdy.api.DataInfo;
import org.eclipse.jetty.spdy.api.Stream;
+import org.eclipse.jetty.spdy.http.HTTPSPDYHeader;
import org.eclipse.jetty.util.BufferUtil;
import org.eclipse.jetty.util.Fields;
import org.eclipse.jetty.util.log.Log;
@@ -42,9 +43,9 @@
{
private static final Logger LOG = Log.getLogger(HttpChannelOverSPDY.class);
- private final Queue<Runnable> tasks = new LinkedList<>();
private final Stream stream;
private boolean dispatched; // Guarded by synchronization on tasks
+ private boolean redispatch; // Guarded by synchronization on tasks
private boolean headersComplete;
public HttpChannelOverSPDY(Connector connector, HttpConfiguration configuration, EndPoint endPoint, HttpTransport transport, HttpInputOverSPDY input, Stream stream)
@@ -60,44 +61,47 @@
return super.headerComplete();
}
- private void post(Runnable task)
- {
- synchronized (tasks)
- {
- LOG.debug("Posting task {}", task);
- tasks.offer(task);
- dispatch();
- }
- }
-
private void dispatch()
{
- synchronized (tasks)
+ synchronized (this)
{
if (dispatched)
- return;
-
- final Runnable task = tasks.poll();
- if (task != null)
+ redispatch=true;
+ else
{
- dispatched = true;
- LOG.debug("Dispatching task {}", task);
- execute(new Runnable()
- {
- @Override
- public void run()
- {
- LOG.debug("Executing task {}", task);
- task.run();
- LOG.debug("Completing task {}", task);
- dispatched = false;
- dispatch();
- }
- });
+ LOG.debug("Dispatch {}", this);
+ dispatched=true;
+ execute(this);
}
}
}
+ @Override
+ public void run()
+ {
+ boolean execute=true;
+
+ while(execute)
+ {
+ try
+ {
+ LOG.debug("Executing {}",this);
+ super.run();
+ }
+ finally
+ {
+ LOG.debug("Completing {}", this);
+ synchronized (this)
+ {
+ dispatched = redispatch;
+ redispatch=false;
+ execute=dispatched;
+ }
+ }
+ }
+ }
+
+
public void requestStart(final Fields headers, final boolean endRequest)
{
if (!headers.isEmpty())
@@ -114,20 +118,19 @@
if (endRequest)
{
- if (headerComplete())
- post(this);
+ boolean dispatch = headerComplete();
if (messageComplete())
- post(this);
+ dispatch=true;
+ if (dispatch)
+ dispatch();
}
}
public void requestContent(final DataInfo dataInfo, boolean endRequest)
{
- if (!headersComplete)
- {
- if (headerComplete())
- post(this);
- }
+ boolean dispatch=false;
+ if (!headersComplete && headerComplete())
+ dispatch=true;
LOG.debug("HTTP > {} bytes of content", dataInfo.length());
@@ -147,13 +150,13 @@
LOG.debug("Queuing last={} content {}", endRequest, copyDataInfo);
if (content(copyDataInfo))
- post(this);
+ dispatch=true;
- if (endRequest)
- {
- if (messageComplete())
- post(this);
- }
+ if (endRequest && messageComplete())
+ dispatch=true;
+
+ if (dispatch)
+ dispatch();
}
private boolean performBeginRequest(Fields headers)
@@ -171,7 +174,7 @@
HttpMethod httpMethod = HttpMethod.fromString(methodHeader.value());
HttpVersion httpVersion = HttpVersion.fromString(versionHeader.value());
-
+
// TODO should handle URI as byte buffer as some bad clients send WRONG encodings in query string
// that we have to deal with
ByteBuffer uri = BufferUtil.toBuffer(uriHeader.value());
diff --git a/jetty-spdy/spdy-http-server/src/main/java/org/eclipse/jetty/spdy/server/http/HttpInputOverSPDY.java b/jetty-spdy/spdy-http-server/src/main/java/org/eclipse/jetty/spdy/server/http/HttpInputOverSPDY.java
index eff52a4..326e6bd 100644
--- a/jetty-spdy/spdy-http-server/src/main/java/org/eclipse/jetty/spdy/server/http/HttpInputOverSPDY.java
+++ b/jetty-spdy/spdy-http-server/src/main/java/org/eclipse/jetty/spdy/server/http/HttpInputOverSPDY.java
@@ -18,10 +18,10 @@
package org.eclipse.jetty.spdy.server.http;
-import org.eclipse.jetty.server.HttpInput;
+import org.eclipse.jetty.server.QueuedHttpInput;
import org.eclipse.jetty.spdy.api.DataInfo;
-public class HttpInputOverSPDY extends HttpInput<DataInfo>
+public class HttpInputOverSPDY extends QueuedHttpInput<DataInfo>
{
@Override
protected int remaining(DataInfo item)
@@ -34,6 +34,12 @@
{
return item.readInto(buffer, offset, length);
}
+
+ @Override
+ protected void consume(DataInfo item, int length)
+ {
+ item.consume(length);
+ }
@Override
protected void onContentConsumed(DataInfo dataInfo)
diff --git a/jetty-spdy/spdy-http-server/src/main/java/org/eclipse/jetty/spdy/server/http/HttpTransportOverSPDY.java b/jetty-spdy/spdy-http-server/src/main/java/org/eclipse/jetty/spdy/server/http/HttpTransportOverSPDY.java
index 0c352e8..4168495 100644
--- a/jetty-spdy/spdy-http-server/src/main/java/org/eclipse/jetty/spdy/server/http/HttpTransportOverSPDY.java
+++ b/jetty-spdy/spdy-http-server/src/main/java/org/eclipse/jetty/spdy/server/http/HttpTransportOverSPDY.java
@@ -46,6 +46,7 @@
import org.eclipse.jetty.spdy.api.Session;
import org.eclipse.jetty.spdy.api.Stream;
import org.eclipse.jetty.spdy.api.StreamStatus;
+import org.eclipse.jetty.spdy.http.HTTPSPDYHeader;
import org.eclipse.jetty.util.BlockingCallback;
import org.eclipse.jetty.util.BufferUtil;
import org.eclipse.jetty.util.Callback;
diff --git a/jetty-spdy/spdy-http-server/src/main/java/org/eclipse/jetty/spdy/server/http/ReferrerPushStrategy.java b/jetty-spdy/spdy-http-server/src/main/java/org/eclipse/jetty/spdy/server/http/ReferrerPushStrategy.java
index c76bc88..b6022b5 100644
--- a/jetty-spdy/spdy-http-server/src/main/java/org/eclipse/jetty/spdy/server/http/ReferrerPushStrategy.java
+++ b/jetty-spdy/spdy-http-server/src/main/java/org/eclipse/jetty/spdy/server/http/ReferrerPushStrategy.java
@@ -33,6 +33,7 @@
import java.util.regex.Pattern;
import org.eclipse.jetty.spdy.api.Stream;
+import org.eclipse.jetty.spdy.http.HTTPSPDYHeader;
import org.eclipse.jetty.util.Fields;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;
@@ -40,23 +41,23 @@
/**
* <p>A SPDY push strategy that auto-populates push metadata based on referrer URLs.<p>A typical request for a main
* resource such as {@code index.html} is immediately followed by a number of requests for associated resources.
- * Associated resource requests will have a {@code Referer} HTTP header that points to {@code index.html}, which is
- * used to link the associated resource to the main resource.<p>However, also following a hyperlink generates a
- * HTTP request with a {@code Referer} HTTP header that points to {@code index.html}; therefore a proper value for
- * {@link #setReferrerPushPeriod(int)} has to be set. If the referrerPushPeriod for a main resource has elapsed,
- * no more associated resources will be added for that main resource.<p>This class distinguishes associated main
- * resources by their URL path suffix and content type. CSS stylesheets, images and JavaScript files have
- * recognizable URL path suffixes that are classified as associated resources. The suffix regexs can be configured by
- * constructor argument</p>
+ * Associated resource requests will have a {@code Referer} HTTP header that points to {@code index.html}, which is used
+ * to link the associated resource to the main resource.<p>However, also following a hyperlink generates a HTTP request
+ * with a {@code Referer} HTTP header that points to {@code index.html}; therefore a proper value for {@link
+ * #setReferrerPushPeriod(int)} has to be set. If the referrerPushPeriod for a main resource has elapsed, no more
+ * associated resources will be added for that main resource.<p>This class distinguishes associated main resources by
+ * their URL path suffix and content type. CSS stylesheets, images and JavaScript files have recognizable URL path
+ * suffixes that are classified as associated resources. The suffix regexs can be configured by constructor argument</p>
* <p>When CSS stylesheets refer to images, the CSS image request will have the CSS stylesheet as referrer. This
* implementation will push also the CSS image.<p>The push metadata built by this implementation is limited by the
- * number of pages of the application itself, and by the {@link #setMaxAssociatedResources(int)} max associated resources}
- * parameter. This parameter limits the number of associated resources per each main resource, so that if a main
- * resource has hundreds of associated resources, only up to the number specified by this parameter will be pushed.
+ * number of pages of the application itself, and by the {@link #setMaxAssociatedResources(int)} max associated
+ * resources} parameter. This parameter limits the number of associated resources per each main resource, so that if a
+ * main resource has hundreds of associated resources, only up to the number specified by this parameter will be
+ * pushed.
*/
public class ReferrerPushStrategy implements PushStrategy
{
- private static final Logger logger = Log.getLogger(ReferrerPushStrategy.class);
+ private static final Logger LOG = Log.getLogger(ReferrerPushStrategy.class);
private final ConcurrentMap<String, MainResource> mainResources = new ConcurrentHashMap<>();
private final Set<Pattern> pushRegexps = new HashSet<>();
private final Set<String> pushContentTypes = new HashSet<>();
@@ -166,7 +167,7 @@
String origin = scheme + "://" + host;
String url = requestHeaders.get(HTTPSPDYHeader.URI.name(version)).value();
String absoluteURL = origin + url;
- logger.debug("Applying push strategy for {}", absoluteURL);
+ LOG.debug("Applying push strategy for {}", absoluteURL);
if (isMainResource(url, responseHeaders))
{
MainResource mainResource = getOrCreateMainResource(absoluteURL);
@@ -189,7 +190,7 @@
result = getPushResources(absoluteURL);
}
}
- logger.debug("Pushing {} resources for {}: {}", result.size(), absoluteURL, result);
+ LOG.debug("Pushing {} resources for {}: {}", result.size(), absoluteURL, result);
}
return result;
}
@@ -207,7 +208,7 @@
MainResource mainResource = mainResources.get(absoluteURL);
if (mainResource == null)
{
- logger.debug("Creating new main resource for {}", absoluteURL);
+ LOG.debug("Creating new main resource for {}", absoluteURL);
MainResource value = new MainResource(absoluteURL);
mainResource = mainResources.putIfAbsent(absoluteURL, value);
if (mainResource == null)
@@ -282,7 +283,7 @@
long delay = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - firstResourceAdded.get());
if (!referrer.startsWith(origin) && !isPushOriginAllowed(origin))
{
- logger.debug("Skipped store of push metadata {} for {}: Origin: {} doesn't match or origin not allowed",
+ LOG.debug("Skipped store of push metadata {} for {}: Origin: {} doesn't match or origin not allowed",
url, name, origin);
return false;
}
@@ -292,17 +293,18 @@
// although in rare cases few more resources will be stored
if (resources.size() >= maxAssociatedResources)
{
- logger.debug("Skipped store of push metadata {} for {}: max associated resources ({}) reached",
+ LOG.debug("Skipped store of push metadata {} for {}: max associated resources ({}) reached",
url, name, maxAssociatedResources);
return false;
}
if (delay > referrerPushPeriod)
{
- logger.debug("Delay: {}ms longer than referrerPushPeriod: {}ms. Not adding resource: {} for: {}", delay, referrerPushPeriod, url, name);
+ LOG.debug("Delay: {}ms longer than referrerPushPeriod ({}ms). Not adding resource: {} for: {}", delay,
+ referrerPushPeriod, url, name);
return false;
}
- logger.debug("Adding resource: {} for: {} with delay: {}ms.", url, name, delay);
+ LOG.debug("Adding: {} to: {} with delay: {}ms.", url, this, delay);
resources.add(url);
return true;
}
@@ -314,7 +316,12 @@
public String toString()
{
- return "MainResource: " + name + " associated resources:" + resources.size();
+ return String.format("%s@%x{name=%s,resources=%s}",
+ getClass().getSimpleName(),
+ hashCode(),
+ name,
+ resources
+ );
}
private boolean isPushOriginAllowed(String origin)
diff --git a/jetty-spdy/spdy-http-server/src/main/java/org/eclipse/jetty/spdy/server/proxy/HTTPProxyEngine.java b/jetty-spdy/spdy-http-server/src/main/java/org/eclipse/jetty/spdy/server/proxy/HTTPProxyEngine.java
index b46d11c..ef9596a 100644
--- a/jetty-spdy/spdy-http-server/src/main/java/org/eclipse/jetty/spdy/server/proxy/HTTPProxyEngine.java
+++ b/jetty-spdy/spdy-http-server/src/main/java/org/eclipse/jetty/spdy/server/proxy/HTTPProxyEngine.java
@@ -40,7 +40,7 @@
import org.eclipse.jetty.spdy.api.StreamFrameListener;
import org.eclipse.jetty.spdy.api.StreamStatus;
import org.eclipse.jetty.spdy.api.SynInfo;
-import org.eclipse.jetty.spdy.server.http.HTTPSPDYHeader;
+import org.eclipse.jetty.spdy.http.HTTPSPDYHeader;
import org.eclipse.jetty.util.BufferUtil;
import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.util.Fields;
diff --git a/jetty-spdy/spdy-http-server/src/main/java/org/eclipse/jetty/spdy/server/proxy/ProxyEngine.java b/jetty-spdy/spdy-http-server/src/main/java/org/eclipse/jetty/spdy/server/proxy/ProxyEngine.java
index 0b980b5..ff116cc 100644
--- a/jetty-spdy/spdy-http-server/src/main/java/org/eclipse/jetty/spdy/server/proxy/ProxyEngine.java
+++ b/jetty-spdy/spdy-http-server/src/main/java/org/eclipse/jetty/spdy/server/proxy/ProxyEngine.java
@@ -28,7 +28,7 @@
import org.eclipse.jetty.spdy.api.Stream;
import org.eclipse.jetty.spdy.api.StreamFrameListener;
import org.eclipse.jetty.spdy.api.SynInfo;
-import org.eclipse.jetty.spdy.server.http.HTTPSPDYHeader;
+import org.eclipse.jetty.spdy.http.HTTPSPDYHeader;
import org.eclipse.jetty.util.Fields;
/**
diff --git a/jetty-spdy/spdy-http-server/src/main/java/org/eclipse/jetty/spdy/server/proxy/ProxyEngineSelector.java b/jetty-spdy/spdy-http-server/src/main/java/org/eclipse/jetty/spdy/server/proxy/ProxyEngineSelector.java
index 452e32b..69cb23c 100644
--- a/jetty-spdy/spdy-http-server/src/main/java/org/eclipse/jetty/spdy/server/proxy/ProxyEngineSelector.java
+++ b/jetty-spdy/spdy-http-server/src/main/java/org/eclipse/jetty/spdy/server/proxy/ProxyEngineSelector.java
@@ -32,7 +32,7 @@
import org.eclipse.jetty.spdy.api.StreamStatus;
import org.eclipse.jetty.spdy.api.SynInfo;
import org.eclipse.jetty.spdy.api.server.ServerSessionFrameListener;
-import org.eclipse.jetty.spdy.server.http.HTTPSPDYHeader;
+import org.eclipse.jetty.spdy.http.HTTPSPDYHeader;
import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.util.Fields;
import org.eclipse.jetty.util.log.Log;
diff --git a/jetty-spdy/spdy-http-server/src/main/java/org/eclipse/jetty/spdy/server/proxy/ProxyHTTPSPDYConnection.java b/jetty-spdy/spdy-http-server/src/main/java/org/eclipse/jetty/spdy/server/proxy/ProxyHTTPSPDYConnection.java
index 602255f..fda3dcb 100644
--- a/jetty-spdy/spdy-http-server/src/main/java/org/eclipse/jetty/spdy/server/proxy/ProxyHTTPSPDYConnection.java
+++ b/jetty-spdy/spdy-http-server/src/main/java/org/eclipse/jetty/spdy/server/proxy/ProxyHTTPSPDYConnection.java
@@ -18,7 +18,6 @@
package org.eclipse.jetty.spdy.server.proxy;
-import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -51,7 +50,7 @@
import org.eclipse.jetty.spdy.api.Stream;
import org.eclipse.jetty.spdy.api.StreamFrameListener;
import org.eclipse.jetty.spdy.api.SynInfo;
-import org.eclipse.jetty.spdy.server.http.HTTPSPDYHeader;
+import org.eclipse.jetty.spdy.http.HTTPSPDYHeader;
import org.eclipse.jetty.util.BufferUtil;
import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.util.Fields;
@@ -232,51 +231,50 @@
}
@Override
- public void reply(ReplyInfo replyInfo, Callback handler)
+ public void reply(ReplyInfo replyInfo, final Callback handler)
{
- try
+ Fields headers = new Fields(replyInfo.getHeaders(), false);
+
+ headers.remove(HTTPSPDYHeader.SCHEME.name(version));
+
+ String status = headers.remove(HTTPSPDYHeader.STATUS.name(version)).value();
+ Matcher matcher = statusRegexp.matcher(status);
+ matcher.matches();
+ int code = Integer.parseInt(matcher.group(1));
+ String reason = matcher.group(2).trim();
+
+ HttpVersion httpVersion = HttpVersion.fromString(headers.remove(HTTPSPDYHeader.VERSION.name(version)).value());
+
+ // Convert the Host header from a SPDY special header to a normal header
+ Fields.Field host = headers.remove(HTTPSPDYHeader.HOST.name(version));
+ if (host != null)
+ headers.put("host", host.value());
+
+ HttpFields fields = new HttpFields();
+ for (Fields.Field header : headers)
{
- Fields headers = new Fields(replyInfo.getHeaders(), false);
+ String name = camelize(header.name());
+ fields.put(name, header.value());
+ }
- headers.remove(HTTPSPDYHeader.SCHEME.name(version));
+ // TODO: handle better the HEAD last parameter
+ long contentLength = fields.getLongField(HttpHeader.CONTENT_LENGTH.asString());
+ HttpGenerator.ResponseInfo info = new HttpGenerator.ResponseInfo(httpVersion, fields, contentLength, code,
+ reason, false);
- String status = headers.remove(HTTPSPDYHeader.STATUS.name(version)).value();
- Matcher matcher = statusRegexp.matcher(status);
- matcher.matches();
- int code = Integer.parseInt(matcher.group(1));
- String reason = matcher.group(2).trim();
-
- HttpVersion httpVersion = HttpVersion.fromString(headers.remove(HTTPSPDYHeader.VERSION.name(version)).value());
-
- // Convert the Host header from a SPDY special header to a normal header
- Fields.Field host = headers.remove(HTTPSPDYHeader.HOST.name(version));
- if (host != null)
- headers.put("host", host.value());
-
- HttpFields fields = new HttpFields();
- for (Fields.Field header : headers)
+ send(info, null, replyInfo.isClose(), new Adapter()
+ {
+ @Override
+ public void failed(Throwable x)
{
- String name = camelize(header.name());
- fields.put(name, header.value());
+ handler.failed(x);
}
+ });
- // TODO: handle better the HEAD last parameter
- long contentLength = fields.getLongField(HttpHeader.CONTENT_LENGTH.asString());
- HttpGenerator.ResponseInfo info = new HttpGenerator.ResponseInfo(httpVersion, fields, contentLength, code,
- reason, false);
-
- // TODO use the async send
- send(info, null, replyInfo.isClose());
+ if (replyInfo.isClose())
+ completed();
- if (replyInfo.isClose())
- completed();
-
- handler.succeeded();
- }
- catch (IOException x)
- {
- handler.failed(x);
- }
+ handler.succeeded();
}
private String camelize(String name)
@@ -295,25 +293,24 @@
}
@Override
- public void data(DataInfo dataInfo, Callback handler)
+ public void data(DataInfo dataInfo, final Callback handler)
{
- try
+ // Data buffer must be copied, as the ByteBuffer is pooled
+ ByteBuffer byteBuffer = dataInfo.asByteBuffer(false);
+
+ send(null, byteBuffer, dataInfo.isClose(), new Adapter()
{
- // Data buffer must be copied, as the ByteBuffer is pooled
- ByteBuffer byteBuffer = dataInfo.asByteBuffer(false);
+ @Override
+ public void failed(Throwable x)
+ {
+ handler.failed(x);
+ }
+ });
- // TODO use the async send with callback!
- send(null, byteBuffer, dataInfo.isClose());
+ if (dataInfo.isClose())
+ completed();
- if (dataInfo.isClose())
- completed();
-
- handler.succeeded();
- }
- catch (IOException x)
- {
- handler.failed(x);
- }
+ handler.succeeded();
}
}
diff --git a/jetty-spdy/spdy-http-server/src/main/java/org/eclipse/jetty/spdy/server/proxy/SPDYProxyEngine.java b/jetty-spdy/spdy-http-server/src/main/java/org/eclipse/jetty/spdy/server/proxy/SPDYProxyEngine.java
index 5fe7909..2f1401f 100644
--- a/jetty-spdy/spdy-http-server/src/main/java/org/eclipse/jetty/spdy/server/proxy/SPDYProxyEngine.java
+++ b/jetty-spdy/spdy-http-server/src/main/java/org/eclipse/jetty/spdy/server/proxy/SPDYProxyEngine.java
@@ -43,7 +43,7 @@
import org.eclipse.jetty.spdy.api.StreamStatus;
import org.eclipse.jetty.spdy.api.SynInfo;
import org.eclipse.jetty.spdy.client.SPDYClient;
-import org.eclipse.jetty.spdy.server.http.HTTPSPDYHeader;
+import org.eclipse.jetty.spdy.http.HTTPSPDYHeader;
import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.util.Fields;
import org.eclipse.jetty.util.Promise;
@@ -178,7 +178,7 @@
if (session == null)
{
SPDYClient client = factory.newSPDYClient(version);
- session = client.connect(address, sessionListener).get(getConnectTimeout(), TimeUnit.MILLISECONDS);
+ session = client.connect(address, sessionListener);
LOG.debug("Proxy session connected to {}", address);
Session existing = serverSessions.putIfAbsent(host, session);
if (existing != null)
diff --git a/jetty-spdy/spdy-http-server/src/test/java/org/eclipse/jetty/spdy/server/http/AbstractHTTPSPDYTest.java b/jetty-spdy/spdy-http-server/src/test/java/org/eclipse/jetty/spdy/server/http/AbstractHTTPSPDYTest.java
index de96a4c..66b140a 100644
--- a/jetty-spdy/spdy-http-server/src/test/java/org/eclipse/jetty/spdy/server/http/AbstractHTTPSPDYTest.java
+++ b/jetty-spdy/spdy-http-server/src/test/java/org/eclipse/jetty/spdy/server/http/AbstractHTTPSPDYTest.java
@@ -22,7 +22,6 @@
import java.util.Arrays;
import java.util.Collection;
import java.util.concurrent.Executor;
-import java.util.concurrent.TimeUnit;
import org.eclipse.jetty.server.Handler;
import org.eclipse.jetty.server.HttpConfiguration;
@@ -81,7 +80,9 @@
protected InetSocketAddress startHTTPServer(short version, Handler handler) throws Exception
{
- server = new Server();
+ QueuedThreadPool threadPool = new QueuedThreadPool(256);
+ threadPool.setName("serverQTP");
+ server = new Server(threadPool);
connector = newHTTPSPDYServerConnector(version);
connector.setPort(0);
connector.setIdleTimeout(30000);
@@ -120,7 +121,7 @@
clientFactory = newSPDYClientFactory(threadPool);
clientFactory.start();
}
- return clientFactory.newSPDYClient(version).connect(socketAddress, listener).get(5, TimeUnit.SECONDS);
+ return clientFactory.newSPDYClient(version).connect(socketAddress, listener);
}
protected SPDYClient.Factory newSPDYClientFactory(Executor threadPool)
diff --git a/jetty-spdy/spdy-http-server/src/test/java/org/eclipse/jetty/spdy/server/http/ConcurrentStreamsTest.java b/jetty-spdy/spdy-http-server/src/test/java/org/eclipse/jetty/spdy/server/http/ConcurrentStreamsTest.java
index e6816a3..1f35b35 100644
--- a/jetty-spdy/spdy-http-server/src/test/java/org/eclipse/jetty/spdy/server/http/ConcurrentStreamsTest.java
+++ b/jetty-spdy/spdy-http-server/src/test/java/org/eclipse/jetty/spdy/server/http/ConcurrentStreamsTest.java
@@ -21,7 +21,6 @@
import java.io.IOException;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
-
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@@ -33,6 +32,7 @@
import org.eclipse.jetty.spdy.api.Stream;
import org.eclipse.jetty.spdy.api.StreamFrameListener;
import org.eclipse.jetty.spdy.api.SynInfo;
+import org.eclipse.jetty.spdy.http.HTTPSPDYHeader;
import org.eclipse.jetty.util.Fields;
import org.junit.Assert;
import org.junit.Test;
diff --git a/jetty-spdy/spdy-http-server/src/test/java/org/eclipse/jetty/spdy/server/http/HttpTransportOverSPDYTest.java b/jetty-spdy/spdy-http-server/src/test/java/org/eclipse/jetty/spdy/server/http/HttpTransportOverSPDYTest.java
index 9689a2d..451e303 100644
--- a/jetty-spdy/spdy-http-server/src/test/java/org/eclipse/jetty/spdy/server/http/HttpTransportOverSPDYTest.java
+++ b/jetty-spdy/spdy-http-server/src/test/java/org/eclipse/jetty/spdy/server/http/HttpTransportOverSPDYTest.java
@@ -33,6 +33,7 @@
import org.eclipse.jetty.spdy.api.SPDY;
import org.eclipse.jetty.spdy.api.Session;
import org.eclipse.jetty.spdy.api.Stream;
+import org.eclipse.jetty.spdy.http.HTTPSPDYHeader;
import org.eclipse.jetty.util.BufferUtil;
import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.util.Fields;
diff --git a/jetty-spdy/spdy-http-server/src/test/java/org/eclipse/jetty/spdy/server/http/PushStrategyBenchmarkTest.java b/jetty-spdy/spdy-http-server/src/test/java/org/eclipse/jetty/spdy/server/http/PushStrategyBenchmarkTest.java
index 17f0e17..25b5b87 100644
--- a/jetty-spdy/spdy-http-server/src/test/java/org/eclipse/jetty/spdy/server/http/PushStrategyBenchmarkTest.java
+++ b/jetty-spdy/spdy-http-server/src/test/java/org/eclipse/jetty/spdy/server/http/PushStrategyBenchmarkTest.java
@@ -51,6 +51,7 @@
import org.eclipse.jetty.spdy.api.Stream;
import org.eclipse.jetty.spdy.api.StreamFrameListener;
import org.eclipse.jetty.spdy.api.SynInfo;
+import org.eclipse.jetty.spdy.http.HTTPSPDYHeader;
import org.eclipse.jetty.util.Fields;
import org.junit.Assert;
import org.junit.Ignore;
diff --git a/jetty-spdy/spdy-http-server/src/test/java/org/eclipse/jetty/spdy/server/http/ReferrerPushStrategyTest.java b/jetty-spdy/spdy-http-server/src/test/java/org/eclipse/jetty/spdy/server/http/ReferrerPushStrategyTest.java
index d120166..fa556a8 100644
--- a/jetty-spdy/spdy-http-server/src/test/java/org/eclipse/jetty/spdy/server/http/ReferrerPushStrategyTest.java
+++ b/jetty-spdy/spdy-http-server/src/test/java/org/eclipse/jetty/spdy/server/http/ReferrerPushStrategyTest.java
@@ -51,6 +51,7 @@
import org.eclipse.jetty.spdy.api.StreamFrameListener;
import org.eclipse.jetty.spdy.api.StreamStatus;
import org.eclipse.jetty.spdy.api.SynInfo;
+import org.eclipse.jetty.spdy.http.HTTPSPDYHeader;
import org.eclipse.jetty.spdy.server.NPNServerConnectionFactory;
import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.util.Fields;
@@ -459,6 +460,7 @@
private void sendRequest(Session session, Fields requestHeaders, final CountDownLatch pushSynHeadersValid,
final CountDownLatch pushDataLatch, final boolean resetPush) throws InterruptedException
{
+ LOG.info("sendRequest. headers={},resetPush={}", requestHeaders, resetPush);
final CountDownLatch dataReceivedLatch = new CountDownLatch(1);
session.syn(new SynInfo(requestHeaders, true), new StreamFrameListener.Adapter()
{
diff --git a/jetty-spdy/spdy-http-server/src/test/java/org/eclipse/jetty/spdy/server/http/ReferrerPushStrategyUnitTest.java b/jetty-spdy/spdy-http-server/src/test/java/org/eclipse/jetty/spdy/server/http/ReferrerPushStrategyUnitTest.java
index 7d038bc..e2e5acc 100644
--- a/jetty-spdy/spdy-http-server/src/test/java/org/eclipse/jetty/spdy/server/http/ReferrerPushStrategyUnitTest.java
+++ b/jetty-spdy/spdy-http-server/src/test/java/org/eclipse/jetty/spdy/server/http/ReferrerPushStrategyUnitTest.java
@@ -24,6 +24,7 @@
import org.eclipse.jetty.spdy.api.SPDY;
import org.eclipse.jetty.spdy.api.Session;
import org.eclipse.jetty.spdy.api.Stream;
+import org.eclipse.jetty.spdy.http.HTTPSPDYHeader;
import org.eclipse.jetty.util.Fields;
import org.junit.Before;
import org.junit.Test;
diff --git a/jetty-spdy/spdy-http-server/src/test/java/org/eclipse/jetty/spdy/server/http/SPDYTestUtils.java b/jetty-spdy/spdy-http-server/src/test/java/org/eclipse/jetty/spdy/server/http/SPDYTestUtils.java
index d861a73..1acf93d 100644
--- a/jetty-spdy/spdy-http-server/src/test/java/org/eclipse/jetty/spdy/server/http/SPDYTestUtils.java
+++ b/jetty-spdy/spdy-http-server/src/test/java/org/eclipse/jetty/spdy/server/http/SPDYTestUtils.java
@@ -19,6 +19,7 @@
package org.eclipse.jetty.spdy.server.http;
+import org.eclipse.jetty.spdy.http.HTTPSPDYHeader;
import org.eclipse.jetty.util.Fields;
import org.eclipse.jetty.util.ssl.SslContextFactory;
diff --git a/jetty-spdy/spdy-http-server/src/test/java/org/eclipse/jetty/spdy/server/http/SSLExternalServerTest.java b/jetty-spdy/spdy-http-server/src/test/java/org/eclipse/jetty/spdy/server/http/SSLExternalServerTest.java
index 0613f92..734fe4d 100644
--- a/jetty-spdy/spdy-http-server/src/test/java/org/eclipse/jetty/spdy/server/http/SSLExternalServerTest.java
+++ b/jetty-spdy/spdy-http-server/src/test/java/org/eclipse/jetty/spdy/server/http/SSLExternalServerTest.java
@@ -33,6 +33,7 @@
import org.eclipse.jetty.spdy.api.StreamFrameListener;
import org.eclipse.jetty.spdy.api.SynInfo;
import org.eclipse.jetty.spdy.client.SPDYClient;
+import org.eclipse.jetty.spdy.http.HTTPSPDYHeader;
import org.eclipse.jetty.util.Fields;
import org.eclipse.jetty.util.ssl.SslContextFactory;
import org.junit.Assert;
diff --git a/jetty-spdy/spdy-http-server/src/test/java/org/eclipse/jetty/spdy/server/http/ServerHTTPSPDYTest.java b/jetty-spdy/spdy-http-server/src/test/java/org/eclipse/jetty/spdy/server/http/ServerHTTPSPDYTest.java
index de6c4d7..30cd433 100644
--- a/jetty-spdy/spdy-http-server/src/test/java/org/eclipse/jetty/spdy/server/http/ServerHTTPSPDYTest.java
+++ b/jetty-spdy/spdy-http-server/src/test/java/org/eclipse/jetty/spdy/server/http/ServerHTTPSPDYTest.java
@@ -18,7 +18,6 @@
package org.eclipse.jetty.spdy.server.http;
-import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
@@ -49,10 +48,10 @@
import org.eclipse.jetty.spdy.api.StreamFrameListener;
import org.eclipse.jetty.spdy.api.StringDataInfo;
import org.eclipse.jetty.spdy.api.SynInfo;
+import org.eclipse.jetty.spdy.http.HTTPSPDYHeader;
import org.eclipse.jetty.util.Fields;
import org.eclipse.jetty.util.log.StdErrLog;
import org.junit.Assert;
-import org.junit.Ignore;
import org.junit.Test;
import static org.hamcrest.CoreMatchers.containsString;
@@ -1039,31 +1038,11 @@
assertTrue(dataLatch.await(5, TimeUnit.SECONDS));
}
- @Ignore("The correspondent functionality in HttpOutput is not yet implemented")
- @Test
- public void testGETWithMediumContentAsInputStreamByPassed() throws Exception
- {
- byte[] data = new byte[2048];
- testGETWithContentByPassed(new ByteArrayInputStream(data), data.length);
- }
-
- @Ignore("The correspondent functionality in HttpOutput is not yet implemented")
- @Test
- public void testGETWithBigContentAsInputStreamByPassed() throws Exception
- {
- byte[] data = new byte[128 * 1024];
- testGETWithContentByPassed(new ByteArrayInputStream(data), data.length);
- }
-
@Test
public void testGETWithMediumContentAsBufferByPassed() throws Exception
{
- byte[] data = new byte[2048];
- testGETWithContentByPassed(ByteBuffer.wrap(data), data.length);
- }
+ final byte[] data = new byte[2048];
- private void testGETWithContentByPassed(final Object content, final int length) throws Exception
- {
final CountDownLatch handlerLatch = new CountDownLatch(1);
Session session = startClient(version, startHTTPServer(version, new AbstractHandler()
{
@@ -1072,10 +1051,7 @@
throws IOException, ServletException
{
request.setHandled(true);
- // We use this trick that's present in Jetty code: if we add a request attribute
- // called "org.eclipse.jetty.server.sendContent", then it will trigger the
- // content bypass that we want to test
- request.setAttribute("org.eclipse.jetty.server.sendContent", content);
+ request.getResponse().getHttpOutput().sendContent(ByteBuffer.wrap(data));
handlerLatch.countDown();
}
}), null);
@@ -1104,7 +1080,7 @@
contentLength.addAndGet(dataInfo.asBytes(true).length);
if (dataInfo.isClose())
{
- assertEquals(length, contentLength.get());
+ Assert.assertEquals(data.length, contentLength.get());
dataLatch.countDown();
}
}
diff --git a/jetty-spdy/spdy-http-server/src/test/java/org/eclipse/jetty/spdy/server/proxy/ProxyHTTPToSPDYTest.java b/jetty-spdy/spdy-http-server/src/test/java/org/eclipse/jetty/spdy/server/proxy/ProxyHTTPToSPDYTest.java
index bcb46a9..0b2dcd1 100644
--- a/jetty-spdy/spdy-http-server/src/test/java/org/eclipse/jetty/spdy/server/proxy/ProxyHTTPToSPDYTest.java
+++ b/jetty-spdy/spdy-http-server/src/test/java/org/eclipse/jetty/spdy/server/proxy/ProxyHTTPToSPDYTest.java
@@ -45,9 +45,9 @@
import org.eclipse.jetty.spdy.api.SynInfo;
import org.eclipse.jetty.spdy.api.server.ServerSessionFrameListener;
import org.eclipse.jetty.spdy.client.SPDYClient;
+import org.eclipse.jetty.spdy.http.HTTPSPDYHeader;
import org.eclipse.jetty.spdy.server.SPDYServerConnectionFactory;
import org.eclipse.jetty.spdy.server.SPDYServerConnector;
-import org.eclipse.jetty.spdy.server.http.HTTPSPDYHeader;
import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.util.Fields;
import org.eclipse.jetty.util.Promise;
diff --git a/jetty-spdy/spdy-http-server/src/test/java/org/eclipse/jetty/spdy/server/proxy/ProxySPDYToHTTPLoadTest.java b/jetty-spdy/spdy-http-server/src/test/java/org/eclipse/jetty/spdy/server/proxy/ProxySPDYToHTTPLoadTest.java
index 6d6ba31..080f9ae 100644
--- a/jetty-spdy/spdy-http-server/src/test/java/org/eclipse/jetty/spdy/server/proxy/ProxySPDYToHTTPLoadTest.java
+++ b/jetty-spdy/spdy-http-server/src/test/java/org/eclipse/jetty/spdy/server/proxy/ProxySPDYToHTTPLoadTest.java
@@ -56,9 +56,13 @@
import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.util.Fields;
import org.eclipse.jetty.util.IO;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
import org.eclipse.jetty.util.ssl.SslContextFactory;
+import org.eclipse.jetty.util.thread.QueuedThreadPool;
import org.junit.After;
import org.junit.Before;
+import org.junit.Ignore;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TestWatcher;
@@ -71,9 +75,11 @@
import static org.hamcrest.Matchers.notNullValue;
import static org.junit.Assert.assertThat;
+@Ignore
@RunWith(value = Parameterized.class)
public class ProxySPDYToHTTPLoadTest
{
+ private static final Logger LOG = Log.getLogger(ProxySPDYToHTTPLoadTest.class);
@Rule
public final TestWatcher testName = new TestWatcher()
{
@@ -89,6 +95,8 @@
};
private final short version;
+ private final String server1String = "server1";
+ private final String server2String = "server2";
@Parameterized.Parameters
public static Collection<Short[]> parameters()
@@ -97,7 +105,8 @@
}
private SPDYClient.Factory factory;
- private Server server;
+ private Server server1;
+ private Server server2;
private Server proxy;
private ServerConnector proxyConnector;
private SslContextFactory sslContextFactory = SPDYTestUtils.newSslContextFactory();
@@ -107,20 +116,62 @@
this.version = version;
}
- protected InetSocketAddress startServer(Handler handler) throws Exception
+ @Before
+ public void init() throws Exception
{
- server = new Server();
+ // change the ports if you want to trace the network traffic
+ server1 = startServer(new TestServerHandler(server1String, null), 0);
+ server2 = startServer(new TestServerHandler(server2String, null), 0);
+ factory = new SPDYClient.Factory(sslContextFactory);
+ factory.start();
+ }
+
+ @After
+ public void destroy() throws Exception
+ {
+ stopServer(server1);
+ stopServer(server2);
+ if (proxy != null)
+ {
+ proxy.stop();
+ proxy.join();
+ }
+ factory.stop();
+ }
+
+ private void stopServer(Server server) throws Exception
+ {
+ if (server != null)
+ {
+ server.stop();
+ server.join();
+ }
+ }
+
+ protected Server startServer(Handler handler, int port) throws Exception
+ {
+ QueuedThreadPool threadPool = new QueuedThreadPool(256);
+ threadPool.setName("upstreamServerQTP");
+ Server server = new Server(threadPool);
ServerConnector connector = new ServerConnector(server);
+ connector.setPort(port);
server.setHandler(handler);
server.addConnector(connector);
server.start();
- return new InetSocketAddress("localhost", connector.getLocalPort());
+ return server;
+ }
+
+ private InetSocketAddress getServerAddress(Server server)
+ {
+ return new InetSocketAddress("localhost", ((ServerConnector)server.getConnectors()[0]).getLocalPort());
}
protected InetSocketAddress startProxy(InetSocketAddress server1, InetSocketAddress server2,
long proxyConnectorTimeout, long proxyEngineTimeout) throws Exception
{
- proxy = new Server();
+ QueuedThreadPool threadPool = new QueuedThreadPool(256);
+ threadPool.setName("proxyQTP");
+ proxy = new Server(threadPool);
ProxyEngineSelector proxyEngineSelector = new ProxyEngineSelector();
HttpClient httpClient = new HttpClient();
httpClient.start();
@@ -144,38 +195,11 @@
return new InetSocketAddress("localhost", proxyConnector.getLocalPort());
}
- @Before
- public void init() throws Exception
- {
- factory = new SPDYClient.Factory(sslContextFactory);
- factory.start();
- }
-
- @After
- public void destroy() throws Exception
- {
- if (server != null)
- {
- server.stop();
- server.join();
- }
- if (proxy != null)
- {
- proxy.stop();
- proxy.join();
- }
- factory.stop();
- }
-
@Test
public void testSimpleLoadTest() throws Exception
{
- String server1String = "server1";
- String server2String = "server2";
-
- InetSocketAddress server1 = startServer(new TestServerHandler(server1String, null));
- InetSocketAddress server2 = startServer(new TestServerHandler(server2String, null));
- final InetSocketAddress proxyAddress = startProxy(server1, server2, 30000, 30000);
+ final InetSocketAddress proxyAddress = startProxy(getServerAddress(server1), getServerAddress(server2), 30000,
+ 30000);
final int requestsPerClient = 50;
@@ -198,7 +222,7 @@
}
private Runnable createClientRunnable(final InetSocketAddress proxyAddress, final int requestsPerClient,
- final String serverIdentificationString, final String serverHost)
+ final String serverIdentificationString, final String serverHost)
{
Runnable client = new Runnable()
{
@@ -207,16 +231,16 @@
{
try
{
- Session client = factory.newSPDYClient(version).connect(proxyAddress, null).get(5, TimeUnit.SECONDS);
+ Session client = factory.newSPDYClient(version).connect(proxyAddress, null);
for (int i = 0; i < requestsPerClient; i++)
{
sendSingleClientRequest(proxyAddress, client, serverIdentificationString, serverHost);
}
}
- catch (InterruptedException | ExecutionException | TimeoutException | IOException e)
+ catch (InterruptedException | ExecutionException | TimeoutException e)
{
- fail();
e.printStackTrace();
+ fail();
}
}
};
@@ -239,6 +263,7 @@
@Override
public void onReply(Stream stream, ReplyInfo replyInfo)
{
+ LOG.debug("Got reply: {}", replyInfo);
Fields headers = replyInfo.getHeaders();
assertThat("response comes from the given server", headers.get(serverIdentificationString),
is(notNullValue()));
@@ -251,7 +276,8 @@
result.write(dataInfo.asBytes(true), 0, dataInfo.length());
if (dataInfo.isClose())
{
- assertThat("received data matches send data", data, is(result.toString()));
+ LOG.debug("Got last dataFrame: {}", dataInfo);
+ assertThat("received data matches send data", result.toString(), is(data));
dataLatch.countDown();
}
}
@@ -259,8 +285,9 @@
stream.data(new StringDataInfo(data, true), new Callback.Adapter());
- assertThat("reply has been received", replyLatch.await(5, TimeUnit.SECONDS), is(true));
- assertThat("data has been received", dataLatch.await(5, TimeUnit.SECONDS), is(true));
+ assertThat("reply has been received", replyLatch.await(15, TimeUnit.SECONDS), is(true));
+ assertThat("data has been received", dataLatch.await(15, TimeUnit.SECONDS), is(true));
+ LOG.debug("Successfully received response");
}
private class TestServerHandler extends DefaultHandler
diff --git a/jetty-spdy/spdy-http-server/src/test/java/org/eclipse/jetty/spdy/server/proxy/ProxySPDYToHTTPTest.java b/jetty-spdy/spdy-http-server/src/test/java/org/eclipse/jetty/spdy/server/proxy/ProxySPDYToHTTPTest.java
index 556c148..9f4d2e3 100644
--- a/jetty-spdy/spdy-http-server/src/test/java/org/eclipse/jetty/spdy/server/proxy/ProxySPDYToHTTPTest.java
+++ b/jetty-spdy/spdy-http-server/src/test/java/org/eclipse/jetty/spdy/server/proxy/ProxySPDYToHTTPTest.java
@@ -52,7 +52,7 @@
import org.eclipse.jetty.spdy.api.SynInfo;
import org.eclipse.jetty.spdy.api.server.ServerSessionFrameListener;
import org.eclipse.jetty.spdy.client.SPDYClient;
-import org.eclipse.jetty.spdy.server.http.HTTPSPDYHeader;
+import org.eclipse.jetty.spdy.http.HTTPSPDYHeader;
import org.eclipse.jetty.spdy.server.http.SPDYTestUtils;
import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.util.Fields;
@@ -169,7 +169,7 @@
InetSocketAddress proxyAddress = startProxy(startServer(new TestServerHandler(header, null)), 30000, 30000);
- Session client = factory.newSPDYClient(version).connect(proxyAddress, null).get(5, TimeUnit.SECONDS);
+ Session client = factory.newSPDYClient(version).connect(proxyAddress, null);
final CountDownLatch replyLatch = new CountDownLatch(1);
@@ -202,7 +202,7 @@
InetSocketAddress proxyAddress = startProxy(startServer(new TestServerHandler(header, data)), 30000, 30000);
- Session client = factory.newSPDYClient(version).connect(proxyAddress, null).get(5, TimeUnit.SECONDS);
+ Session client = factory.newSPDYClient(version).connect(proxyAddress, null);
final CountDownLatch replyLatch = new CountDownLatch(1);
final CountDownLatch dataLatch = new CountDownLatch(1);
@@ -275,7 +275,7 @@
{
resetLatch.countDown();
}
- }).get(5, TimeUnit.SECONDS);
+ });
Fields headers = SPDYTestUtils.createHeaders("localhost", proxyAddress.getPort(), version, "GET", "/");
headers.put("connection", "close");
@@ -309,7 +309,7 @@
final CountDownLatch replyLatch = new CountDownLatch(1);
- Session client = factory.newSPDYClient(version).connect(proxyAddress, null).get(5, TimeUnit.SECONDS);
+ Session client = factory.newSPDYClient(version).connect(proxyAddress, null);
Fields headers = SPDYTestUtils.createHeaders("localhost", proxyAddress.getPort(), version, "GET", "/");
client.syn(new SynInfo(headers, true), new StreamFrameListener.Adapter()
@@ -335,7 +335,7 @@
InetSocketAddress proxyAddress = startProxy(startServer(new TestServerHandler(header, null)), 30000, 30000);
- Session client = factory.newSPDYClient(version).connect(proxyAddress, null).get(5, TimeUnit.SECONDS);
+ Session client = factory.newSPDYClient(version).connect(proxyAddress, null);
final CountDownLatch replyLatch = new CountDownLatch(1);
final CountDownLatch dataLatch = new CountDownLatch(1);
@@ -383,7 +383,7 @@
InetSocketAddress proxyAddress = startProxy(startServer(new TestServerHandler(header, null)), 30000, 30000);
- Session client = factory.newSPDYClient(version).connect(proxyAddress, null).get(5, TimeUnit.SECONDS);
+ Session client = factory.newSPDYClient(version).connect(proxyAddress, null);
final CountDownLatch replyLatch = new CountDownLatch(1);
final CountDownLatch dataLatch = new CountDownLatch(1);
@@ -439,7 +439,7 @@
{
goAwayLatch.countDown();
}
- }).get(5, TimeUnit.SECONDS);
+ });
Fields headers = SPDYTestUtils.createHeaders("localhost", proxyAddress.getPort(), version, "POST", "/");
((StdErrLog)Log.getLogger(HttpChannel.class)).setHideStacks(true);
@@ -470,7 +470,7 @@
}
}), 30000, timeout);
- Session client = factory.newSPDYClient(version).connect(proxyAddress, null).get(5, TimeUnit.SECONDS);
+ Session client = factory.newSPDYClient(version).connect(proxyAddress, null);
final CountDownLatch replyLatch = new CountDownLatch(1);
@@ -510,7 +510,7 @@
{
pingLatch.countDown();
}
- }).get(5, TimeUnit.SECONDS);
+ });
client.ping(new PingInfo(5, TimeUnit.SECONDS));
diff --git a/jetty-spdy/spdy-http-server/src/test/java/org/eclipse/jetty/spdy/server/proxy/ProxySPDYToSPDYLoadTest.java b/jetty-spdy/spdy-http-server/src/test/java/org/eclipse/jetty/spdy/server/proxy/ProxySPDYToSPDYLoadTest.java
index dfb7f38..5f5ac53 100644
--- a/jetty-spdy/spdy-http-server/src/test/java/org/eclipse/jetty/spdy/server/proxy/ProxySPDYToSPDYLoadTest.java
+++ b/jetty-spdy/spdy-http-server/src/test/java/org/eclipse/jetty/spdy/server/proxy/ProxySPDYToSPDYLoadTest.java
@@ -19,7 +19,6 @@
package org.eclipse.jetty.spdy.server.proxy;
import java.io.ByteArrayOutputStream;
-import java.io.IOException;
import java.net.InetSocketAddress;
import java.util.ArrayList;
import java.util.Arrays;
@@ -150,16 +149,10 @@
@After
public void destroy() throws Exception
{
- if (server != null)
- {
- server.stop();
- server.join();
- }
- if (proxy != null)
- {
- proxy.stop();
- proxy.join();
- }
+ server.stop();
+ server.join();
+ proxy.stop();
+ proxy.join();
factory.stop();
}
@@ -203,13 +196,13 @@
{
try
{
- Session client = factory.newSPDYClient(version).connect(proxyAddress, null).get(5, TimeUnit.SECONDS);
+ Session client = factory.newSPDYClient(version).connect(proxyAddress, null);
for (int i = 0; i < requestsPerClient; i++)
{
sendSingleClientRequest(proxyAddress, client, serverIdentificationString, serverHost);
}
}
- catch (InterruptedException | ExecutionException | TimeoutException | IOException e)
+ catch (InterruptedException | ExecutionException | TimeoutException e)
{
fail();
e.printStackTrace();
@@ -271,7 +264,7 @@
}
@Override
- public StreamFrameListener onSyn (Stream stream, SynInfo synInfo)
+ public StreamFrameListener onSyn(Stream stream, SynInfo synInfo)
{
Fields requestHeaders = synInfo.getHeaders();
Assert.assertNotNull(requestHeaders.get("via"));
diff --git a/jetty-spdy/spdy-http-server/src/test/java/org/eclipse/jetty/spdy/server/proxy/ProxySPDYToSPDYTest.java b/jetty-spdy/spdy-http-server/src/test/java/org/eclipse/jetty/spdy/server/proxy/ProxySPDYToSPDYTest.java
index 064f556..0786e01 100644
--- a/jetty-spdy/spdy-http-server/src/test/java/org/eclipse/jetty/spdy/server/proxy/ProxySPDYToSPDYTest.java
+++ b/jetty-spdy/spdy-http-server/src/test/java/org/eclipse/jetty/spdy/server/proxy/ProxySPDYToSPDYTest.java
@@ -44,9 +44,9 @@
import org.eclipse.jetty.spdy.api.SynInfo;
import org.eclipse.jetty.spdy.api.server.ServerSessionFrameListener;
import org.eclipse.jetty.spdy.client.SPDYClient;
+import org.eclipse.jetty.spdy.http.HTTPSPDYHeader;
import org.eclipse.jetty.spdy.server.SPDYServerConnectionFactory;
import org.eclipse.jetty.spdy.server.SPDYServerConnector;
-import org.eclipse.jetty.spdy.server.http.HTTPSPDYHeader;
import org.eclipse.jetty.spdy.server.http.SPDYTestUtils;
import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.util.Fields;
@@ -169,7 +169,7 @@
}));
proxyConnector.addConnectionFactory(proxyConnector.getConnectionFactory("spdy/" + version));
- Session client = factory.newSPDYClient(version).connect(proxyAddress, null).get(5, TimeUnit.SECONDS);
+ Session client = factory.newSPDYClient(version).connect(proxyAddress, null);
final CountDownLatch replyLatch = new CountDownLatch(1);
Fields headers = SPDYTestUtils.createHeaders("localhost", proxyAddress.getPort(), version, "GET", "/");
@@ -215,7 +215,7 @@
{
resetLatch.countDown();
}
- }).get(5, TimeUnit.SECONDS);
+ });
Fields headers = SPDYTestUtils.createHeaders("localhost", proxyAddress.getPort(), version, "GET", "/");
headers.put(header, "bar");
@@ -247,7 +247,7 @@
}));
proxyConnector.addConnectionFactory(proxyConnector.getConnectionFactory("spdy/" + version));
- Session client = factory.newSPDYClient(version).connect(proxyAddress, null).get(5, TimeUnit.SECONDS);
+ Session client = factory.newSPDYClient(version).connect(proxyAddress, null);
final CountDownLatch replyLatch = new CountDownLatch(1);
final CountDownLatch dataLatch = new CountDownLatch(1);
@@ -318,7 +318,7 @@
final CountDownLatch pushSynLatch = new CountDownLatch(1);
final CountDownLatch pushDataLatch = new CountDownLatch(1);
- Session client = factory.newSPDYClient(version).connect(proxyAddress, null).get(5, TimeUnit.SECONDS);
+ Session client = factory.newSPDYClient(version).connect(proxyAddress, null);
Fields headers = new Fields();
headers.put(HTTPSPDYHeader.HOST.name(version), "localhost:" + proxyAddress.getPort());
@@ -417,7 +417,7 @@
final CountDownLatch pushSynLatch = new CountDownLatch(3);
final CountDownLatch pushDataLatch = new CountDownLatch(3);
- Session client = factory.newSPDYClient(version).connect(proxyAddress, null).get(5, TimeUnit.SECONDS);
+ Session client = factory.newSPDYClient(version).connect(proxyAddress, null);
Fields headers = new Fields();
headers.put(HTTPSPDYHeader.HOST.name(version), "localhost:" + proxyAddress.getPort());
@@ -516,7 +516,7 @@
{
pingLatch.countDown();
}
- }).get(5, TimeUnit.SECONDS);
+ });
client.ping(new PingInfo(5, TimeUnit.SECONDS));
@@ -552,7 +552,7 @@
{
resetLatch.countDown();
}
- }).get(5, TimeUnit.SECONDS);
+ });
Fields headers = new Fields();
headers.put(HTTPSPDYHeader.HOST.name(version), "localhost:" + proxyAddress.getPort());
diff --git a/jetty-spdy/spdy-http-server/src/test/resources/jetty-logging.properties b/jetty-spdy/spdy-http-server/src/test/resources/jetty-logging.properties
index 30da0a8..25faad4 100644
--- a/jetty-spdy/spdy-http-server/src/test/resources/jetty-logging.properties
+++ b/jetty-spdy/spdy-http-server/src/test/resources/jetty-logging.properties
@@ -5,4 +5,5 @@
#org.eclipse.jetty.spdy.LEVEL=DEBUG
#org.eclipse.jetty.client.LEVEL=DEBUG
#org.eclipse.jetty.spdy.server.http.ReferrerPushStrategy.LEVEL=DEBUG
+#org.eclipse.jetty.spdy.server.proxy.LEVEL=DEBUG
#org.mortbay.LEVEL=DEBUG
diff --git a/jetty-spdy/spdy-server/pom.xml b/jetty-spdy/spdy-server/pom.xml
index 699a2e1..ae44279 100644
--- a/jetty-spdy/spdy-server/pom.xml
+++ b/jetty-spdy/spdy-server/pom.xml
@@ -3,12 +3,12 @@
<parent>
<groupId>org.eclipse.jetty.spdy</groupId>
<artifactId>spdy-parent</artifactId>
- <version>9.0.5-SNAPSHOT</version>
+ <version>9.1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>spdy-server</artifactId>
- <name>Jetty :: SPDY :: Jetty Server Binding</name>
+ <name>Jetty :: SPDY :: Server Binding</name>
<properties>
<bundle-symbolic-name>${project.groupId}.server</bundle-symbolic-name>
@@ -58,7 +58,7 @@
</goals>
<configuration>
<instructions>
- <Export-Package>org.eclipse.jetty.spdy.server;version="9.0"</Export-Package>
+ <Export-Package>org.eclipse.jetty.spdy.server;version="9.1"</Export-Package>
<Import-Package>org.eclipse.jetty.npn,org.eclipse.jetty.*;version="[9.0,10.0)",*</Import-Package>
<_nouses>true</_nouses>
</instructions>
diff --git a/jetty-spdy/spdy-server/src/test/java/org/eclipse/jetty/spdy/server/AbstractTest.java b/jetty-spdy/spdy-server/src/test/java/org/eclipse/jetty/spdy/server/AbstractTest.java
index cdbe2ba..829ceb9 100644
--- a/jetty-spdy/spdy-server/src/test/java/org/eclipse/jetty/spdy/server/AbstractTest.java
+++ b/jetty-spdy/spdy-server/src/test/java/org/eclipse/jetty/spdy/server/AbstractTest.java
@@ -21,7 +21,6 @@
import java.net.InetSocketAddress;
import java.util.concurrent.Executor;
-import java.util.concurrent.TimeUnit;
import org.eclipse.jetty.server.ConnectionFactory;
import org.eclipse.jetty.server.Server;
@@ -114,7 +113,7 @@
clientFactory.start();
}
- return clientFactory.newSPDYClient(version).connect(socketAddress, listener).get(5, TimeUnit.SECONDS);
+ return clientFactory.newSPDYClient(version).connect(socketAddress, listener);
}
protected SPDYClient.Factory newSPDYClientFactory(Executor threadPool)
diff --git a/jetty-spdy/spdy-server/src/test/java/org/eclipse/jetty/spdy/server/IdleTimeoutTest.java b/jetty-spdy/spdy-server/src/test/java/org/eclipse/jetty/spdy/server/IdleTimeoutTest.java
index 708e6dc..0291fd3 100644
--- a/jetty-spdy/spdy-server/src/test/java/org/eclipse/jetty/spdy/server/IdleTimeoutTest.java
+++ b/jetty-spdy/spdy-server/src/test/java/org/eclipse/jetty/spdy/server/IdleTimeoutTest.java
@@ -173,7 +173,7 @@
clientFactory.start();
SPDYClient client = clientFactory.newSPDYClient(SPDY.V2);
client.setIdleTimeout(idleTimeout);
- Session session = client.connect(address, null).get(5, TimeUnit.SECONDS);
+ Session session = client.connect(address, null);
session.syn(new SynInfo(new Fields(), true), null);
@@ -199,7 +199,7 @@
clientFactory.start();
SPDYClient client = clientFactory.newSPDYClient(SPDY.V2);
client.setIdleTimeout(idleTimeout);
- Session session = client.connect(address, null).get(5, TimeUnit.SECONDS);
+ Session session = client.connect(address, null);
session.syn(new SynInfo(new Fields(), true), null);
@@ -232,7 +232,7 @@
clientFactory.start();
SPDYClient client = clientFactory.newSPDYClient(SPDY.V2);
client.setIdleTimeout(idleTimeout);
- Session session = client.connect(address, null).get(5, TimeUnit.SECONDS);
+ Session session = client.connect(address, null);
final CountDownLatch replyLatch = new CountDownLatch(1);
session.syn(new SynInfo(new Fields(), true), new StreamFrameListener.Adapter()
diff --git a/jetty-spdy/spdy-server/src/test/java/org/eclipse/jetty/spdy/server/SettingsTest.java b/jetty-spdy/spdy-server/src/test/java/org/eclipse/jetty/spdy/server/SettingsTest.java
index 3f7963b..692109a 100644
--- a/jetty-spdy/spdy-server/src/test/java/org/eclipse/jetty/spdy/server/SettingsTest.java
+++ b/jetty-spdy/spdy-server/src/test/java/org/eclipse/jetty/spdy/server/SettingsTest.java
@@ -159,7 +159,7 @@
Session sessionV2 = startClient(address, null);
sessionV2.settings(settingsInfo);
- Session sessionV3 = clientFactory.newSPDYClient(SPDY.V3).connect(address, null).get(5, TimeUnit.SECONDS);
+ Session sessionV3 = clientFactory.newSPDYClient(SPDY.V3).connect(address, null);
sessionV3.settings(settingsInfo);
Assert.assertTrue(settingsLatch.await(5, TimeUnit.SECONDS));
diff --git a/jetty-spring/pom.xml b/jetty-spring/pom.xml
index 0af3ab1..b285b77 100644
--- a/jetty-spring/pom.xml
+++ b/jetty-spring/pom.xml
@@ -2,7 +2,7 @@
<parent>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-project</artifactId>
- <version>9.0.5-SNAPSHOT</version>
+ <version>9.1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>jetty-spring</artifactId>
diff --git a/jetty-start/pom.xml b/jetty-start/pom.xml
index df3606b..9dfb945 100644
--- a/jetty-start/pom.xml
+++ b/jetty-start/pom.xml
@@ -2,7 +2,7 @@
<parent>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-project</artifactId>
- <version>9.0.5-SNAPSHOT</version>
+ <version>9.1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>jetty-start</artifactId>
diff --git a/jetty-start/src/main/resources/org/eclipse/jetty/start/start.config b/jetty-start/src/main/resources/org/eclipse/jetty/start/start.config
index 4dde6bf..0fffaf1 100644
--- a/jetty-start/src/main/resources/org/eclipse/jetty/start/start.config
+++ b/jetty-start/src/main/resources/org/eclipse/jetty/start/start.config
@@ -92,7 +92,8 @@
$(jetty.home)/lib/jetty-xml-$(version).jar ! available org.eclipse.jetty.xml.XmlParser
[Server,All,server,default]
-$(jetty.home)/lib/servlet-api-3.0.jar ! available javax.servlet.ServletContext
+$(jetty.home)/lib/servlet-api-3.1.jar ! available javax.servlet.ServletContext
+$(jetty.home)/lib/jetty-schemas-3.1.jar
$(jetty.home)/lib/jetty-http-$(version).jar ! available org.eclipse.jetty.http.HttpParser
$(jetty.home)/lib/jetty-continuation-$(version).jar ! available org.eclipse.jetty.continuation.Continuation
$(jetty.home)/lib/jetty-server-$(version).jar ! available org.eclipse.jetty.server.Server
diff --git a/jetty-util-ajax/pom.xml b/jetty-util-ajax/pom.xml
index 9a7f15f..4bc3464 100644
--- a/jetty-util-ajax/pom.xml
+++ b/jetty-util-ajax/pom.xml
@@ -2,7 +2,7 @@
<parent>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-project</artifactId>
- <version>9.0.5-SNAPSHOT</version>
+ <version>9.1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>jetty-util-ajax</artifactId>
@@ -25,7 +25,7 @@
</goals>
<configuration>
<instructions>
- <Import-Package>javax.servlet.*;version="2.6.0",org.slf4j;version="[1.5,2.0)";resolution:=optional,org.slf4j.impl;version="[1.5,2.0)";resolution:=optional,*</Import-Package>
+ <Import-Package>javax.servlet.*;version="[2.6.0,3.2)",org.slf4j;version="[1.5,2.0)";resolution:=optional,org.slf4j.impl;version="[1.5,2.0)";resolution:=optional,*</Import-Package>
</instructions>
</configuration>
</execution>
@@ -78,8 +78,8 @@
<version>${project.version}</version>
</dependency>
<dependency>
- <groupId>org.eclipse.jetty.orbit</groupId>
- <artifactId>javax.servlet</artifactId>
+ <groupId>javax.servlet</groupId>
+ <artifactId>javax.servlet-api</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
diff --git a/jetty-util/pom.xml b/jetty-util/pom.xml
index c0ceceb..a3a96ab 100644
--- a/jetty-util/pom.xml
+++ b/jetty-util/pom.xml
@@ -2,7 +2,7 @@
<parent>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-project</artifactId>
- <version>9.0.5-SNAPSHOT</version>
+ <version>9.1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>jetty-util</artifactId>
@@ -25,7 +25,7 @@
</goals>
<configuration>
<instructions>
- <Import-Package>javax.servlet.*;version="2.6.0",org.slf4j;version="[1.5,2.0)";resolution:=optional,org.slf4j.impl;version="[1.5,2.0)";resolution:=optional,*</Import-Package>
+ <Import-Package>javax.servlet.*;version="[2.6.0,3.2)",org.slf4j;version="[1.5,2.0)";resolution:=optional,org.slf4j.impl;version="[1.5,2.0)";resolution:=optional,*</Import-Package>
</instructions>
</configuration>
</execution>
@@ -71,8 +71,8 @@
</build>
<dependencies>
<dependency>
- <groupId>org.eclipse.jetty.orbit</groupId>
- <artifactId>javax.servlet</artifactId>
+ <groupId>javax.servlet</groupId>
+ <artifactId>javax.servlet-api</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
diff --git a/jetty-util/src/main/java/org/eclipse/jetty/util/ArrayQueue.java b/jetty-util/src/main/java/org/eclipse/jetty/util/ArrayQueue.java
index e4f04d6..4bebefd 100644
--- a/jetty-util/src/main/java/org/eclipse/jetty/util/ArrayQueue.java
+++ b/jetty-util/src/main/java/org/eclipse/jetty/util/ArrayQueue.java
@@ -48,6 +48,12 @@
{
this(DEFAULT_CAPACITY, -1);
}
+
+ /* ------------------------------------------------------------ */
+ public ArrayQueue(Object lock)
+ {
+ this(DEFAULT_CAPACITY, -1,lock);
+ }
/* ------------------------------------------------------------ */
public ArrayQueue(int capacity)
diff --git a/jetty-util/src/main/java/org/eclipse/jetty/util/BlockingArrayQueue.java b/jetty-util/src/main/java/org/eclipse/jetty/util/BlockingArrayQueue.java
index 7d34cfc..6c62b3b 100644
--- a/jetty-util/src/main/java/org/eclipse/jetty/util/BlockingArrayQueue.java
+++ b/jetty-util/src/main/java/org/eclipse/jetty/util/BlockingArrayQueue.java
@@ -34,29 +34,25 @@
/**
* A BlockingQueue backed by a circular array capable or growing.
* <p/>
- * This queue is uses a variant of the two lock queue algorithm to provide an
- * efficient queue or list backed by a growable circular array.
+ * This queue is uses a variant of the two lock queue algorithm to provide an efficient queue or list backed by a growable circular array.
* <p/>
- * Unlike {@link java.util.concurrent.ArrayBlockingQueue}, this class is
- * able to grow and provides a blocking put call.
+ * Unlike {@link java.util.concurrent.ArrayBlockingQueue}, this class is able to grow and provides a blocking put call.
* <p/>
- * The queue has both a capacity (the size of the array currently allocated)
- * and a max capacity (the maximum size that may be allocated), which defaults to
+ * The queue has both a capacity (the size of the array currently allocated) and a max capacity (the maximum size that may be allocated), which defaults to
* {@link Integer#MAX_VALUE}.
- *
- * @param <E> The element type
+ *
+ * @param <E>
+ * The element type
*/
public class BlockingArrayQueue<E> extends AbstractList<E> implements BlockingQueue<E>
{
/**
- * The head offset in the {@link #_indexes} array, displaced
- * by 15 slots to avoid false sharing with the array length
- * (stored before the first element of the array itself).
+ * The head offset in the {@link #_indexes} array, displaced by 15 slots to avoid false sharing with the array length (stored before the first element of
+ * the array itself).
*/
private static final int HEAD_OFFSET = MemoryUtils.getIntegersPerCacheLine() - 1;
/**
- * The tail offset in the {@link #_indexes} array, displaced
- * by 16 slots from the head to avoid false sharing with it.
+ * The tail offset in the {@link #_indexes} array, displaced by 16 slots from the head to avoid false sharing with it.
*/
private static final int TAIL_OFFSET = HEAD_OFFSET + MemoryUtils.getIntegersPerCacheLine();
/**
@@ -82,7 +78,7 @@
/**
* Creates an unbounded {@link BlockingArrayQueue} with default initial capacity and grow factor.
- *
+ *
* @see #DEFAULT_CAPACITY
* @see #DEFAULT_GROWTH
*/
@@ -94,10 +90,10 @@
}
/**
- * Creates a bounded {@link BlockingArrayQueue} that does not grow.
- * The capacity of the queue is fixed and equal to the given parameter.
- *
- * @param maxCapacity the maximum capacity
+ * Creates a bounded {@link BlockingArrayQueue} that does not grow. The capacity of the queue is fixed and equal to the given parameter.
+ *
+ * @param maxCapacity
+ * the maximum capacity
*/
public BlockingArrayQueue(int maxCapacity)
{
@@ -108,9 +104,11 @@
/**
* Creates an unbounded {@link BlockingArrayQueue} that grows by the given parameter.
- *
- * @param capacity the initial capacity
- * @param growBy the growth factor
+ *
+ * @param capacity
+ * the initial capacity
+ * @param growBy
+ * the growth factor
*/
public BlockingArrayQueue(int capacity, int growBy)
{
@@ -121,10 +119,13 @@
/**
* Create a bounded {@link BlockingArrayQueue} that grows by the given parameter.
- *
- * @param capacity the initial capacity
- * @param growBy the growth factor
- * @param maxCapacity the maximum capacity
+ *
+ * @param capacity
+ * the initial capacity
+ * @param growBy
+ * the growth factor
+ * @param maxCapacity
+ * the maximum capacity
*/
public BlockingArrayQueue(int capacity, int growBy, int maxCapacity)
{
@@ -136,18 +137,18 @@
}
/*----------------------------------------------------------------------------*/
- /* Collection methods */
+ /* Collection methods */
/*----------------------------------------------------------------------------*/
@Override
public void clear()
{
- final Lock tailLock = _tailLock;
- tailLock.lock();
+
+ _tailLock.lock();
try
{
- final Lock headLock = _headLock;
- headLock.lock();
+
+ _headLock.lock();
try
{
_indexes[HEAD_OFFSET] = 0;
@@ -156,12 +157,12 @@
}
finally
{
- headLock.unlock();
+ _headLock.unlock();
}
}
finally
{
- tailLock.unlock();
+ _tailLock.unlock();
}
}
@@ -178,7 +179,7 @@
}
/*----------------------------------------------------------------------------*/
- /* Queue methods */
+ /* Queue methods */
/*----------------------------------------------------------------------------*/
@SuppressWarnings("unchecked")
@@ -189,8 +190,8 @@
return null;
E e = null;
- final Lock headLock = _headLock;
- headLock.lock(); // Size cannot shrink
+
+ _headLock.lock(); // Size cannot shrink
try
{
if (_size.get() > 0)
@@ -205,7 +206,7 @@
}
finally
{
- headLock.unlock();
+ _headLock.unlock();
}
return e;
}
@@ -218,8 +219,8 @@
return null;
E e = null;
- final Lock headLock = _headLock;
- headLock.lock(); // Size cannot shrink
+
+ _headLock.lock(); // Size cannot shrink
try
{
if (_size.get() > 0)
@@ -227,7 +228,7 @@
}
finally
{
- headLock.unlock();
+ _headLock.unlock();
}
return e;
}
@@ -251,7 +252,7 @@
}
/*----------------------------------------------------------------------------*/
- /* BlockingQueue methods */
+ /* BlockingQueue methods */
/*----------------------------------------------------------------------------*/
@Override
@@ -259,10 +260,8 @@
{
Objects.requireNonNull(e);
- final Lock tailLock = _tailLock;
- final Lock headLock = _headLock;
boolean notEmpty = false;
- tailLock.lock(); // Size cannot grow... only shrink
+ _tailLock.lock(); // Size cannot grow... only shrink
try
{
int size = _size.get();
@@ -272,7 +271,7 @@
// Should we expand array?
if (size == _elements.length)
{
- headLock.lock();
+ _headLock.lock();
try
{
if (!grow())
@@ -280,7 +279,7 @@
}
finally
{
- headLock.unlock();
+ _headLock.unlock();
}
}
@@ -292,19 +291,19 @@
}
finally
{
- tailLock.unlock();
+ _tailLock.unlock();
}
if (notEmpty)
{
- headLock.lock();
+ _headLock.lock();
try
{
_notEmpty.signal();
}
finally
{
- headLock.unlock();
+ _headLock.unlock();
}
}
@@ -339,8 +338,8 @@
public E take() throws InterruptedException
{
E e = null;
- final Lock headLock = _headLock;
- headLock.lockInterruptibly(); // Size cannot shrink
+
+ _headLock.lockInterruptibly(); // Size cannot shrink
try
{
try
@@ -366,7 +365,7 @@
}
finally
{
- headLock.unlock();
+ _headLock.unlock();
}
return e;
}
@@ -377,8 +376,8 @@
{
long nanos = unit.toNanos(time);
E e = null;
- final Lock headLock = _headLock;
- headLock.lockInterruptibly(); // Size cannot shrink
+
+ _headLock.lockInterruptibly(); // Size cannot shrink
try
{
try
@@ -406,7 +405,7 @@
}
finally
{
- headLock.unlock();
+ _headLock.unlock();
}
return e;
}
@@ -414,12 +413,12 @@
@Override
public boolean remove(Object o)
{
- final Lock tailLock = _tailLock;
- tailLock.lock();
+
+ _tailLock.lock();
try
{
- final Lock headLock = _headLock;
- headLock.lock();
+
+ _headLock.lock();
try
{
if (isEmpty())
@@ -432,9 +431,9 @@
int i = head;
while (true)
{
- if (Objects.equals(_elements[i], o))
+ if (Objects.equals(_elements[i],o))
{
- remove(i >= head ? i - head : capacity - head + i);
+ remove(i >= head?i - head:capacity - head + i);
return true;
}
++i;
@@ -446,36 +445,36 @@
}
finally
{
- headLock.unlock();
+ _headLock.unlock();
}
}
finally
{
- tailLock.unlock();
+ _tailLock.unlock();
}
}
@Override
public int remainingCapacity()
{
- final Lock tailLock = _tailLock;
- tailLock.lock();
+
+ _tailLock.lock();
try
{
- final Lock headLock = _headLock;
- headLock.lock();
+
+ _headLock.lock();
try
{
return getCapacity() - size();
}
finally
{
- headLock.unlock();
+ _headLock.unlock();
}
}
finally
{
- tailLock.unlock();
+ _tailLock.unlock();
}
}
@@ -492,19 +491,19 @@
}
/*----------------------------------------------------------------------------*/
- /* List methods */
+ /* List methods */
/*----------------------------------------------------------------------------*/
@SuppressWarnings("unchecked")
@Override
public E get(int index)
{
- final Lock tailLock = _tailLock;
- tailLock.lock();
+
+ _tailLock.lock();
try
{
- final Lock headLock = _headLock;
- headLock.lock();
+
+ _headLock.lock();
try
{
if (index < 0 || index >= _size.get())
@@ -517,12 +516,12 @@
}
finally
{
- headLock.unlock();
+ _headLock.unlock();
}
}
finally
{
- tailLock.unlock();
+ _tailLock.unlock();
}
}
@@ -532,12 +531,11 @@
if (e == null)
throw new NullPointerException();
- final Lock tailLock = _tailLock;
- tailLock.lock();
+ _tailLock.lock();
try
{
- final Lock headLock = _headLock;
- headLock.lock();
+
+ _headLock.lock();
try
{
final int size = _size.get();
@@ -568,30 +566,30 @@
if (i < tail)
{
- System.arraycopy(_elements, i, _elements, i + 1, tail - i);
+ System.arraycopy(_elements,i,_elements,i + 1,tail - i);
_elements[i] = e;
}
else
{
if (tail > 0)
{
- System.arraycopy(_elements, 0, _elements, 1, tail);
+ System.arraycopy(_elements,0,_elements,1,tail);
_elements[0] = _elements[capacity - 1];
}
- System.arraycopy(_elements, i, _elements, i + 1, capacity - i - 1);
+ System.arraycopy(_elements,i,_elements,i + 1,capacity - i - 1);
_elements[i] = e;
}
}
}
finally
{
- headLock.unlock();
+ _headLock.unlock();
}
}
finally
{
- tailLock.unlock();
+ _tailLock.unlock();
}
}
@@ -601,12 +599,11 @@
{
Objects.requireNonNull(e);
- final Lock tailLock = _tailLock;
- tailLock.lock();
+ _tailLock.lock();
try
{
- final Lock headLock = _headLock;
- headLock.lock();
+
+ _headLock.lock();
try
{
if (index < 0 || index >= _size.get())
@@ -622,12 +619,12 @@
}
finally
{
- headLock.unlock();
+ _headLock.unlock();
}
}
finally
{
- tailLock.unlock();
+ _tailLock.unlock();
}
}
@@ -635,12 +632,12 @@
@Override
public E remove(int index)
{
- final Lock tailLock = _tailLock;
- tailLock.lock();
+
+ _tailLock.lock();
try
{
- final Lock headLock = _headLock;
- headLock.lock();
+
+ _headLock.lock();
try
{
if (index < 0 || index >= _size.get())
@@ -655,16 +652,16 @@
int tail = _indexes[TAIL_OFFSET];
if (i < tail)
{
- System.arraycopy(_elements, i + 1, _elements, i, tail - i);
+ System.arraycopy(_elements,i + 1,_elements,i,tail - i);
--_indexes[TAIL_OFFSET];
}
else
{
- System.arraycopy(_elements, i + 1, _elements, i, capacity - i - 1);
+ System.arraycopy(_elements,i + 1,_elements,i,capacity - i - 1);
_elements[capacity - 1] = _elements[0];
if (tail > 0)
{
- System.arraycopy(_elements, 1, _elements, 0, tail);
+ System.arraycopy(_elements,1,_elements,0,tail);
--_indexes[TAIL_OFFSET];
}
else
@@ -680,24 +677,24 @@
}
finally
{
- headLock.unlock();
+ _headLock.unlock();
}
}
finally
{
- tailLock.unlock();
+ _tailLock.unlock();
}
}
@Override
public ListIterator<E> listIterator(int index)
{
- final Lock tailLock = _tailLock;
- tailLock.lock();
+
+ _tailLock.lock();
try
{
- final Lock headLock = _headLock;
- headLock.lock();
+
+ _headLock.lock();
try
{
Object[] elements = new Object[size()];
@@ -707,30 +704,30 @@
int tail = _indexes[TAIL_OFFSET];
if (head < tail)
{
- System.arraycopy(_elements, head, elements, 0, tail - head);
+ System.arraycopy(_elements,head,elements,0,tail - head);
}
else
{
int chunk = _elements.length - head;
- System.arraycopy(_elements, head, elements, 0, chunk);
- System.arraycopy(_elements, 0, elements, chunk, tail);
+ System.arraycopy(_elements,head,elements,0,chunk);
+ System.arraycopy(_elements,0,elements,chunk,tail);
}
}
- return new Itr(elements, index);
+ return new Itr(elements,index);
}
finally
{
- headLock.unlock();
+ _headLock.unlock();
}
}
finally
{
- tailLock.unlock();
+ _tailLock.unlock();
}
}
/*----------------------------------------------------------------------------*/
- /* Additional methods */
+ /* Additional methods */
/*----------------------------------------------------------------------------*/
/**
@@ -738,7 +735,15 @@
*/
public int getCapacity()
{
- return _elements.length;
+ _tailLock.lock();
+ try
+ {
+ return _elements.length;
+ }
+ finally
+ {
+ _tailLock.unlock();
+ }
}
/**
@@ -750,7 +755,7 @@
}
/*----------------------------------------------------------------------------*/
- /* Implementation methods */
+ /* Implementation methods */
/*----------------------------------------------------------------------------*/
private boolean grow()
@@ -758,12 +763,11 @@
if (_growCapacity <= 0)
return false;
- final Lock tailLock = _tailLock;
- tailLock.lock();
+ _tailLock.lock();
try
{
- final Lock headLock = _headLock;
- headLock.lock();
+
+ _headLock.lock();
try
{
final int head = _indexes[HEAD_OFFSET];
@@ -776,14 +780,14 @@
if (head < tail)
{
newTail = tail - head;
- System.arraycopy(_elements, head, elements, 0, newTail);
+ System.arraycopy(_elements,head,elements,0,newTail);
}
else if (head > tail || _size.get() > 0)
{
newTail = capacity + tail - head;
int cut = capacity - head;
- System.arraycopy(_elements, head, elements, 0, cut);
- System.arraycopy(_elements, 0, elements, cut, tail);
+ System.arraycopy(_elements,head,elements,0,cut);
+ System.arraycopy(_elements,0,elements,cut,tail);
}
else
{
@@ -797,12 +801,12 @@
}
finally
{
- headLock.unlock();
+ _headLock.unlock();
}
}
finally
{
- tailLock.unlock();
+ _tailLock.unlock();
}
}
diff --git a/jetty-util/src/main/java/org/eclipse/jetty/util/BufferUtil.java b/jetty-util/src/main/java/org/eclipse/jetty/util/BufferUtil.java
index 198e9e7..3c77949 100644
--- a/jetty-util/src/main/java/org/eclipse/jetty/util/BufferUtil.java
+++ b/jetty-util/src/main/java/org/eclipse/jetty/util/BufferUtil.java
@@ -30,6 +30,8 @@
import java.nio.channels.FileChannel.MapMode;
import java.nio.charset.Charset;
+import org.eclipse.jetty.util.resource.Resource;
+
/* ------------------------------------------------------------------------------- */
/**
@@ -787,7 +789,7 @@
return ByteBuffer.wrap(array, offset, length);
}
- public static ByteBuffer toBuffer(File file) throws IOException
+ public static ByteBuffer toMappedBuffer(File file) throws IOException
{
try (RandomAccessFile raf = new RandomAccessFile(file, "r"))
{
@@ -795,6 +797,29 @@
}
}
+ public static ByteBuffer toBuffer(Resource resource,boolean direct) throws IOException
+ {
+ int len=(int)resource.length();
+ if (len<0)
+ throw new IllegalArgumentException("invalid resource: "+String.valueOf(resource)+" len="+len);
+
+ ByteBuffer buffer = direct?BufferUtil.allocateDirect(len):BufferUtil.allocate(len);
+
+ int pos=BufferUtil.flipToFill(buffer);
+ if (resource.getFile()!=null)
+ BufferUtil.readFrom(resource.getFile(),buffer);
+ else
+ {
+ try (InputStream is = resource.getInputStream();)
+ {
+ BufferUtil.readFrom(is,len,buffer);
+ }
+ }
+ BufferUtil.flipToFlush(buffer,pos);
+
+ return buffer;
+ }
+
public static String toSummaryString(ByteBuffer buffer)
{
if (buffer == null)
diff --git a/jetty-util/src/main/java/org/eclipse/jetty/util/IteratingCallback.java b/jetty-util/src/main/java/org/eclipse/jetty/util/IteratingCallback.java
index 0c52fa6..bb5bce1 100644
--- a/jetty-util/src/main/java/org/eclipse/jetty/util/IteratingCallback.java
+++ b/jetty-util/src/main/java/org/eclipse/jetty/util/IteratingCallback.java
@@ -23,32 +23,28 @@
/* ------------------------------------------------------------ */
/** Iterating Callback.
- * <p>This specialised callback is used when breaking up an
+ * <p>This specialized callback is used when breaking up an
* asynchronous task into smaller asynchronous tasks. A typical pattern
* is that a successful callback is used to schedule the next sub task, but
* if that task completes quickly and uses the calling thread to callback
* the success notification, this can result in a growing stack depth.
* </p>
- * <p>To avoid this issue, this callback uses an Atomicboolean to note
+ * <p>To avoid this issue, this callback uses an AtomicBoolean to note
* if the success callback has been called during the processing of a
* sub task, and if so then the processing iterates rather than recurses.
* </p>
* <p>This callback is passed to the asynchronous handling of each sub
* task and a call the {@link #succeeded()} on this call back represents
* completion of the subtask. Only once all the subtasks are completed is
- * the {@link Callback#succeeded()} method called on the {@link Callback} instance
- * passed the the {@link #IteratingCallback(Callback)} constructor.</p>
+ * the {#completed()} method called.</p>
*
*/
public abstract class IteratingCallback implements Callback
{
- final AtomicBoolean _iterating = new AtomicBoolean();
- final Callback _callback;
+ private final AtomicBoolean _iterating = new AtomicBoolean();
-
- public IteratingCallback(Callback callback)
+ public IteratingCallback()
{
- _callback=callback;
}
/* ------------------------------------------------------------ */
@@ -63,6 +59,8 @@
*/
abstract protected boolean process() throws Exception;
+ abstract protected void completed();
+
/* ------------------------------------------------------------ */
/** This method is called initially to start processing and
* is then called by subsequent sub task success to continue
@@ -78,7 +76,7 @@
// process and test if we are complete
if (process())
{
- _callback.succeeded();
+ completed();
return;
}
}
@@ -86,26 +84,19 @@
catch(Exception e)
{
_iterating.set(false);
- _callback.failed(e);
+ failed(e);
}
finally
{
_iterating.set(false);
}
}
-
-
+
+ /* ------------------------------------------------------------ */
@Override
public void succeeded()
{
if (!_iterating.compareAndSet(true,false))
iterate();
}
-
- @Override
- public void failed(Throwable x)
- {
- _callback.failed(x);
- }
-
}
diff --git a/jetty-util/src/main/java/org/eclipse/jetty/util/IteratingNestedCallback.java b/jetty-util/src/main/java/org/eclipse/jetty/util/IteratingNestedCallback.java
new file mode 100644
index 0000000..375bb7d
--- /dev/null
+++ b/jetty-util/src/main/java/org/eclipse/jetty/util/IteratingNestedCallback.java
@@ -0,0 +1,67 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util;
+
+
+/* ------------------------------------------------------------ */
+/** Iterating Nested Callback.
+ * <p>This specialized callback is used when breaking up an
+ * asynchronous task into smaller asynchronous tasks. A typical pattern
+ * is that a successful callback is used to schedule the next sub task, but
+ * if that task completes quickly and uses the calling thread to callback
+ * the success notification, this can result in a growing stack depth.
+ * </p>
+ * <p>To avoid this issue, this callback uses an AtomicBoolean to note
+ * if the success callback has been called during the processing of a
+ * sub task, and if so then the processing iterates rather than recurses.
+ * </p>
+ * <p>This callback is passed to the asynchronous handling of each sub
+ * task and a call the {@link #succeeded()} on this call back represents
+ * completion of the subtask. Only once all the subtasks are completed is
+ * the {@link Callback#succeeded()} method called on the {@link Callback} instance
+ * passed the the {@link #IteratingCallback(Callback)} constructor.</p>
+ *
+ */
+public abstract class IteratingNestedCallback extends IteratingCallback
+{
+ final Callback _callback;
+
+ public IteratingNestedCallback(Callback callback)
+ {
+ _callback=callback;
+ }
+
+ @Override
+ protected void completed()
+ {
+ _callback.succeeded();
+ }
+
+ @Override
+ public void failed(Throwable x)
+ {
+ _callback.failed(x);
+ }
+
+ @Override
+ public String toString()
+ {
+ return String.format("%s@%x",getClass().getSimpleName(),hashCode());
+ }
+}
diff --git a/jetty-util/src/main/java/org/eclipse/jetty/util/Jetty.java b/jetty-util/src/main/java/org/eclipse/jetty/util/Jetty.java
index d79365c..805a310 100644
--- a/jetty-util/src/main/java/org/eclipse/jetty/util/Jetty.java
+++ b/jetty-util/src/main/java/org/eclipse/jetty/util/Jetty.java
@@ -30,7 +30,7 @@
pkg.getImplementationVersion() != null)
VERSION = pkg.getImplementationVersion();
else
- VERSION = System.getProperty("jetty.version", "9.0.z-SNAPSHOT");
+ VERSION = System.getProperty("jetty.version", "9.1.z-SNAPSHOT");
}
private Jetty()
diff --git a/jetty-util/src/main/java/org/eclipse/jetty/util/MultiPartInputStreamParser.java b/jetty-util/src/main/java/org/eclipse/jetty/util/MultiPartInputStreamParser.java
index f47a197..ee29487 100644
--- a/jetty-util/src/main/java/org/eclipse/jetty/util/MultiPartInputStreamParser.java
+++ b/jetty-util/src/main/java/org/eclipse/jetty/util/MultiPartInputStreamParser.java
@@ -216,6 +216,16 @@
}
}
+
+ /**
+ * @see javax.servlet.http.Part#getSubmittedFileName()
+ */
+ @Override
+ public String getSubmittedFileName()
+ {
+ return getContentDispositionFilename();
+ }
+
public byte[] getBytes()
{
if (_bout!=null)
diff --git a/jetty-util/src/main/java/org/eclipse/jetty/util/UrlEncoded.java b/jetty-util/src/main/java/org/eclipse/jetty/util/UrlEncoded.java
index dfe0203..b922d83 100644
--- a/jetty-util/src/main/java/org/eclipse/jetty/util/UrlEncoded.java
+++ b/jetty-util/src/main/java/org/eclipse/jetty/util/UrlEncoded.java
@@ -83,6 +83,11 @@
{
}
+ public UrlEncoded(String query)
+ {
+ decodeTo(query,this,ENCODING,-1);
+ }
+
/* ----------------------------------------------------------------- */
public void decode(String query)
{
diff --git a/jetty-util/src/main/java/org/eclipse/jetty/util/security/Constraint.java b/jetty-util/src/main/java/org/eclipse/jetty/util/security/Constraint.java
index 4d2d911..85ae39f 100644
--- a/jetty-util/src/main/java/org/eclipse/jetty/util/security/Constraint.java
+++ b/jetty-util/src/main/java/org/eclipse/jetty/util/security/Constraint.java
@@ -23,6 +23,8 @@
/* ------------------------------------------------------------ */
/**
+ * Constraint
+ *
* Describe an auth and/or data constraint.
*
*
@@ -65,6 +67,8 @@
public final static String NONE = "NONE";
public final static String ANY_ROLE = "*";
+
+ public final static String ANY_AUTH = "**"; //Servlet Spec 3.1 pg 140
/* ------------------------------------------------------------ */
private String _name;
@@ -74,6 +78,8 @@
private int _dataConstraint = DC_UNSET;
private boolean _anyRole = false;
+
+ private boolean _anyAuth = false;
private boolean _authenticate = false;
@@ -119,9 +125,15 @@
{
_roles = roles;
_anyRole = false;
+ _anyAuth = false;
if (roles != null)
- for (int i = roles.length; !_anyRole && i-- > 0;)
+ {
+ for (int i = roles.length; i-- > 0;)
+ {
_anyRole |= ANY_ROLE.equals(roles[i]);
+ _anyAuth |= ANY_AUTH.equals(roles[i]);
+ }
+ }
}
/* ------------------------------------------------------------ */
@@ -132,6 +144,16 @@
{
return _anyRole;
}
+
+
+ /* ------------------------------------------------------------ */
+ /** Servlet Spec 3.1, pg 140
+ * @return True if any authenticated user is permitted (ie a role "**" was specified in the constraint).
+ */
+ public boolean isAnyAuth()
+ {
+ return _anyAuth;
+ }
/* ------------------------------------------------------------ */
/**
diff --git a/jetty-util/src/test/java/org/eclipse/jetty/util/BenchmarkHelper.java b/jetty-util/src/test/java/org/eclipse/jetty/util/BenchmarkHelper.java
index 49e7195..0f0ced9 100644
--- a/jetty-util/src/test/java/org/eclipse/jetty/util/BenchmarkHelper.java
+++ b/jetty-util/src/test/java/org/eclipse/jetty/util/BenchmarkHelper.java
@@ -39,14 +39,14 @@
private final CompilationMXBean jitCompiler;
private final MemoryMXBean heapMemory;
private final AtomicInteger starts = new AtomicInteger();
- private volatile MemoryPoolMXBean youngMemoryPool;
- private volatile MemoryPoolMXBean survivorMemoryPool;
- private volatile MemoryPoolMXBean oldMemoryPool;
- private volatile boolean hasMemoryPools;
+ private final MemoryPoolMXBean youngMemoryPool;
+ private final MemoryPoolMXBean survivorMemoryPool;
+ private final MemoryPoolMXBean oldMemoryPool;
+ private final boolean hasMemoryPools;
+ private final GarbageCollectorMXBean youngCollector;
+ private final GarbageCollectorMXBean oldCollector;
+ private final boolean hasCollectors;
private volatile ScheduledFuture<?> memoryPoller;
- private volatile GarbageCollectorMXBean youngCollector;
- private volatile GarbageCollectorMXBean oldCollector;
- private volatile boolean hasCollectors;
private volatile ScheduledExecutorService scheduler;
private volatile boolean polling;
private volatile long lastYoungUsed;
@@ -70,35 +70,47 @@
this.heapMemory = ManagementFactory.getMemoryMXBean();
List<MemoryPoolMXBean> memoryPools = ManagementFactory.getMemoryPoolMXBeans();
+ MemoryPoolMXBean ymp=null;
+ MemoryPoolMXBean smp=null;
+ MemoryPoolMXBean omp=null;
+
for (MemoryPoolMXBean memoryPool : memoryPools)
{
if ("PS Eden Space".equals(memoryPool.getName()) ||
"Par Eden Space".equals(memoryPool.getName()) ||
"G1 Eden".equals(memoryPool.getName()))
- youngMemoryPool = memoryPool;
+ ymp = memoryPool;
else if ("PS Survivor Space".equals(memoryPool.getName()) ||
"Par Survivor Space".equals(memoryPool.getName()) ||
"G1 Survivor".equals(memoryPool.getName()))
- survivorMemoryPool = memoryPool;
+ smp = memoryPool;
else if ("PS Old Gen".equals(memoryPool.getName()) ||
"CMS Old Gen".equals(memoryPool.getName()) ||
"G1 Old Gen".equals(memoryPool.getName()))
- oldMemoryPool = memoryPool;
+ omp = memoryPool;
}
+ youngMemoryPool=ymp;
+ survivorMemoryPool=smp;
+ oldMemoryPool=omp;
+
hasMemoryPools = youngMemoryPool != null && survivorMemoryPool != null && oldMemoryPool != null;
List<GarbageCollectorMXBean> garbageCollectors = ManagementFactory.getGarbageCollectorMXBeans();
+ GarbageCollectorMXBean yc=null;
+ GarbageCollectorMXBean oc=null;
for (GarbageCollectorMXBean garbageCollector : garbageCollectors)
{
if ("PS Scavenge".equals(garbageCollector.getName()) ||
"ParNew".equals(garbageCollector.getName()) ||
"G1 Young Generation".equals(garbageCollector.getName()))
- youngCollector = garbageCollector;
+ yc = garbageCollector;
else if ("PS MarkSweep".equals(garbageCollector.getName()) ||
"ConcurrentMarkSweep".equals(garbageCollector.getName()) ||
"G1 Old Generation".equals(garbageCollector.getName()))
- oldCollector = garbageCollector;
+ oc = garbageCollector;
}
+ youngCollector=yc;
+ oldCollector=oc;
hasCollectors = youngCollector != null && oldCollector != null;
}
diff --git a/jetty-util/src/test/java/org/eclipse/jetty/util/MultiPartInputStreamTest.java b/jetty-util/src/test/java/org/eclipse/jetty/util/MultiPartInputStreamTest.java
index a352d12..e096b24 100644
--- a/jetty-util/src/test/java/org/eclipse/jetty/util/MultiPartInputStreamTest.java
+++ b/jetty-util/src/test/java/org/eclipse/jetty/util/MultiPartInputStreamTest.java
@@ -589,7 +589,7 @@
mpis.setDeleteOnExit(true);
Collection<Part> parts = mpis.getParts();
assertThat(parts.size(), is(1));
- assertThat(((MultiPartInputStreamParser.MultiPart)parts.iterator().next()).getContentDispositionFilename(), is("Taken on Aug 22 \\ 2012.jpg"));
+ assertThat(((MultiPartInputStreamParser.MultiPart)parts.iterator().next()).getSubmittedFileName(), is("Taken on Aug 22 \\ 2012.jpg"));
}
@Test
@@ -611,7 +611,7 @@
mpis.setDeleteOnExit(true);
Collection<Part> parts = mpis.getParts();
assertThat(parts.size(), is(1));
- assertThat(((MultiPartInputStreamParser.MultiPart)parts.iterator().next()).getContentDispositionFilename(), is("c:\\this\\really\\is\\some\\path\\to\\a\\file.txt"));
+ assertThat(((MultiPartInputStreamParser.MultiPart)parts.iterator().next()).getSubmittedFileName(), is("c:\\this\\really\\is\\some\\path\\to\\a\\file.txt"));
}
@Test
@@ -632,7 +632,7 @@
mpis.setDeleteOnExit(true);
Collection<Part> parts = mpis.getParts();
assertThat(parts.size(), is(1));
- assertThat(((MultiPartInputStreamParser.MultiPart)parts.iterator().next()).getContentDispositionFilename(), is("c:\\this\\really\\is\\some\\path\\to\\a\\file.txt"));
+ assertThat(((MultiPartInputStreamParser.MultiPart)parts.iterator().next()).getSubmittedFileName(), is("c:\\this\\really\\is\\some\\path\\to\\a\\file.txt"));
}
public void testMulti ()
@@ -683,7 +683,7 @@
assertFalse(f2.exists()); //2nd written file was explicitly deleted
MultiPart stuff = (MultiPart)mpis.getPart("stuff");
- assertThat(stuff.getContentDispositionFilename(), is(filename));
+ assertThat(stuff.getSubmittedFileName(), is(filename));
assertThat(stuff.getContentType(),is("text/plain"));
assertThat(stuff.getHeader("Content-Type"),is("text/plain"));
assertThat(stuff.getHeaders("content-type").size(),is(1));
diff --git a/jetty-util/src/test/resources/TestData/WindowsDir.zip b/jetty-util/src/test/resources/TestData/WindowsDir.zip
new file mode 100644
index 0000000..26f2357
--- /dev/null
+++ b/jetty-util/src/test/resources/TestData/WindowsDir.zip
Binary files differ
diff --git a/jetty-webapp/pom.xml b/jetty-webapp/pom.xml
index 4428c65..b5d5aef 100644
--- a/jetty-webapp/pom.xml
+++ b/jetty-webapp/pom.xml
@@ -2,7 +2,7 @@
<parent>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-project</artifactId>
- <version>9.0.5-SNAPSHOT</version>
+ <version>9.1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>jetty-webapp</artifactId>
@@ -55,7 +55,7 @@
</goals>
<configuration>
<instructions>
- <Import-Package>javax.servlet.*;version="2.6.0",*</Import-Package>
+ <Import-Package>javax.servlet.*;version="[2.6.0,3.2]",*</Import-Package>
</instructions>
</configuration>
</execution>
diff --git a/jetty-webapp/src/main/java/org/eclipse/jetty/webapp/StandardDescriptorProcessor.java b/jetty-webapp/src/main/java/org/eclipse/jetty/webapp/StandardDescriptorProcessor.java
index 2986c74..11cdb03 100644
--- a/jetty-webapp/src/main/java/org/eclipse/jetty/webapp/StandardDescriptorProcessor.java
+++ b/jetty-webapp/src/main/java/org/eclipse/jetty/webapp/StandardDescriptorProcessor.java
@@ -92,6 +92,7 @@
registerVisitor("filter-mapping", this.getClass().getDeclaredMethod("visitFilterMapping", __signature));
registerVisitor("listener", this.getClass().getDeclaredMethod("visitListener", __signature));
registerVisitor("distributable", this.getClass().getDeclaredMethod("visitDistributable", __signature));
+ registerVisitor("deny-uncovered-http-methods", this.getClass().getDeclaredMethod("visitDenyUncoveredHttpMethods", __signature));
}
catch (Exception e)
{
@@ -622,16 +623,15 @@
{
//no servlet mappings
context.getMetaData().setOrigin(servlet_name+".servlet.mappings", descriptor);
- ServletMapping mapping = addServletMapping(servlet_name, node, context, descriptor);
- mapping.setDefault(context.getMetaData().getOrigin(servlet_name+".servlet.mappings") == Origin.WebDefaults);
+ addServletMapping(servlet_name, node, context, descriptor);
break;
}
- case WebXml:
case WebDefaults:
+ case WebXml:
case WebOverride:
{
//previously set by a web xml descriptor, if we're parsing another web xml descriptor allow override
- //otherwise just ignore it
+ //otherwise just ignore it as web.xml takes precedence (pg 8-81 5.g.vi)
if (!(descriptor instanceof FragmentDescriptor))
{
addServletMapping(servlet_name, node, context, descriptor);
@@ -1180,7 +1180,8 @@
{
ServletMapping mapping = new ServletMapping();
mapping.setServletName(servletName);
-
+ mapping.setDefault(descriptor instanceof DefaultsDescriptor);
+
List<String> paths = new ArrayList<String>();
Iterator<XmlParser.Node> iter = node.iterator("url-pattern");
while (iter.hasNext())
@@ -1887,6 +1888,21 @@
//Servlet Spec 3.0 p.74 distributable only if all fragments are distributable
((WebDescriptor)descriptor).setDistributable(true);
}
+
+
+ /**
+ * Servlet spec 3.1. When present in web.xml, this means that http methods that are
+ * not covered by security constraints should have access denied.
+ *
+ * See section 13.8.4, pg 145
+ * @param context
+ * @param descriptor
+ * @param node
+ */
+ protected void visitDenyUncoveredHttpMethods(WebAppContext context, Descriptor descriptor, XmlParser.Node node)
+ {
+ ((ConstraintAware)context.getSecurityHandler()).setDenyUncoveredHttpMethods(true);
+ }
/**
* @param context
diff --git a/jetty-webapp/src/main/java/org/eclipse/jetty/webapp/WebAppContext.java b/jetty-webapp/src/main/java/org/eclipse/jetty/webapp/WebAppContext.java
index c52b7bc..ae43203 100644
--- a/jetty-webapp/src/main/java/org/eclipse/jetty/webapp/WebAppContext.java
+++ b/jetty-webapp/src/main/java/org/eclipse/jetty/webapp/WebAppContext.java
@@ -41,6 +41,7 @@
import javax.servlet.http.HttpSessionActivationListener;
import javax.servlet.http.HttpSessionAttributeListener;
import javax.servlet.http.HttpSessionBindingListener;
+import javax.servlet.http.HttpSessionIdListener;
import javax.servlet.http.HttpSessionListener;
import org.eclipse.jetty.security.ConstraintAware;
@@ -94,6 +95,7 @@
private String[] __dftProtectedTargets = {"/web-inf", "/meta-inf"};
+
public static String[] DEFAULT_CONFIGURATION_CLASSES =
{
"org.eclipse.jetty.webapp.WebInfConfiguration",
@@ -1058,7 +1060,8 @@
if ((listener instanceof HttpSessionActivationListener)
|| (listener instanceof HttpSessionAttributeListener)
|| (listener instanceof HttpSessionBindingListener)
- || (listener instanceof HttpSessionListener))
+ || (listener instanceof HttpSessionListener)
+ || (listener instanceof HttpSessionIdListener))
{
if (_sessionHandler!=null)
_sessionHandler.addEventListener(listener);
@@ -1072,7 +1075,8 @@
if ((listener instanceof HttpSessionActivationListener)
|| (listener instanceof HttpSessionAttributeListener)
|| (listener instanceof HttpSessionBindingListener)
- || (listener instanceof HttpSessionListener))
+ || (listener instanceof HttpSessionListener)
+ || (listener instanceof HttpSessionIdListener))
{
if (_sessionHandler!=null)
_sessionHandler.removeEventListener(listener);
@@ -1326,7 +1330,6 @@
@Override
public Set<String> setServletSecurity(Dynamic registration, ServletSecurityElement servletSecurityElement)
{
-
Set<String> unchangedURLMappings = new HashSet<String>();
//From javadoc for ServletSecurityElement:
/*
@@ -1361,6 +1364,7 @@
List<ConstraintMapping> mappings = ConstraintSecurityHandler.createConstraintsWithMappingsForPath(registration.getName(), pathSpec, servletSecurityElement);
for (ConstraintMapping m:mappings)
((ConstraintAware)getSecurityHandler()).addConstraintMapping(m);
+ ((ConstraintAware)getSecurityHandler()).checkPathsWithUncoveredHttpMethods();
getMetaData().setOrigin("constraint.url."+pathSpec, Origin.API);
break;
}
@@ -1384,6 +1388,7 @@
constraintMappings.addAll(freshMappings);
((ConstraintSecurityHandler)getSecurityHandler()).setConstraintMappings(constraintMappings);
+ ((ConstraintAware)getSecurityHandler()).checkPathsWithUncoveredHttpMethods();
break;
}
}
@@ -1398,6 +1403,32 @@
/* ------------------------------------------------------------ */
public class Context extends ServletContextHandler.Context
{
+
+ /* ------------------------------------------------------------ */
+ @Override
+ public void checkListener(Class<? extends EventListener> listener) throws IllegalStateException
+ {
+ try
+ {
+ super.checkListener(listener);
+ }
+ catch (IllegalArgumentException e)
+ {
+ //not one of the standard servlet listeners, check our extended session listener types
+ boolean ok = false;
+ for (Class l:SessionHandler.SESSION_LISTENER_TYPES)
+ {
+ if (l.isAssignableFrom(listener))
+ {
+ ok = true;
+ break;
+ }
+ }
+ if (!ok)
+ throw new IllegalArgumentException("Inappropriate listener type "+listener.getName());
+ }
+ }
+
/* ------------------------------------------------------------ */
@Override
public URL getResource(String path) throws MalformedURLException
@@ -1444,7 +1475,6 @@
}
}
-
}
/* ------------------------------------------------------------ */
diff --git a/jetty-websocket/javax-websocket-client-impl/pom.xml b/jetty-websocket/javax-websocket-client-impl/pom.xml
new file mode 100644
index 0000000..0d8b81d
--- /dev/null
+++ b/jetty-websocket/javax-websocket-client-impl/pom.xml
@@ -0,0 +1,78 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+ <parent>
+ <groupId>org.eclipse.jetty.websocket</groupId>
+ <artifactId>websocket-parent</artifactId>
+ <version>9.1.0-SNAPSHOT</version>
+ </parent>
+
+ <modelVersion>4.0.0</modelVersion>
+ <artifactId>javax-websocket-client-impl</artifactId>
+ <name>Jetty :: Websocket :: javax.websocket :: Client Implementation</name>
+
+ <properties>
+ <bundle-symbolic-name>${project.groupId}.javax.websocket</bundle-symbolic-name>
+ </properties>
+
+ <dependencies>
+ <dependency>
+ <groupId>org.eclipse.jetty.websocket</groupId>
+ <artifactId>websocket-client</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>javax.websocket</groupId>
+ <artifactId>javax.websocket-client-api</artifactId>
+ <version>1.0</version>
+ </dependency>
+ <dependency>
+ <groupId>org.eclipse.jetty</groupId>
+ <artifactId>jetty-server</artifactId>
+ <version>${project.version}</version>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.eclipse.jetty.websocket</groupId>
+ <artifactId>websocket-server</artifactId>
+ <version>${project.version}</version>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.eclipse.jetty.toolchain</groupId>
+ <artifactId>jetty-test-helper</artifactId>
+ <scope>test</scope>
+ </dependency>
+ </dependencies>
+
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-enforcer-plugin</artifactId>
+ <version>1.1</version>
+ <executions>
+ <execution>
+ <id>ban-java-servlet-api</id>
+ <goals>
+ <goal>enforce</goal>
+ </goals>
+ <configuration>
+ <rules>
+ <bannedDependencies>
+ <includes>
+ <include>javax.servlet</include>
+ <include>servletapi</include>
+ <include>org.eclipse.jetty.orbit:javax.servlet</include>
+ <include>org.mortbay.jetty:servlet-api</include>
+ <include>jetty:servlet-api</include>
+ </includes>
+ </bannedDependencies>
+ </rules>
+ </configuration>
+ </execution>
+ </executions>
+ </plugin>
+ </plugins>
+ </build>
+</project>
diff --git a/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/AbstractJsrRemote.java b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/AbstractJsrRemote.java
new file mode 100644
index 0000000..6f1184e
--- /dev/null
+++ b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/AbstractJsrRemote.java
@@ -0,0 +1,194 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.util.concurrent.Future;
+
+import javax.websocket.EncodeException;
+import javax.websocket.Encoder;
+import javax.websocket.RemoteEndpoint;
+import javax.websocket.SendHandler;
+
+import org.eclipse.jetty.util.BufferUtil;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+import org.eclipse.jetty.websocket.common.WebSocketRemoteEndpoint;
+import org.eclipse.jetty.websocket.common.io.FutureWriteCallback;
+import org.eclipse.jetty.websocket.common.message.MessageOutputStream;
+import org.eclipse.jetty.websocket.common.message.MessageWriter;
+import org.eclipse.jetty.websocket.jsr356.encoders.EncodeFailedFuture;
+
+public abstract class AbstractJsrRemote implements RemoteEndpoint
+{
+ private static final Logger LOG = Log.getLogger(AbstractJsrRemote.class);
+
+ protected final JsrSession session;
+ protected final WebSocketRemoteEndpoint jettyRemote;
+ protected final EncoderFactory encoders;
+
+ protected AbstractJsrRemote(JsrSession session)
+ {
+ this.session = session;
+ if (!(session.getRemote() instanceof WebSocketRemoteEndpoint))
+ {
+ StringBuilder err = new StringBuilder();
+ err.append("Unexpected implementation [");
+ err.append(session.getRemote().getClass().getName());
+ err.append("]. Expected an instanceof [");
+ err.append(WebSocketRemoteEndpoint.class.getName());
+ err.append("]");
+ throw new IllegalStateException(err.toString());
+ }
+ this.jettyRemote = (WebSocketRemoteEndpoint)session.getRemote();
+ this.encoders = session.getEncoderFactory();
+ }
+
+ protected void assertMessageNotNull(Object data)
+ {
+ if (data == null)
+ {
+ throw new IllegalArgumentException("message cannot be null");
+ }
+ }
+
+ protected void assertSendHandlerNotNull(SendHandler handler)
+ {
+ if (handler == null)
+ {
+ throw new IllegalArgumentException("SendHandler cannot be null");
+ }
+ }
+
+ @Override
+ public void flushBatch() throws IOException
+ {
+ // TODO Auto-generated method stub
+ }
+
+ @Override
+ public boolean getBatchingAllowed()
+ {
+ // TODO Auto-generated method stub
+ return false;
+ }
+
+ @SuppressWarnings(
+ { "rawtypes", "unchecked" })
+ public Future<Void> sendObjectViaFuture(Object data)
+ {
+ assertMessageNotNull(data);
+ if (LOG.isDebugEnabled())
+ {
+ LOG.debug("sendObject({})",data);
+ }
+
+ Encoder encoder = encoders.getEncoderFor(data.getClass());
+ if (encoder == null)
+ {
+ throw new IllegalArgumentException("No encoder for type: " + data.getClass());
+ }
+
+ if (encoder instanceof Encoder.Text)
+ {
+ Encoder.Text etxt = (Encoder.Text)encoder;
+ try
+ {
+ String msg = etxt.encode(data);
+ return jettyRemote.sendStringByFuture(msg);
+ }
+ catch (EncodeException e)
+ {
+ return new EncodeFailedFuture(data,etxt,Encoder.Text.class,e);
+ }
+ }
+ else if (encoder instanceof Encoder.TextStream)
+ {
+ Encoder.TextStream etxt = (Encoder.TextStream)encoder;
+ FutureWriteCallback callback = new FutureWriteCallback();
+ try (MessageWriter writer = new MessageWriter(session))
+ {
+ writer.setCallback(callback);
+ etxt.encode(data,writer);
+ return callback;
+ }
+ catch (EncodeException | IOException e)
+ {
+ return new EncodeFailedFuture(data,etxt,Encoder.Text.class,e);
+ }
+ }
+ else if (encoder instanceof Encoder.Binary)
+ {
+ Encoder.Binary ebin = (Encoder.Binary)encoder;
+ try
+ {
+ ByteBuffer buf = ebin.encode(data);
+ return jettyRemote.sendBytesByFuture(buf);
+ }
+ catch (EncodeException e)
+ {
+ return new EncodeFailedFuture(data,ebin,Encoder.Binary.class,e);
+ }
+ }
+ else if (encoder instanceof Encoder.BinaryStream)
+ {
+ Encoder.BinaryStream ebin = (Encoder.BinaryStream)encoder;
+ FutureWriteCallback callback = new FutureWriteCallback();
+ try (MessageOutputStream out = new MessageOutputStream(session))
+ {
+ out.setCallback(callback);
+ ebin.encode(data,out);
+ return callback;
+ }
+ catch (EncodeException | IOException e)
+ {
+ return new EncodeFailedFuture(data,ebin,Encoder.Binary.class,e);
+ }
+ }
+
+ throw new IllegalArgumentException("Unknown encoder type: " + encoder);
+ }
+
+ @Override
+ public void sendPing(ByteBuffer data) throws IOException, IllegalArgumentException
+ {
+ if (LOG.isDebugEnabled())
+ {
+ LOG.debug("sendPing({})",BufferUtil.toDetailString(data));
+ }
+ jettyRemote.sendPing(data);
+ }
+
+ @Override
+ public void sendPong(ByteBuffer data) throws IOException, IllegalArgumentException
+ {
+ if (LOG.isDebugEnabled())
+ {
+ LOG.debug("sendPong({})",BufferUtil.toDetailString(data));
+ }
+ jettyRemote.sendPong(data);
+ }
+
+ @Override
+ public void setBatchingAllowed(boolean allowed) throws IOException
+ {
+ // TODO Auto-generated method stub
+ }
+}
diff --git a/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/BasicEndpointConfig.java b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/BasicEndpointConfig.java
new file mode 100644
index 0000000..72a3271
--- /dev/null
+++ b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/BasicEndpointConfig.java
@@ -0,0 +1,63 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import javax.websocket.Decoder;
+import javax.websocket.Encoder;
+import javax.websocket.EndpointConfig;
+
+/**
+ * Basic EndpointConfig (used when no EndpointConfig is provided or discovered)
+ */
+public class BasicEndpointConfig implements EndpointConfig
+{
+ private List<Class<? extends Decoder>> decoders;
+ private List<Class<? extends Encoder>> encoders;
+ private Map<String, Object> userProperties;
+
+ public BasicEndpointConfig()
+ {
+ decoders = Collections.emptyList();
+ encoders = Collections.emptyList();
+ userProperties = new HashMap<>();
+ }
+
+ @Override
+ public List<Class<? extends Decoder>> getDecoders()
+ {
+ return decoders;
+ }
+
+ @Override
+ public List<Class<? extends Encoder>> getEncoders()
+ {
+ return encoders;
+ }
+
+ @Override
+ public Map<String, Object> getUserProperties()
+ {
+ return userProperties;
+ }
+}
diff --git a/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/ClientContainer.java b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/ClientContainer.java
new file mode 100644
index 0000000..2ff9a89
--- /dev/null
+++ b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/ClientContainer.java
@@ -0,0 +1,343 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356;
+
+import java.io.IOException;
+import java.net.URI;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Future;
+import javax.websocket.ClientEndpoint;
+import javax.websocket.ClientEndpointConfig;
+import javax.websocket.DeploymentException;
+import javax.websocket.Endpoint;
+import javax.websocket.Extension;
+import javax.websocket.Session;
+import javax.websocket.WebSocketContainer;
+
+import org.eclipse.jetty.util.component.ContainerLifeCycle;
+import org.eclipse.jetty.util.thread.ShutdownThread;
+import org.eclipse.jetty.websocket.api.InvalidWebSocketException;
+import org.eclipse.jetty.websocket.api.extensions.ExtensionFactory;
+import org.eclipse.jetty.websocket.client.ClientUpgradeRequest;
+import org.eclipse.jetty.websocket.client.WebSocketClient;
+import org.eclipse.jetty.websocket.client.io.UpgradeListener;
+import org.eclipse.jetty.websocket.jsr356.annotations.AnnotatedEndpointScanner;
+import org.eclipse.jetty.websocket.jsr356.client.AnnotatedClientEndpointMetadata;
+import org.eclipse.jetty.websocket.jsr356.client.EmptyClientEndpointConfig;
+import org.eclipse.jetty.websocket.jsr356.client.SimpleEndpointMetadata;
+import org.eclipse.jetty.websocket.jsr356.decoders.PrimitiveDecoderMetadataSet;
+import org.eclipse.jetty.websocket.jsr356.encoders.PrimitiveEncoderMetadataSet;
+import org.eclipse.jetty.websocket.jsr356.endpoints.EndpointInstance;
+import org.eclipse.jetty.websocket.jsr356.endpoints.JsrEventDriverFactory;
+import org.eclipse.jetty.websocket.jsr356.metadata.EndpointMetadata;
+
+/**
+ * Container for Client use of the javax.websocket API.
+ * <p>
+ * This should be specific to a JVM if run in a standalone mode. or specific to a WebAppContext if running on the Jetty server.
+ */
+public class ClientContainer extends ContainerLifeCycle implements WebSocketContainer
+{
+ /** Tracking all primitive decoders for the container */
+ private final DecoderFactory decoderFactory;
+ /** Tracking all primitive encoders for the container */
+ private final EncoderFactory encoderFactory;
+
+ /** Tracking for all declared Client endpoints */
+ private final Map<Class<?>, EndpointMetadata> endpointClientMetadataCache;
+ /** The jetty websocket client in use for this container */
+ private final WebSocketClient client;
+
+ public ClientContainer()
+ {
+ endpointClientMetadataCache = new ConcurrentHashMap<>();
+ decoderFactory = new DecoderFactory(PrimitiveDecoderMetadataSet.INSTANCE);
+ encoderFactory = new EncoderFactory(PrimitiveEncoderMetadataSet.INSTANCE);
+
+ EmptyClientEndpointConfig empty = new EmptyClientEndpointConfig();
+ decoderFactory.init(empty);
+ encoderFactory.init(empty);
+
+ client = new WebSocketClient();
+ client.setEventDriverFactory(new JsrEventDriverFactory(client.getPolicy()));
+ client.setSessionFactory(new JsrSessionFactory(this));
+ addBean(client);
+ }
+
+ @Override
+ protected void doStart() throws Exception
+ {
+ super.doStart();
+ ShutdownThread.register(client);
+ }
+
+ @Override
+ protected void doStop() throws Exception
+ {
+ endpointClientMetadataCache.clear();
+ ShutdownThread.deregister(client);
+ super.doStop();
+ }
+
+ private Session connect(EndpointInstance instance, URI path) throws IOException
+ {
+ Objects.requireNonNull(instance,"EndpointInstance cannot be null");
+ Objects.requireNonNull(path,"Path cannot be null");
+
+ ClientEndpointConfig config = (ClientEndpointConfig)instance.getConfig();
+ ClientUpgradeRequest req = new ClientUpgradeRequest();
+ UpgradeListener upgradeListener = null;
+
+ for (Extension ext : config.getExtensions())
+ {
+ req.addExtensions(new JsrExtensionConfig(ext));
+ }
+
+ if (config.getPreferredSubprotocols().size() > 0)
+ {
+ req.setSubProtocols(config.getPreferredSubprotocols());
+ }
+
+ if (config.getConfigurator() != null)
+ {
+ upgradeListener = new JsrUpgradeListener(config.getConfigurator());
+ }
+
+ Future<org.eclipse.jetty.websocket.api.Session> futSess = client.connect(instance,path,req,upgradeListener);
+ try
+ {
+ return (JsrSession)futSess.get();
+ }
+ catch (InterruptedException e)
+ {
+ throw new IOException("Connect failure",e);
+ }
+ catch (ExecutionException e)
+ {
+ // Unwrap Actual Cause
+ Throwable cause = e.getCause();
+
+ if (cause instanceof IOException)
+ {
+ // Just rethrow
+ throw (IOException)cause;
+ }
+ else
+ {
+ throw new IOException("Connect failure",cause);
+ }
+ }
+ }
+
+ @Override
+ public Session connectToServer(Class<? extends Endpoint> endpointClass, ClientEndpointConfig config, URI path) throws DeploymentException, IOException
+ {
+ EndpointInstance instance = newClientEndpointInstance(endpointClass,config);
+ return connect(instance,path);
+ }
+
+ @Override
+ public Session connectToServer(Class<?> annotatedEndpointClass, URI path) throws DeploymentException, IOException
+ {
+ EndpointInstance instance = newClientEndpointInstance(annotatedEndpointClass,null);
+ return connect(instance,path);
+ }
+
+ @Override
+ public Session connectToServer(Endpoint endpoint, ClientEndpointConfig config, URI path) throws DeploymentException, IOException
+ {
+ EndpointInstance instance = newClientEndpointInstance(endpoint,config);
+ return connect(instance,path);
+ }
+
+ @Override
+ public Session connectToServer(Object endpoint, URI path) throws DeploymentException, IOException
+ {
+ EndpointInstance instance = newClientEndpointInstance(endpoint,null);
+ return connect(instance,path);
+ }
+
+ public EndpointMetadata getClientEndpointMetadata(Class<?> endpoint)
+ {
+ EndpointMetadata metadata = null;
+
+ synchronized (endpointClientMetadataCache)
+ {
+ metadata = endpointClientMetadataCache.get(endpoint);
+
+ if (metadata != null)
+ {
+ return metadata;
+ }
+
+ ClientEndpoint anno = endpoint.getAnnotation(ClientEndpoint.class);
+ if (anno != null)
+ {
+ // Annotated takes precedence here
+ AnnotatedClientEndpointMetadata annoMetadata = new AnnotatedClientEndpointMetadata(this,endpoint);
+ AnnotatedEndpointScanner<ClientEndpoint, ClientEndpointConfig> scanner = new AnnotatedEndpointScanner<>(annoMetadata);
+ scanner.scan();
+ metadata = annoMetadata;
+ }
+ else if (Endpoint.class.isAssignableFrom(endpoint))
+ {
+ // extends Endpoint
+ @SuppressWarnings("unchecked")
+ Class<? extends Endpoint> eendpoint = (Class<? extends Endpoint>)endpoint;
+ metadata = new SimpleEndpointMetadata(eendpoint);
+ }
+ else
+ {
+ StringBuilder err = new StringBuilder();
+ err.append("Not a recognized websocket [");
+ err.append(endpoint.getName());
+ err.append("] does not extend @").append(ClientEndpoint.class.getName());
+ err.append(" or extend from ").append(Endpoint.class.getName());
+ throw new InvalidWebSocketException("Unable to identify as valid Endpoint: " + endpoint);
+ }
+
+ endpointClientMetadataCache.put(endpoint,metadata);
+ return metadata;
+ }
+ }
+
+ public DecoderFactory getDecoderFactory()
+ {
+ return decoderFactory;
+ }
+
+ @Override
+ public long getDefaultAsyncSendTimeout()
+ {
+ return client.getAsyncWriteTimeout();
+ }
+
+ @Override
+ public int getDefaultMaxBinaryMessageBufferSize()
+ {
+ return client.getMaxBinaryMessageBufferSize();
+ }
+
+ @Override
+ public long getDefaultMaxSessionIdleTimeout()
+ {
+ return client.getMaxIdleTimeout();
+ }
+
+ @Override
+ public int getDefaultMaxTextMessageBufferSize()
+ {
+ return client.getMaxTextMessageBufferSize();
+ }
+
+ public EncoderFactory getEncoderFactory()
+ {
+ return encoderFactory;
+ }
+
+ @Override
+ public Set<Extension> getInstalledExtensions()
+ {
+ Set<Extension> ret = new HashSet<>();
+ ExtensionFactory extensions = client.getExtensionFactory();
+
+ for (String name : extensions.getExtensionNames())
+ {
+ ret.add(new JsrExtension(name));
+ }
+
+ return ret;
+ }
+
+ /**
+ * Used in {@link Session#getOpenSessions()}
+ *
+ * @return
+ */
+ public Set<Session> getOpenSessions()
+ {
+ // TODO Auto-generated method stub
+ return null;
+ }
+
+ private EndpointInstance newClientEndpointInstance(Class<?> endpointClass, ClientEndpointConfig config)
+ {
+ try
+ {
+ return newClientEndpointInstance(endpointClass.newInstance(),config);
+ }
+ catch (InstantiationException | IllegalAccessException e)
+ {
+ throw new InvalidWebSocketException("Unable to instantiate websocket: " + endpointClass.getClass());
+ }
+ }
+
+ public EndpointInstance newClientEndpointInstance(Object endpoint, ClientEndpointConfig config)
+ {
+ EndpointMetadata metadata = getClientEndpointMetadata(endpoint.getClass());
+ ClientEndpointConfig cec = config;
+ if (config == null)
+ {
+ if (metadata instanceof AnnotatedClientEndpointMetadata)
+ {
+ cec = ((AnnotatedClientEndpointMetadata)metadata).getConfig();
+ }
+ else
+ {
+ cec = new EmptyClientEndpointConfig();
+ }
+ }
+ return new EndpointInstance(endpoint,cec,metadata);
+ }
+
+ @Override
+ public void setAsyncSendTimeout(long ms)
+ {
+ client.setAsyncWriteTimeout(ms);
+ }
+
+ @Override
+ public void setDefaultMaxBinaryMessageBufferSize(int max)
+ {
+ // overall message limit (used in non-streaming)
+ client.getPolicy().setMaxBinaryMessageSize(max);
+ // incoming streaming buffer size
+ client.setMaxBinaryMessageBufferSize(max);
+ }
+
+ @Override
+ public void setDefaultMaxSessionIdleTimeout(long ms)
+ {
+ client.setMaxIdleTimeout(ms);
+ }
+
+ @Override
+ public void setDefaultMaxTextMessageBufferSize(int max)
+ {
+ // overall message limit (used in non-streaming)
+ client.getPolicy().setMaxTextMessageSize(max);
+ // incoming streaming buffer size
+ client.setMaxTextMessageBufferSize(max);
+ }
+}
diff --git a/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/Configurable.java b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/Configurable.java
new file mode 100644
index 0000000..6f8f7d6
--- /dev/null
+++ b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/Configurable.java
@@ -0,0 +1,29 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356;
+
+import javax.websocket.EndpointConfig;
+
+/**
+ * Tag indicating a component that needs to be configured.
+ */
+public interface Configurable
+{
+ public void init(EndpointConfig config);
+}
diff --git a/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/ConfigurationException.java b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/ConfigurationException.java
new file mode 100644
index 0000000..aa4b9c5
--- /dev/null
+++ b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/ConfigurationException.java
@@ -0,0 +1,36 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356;
+
+import org.eclipse.jetty.websocket.api.WebSocketException;
+
+public class ConfigurationException extends WebSocketException
+{
+ private static final long serialVersionUID = 3026803845657799372L;
+
+ public ConfigurationException(String message)
+ {
+ super(message);
+ }
+
+ public ConfigurationException(String message, Throwable cause)
+ {
+ super(message,cause);
+ }
+}
diff --git a/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/DecoderFactory.java b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/DecoderFactory.java
new file mode 100644
index 0000000..5c9c0bb
--- /dev/null
+++ b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/DecoderFactory.java
@@ -0,0 +1,178 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356;
+
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+import javax.websocket.Decoder;
+import javax.websocket.EndpointConfig;
+
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+import org.eclipse.jetty.websocket.jsr356.metadata.DecoderMetadata;
+import org.eclipse.jetty.websocket.jsr356.metadata.DecoderMetadataSet;
+
+/**
+ * Factory for {@link DecoderMetadata}
+ * <p>
+ * Relies on search order of parent {@link DecoderFactory} instances as such.
+ * <ul>
+ * <li>From Static DecoderMetadataSet (based on data in annotations and static EndpointConfig)</li>
+ * <li>From Composite DecoderMetadataSet (based static and instance specific EndpointConfig)</li>
+ * <li>Container declared DecoderMetadataSet (primitives)</li>
+ * </ul>
+ */
+public class DecoderFactory implements Configurable
+{
+ public static class Wrapper implements Configurable
+ {
+ private final Decoder decoder;
+ private final DecoderMetadata metadata;
+
+ private Wrapper(Decoder decoder, DecoderMetadata metadata)
+ {
+ this.decoder = decoder;
+ this.metadata = metadata;
+ }
+
+ public Decoder getDecoder()
+ {
+ return decoder;
+ }
+
+ public DecoderMetadata getMetadata()
+ {
+ return metadata;
+ }
+
+ @Override
+ public void init(EndpointConfig config)
+ {
+ this.decoder.init(config);
+ }
+ }
+
+ private static final Logger LOG = Log.getLogger(DecoderFactory.class);
+
+ private final DecoderMetadataSet metadatas;
+ private DecoderFactory parentFactory;
+ private Map<Class<?>, Wrapper> activeWrappers;
+
+ public DecoderFactory(DecoderMetadataSet metadatas)
+ {
+ this.metadatas = metadatas;
+ this.activeWrappers = new ConcurrentHashMap<>();
+ }
+
+ public DecoderFactory(DecoderMetadataSet metadatas, DecoderFactory parentFactory)
+ {
+ this(metadatas);
+ this.parentFactory = parentFactory;
+ }
+
+ public Decoder getDecoderFor(Class<?> type)
+ {
+ Wrapper wrapper = getWrapperFor(type);
+ if (wrapper == null)
+ {
+ return null;
+ }
+ return wrapper.decoder;
+ }
+
+ public DecoderMetadata getMetadataFor(Class<?> type)
+ {
+ LOG.debug("getMetadataFor({})",type);
+ DecoderMetadata metadata = metadatas.getMetadataByType(type);
+
+ if (metadata != null)
+ {
+ return metadata;
+ }
+
+ if (parentFactory != null)
+ {
+ return parentFactory.getMetadataFor(type);
+ }
+
+ return null;
+ }
+
+ public Wrapper getWrapperFor(Class<?> type)
+ {
+ synchronized (activeWrappers)
+ {
+ Wrapper wrapper = activeWrappers.get(type);
+
+ // Try parent (if needed)
+ if ((wrapper == null) && (parentFactory != null))
+ {
+ wrapper = parentFactory.getWrapperFor(type);
+ }
+
+ if (wrapper == null)
+ {
+ // Attempt to create Wrapper on demand
+ DecoderMetadata metadata = metadatas.getMetadataByType(type);
+ if (metadata == null)
+ {
+ return null;
+ }
+ wrapper = newWrapper(metadata);
+ // track wrapper
+ activeWrappers.put(type,wrapper);
+ }
+
+ return wrapper;
+ }
+ }
+
+ @Override
+ public void init(EndpointConfig config)
+ {
+ LOG.debug("init({})",config);
+ // Instantiate all declared decoders
+ for (DecoderMetadata metadata : metadatas)
+ {
+ Wrapper wrapper = newWrapper(metadata);
+ activeWrappers.put(metadata.getObjectType(),wrapper);
+ }
+
+ // Initialize all decoders
+ for (Wrapper wrapper : activeWrappers.values())
+ {
+ wrapper.decoder.init(config);
+ }
+ }
+
+ public Wrapper newWrapper(DecoderMetadata metadata)
+ {
+ Class<? extends Decoder> decoderClass = metadata.getCoderClass();
+ try
+ {
+ Decoder decoder = decoderClass.newInstance();
+ return new Wrapper(decoder,metadata);
+ }
+ catch (InstantiationException | IllegalAccessException e)
+ {
+ throw new IllegalStateException("Unable to instantiate Decoder: " + decoderClass.getName());
+ }
+ }
+}
diff --git a/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/EncoderFactory.java b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/EncoderFactory.java
new file mode 100644
index 0000000..0e6527f
--- /dev/null
+++ b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/EncoderFactory.java
@@ -0,0 +1,172 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356;
+
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+import javax.websocket.Encoder;
+import javax.websocket.EndpointConfig;
+
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+import org.eclipse.jetty.websocket.jsr356.metadata.EncoderMetadata;
+import org.eclipse.jetty.websocket.jsr356.metadata.EncoderMetadataSet;
+
+/**
+ * Represents all of the declared {@link Encoder}s that the Container is aware of.
+ */
+public class EncoderFactory implements Configurable
+{
+ public static class Wrapper implements Configurable
+ {
+ private final Encoder encoder;
+ private final EncoderMetadata metadata;
+
+ private Wrapper(Encoder encoder, EncoderMetadata metadata)
+ {
+ this.encoder = encoder;
+ this.metadata = metadata;
+ }
+
+ public Encoder getEncoder()
+ {
+ return encoder;
+ }
+
+ public EncoderMetadata getMetadata()
+ {
+ return metadata;
+ }
+
+ @Override
+ public void init(EndpointConfig config)
+ {
+ this.encoder.init(config);
+ }
+ }
+
+ private static final Logger LOG = Log.getLogger(EncoderFactory.class);
+
+ private final EncoderMetadataSet metadatas;
+ private EncoderFactory parentFactory;
+ private Map<Class<?>, Wrapper> activeWrappers;
+
+ public EncoderFactory(EncoderMetadataSet metadatas)
+ {
+ this.metadatas = metadatas;
+ this.activeWrappers = new ConcurrentHashMap<>();
+ }
+
+ public EncoderFactory(EncoderMetadataSet metadatas, EncoderFactory parentFactory)
+ {
+ this(metadatas);
+ this.parentFactory = parentFactory;
+ }
+
+ public Encoder getEncoderFor(Class<?> type)
+ {
+ Wrapper wrapper = getWrapperFor(type);
+ if (wrapper == null)
+ {
+ return null;
+ }
+ return wrapper.encoder;
+ }
+
+ public EncoderMetadata getMetadataFor(Class<?> type)
+ {
+ LOG.debug("getMetadataFor({})",type);
+ EncoderMetadata metadata = metadatas.getMetadataByType(type);
+
+ if (metadata != null)
+ {
+ return metadata;
+ }
+
+ if (parentFactory != null)
+ {
+ return parentFactory.getMetadataFor(type);
+ }
+
+ return null;
+ }
+
+ public Wrapper getWrapperFor(Class<?> type)
+ {
+ synchronized (activeWrappers)
+ {
+ Wrapper wrapper = activeWrappers.get(type);
+
+ // Try parent (if needed)
+ if ((wrapper == null) && (parentFactory != null))
+ {
+ wrapper = parentFactory.getWrapperFor(type);
+ }
+
+ if (wrapper == null)
+ {
+ // Attempt to create Wrapper on demand
+ EncoderMetadata metadata = metadatas.getMetadataByType(type);
+ if (metadata == null)
+ {
+ return null;
+ }
+ wrapper = newWrapper(metadata);
+ // track wrapper
+ activeWrappers.put(type,wrapper);
+ }
+
+ return wrapper;
+ }
+ }
+
+ @Override
+ public void init(EndpointConfig config)
+ {
+ LOG.debug("init({})",config);
+
+ // Instantiate all declared encoders
+ for (EncoderMetadata metadata : metadatas)
+ {
+ Wrapper wrapper = newWrapper(metadata);
+ activeWrappers.put(metadata.getObjectType(),wrapper);
+ }
+
+ // Initialize all encoders
+ for (Wrapper wrapper : activeWrappers.values())
+ {
+ wrapper.encoder.init(config);
+ }
+ }
+
+ private Wrapper newWrapper(EncoderMetadata metadata)
+ {
+ Class<? extends Encoder> encoderClass = metadata.getCoderClass();
+ try
+ {
+ Encoder encoder = encoderClass.newInstance();
+ return new Wrapper(encoder,metadata);
+ }
+ catch (InstantiationException | IllegalAccessException e)
+ {
+ throw new IllegalStateException("Unable to instantiate Encoder: " + encoderClass.getName());
+ }
+ }
+}
diff --git a/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/InitException.java b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/InitException.java
new file mode 100644
index 0000000..7bb67b8
--- /dev/null
+++ b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/InitException.java
@@ -0,0 +1,42 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356;
+
+/**
+ * Exception during initialization of the Endpoint
+ */
+public class InitException extends IllegalStateException
+{
+ private static final long serialVersionUID = -4691138423037387558L;
+
+ public InitException(String s)
+ {
+ super(s);
+ }
+
+ public InitException(String message, Throwable cause)
+ {
+ super(message,cause);
+ }
+
+ public InitException(Throwable cause)
+ {
+ super(cause);
+ }
+}
diff --git a/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/JettyClientContainerProvider.java b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/JettyClientContainerProvider.java
new file mode 100644
index 0000000..3463ae8
--- /dev/null
+++ b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/JettyClientContainerProvider.java
@@ -0,0 +1,43 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356;
+
+import javax.websocket.ContainerProvider;
+import javax.websocket.WebSocketContainer;
+
+/**
+ * Client {@link ContainerProvider} implementation
+ */
+public class JettyClientContainerProvider extends ContainerProvider
+{
+ @Override
+ protected WebSocketContainer getContainer()
+ {
+ ClientContainer container = new ClientContainer();
+ try
+ {
+ container.start();
+ return container;
+ }
+ catch (Exception e)
+ {
+ throw new RuntimeException("Unable to start Client Container",e);
+ }
+ }
+}
diff --git a/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/JsrAsyncRemote.java b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/JsrAsyncRemote.java
new file mode 100644
index 0000000..574aa26
--- /dev/null
+++ b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/JsrAsyncRemote.java
@@ -0,0 +1,195 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.util.concurrent.Future;
+
+import javax.websocket.EncodeException;
+import javax.websocket.Encoder;
+import javax.websocket.RemoteEndpoint;
+import javax.websocket.SendHandler;
+import javax.websocket.SendResult;
+
+import org.eclipse.jetty.util.BufferUtil;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+import org.eclipse.jetty.websocket.common.WebSocketFrame;
+import org.eclipse.jetty.websocket.common.message.MessageOutputStream;
+import org.eclipse.jetty.websocket.common.message.MessageWriter;
+import org.eclipse.jetty.websocket.common.util.TextUtil;
+import org.eclipse.jetty.websocket.jsr356.messages.SendHandlerWriteCallback;
+
+public class JsrAsyncRemote extends AbstractJsrRemote implements RemoteEndpoint.Async
+{
+ static final Logger LOG = Log.getLogger(JsrAsyncRemote.class);
+
+ protected JsrAsyncRemote(JsrSession session)
+ {
+ super(session);
+ }
+
+ @Override
+ public long getSendTimeout()
+ {
+ // TODO Auto-generated method stub
+ return 0;
+ }
+
+ @Override
+ public Future<Void> sendBinary(ByteBuffer data)
+ {
+ assertMessageNotNull(data);
+ if (LOG.isDebugEnabled())
+ {
+ LOG.debug("sendBinary({})",BufferUtil.toDetailString(data));
+ }
+ return jettyRemote.sendBytesByFuture(data);
+ }
+
+ @Override
+ public void sendBinary(ByteBuffer data, SendHandler handler)
+ {
+ assertMessageNotNull(data);
+ assertSendHandlerNotNull(handler);
+ if (LOG.isDebugEnabled())
+ {
+ LOG.debug("sendBinary({},{})",BufferUtil.toDetailString(data),handler);
+ }
+ WebSocketFrame frame = WebSocketFrame.binary().setPayload(data).setFin(true);
+ jettyRemote.sendFrame(frame,new SendHandlerWriteCallback(handler));
+ }
+
+ @Override
+ public Future<Void> sendObject(Object data)
+ {
+ return sendObjectViaFuture(data);
+ }
+
+ @SuppressWarnings(
+ { "rawtypes", "unchecked" })
+ @Override
+ public void sendObject(Object data, SendHandler handler)
+ {
+ assertMessageNotNull(data);
+ assertSendHandlerNotNull(handler);
+ if (LOG.isDebugEnabled())
+ {
+ LOG.debug("sendObject({},{})",data,handler);
+ }
+
+ Encoder encoder = encoders.getEncoderFor(data.getClass());
+ if (encoder == null)
+ {
+ throw new IllegalArgumentException("No encoder for type: " + data.getClass());
+ }
+
+ if (encoder instanceof Encoder.Text)
+ {
+ Encoder.Text etxt = (Encoder.Text)encoder;
+ try
+ {
+ String msg = etxt.encode(data);
+ sendText(msg,handler);
+ return;
+ }
+ catch (EncodeException e)
+ {
+ handler.onResult(new SendResult(e));
+ }
+ }
+ else if (encoder instanceof Encoder.TextStream)
+ {
+ Encoder.TextStream etxt = (Encoder.TextStream)encoder;
+ SendHandlerWriteCallback callback = new SendHandlerWriteCallback(handler);
+ try (MessageWriter writer = new MessageWriter(session))
+ {
+ writer.setCallback(callback);
+ etxt.encode(data,writer);
+ return;
+ }
+ catch (EncodeException | IOException e)
+ {
+ handler.onResult(new SendResult(e));
+ }
+ }
+ else if (encoder instanceof Encoder.Binary)
+ {
+ Encoder.Binary ebin = (Encoder.Binary)encoder;
+ try
+ {
+ ByteBuffer buf = ebin.encode(data);
+ sendBinary(buf,handler);
+ return;
+ }
+ catch (EncodeException e)
+ {
+ handler.onResult(new SendResult(e));
+ }
+ }
+ else if (encoder instanceof Encoder.BinaryStream)
+ {
+ Encoder.BinaryStream ebin = (Encoder.BinaryStream)encoder;
+ SendHandlerWriteCallback callback = new SendHandlerWriteCallback(handler);
+ try (MessageOutputStream out = new MessageOutputStream(session))
+ {
+ out.setCallback(callback);
+ ebin.encode(data,out);
+ return;
+ }
+ catch (EncodeException | IOException e)
+ {
+ handler.onResult(new SendResult(e));
+ }
+ }
+
+ throw new IllegalArgumentException("Unknown encoder type: " + encoder);
+ }
+
+ @Override
+ public Future<Void> sendText(String text)
+ {
+ assertMessageNotNull(text);
+ if (LOG.isDebugEnabled())
+ {
+ LOG.debug("sendText({})",TextUtil.hint(text));
+ }
+ return jettyRemote.sendStringByFuture(text);
+ }
+
+ @Override
+ public void sendText(String text, SendHandler handler)
+ {
+ assertMessageNotNull(text);
+ assertSendHandlerNotNull(handler);
+ if (LOG.isDebugEnabled())
+ {
+ LOG.debug("sendText({},{})",TextUtil.hint(text),handler);
+ }
+ WebSocketFrame frame = WebSocketFrame.text(text).setFin(true);
+ jettyRemote.sendFrame(frame,new SendHandlerWriteCallback(handler));
+ }
+
+ @Override
+ public void setSendTimeout(long timeoutmillis)
+ {
+ // TODO Auto-generated method stub
+ }
+}
diff --git a/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/JsrBasicRemote.java b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/JsrBasicRemote.java
new file mode 100644
index 0000000..3851c63
--- /dev/null
+++ b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/JsrBasicRemote.java
@@ -0,0 +1,120 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.Writer;
+import java.nio.ByteBuffer;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Future;
+
+import javax.websocket.EncodeException;
+import javax.websocket.RemoteEndpoint;
+
+import org.eclipse.jetty.util.BufferUtil;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+import org.eclipse.jetty.websocket.common.message.MessageOutputStream;
+import org.eclipse.jetty.websocket.common.message.MessageWriter;
+import org.eclipse.jetty.websocket.common.util.TextUtil;
+
+public class JsrBasicRemote extends AbstractJsrRemote implements RemoteEndpoint.Basic
+{
+ private static final Logger LOG = Log.getLogger(JsrBasicRemote.class);
+
+ protected JsrBasicRemote(JsrSession session)
+ {
+ super(session);
+ }
+
+ @Override
+ public OutputStream getSendStream() throws IOException
+ {
+ return new MessageOutputStream(session);
+ }
+
+ @Override
+ public Writer getSendWriter() throws IOException
+ {
+ return new MessageWriter(session);
+ }
+
+ @Override
+ public void sendBinary(ByteBuffer data) throws IOException
+ {
+ assertMessageNotNull(data);
+ if (LOG.isDebugEnabled())
+ {
+ LOG.debug("sendBinary({})",BufferUtil.toDetailString(data));
+ }
+ jettyRemote.sendBytes(data);
+ }
+
+ @Override
+ public void sendBinary(ByteBuffer partialByte, boolean isLast) throws IOException
+ {
+ assertMessageNotNull(partialByte);
+ if (LOG.isDebugEnabled())
+ {
+ LOG.debug("sendBinary({},{})",BufferUtil.toDetailString(partialByte),isLast);
+ }
+ jettyRemote.sendPartialBytes(partialByte,isLast);
+ }
+
+ @Override
+ public void sendObject(Object data) throws IOException, EncodeException
+ {
+ Future<Void> fut = sendObjectViaFuture(data);
+ try
+ {
+ fut.get(); // block till done
+ }
+ catch (ExecutionException e)
+ {
+ throw new IOException("Failed to write object",e.getCause());
+ }
+ catch (InterruptedException e)
+ {
+ throw new IOException("Failed to write object",e);
+ }
+ }
+
+ @Override
+ public void sendText(String text) throws IOException
+ {
+ assertMessageNotNull(text);
+ if (LOG.isDebugEnabled())
+ {
+ LOG.debug("sendText({})",TextUtil.hint(text));
+ }
+ jettyRemote.sendString(text);
+ }
+
+ @Override
+ public void sendText(String partialMessage, boolean isLast) throws IOException
+ {
+ assertMessageNotNull(partialMessage);
+ if (LOG.isDebugEnabled())
+ {
+ LOG.debug("sendText({},{})",TextUtil.hint(partialMessage),isLast);
+ }
+ jettyRemote.sendPartialString(partialMessage,isLast);
+ }
+}
diff --git a/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/JsrExtension.java b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/JsrExtension.java
new file mode 100644
index 0000000..1c050f6
--- /dev/null
+++ b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/JsrExtension.java
@@ -0,0 +1,92 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+import javax.websocket.Extension;
+
+import org.eclipse.jetty.websocket.api.extensions.ExtensionConfig;
+
+public class JsrExtension implements Extension
+{
+ private static class JsrParameter implements Extension.Parameter
+ {
+ private String name;
+ private String value;
+
+ private JsrParameter(String key, String value)
+ {
+ this.name = key;
+ this.value = value;
+ }
+
+ @Override
+ public String getName()
+ {
+ return this.name;
+ }
+
+ @Override
+ public String getValue()
+ {
+ return this.value;
+ }
+ }
+
+ private final String name;
+ private List<Parameter> parameters = new ArrayList<>();
+
+ /**
+ * A configured extension
+ */
+ public JsrExtension(ExtensionConfig cfg)
+ {
+ this.name = cfg.getName();
+ if (cfg.getParameters() != null)
+ {
+ for (Map.Entry<String, String> entry : cfg.getParameters().entrySet())
+ {
+ parameters.add(new JsrParameter(entry.getKey(),entry.getValue()));
+ }
+ }
+ }
+
+ /**
+ * A potential (unconfigured) extension
+ */
+ public JsrExtension(String name)
+ {
+ this.name = name;
+ }
+
+ @Override
+ public String getName()
+ {
+ return name;
+ }
+
+ @Override
+ public List<Parameter> getParameters()
+ {
+ return parameters;
+ }
+}
diff --git a/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/JsrExtensionConfig.java b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/JsrExtensionConfig.java
new file mode 100644
index 0000000..1e01dd3
--- /dev/null
+++ b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/JsrExtensionConfig.java
@@ -0,0 +1,35 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356;
+
+import javax.websocket.Extension;
+
+import org.eclipse.jetty.websocket.api.extensions.ExtensionConfig;
+
+public class JsrExtensionConfig extends ExtensionConfig
+{
+ public JsrExtensionConfig(Extension ext)
+ {
+ super(ext.getName());
+ for (Extension.Parameter param : ext.getParameters())
+ {
+ this.setParameter(param.getName(),param.getValue());
+ }
+ }
+}
diff --git a/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/JsrHandshakeResponse.java b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/JsrHandshakeResponse.java
new file mode 100644
index 0000000..0ce1597
--- /dev/null
+++ b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/JsrHandshakeResponse.java
@@ -0,0 +1,42 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356;
+
+import java.util.List;
+import java.util.Map;
+
+import javax.websocket.HandshakeResponse;
+
+import org.eclipse.jetty.websocket.api.UpgradeResponse;
+
+public class JsrHandshakeResponse implements HandshakeResponse
+{
+ private final Map<String, List<String>> headers;
+
+ public JsrHandshakeResponse(UpgradeResponse response)
+ {
+ this.headers = response.getHeaders();
+ }
+
+ @Override
+ public Map<String, List<String>> getHeaders()
+ {
+ return this.headers;
+ }
+}
diff --git a/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/JsrPongMessage.java b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/JsrPongMessage.java
new file mode 100644
index 0000000..a4edcf9
--- /dev/null
+++ b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/JsrPongMessage.java
@@ -0,0 +1,39 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356;
+
+import java.nio.ByteBuffer;
+
+import javax.websocket.PongMessage;
+
+public class JsrPongMessage implements PongMessage
+{
+ private final ByteBuffer data;
+
+ public JsrPongMessage(ByteBuffer buf)
+ {
+ this.data = buf;
+ }
+
+ @Override
+ public ByteBuffer getApplicationData()
+ {
+ return data;
+ }
+}
diff --git a/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/JsrSession.java b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/JsrSession.java
new file mode 100644
index 0000000..ab62677
--- /dev/null
+++ b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/JsrSession.java
@@ -0,0 +1,380 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356;
+
+import java.io.IOException;
+import java.net.URI;
+import java.security.Principal;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+
+import javax.websocket.CloseReason;
+import javax.websocket.EndpointConfig;
+import javax.websocket.Extension;
+import javax.websocket.MessageHandler;
+import javax.websocket.RemoteEndpoint.Async;
+import javax.websocket.RemoteEndpoint.Basic;
+import javax.websocket.Session;
+import javax.websocket.WebSocketContainer;
+
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+import org.eclipse.jetty.websocket.api.extensions.ExtensionConfig;
+import org.eclipse.jetty.websocket.common.LogicalConnection;
+import org.eclipse.jetty.websocket.common.WebSocketSession;
+import org.eclipse.jetty.websocket.common.events.EventDriver;
+import org.eclipse.jetty.websocket.jsr356.endpoints.AbstractJsrEventDriver;
+import org.eclipse.jetty.websocket.jsr356.metadata.DecoderMetadata;
+import org.eclipse.jetty.websocket.jsr356.metadata.EndpointMetadata;
+import org.eclipse.jetty.websocket.jsr356.metadata.MessageHandlerMetadata;
+
+/**
+ * Session for the JSR.
+ */
+public class JsrSession extends WebSocketSession implements javax.websocket.Session, Configurable
+{
+ private static final Logger LOG = Log.getLogger(JsrSession.class);
+ private final ClientContainer container;
+ private final String id;
+ private final EndpointConfig config;
+ private final EndpointMetadata metadata;
+ private final DecoderFactory decoderFactory;
+ private final EncoderFactory encoderFactory;
+ /** Factory for MessageHandlers */
+ private final MessageHandlerFactory messageHandlerFactory;
+ /** Array of MessageHandlerWrappers, indexed by {@link MessageType#ordinal()} */
+ private final MessageHandlerWrapper wrappers[];
+ private Set<MessageHandler> messageHandlerSet;
+ private List<Extension> negotiatedExtensions;
+ private Map<String, String> pathParameters = new HashMap<>();
+ private JsrAsyncRemote asyncRemote;
+ private JsrBasicRemote basicRemote;
+
+ public JsrSession(URI requestURI, EventDriver websocket, LogicalConnection connection, ClientContainer container, String id)
+ {
+ super(requestURI,websocket,connection);
+ if (!(websocket instanceof AbstractJsrEventDriver))
+ {
+ throw new IllegalArgumentException("Cannot use, not a JSR WebSocket: " + websocket);
+ }
+ AbstractJsrEventDriver jsr = (AbstractJsrEventDriver)websocket;
+ this.config = jsr.getConfig();
+ this.metadata = jsr.getMetadata();
+ this.container = container;
+ this.id = id;
+ this.decoderFactory = new DecoderFactory(metadata.getDecoders(),container.getDecoderFactory());
+ this.encoderFactory = new EncoderFactory(metadata.getEncoders(),container.getEncoderFactory());
+ this.messageHandlerFactory = new MessageHandlerFactory();
+ this.wrappers = new MessageHandlerWrapper[MessageType.values().length];
+ this.messageHandlerSet = new HashSet<>();
+ }
+
+ @Override
+ public void addMessageHandler(MessageHandler handler) throws IllegalStateException
+ {
+ Objects.requireNonNull(handler,"MessageHandler cannot be null");
+
+ synchronized (wrappers)
+ {
+ for (MessageHandlerMetadata metadata : messageHandlerFactory.getMetadata(handler.getClass()))
+ {
+ DecoderFactory.Wrapper wrapper = decoderFactory.getWrapperFor(metadata.getMessageClass());
+ if (wrapper == null)
+ {
+ StringBuilder err = new StringBuilder();
+ err.append("Unable to find decoder for type <");
+ err.append(metadata.getMessageClass().getName());
+ err.append("> used in <");
+ err.append(metadata.getHandlerClass().getName());
+ err.append(">");
+ throw new IllegalStateException(err.toString());
+ }
+
+ MessageType key = wrapper.getMetadata().getMessageType();
+ MessageHandlerWrapper other = wrappers[key.ordinal()];
+ if (other != null)
+ {
+ StringBuilder err = new StringBuilder();
+ err.append("Encountered duplicate MessageHandler handling message type <");
+ err.append(wrapper.getMetadata().getObjectType().getName());
+ err.append(">, ").append(metadata.getHandlerClass().getName());
+ err.append("<");
+ err.append(metadata.getMessageClass().getName());
+ err.append("> and ");
+ err.append(other.getMetadata().getHandlerClass().getName());
+ err.append("<");
+ err.append(other.getMetadata().getMessageClass().getName());
+ err.append("> both implement this message type");
+ throw new IllegalStateException(err.toString());
+ }
+ else
+ {
+ MessageHandlerWrapper handlerWrapper = new MessageHandlerWrapper(handler,metadata,wrapper);
+ wrappers[key.ordinal()] = handlerWrapper;
+ }
+ }
+
+ // Update handlerSet
+ updateMessageHandlerSet();
+ }
+ }
+
+ @Override
+ public void close(CloseReason closeReason) throws IOException
+ {
+ close(closeReason.getCloseCode().getCode(),closeReason.getReasonPhrase());
+ }
+
+ @Override
+ public Async getAsyncRemote()
+ {
+ if (asyncRemote == null)
+ {
+ asyncRemote = new JsrAsyncRemote(this);
+ }
+ return asyncRemote;
+ }
+
+ @Override
+ public Basic getBasicRemote()
+ {
+ if (basicRemote == null)
+ {
+ basicRemote = new JsrBasicRemote(this);
+ }
+ return basicRemote;
+ }
+
+ @Override
+ public WebSocketContainer getContainer()
+ {
+ return this.container;
+ }
+
+ public DecoderFactory getDecoderFactory()
+ {
+ return decoderFactory;
+ }
+
+ public EncoderFactory getEncoderFactory()
+ {
+ return encoderFactory;
+ }
+
+ public EndpointConfig getEndpointConfig()
+ {
+ return config;
+ }
+
+ public EndpointMetadata getEndpointMetadata()
+ {
+ return metadata;
+ }
+
+ @Override
+ public String getId()
+ {
+ return this.id;
+ }
+
+ @Override
+ public int getMaxBinaryMessageBufferSize()
+ {
+ return getPolicy().getMaxBinaryMessageSize();
+ }
+
+ @Override
+ public long getMaxIdleTimeout()
+ {
+ return getPolicy().getIdleTimeout();
+ }
+
+ @Override
+ public int getMaxTextMessageBufferSize()
+ {
+ return getPolicy().getMaxTextMessageSize();
+ }
+
+ public MessageHandlerFactory getMessageHandlerFactory()
+ {
+ return messageHandlerFactory;
+ }
+
+ @Override
+ public Set<MessageHandler> getMessageHandlers()
+ {
+ // Always return copy of set, as it is common to iterate and remove from the real set.
+ return new HashSet<MessageHandler>(messageHandlerSet);
+ }
+
+ public MessageHandlerWrapper getMessageHandlerWrapper(MessageType type)
+ {
+ synchronized (wrappers)
+ {
+ return wrappers[type.ordinal()];
+ }
+ }
+
+ @Override
+ public List<Extension> getNegotiatedExtensions()
+ {
+ if (negotiatedExtensions == null)
+ {
+ negotiatedExtensions = new ArrayList<Extension>();
+ for (ExtensionConfig cfg : getUpgradeResponse().getExtensions())
+ {
+ negotiatedExtensions.add(new JsrExtension(cfg));
+ }
+ }
+ return negotiatedExtensions;
+ }
+
+ @Override
+ public String getNegotiatedSubprotocol()
+ {
+ String acceptedSubProtocol = getUpgradeResponse().getAcceptedSubProtocol();
+ if (acceptedSubProtocol == null)
+ {
+ return "";
+ }
+ return acceptedSubProtocol;
+ }
+
+ @Override
+ public Set<Session> getOpenSessions()
+ {
+ return container.getOpenSessions();
+ }
+
+ @Override
+ public Map<String, String> getPathParameters()
+ {
+ return Collections.unmodifiableMap(pathParameters);
+ }
+
+ @Override
+ public String getQueryString()
+ {
+ return getUpgradeRequest().getRequestURI().getQuery();
+ }
+
+ @Override
+ public Map<String, List<String>> getRequestParameterMap()
+ {
+ return getUpgradeRequest().getParameterMap();
+ }
+
+ @Override
+ public URI getRequestURI()
+ {
+ return getUpgradeRequest().getRequestURI();
+ }
+
+ @Override
+ public Principal getUserPrincipal()
+ {
+ return getUpgradeRequest().getUserPrincipal();
+ }
+
+ @Override
+ public Map<String, Object> getUserProperties()
+ {
+ return config.getUserProperties();
+ }
+
+ @Override
+ public void init(EndpointConfig config)
+ {
+ // Initialize encoders
+ encoderFactory.init(config);
+ // Initialize decoders
+ decoderFactory.init(config);
+ }
+
+ @Override
+ public void removeMessageHandler(MessageHandler handler)
+ {
+ synchronized (wrappers)
+ {
+ try
+ {
+ for (MessageHandlerMetadata metadata : messageHandlerFactory.getMetadata(handler.getClass()))
+ {
+ DecoderMetadata decoder = decoderFactory.getMetadataFor(metadata.getMessageClass());
+ MessageType key = decoder.getMessageType();
+ wrappers[key.ordinal()] = null;
+ }
+ updateMessageHandlerSet();
+ }
+ catch (IllegalStateException e)
+ {
+ LOG.warn("Unable to identify MessageHandler: " + handler.getClass().getName(),e);
+ }
+ }
+ }
+
+ @Override
+ public void setMaxBinaryMessageBufferSize(int length)
+ {
+ getPolicy().setMaxBinaryMessageSize(length);
+ getPolicy().setMaxBinaryMessageBufferSize(length);
+ }
+
+ @Override
+ public void setMaxIdleTimeout(long milliseconds)
+ {
+ getPolicy().setIdleTimeout(milliseconds);
+ }
+
+ @Override
+ public void setMaxTextMessageBufferSize(int length)
+ {
+ getPolicy().setMaxTextMessageSize(length);
+ getPolicy().setMaxTextMessageBufferSize(length);
+ }
+
+ public void setPathParameters(Map<String, String> pathParams)
+ {
+ this.pathParameters.clear();
+ if (pathParams != null)
+ {
+ this.pathParameters.putAll(pathParams);
+ }
+ }
+
+ private void updateMessageHandlerSet()
+ {
+ messageHandlerSet.clear();
+ for (MessageHandlerWrapper wrapper : wrappers)
+ {
+ if (wrapper == null)
+ {
+ // skip empty
+ continue;
+ }
+ messageHandlerSet.add(wrapper.getHandler());
+ }
+ }
+}
diff --git a/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/JsrSessionFactory.java b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/JsrSessionFactory.java
new file mode 100644
index 0000000..7a5c1c5
--- /dev/null
+++ b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/JsrSessionFactory.java
@@ -0,0 +1,56 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356;
+
+import java.net.URI;
+import java.util.concurrent.atomic.AtomicLong;
+
+import org.eclipse.jetty.websocket.common.LogicalConnection;
+import org.eclipse.jetty.websocket.common.SessionFactory;
+import org.eclipse.jetty.websocket.common.WebSocketSession;
+import org.eclipse.jetty.websocket.common.events.EventDriver;
+import org.eclipse.jetty.websocket.jsr356.endpoints.AbstractJsrEventDriver;
+
+public class JsrSessionFactory implements SessionFactory
+{
+ private AtomicLong idgen = new AtomicLong(0);
+ private final ClientContainer container;
+
+ public JsrSessionFactory(ClientContainer container)
+ {
+ this.container = container;
+ }
+
+ @Override
+ public WebSocketSession createSession(URI requestURI, EventDriver websocket, LogicalConnection connection)
+ {
+ return new JsrSession(requestURI,websocket,connection,container,getNextId());
+ }
+
+ public String getNextId()
+ {
+ return String.format("websocket-%d",idgen.incrementAndGet());
+ }
+
+ @Override
+ public boolean supports(EventDriver websocket)
+ {
+ return (websocket instanceof AbstractJsrEventDriver);
+ }
+}
diff --git a/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/JsrUpgradeListener.java b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/JsrUpgradeListener.java
new file mode 100644
index 0000000..7d339c1
--- /dev/null
+++ b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/JsrUpgradeListener.java
@@ -0,0 +1,63 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356;
+
+import java.util.List;
+import java.util.Map;
+
+import javax.websocket.ClientEndpointConfig.Configurator;
+
+import org.eclipse.jetty.websocket.api.UpgradeRequest;
+import org.eclipse.jetty.websocket.api.UpgradeResponse;
+import org.eclipse.jetty.websocket.client.io.UpgradeListener;
+
+public class JsrUpgradeListener implements UpgradeListener
+{
+ private Configurator configurator;
+
+ public JsrUpgradeListener(Configurator configurator)
+ {
+ this.configurator = configurator;
+ }
+
+ @Override
+ public void onHandshakeRequest(UpgradeRequest request)
+ {
+ if (configurator == null)
+ {
+ return;
+ }
+
+ Map<String, List<String>> headers = request.getHeaders();
+ configurator.beforeRequest(headers);
+ request.setHeaders(headers);
+ }
+
+ @Override
+ public void onHandshakeResponse(UpgradeResponse response)
+ {
+ if (configurator == null)
+ {
+ return;
+ }
+
+ JsrHandshakeResponse hr = new JsrHandshakeResponse(response);
+ configurator.afterResponse(hr);
+ }
+}
diff --git a/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/MessageHandlerFactory.java b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/MessageHandlerFactory.java
new file mode 100644
index 0000000..1dd6ead
--- /dev/null
+++ b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/MessageHandlerFactory.java
@@ -0,0 +1,86 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+import javax.websocket.MessageHandler;
+
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+import org.eclipse.jetty.websocket.common.util.ReflectUtils;
+import org.eclipse.jetty.websocket.jsr356.metadata.MessageHandlerMetadata;
+
+/**
+ * Factory for {@link MessageHandlerMetadata}
+ */
+public class MessageHandlerFactory
+{
+ private static final Logger LOG = Log.getLogger(MessageHandlerFactory.class);
+ /** Registered MessageHandlers at this level */
+ private Map<Class<? extends MessageHandler>, List<MessageHandlerMetadata>> registered;
+
+ public MessageHandlerFactory()
+ {
+ registered = new ConcurrentHashMap<>();
+ }
+
+ public List<MessageHandlerMetadata> getMetadata(Class<? extends MessageHandler> handler) throws IllegalStateException
+ {
+ LOG.debug("getMetadata({})",handler);
+ List<MessageHandlerMetadata> ret = registered.get(handler);
+ if (ret != null)
+ {
+ return ret;
+ }
+
+ return register(handler);
+ }
+
+ public List<MessageHandlerMetadata> register(Class<? extends MessageHandler> handler)
+ {
+ List<MessageHandlerMetadata> metadatas = new ArrayList<>();
+
+ boolean partial = false;
+
+ if (MessageHandler.Partial.class.isAssignableFrom(handler))
+ {
+ LOG.debug("supports Partial: {}",handler);
+ partial = true;
+ Class<?> onMessageClass = ReflectUtils.findGenericClassFor(handler,MessageHandler.Partial.class);
+ LOG.debug("Partial message class: {}",onMessageClass);
+ metadatas.add(new MessageHandlerMetadata(handler,onMessageClass,partial));
+ }
+
+ if (MessageHandler.Whole.class.isAssignableFrom(handler))
+ {
+ LOG.debug("supports Whole: {}",handler.getName());
+ partial = false;
+ Class<?> onMessageClass = ReflectUtils.findGenericClassFor(handler,MessageHandler.Whole.class);
+ LOG.debug("Whole message class: {}",onMessageClass);
+ metadatas.add(new MessageHandlerMetadata(handler,onMessageClass,partial));
+ }
+
+ registered.put(handler,metadatas);
+ return metadatas;
+ }
+}
diff --git a/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/MessageHandlerWrapper.java b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/MessageHandlerWrapper.java
new file mode 100644
index 0000000..c423809
--- /dev/null
+++ b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/MessageHandlerWrapper.java
@@ -0,0 +1,86 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356;
+
+import javax.websocket.Decoder;
+import javax.websocket.MessageHandler;
+import javax.websocket.MessageHandler.Partial;
+import javax.websocket.MessageHandler.Whole;
+
+import org.eclipse.jetty.websocket.jsr356.metadata.MessageHandlerMetadata;
+
+/**
+ * Expose a {@link MessageHandler} instance along with its associated {@link MessageHandlerMetadata} and {@link DecoderWrapper}
+ */
+public class MessageHandlerWrapper
+{
+ private final MessageHandler handler;
+ private final MessageHandlerMetadata metadata;
+ private final DecoderFactory.Wrapper decoder;
+
+ public MessageHandlerWrapper(MessageHandler handler, MessageHandlerMetadata metadata, DecoderFactory.Wrapper decoder)
+ {
+ this.handler = handler;
+ this.metadata = metadata;
+ this.decoder = decoder;
+ }
+
+ public DecoderFactory.Wrapper getDecoder()
+ {
+ return decoder;
+ }
+
+ public MessageHandler getHandler()
+ {
+ return handler;
+ }
+
+ public MessageHandlerMetadata getMetadata()
+ {
+ return metadata;
+ }
+
+ public boolean isMessageType(Class<?> msgType)
+ {
+ return msgType.isAssignableFrom(metadata.getMessageClass());
+ }
+
+ /**
+ * Flag for a onMessage() that wants partial messages.
+ * <p>
+ * This indicates the use of MessageHandler.{@link Partial}.
+ *
+ * @return true for use of MessageHandler.{@link Partial}, false for use of MessageHandler.{@link Whole}
+ */
+ public boolean wantsPartialMessages()
+ {
+ return metadata.isPartialSupported();
+ }
+
+ /**
+ * Flag for a onMessage() method that wants MessageHandler.{@link Whole} with a Decoder that is based on {@link Decoder.TextStream} or
+ * {@link Decoder.BinaryStream}
+ *
+ * @return true for Streaming based Decoder, false for normal decoder for whole messages.
+ */
+ public boolean wantsStreams()
+ {
+ return decoder.getMetadata().isStreamed();
+ }
+}
diff --git a/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/MessageType.java b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/MessageType.java
new file mode 100644
index 0000000..4e1d952
--- /dev/null
+++ b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/MessageType.java
@@ -0,0 +1,31 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356;
+
+/**
+ * Basic Message Type enum.
+ * <p>
+ * The list of options mirrors the registration limits for "websocket message type" defined in JSR-356 / PFD1 section 2.1.3 "Receiving Messages".
+ */
+public enum MessageType
+{
+ TEXT,
+ BINARY,
+ PONG;
+}
\ No newline at end of file
diff --git a/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/annotations/AnnotatedEndpointMetadata.java b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/annotations/AnnotatedEndpointMetadata.java
new file mode 100644
index 0000000..776e5ef
--- /dev/null
+++ b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/annotations/AnnotatedEndpointMetadata.java
@@ -0,0 +1,138 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.annotations;
+
+import java.lang.annotation.Annotation;
+import java.util.LinkedList;
+
+import javax.websocket.EndpointConfig;
+import javax.websocket.OnClose;
+import javax.websocket.OnError;
+import javax.websocket.OnMessage;
+import javax.websocket.OnOpen;
+
+import org.eclipse.jetty.websocket.jsr356.metadata.DecoderMetadata;
+import org.eclipse.jetty.websocket.jsr356.metadata.DecoderMetadataSet;
+import org.eclipse.jetty.websocket.jsr356.metadata.EncoderMetadataSet;
+import org.eclipse.jetty.websocket.jsr356.metadata.EndpointMetadata;
+
+/**
+ * Static reference to a specific annotated classes metadata.
+ *
+ * @param <T>
+ * the annotation this metadata is based off of
+ */
+public abstract class AnnotatedEndpointMetadata<T extends Annotation, C extends EndpointConfig> implements EndpointMetadata
+{
+ /**
+ * Callable for @{@link OnOpen} annotation.
+ */
+ public OnOpenCallable onOpen;
+
+ /**
+ * Callable for @{@link OnClose} annotation
+ */
+ public OnCloseCallable onClose;
+
+ /**
+ * Callable for @{@link OnError} annotation
+ */
+ public OnErrorCallable onError;
+
+ /**
+ * Callable for @{@link OnMessage} annotation dealing with Text Message Format
+ */
+ public OnMessageTextCallable onText;
+
+ /**
+ * Callable for @{@link OnMessage} annotation dealing with Text Streaming Message Format
+ */
+ public OnMessageTextStreamCallable onTextStream;
+
+ /**
+ * Callable for @{@link OnMessage} annotation dealing with Binary Message Format
+ */
+ public OnMessageBinaryCallable onBinary;
+
+ /**
+ * Callable for @{@link OnMessage} annotation dealing with Binary Streaming Message Format
+ */
+ public OnMessageBinaryStreamCallable onBinaryStream;
+
+ /**
+ * Callable for @{@link OnMessage} annotation dealing with Pong Message Format
+ */
+ public OnMessagePongCallable onPong;
+
+ private final Class<?> endpointClass;
+ private DecoderMetadataSet decoders;
+ private EncoderMetadataSet encoders;
+
+ protected AnnotatedEndpointMetadata(Class<?> endpointClass)
+ {
+ this.endpointClass = endpointClass;
+ this.decoders = new DecoderMetadataSet();
+ this.encoders = new EncoderMetadataSet();
+ }
+
+ public void customizeParamsOnClose(LinkedList<IJsrParamId> params)
+ {
+ /* do nothing */
+ }
+
+ public void customizeParamsOnError(LinkedList<IJsrParamId> params)
+ {
+ /* do nothing */
+ }
+
+ public void customizeParamsOnMessage(LinkedList<IJsrParamId> params)
+ {
+ for (DecoderMetadata metadata : decoders)
+ {
+ params.add(new JsrParamIdDecoder(metadata));
+ }
+ }
+
+ public void customizeParamsOnOpen(LinkedList<IJsrParamId> params)
+ {
+ /* do nothing */
+ }
+
+ public abstract T getAnnotation();
+
+ public abstract C getConfig();
+
+ @Override
+ public DecoderMetadataSet getDecoders()
+ {
+ return decoders;
+ }
+
+ @Override
+ public EncoderMetadataSet getEncoders()
+ {
+ return encoders;
+ }
+
+ @Override
+ public Class<?> getEndpointClass()
+ {
+ return endpointClass;
+ }
+}
diff --git a/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/annotations/AnnotatedEndpointScanner.java b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/annotations/AnnotatedEndpointScanner.java
new file mode 100644
index 0000000..70075e4
--- /dev/null
+++ b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/annotations/AnnotatedEndpointScanner.java
@@ -0,0 +1,205 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.annotations;
+
+import java.lang.annotation.Annotation;
+import java.lang.reflect.Method;
+import java.util.LinkedList;
+import java.util.List;
+
+import javax.websocket.EndpointConfig;
+import javax.websocket.OnClose;
+import javax.websocket.OnError;
+import javax.websocket.OnMessage;
+import javax.websocket.OnOpen;
+
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+import org.eclipse.jetty.websocket.common.events.annotated.AbstractMethodAnnotationScanner;
+import org.eclipse.jetty.websocket.common.events.annotated.InvalidSignatureException;
+import org.eclipse.jetty.websocket.common.util.ReflectUtils;
+
+public class AnnotatedEndpointScanner<T extends Annotation, C extends EndpointConfig> extends AbstractMethodAnnotationScanner<AnnotatedEndpointMetadata<T, C>>
+{
+ private static final Logger LOG = Log.getLogger(AnnotatedEndpointScanner.class);
+
+ private final LinkedList<IJsrParamId> paramsOnOpen;
+ private final LinkedList<IJsrParamId> paramsOnClose;
+ private final LinkedList<IJsrParamId> paramsOnError;
+ private final LinkedList<IJsrParamId> paramsOnMessage;
+ private final AnnotatedEndpointMetadata<T, C> metadata;
+
+ public AnnotatedEndpointScanner(AnnotatedEndpointMetadata<T, C> metadata)
+ {
+ this.metadata = metadata;
+
+ paramsOnOpen = new LinkedList<>();
+ paramsOnClose = new LinkedList<>();
+ paramsOnError = new LinkedList<>();
+ paramsOnMessage = new LinkedList<>();
+
+ metadata.customizeParamsOnOpen(paramsOnOpen);
+ paramsOnOpen.add(JsrParamIdOnOpen.INSTANCE);
+
+ metadata.customizeParamsOnClose(paramsOnClose);
+ paramsOnClose.add(JsrParamIdOnClose.INSTANCE);
+
+ metadata.customizeParamsOnError(paramsOnError);
+ paramsOnError.add(JsrParamIdOnError.INSTANCE);
+
+ metadata.customizeParamsOnMessage(paramsOnMessage);
+ paramsOnMessage.add(JsrParamIdBinary.INSTANCE);
+ paramsOnMessage.add(JsrParamIdText.INSTANCE);
+ paramsOnMessage.add(JsrParamIdPong.INSTANCE);
+ }
+
+ private void assertNotDuplicate(JsrCallable callable, Class<? extends Annotation> methodAnnotationClass, Class<?> pojo, Method method)
+ {
+ if (callable != null)
+ {
+ // Duplicate annotation detected
+ StringBuilder err = new StringBuilder();
+ err.append("Encountered duplicate method annotations @");
+ err.append(methodAnnotationClass.getSimpleName());
+ err.append(" on ");
+ err.append(ReflectUtils.toString(pojo,callable.getMethod()));
+ err.append(" and ");
+ err.append(ReflectUtils.toString(pojo,method));
+
+ throw new InvalidSignatureException(err.toString());
+ }
+ }
+
+ @Override
+ public void onMethodAnnotation(AnnotatedEndpointMetadata<T, C> metadata, Class<?> pojo, Method method, Annotation annotation)
+ {
+ LOG.debug("onMethodAnnotation({}, {}, {}, {})",metadata,pojo,method,annotation);
+
+ if (isAnnotation(annotation,OnOpen.class))
+ {
+ assertIsPublicNonStatic(method);
+ assertIsReturn(method,Void.TYPE);
+ assertNotDuplicate(metadata.onOpen,OnOpen.class,pojo,method);
+ OnOpenCallable onopen = new OnOpenCallable(pojo,method);
+ visitMethod(onopen,pojo,method,paramsOnOpen,OnOpen.class);
+ metadata.onOpen = onopen;
+ return;
+ }
+
+ if (isAnnotation(annotation,OnClose.class))
+ {
+ assertIsPublicNonStatic(method);
+ assertIsReturn(method,Void.TYPE);
+ assertNotDuplicate(metadata.onClose,OnClose.class,pojo,method);
+ OnCloseCallable onclose = new OnCloseCallable(pojo,method);
+ visitMethod(onclose,pojo,method,paramsOnClose,OnClose.class);
+ metadata.onClose = onclose;
+ return;
+ }
+
+ if (isAnnotation(annotation,OnError.class))
+ {
+ assertIsPublicNonStatic(method);
+ assertIsReturn(method,Void.TYPE);
+ assertNotDuplicate(metadata.onError,OnError.class,pojo,method);
+ OnErrorCallable onerror = new OnErrorCallable(pojo,method);
+ visitMethod(onerror,pojo,method,paramsOnError,OnError.class);
+ metadata.onError = onerror;
+ return;
+ }
+
+ if (isAnnotation(annotation,OnMessage.class))
+ {
+ assertIsPublicNonStatic(method);
+ // assertIsReturn(method,Void.TYPE); // no validation, it can be any return type
+ OnMessageCallable onmessage = new OnMessageCallable(pojo,method);
+ visitMethod(onmessage,pojo,method,paramsOnMessage,OnMessage.class);
+
+ Param param = onmessage.getMessageObjectParam();
+ switch (param.role)
+ {
+ case MESSAGE_BINARY:
+ metadata.onBinary = new OnMessageBinaryCallable(onmessage);
+ break;
+ case MESSAGE_BINARY_STREAM:
+ metadata.onBinaryStream = new OnMessageBinaryStreamCallable(onmessage);
+ break;
+ case MESSAGE_TEXT:
+ metadata.onText = new OnMessageTextCallable(onmessage);
+ break;
+ case MESSAGE_TEXT_STREAM:
+ metadata.onTextStream = new OnMessageTextStreamCallable(onmessage);
+ break;
+ case MESSAGE_PONG:
+ metadata.onPong = new OnMessagePongCallable(onmessage);
+ break;
+ default:
+ StringBuilder err = new StringBuilder();
+ err.append("An unrecognized message type <");
+ err.append(param.type);
+ err.append(">: does not meet specified type categories of [TEXT, BINARY, DECODER, or PONG]");
+ throw new InvalidSignatureException(err.toString());
+ }
+ }
+ }
+
+ public AnnotatedEndpointMetadata<T, C> scan()
+ {
+ scanMethodAnnotations(metadata,metadata.getEndpointClass());
+ return metadata;
+ }
+
+ private void visitMethod(JsrCallable callable, Class<?> pojo, Method method, LinkedList<IJsrParamId> paramIds,
+ Class<? extends Annotation> methodAnnotationClass)
+ {
+ // Identify all of the parameters
+ for (Param param : callable.getParams())
+ {
+ if (!visitParam(callable,param,paramIds))
+ {
+ StringBuilder err = new StringBuilder();
+ err.append("Encountered unknown parameter type <");
+ err.append(param.type.getName());
+ err.append("> on @");
+ err.append(methodAnnotationClass.getSimpleName());
+ err.append(" annotated method: ");
+ err.append(ReflectUtils.toString(pojo,method));
+
+ throw new InvalidSignatureException(err.toString());
+ }
+ }
+ }
+
+ private boolean visitParam(JsrCallable callable, Param param, List<IJsrParamId> paramIds)
+ {
+ for (IJsrParamId paramId : paramIds)
+ {
+ LOG.debug("{}.process()",paramId);
+ if (paramId.process(param,callable))
+ {
+ // Successfully identified
+ LOG.debug("Identified: {}",param);
+ return true;
+ }
+ }
+
+ // Failed identification as a known parameter
+ return false;
+ }
+}
diff --git a/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/annotations/IJsrMethod.java b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/annotations/IJsrMethod.java
new file mode 100644
index 0000000..eb84703
--- /dev/null
+++ b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/annotations/IJsrMethod.java
@@ -0,0 +1,84 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.annotations;
+
+import java.lang.reflect.Method;
+
+import javax.websocket.Decoder;
+import javax.websocket.OnMessage;
+
+import org.eclipse.jetty.websocket.jsr356.MessageType;
+
+public interface IJsrMethod
+{
+ /**
+ * Indicate that partial message support is desired
+ */
+ void enablePartialMessageSupport();
+
+ /**
+ * Get the fully qualifed method name {classname}.{methodname}({params}) suitable for using in error messages.
+ *
+ * @return the fully qualified method name for end users
+ */
+ String getFullyQualifiedMethodName();
+
+ /**
+ * Get the Decoder to use for message decoding
+ *
+ * @return the decoder class to use for message decoding
+ */
+ Class<? extends Decoder> getMessageDecoder();
+
+ /**
+ * The type of message this method can handle
+ *
+ * @return the message type if @{@link OnMessage} annotated, {@link MessageType#UNKNOWN} if unknown/unspecified
+ */
+ MessageType getMessageType();
+
+ /**
+ * The reflected method
+ *
+ * @return the method itself
+ */
+ Method getMethod();
+
+ /**
+ * Indicator that partial message support is enabled
+ *
+ * @return true if enabled
+ */
+ boolean isPartialMessageSupportEnabled();
+
+ /**
+ * The message decoder class to use.
+ *
+ * @param decoderClass
+ */
+ void setMessageDecoder(Class<? extends Decoder> decoderClass);
+
+ /**
+ * The type of message this method can handle
+ *
+ * @param type
+ * the type of message
+ */
+ void setMessageType(MessageType type);
+}
diff --git a/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/annotations/IJsrParamId.java b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/annotations/IJsrParamId.java
new file mode 100644
index 0000000..9654f2b
--- /dev/null
+++ b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/annotations/IJsrParamId.java
@@ -0,0 +1,43 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.annotations;
+
+import org.eclipse.jetty.websocket.common.events.annotated.InvalidSignatureException;
+
+/**
+ * JSR-356 Parameter Identification processing.
+ */
+public interface IJsrParamId
+{
+ /**
+ * Process the potential parameter.
+ * <p>
+ * If known to be a valid parameter, bind a role to it.
+ *
+ * @param param
+ * the parameter being processed
+ * @param callable
+ * the callable this param belongs to (used to obtain extra state about the callable that might impact decision making)
+ *
+ * @return true if processed, false if not processed
+ * @throws InvalidSignatureException
+ * if a violation of the signature rules occurred
+ */
+ boolean process(Param param, JsrCallable callable) throws InvalidSignatureException;
+}
diff --git a/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/annotations/JsrCallable.java b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/annotations/JsrCallable.java
new file mode 100644
index 0000000..0261677
--- /dev/null
+++ b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/annotations/JsrCallable.java
@@ -0,0 +1,175 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.annotations;
+
+import java.lang.annotation.Annotation;
+import java.lang.reflect.Method;
+import java.util.Map;
+
+import javax.websocket.DecodeException;
+import javax.websocket.Decoder;
+
+import org.eclipse.jetty.websocket.api.InvalidWebSocketException;
+import org.eclipse.jetty.websocket.common.events.annotated.CallableMethod;
+import org.eclipse.jetty.websocket.common.events.annotated.InvalidSignatureException;
+import org.eclipse.jetty.websocket.common.util.ReflectUtils;
+import org.eclipse.jetty.websocket.jsr356.JsrSession;
+import org.eclipse.jetty.websocket.jsr356.annotations.Param.Role;
+
+public abstract class JsrCallable extends CallableMethod
+{
+ protected final Param[] params;
+ protected final Object[] args;
+ protected int idxSession = -1;
+ protected int idxConfig = -1;
+
+ public JsrCallable(Class<?> pojo, Method method)
+ {
+ super(pojo,method);
+
+ Class<?> ptypes[] = method.getParameterTypes();
+ Annotation pannos[][] = method.getParameterAnnotations();
+ int len = ptypes.length;
+ params = new Param[len];
+ for (int i = 0; i < len; i++)
+ {
+ params[i] = new Param(i,ptypes[i],pannos[i]);
+ }
+
+ args = new Object[len];
+ }
+
+ /**
+ * Copy Constructor
+ */
+ public JsrCallable(JsrCallable copy)
+ {
+ this(copy.getPojo(),copy.getMethod());
+ this.idxSession = copy.idxSession;
+ this.idxConfig = copy.idxConfig;
+ System.arraycopy(copy.params,0,this.params,0,params.length);
+ System.arraycopy(copy.args,0,this.args,0,args.length);
+ }
+
+ protected void assertRoleRequired(int index, String description)
+ {
+ if (index < 0)
+ {
+ StringBuilder err = new StringBuilder();
+ err.append("Unable to find parameter with role [");
+ err.append(description).append("] in method: ");
+ err.append(ReflectUtils.toString(pojo,method));
+ throw new InvalidSignatureException(err.toString());
+ }
+ }
+
+ /**
+ * Search the list of parameters for first one matching the role specified.
+ *
+ * @param role
+ * the role to look for
+ * @return the index for the role specified (or -1 if not found)
+ */
+ protected int findIndexForRole(Role role)
+ {
+ Param param = findParamForRole(role);
+ if (param != null)
+ {
+ return param.index;
+ }
+ return -1;
+ }
+
+ /**
+ * Find first param for specified role.
+ *
+ * @param role
+ * the role specified
+ * @return the param (or null if not found)
+ */
+ protected Param findParamForRole(Role role)
+ {
+ for (Param param : params)
+ {
+ if (param.role == role)
+ {
+ return param;
+ }
+ }
+ return null;
+ }
+
+ public Param[] getParams()
+ {
+ return params;
+ }
+
+ public void init(JsrSession session)
+ {
+ // Default for the session.
+ // Session is an optional parameter (always)
+ idxSession = findIndexForRole(Param.Role.SESSION);
+ if (idxSession >= 0)
+ {
+ args[idxSession] = session;
+ }
+
+ // Optional EndpointConfig
+ idxConfig = findIndexForRole(Param.Role.ENDPOINT_CONFIG);
+ if (idxConfig >= 0)
+ {
+ args[idxConfig] = session.getEndpointConfig();
+ }
+
+ // Default for the path parameters
+ // PathParam's are optional parameters (always)
+ Map<String, String> pathParams = session.getPathParameters();
+ if ((pathParams != null) && (pathParams.size() > 0))
+ {
+ for (Param param : params)
+ {
+ if (param.role == Role.PATH_PARAM)
+ {
+ int idx = param.index;
+ String rawvalue = pathParams.get(param.getPathParamName());
+
+ Decoder decoder = session.getDecoderFactory().getDecoderFor(param.type);
+ if (decoder instanceof Decoder.Text<?>)
+ {
+ Decoder.Text<?> textDecoder = (Decoder.Text<?>)decoder;
+ try
+ {
+ args[idx] = textDecoder.decode(rawvalue);
+ }
+ catch (DecodeException e)
+ {
+ session.notifyError(e);
+ }
+ }
+ else
+ {
+ throw new InvalidWebSocketException("PathParam decoders must use Decoder.Text");
+ }
+ }
+ }
+ }
+ }
+
+ public abstract void setDecoderClass(Class<? extends Decoder> decoderClass);
+}
diff --git a/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/annotations/JsrEvents.java b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/annotations/JsrEvents.java
new file mode 100644
index 0000000..cb11d4d
--- /dev/null
+++ b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/annotations/JsrEvents.java
@@ -0,0 +1,292 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.annotations;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.Reader;
+import java.lang.annotation.Annotation;
+import java.nio.ByteBuffer;
+import java.util.Map;
+
+import javax.websocket.CloseReason;
+import javax.websocket.DecodeException;
+import javax.websocket.EndpointConfig;
+import javax.websocket.OnClose;
+import javax.websocket.OnError;
+import javax.websocket.OnMessage;
+import javax.websocket.OnOpen;
+import javax.websocket.RemoteEndpoint;
+
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+import org.eclipse.jetty.websocket.jsr356.JsrSession;
+
+/**
+ * The live event methods found for a specific Annotated Endpoint
+ */
+public class JsrEvents<T extends Annotation, C extends EndpointConfig>
+{
+ private static final Logger LOG = Log.getLogger(JsrEvents.class);
+ private final AnnotatedEndpointMetadata<T, C> metadata;
+
+ /**
+ * Callable for @{@link OnOpen} annotation.
+ */
+ private final OnOpenCallable onOpen;
+
+ /**
+ * Callable for @{@link OnClose} annotation
+ */
+ private final OnCloseCallable onClose;
+
+ /**
+ * Callable for @{@link OnError} annotation
+ */
+ private final OnErrorCallable onError;
+
+ /**
+ * Callable for @{@link OnMessage} annotation dealing with Text Message Format
+ */
+ private final OnMessageTextCallable onText;
+
+ /**
+ * Callable for @{@link OnMessage} annotation dealing with Text Streaming Message Format
+ */
+ private final OnMessageTextStreamCallable onTextStream;
+
+ /**
+ * Callable for @{@link OnMessage} annotation dealing with Binary Message Format
+ */
+ private final OnMessageBinaryCallable onBinary;
+
+ /**
+ * Callable for @{@link OnMessage} annotation dealing with Binary Streaming Message Format
+ */
+ private final OnMessageBinaryStreamCallable onBinaryStream;
+
+ /**
+ * Callable for @{@link OnMessage} annotation dealing with Pong Message Format
+ */
+ private OnMessagePongCallable onPong;
+
+ /**
+ * The Request Parameters (from resolved javax.websocket.server.PathParam entries)
+ */
+ private Map<String, String> requestParameters;
+
+ public JsrEvents(AnnotatedEndpointMetadata<T, C> metadata)
+ {
+ this.metadata = metadata;
+ this.onOpen = (metadata.onOpen == null)?null:new OnOpenCallable(metadata.onOpen);
+ this.onClose = (metadata.onClose == null)?null:new OnCloseCallable(metadata.onClose);
+ this.onError = (metadata.onError == null)?null:new OnErrorCallable(metadata.onError);
+ this.onBinary = (metadata.onBinary == null)?null:new OnMessageBinaryCallable(metadata.onBinary);
+ this.onBinaryStream = (metadata.onBinaryStream == null)?null:new OnMessageBinaryStreamCallable(metadata.onBinaryStream);
+ this.onText = (metadata.onText == null)?null:new OnMessageTextCallable(metadata.onText);
+ this.onTextStream = (metadata.onTextStream == null)?null:new OnMessageTextStreamCallable(metadata.onTextStream);
+ this.onPong = (metadata.onPong == null)?null:new OnMessagePongCallable(metadata.onPong);
+ }
+
+ public void callBinary(RemoteEndpoint.Async endpoint, Object websocket, ByteBuffer buf, boolean fin) throws DecodeException
+ {
+ if (onBinary == null)
+ {
+ return;
+ }
+
+ Object ret = onBinary.call(websocket,buf,fin);
+ if (ret != null)
+ {
+ LOG.debug("returning: {}",ret);
+ endpoint.sendObject(ret);
+ }
+ }
+
+ public void callBinaryStream(RemoteEndpoint.Async endpoint, Object websocket, InputStream stream) throws DecodeException, IOException
+ {
+ if (onBinaryStream == null)
+ {
+ return;
+ }
+
+ Object ret = onBinaryStream.call(websocket,stream);
+ if (ret != null)
+ {
+ LOG.debug("returning: {}",ret);
+ endpoint.sendObject(ret);
+ }
+ }
+
+ public void callClose(Object websocket, CloseReason close)
+ {
+ if (onClose == null)
+ {
+ return;
+ }
+ onClose.call(websocket,close);
+ }
+
+ public void callError(Object websocket, Throwable cause)
+ {
+ if (onError == null)
+ {
+ return;
+ }
+ onError.call(websocket,cause);
+ }
+
+ public void callOpen(Object websocket, EndpointConfig config)
+ {
+ if (onOpen == null)
+ {
+ return;
+ }
+ onOpen.call(websocket,config);
+ }
+
+ public void callPong(RemoteEndpoint.Async endpoint, Object websocket, ByteBuffer pong) throws DecodeException, IOException
+ {
+ if (onPong == null)
+ {
+ return;
+ }
+
+ Object ret = onPong.call(websocket,pong);
+ if (ret != null)
+ {
+ LOG.debug("returning: {}",ret);
+ endpoint.sendObject(ret);
+ }
+ }
+
+ public void callText(RemoteEndpoint.Async endpoint, Object websocket, String text, boolean fin) throws DecodeException
+ {
+ if (onText == null)
+ {
+ return;
+ }
+ Object ret = onText.call(websocket,text,fin);
+ if (ret != null)
+ {
+ LOG.debug("returning: {}",ret);
+ endpoint.sendObject(ret);
+ }
+ }
+
+ public void callTextStream(RemoteEndpoint.Async endpoint, Object websocket, Reader reader) throws DecodeException, IOException
+ {
+ if (onTextStream == null)
+ {
+ return;
+ }
+ Object ret = onTextStream.call(websocket,reader);
+ if (ret != null)
+ {
+ LOG.debug("returning: {}",ret);
+ endpoint.sendObject(ret);
+ }
+ }
+
+ public AnnotatedEndpointMetadata<T, C> getMetadata()
+ {
+ return metadata;
+ }
+
+ public boolean hasBinary()
+ {
+ return (onBinary != null);
+ }
+
+ public boolean hasBinaryStream()
+ {
+ return (onBinaryStream != null);
+ }
+
+ public boolean hasText()
+ {
+ return (onText != null);
+ }
+
+ public boolean hasTextStream()
+ {
+ return (onTextStream != null);
+ }
+
+ public void init(JsrSession session)
+ {
+ session.setPathParameters(requestParameters);
+
+ if (onOpen != null)
+ {
+ onOpen.init(session);
+ }
+ if (onClose != null)
+ {
+ onClose.init(session);
+ }
+ if (onError != null)
+ {
+ onError.init(session);
+ }
+ if (onText != null)
+ {
+ onText.init(session);
+ }
+ if (onTextStream != null)
+ {
+ onTextStream.init(session);
+ }
+ if (onBinary != null)
+ {
+ onBinary.init(session);
+ }
+ if (onBinaryStream != null)
+ {
+ onBinaryStream.init(session);
+ }
+ if (onPong != null)
+ {
+ onPong.init(session);
+ }
+ }
+
+ public boolean isBinaryPartialSupported()
+ {
+ if (onBinary == null)
+ {
+ return false;
+ }
+ return onBinary.isPartialMessageSupported();
+ }
+
+ public boolean isTextPartialSupported()
+ {
+ if (onText == null)
+ {
+ return false;
+ }
+ return onText.isPartialMessageSupported();
+ }
+
+ public void setRequestParameters(Map<String, String> requestParameters)
+ {
+ this.requestParameters = requestParameters;
+ }
+}
diff --git a/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/annotations/JsrParamIdBase.java b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/annotations/JsrParamIdBase.java
new file mode 100644
index 0000000..19bfdb2
--- /dev/null
+++ b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/annotations/JsrParamIdBase.java
@@ -0,0 +1,51 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.annotations;
+
+import javax.websocket.EndpointConfig;
+import javax.websocket.Session;
+
+import org.eclipse.jetty.websocket.common.events.annotated.InvalidSignatureException;
+import org.eclipse.jetty.websocket.jsr356.annotations.Param.Role;
+
+/**
+ * Common base for Parameter Identification in JSR Callable methods
+ */
+public abstract class JsrParamIdBase implements IJsrParamId
+{
+ @Override
+ public boolean process(Param param, JsrCallable callable) throws InvalidSignatureException
+ {
+ // Session parameter (optional)
+ if (param.type.isAssignableFrom(Session.class))
+ {
+ param.bind(Role.SESSION);
+ return true;
+ }
+
+ // Endpoint Config (optional)
+ if (param.type.isAssignableFrom(EndpointConfig.class))
+ {
+ param.bind(Role.ENDPOINT_CONFIG);
+ return true;
+ }
+
+ return false;
+ }
+}
diff --git a/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/annotations/JsrParamIdBinary.java b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/annotations/JsrParamIdBinary.java
new file mode 100644
index 0000000..aafe1ad
--- /dev/null
+++ b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/annotations/JsrParamIdBinary.java
@@ -0,0 +1,72 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.annotations;
+
+import java.io.InputStream;
+import java.nio.ByteBuffer;
+
+import javax.websocket.OnMessage;
+
+import org.eclipse.jetty.websocket.common.events.annotated.InvalidSignatureException;
+import org.eclipse.jetty.websocket.jsr356.annotations.Param.Role;
+import org.eclipse.jetty.websocket.jsr356.decoders.ByteArrayDecoder;
+import org.eclipse.jetty.websocket.jsr356.decoders.ByteBufferDecoder;
+
+/**
+ * Param handling for static Binary @{@link OnMessage} parameters.
+ */
+public class JsrParamIdBinary extends JsrParamIdOnMessage implements IJsrParamId
+{
+ public static final IJsrParamId INSTANCE = new JsrParamIdBinary();
+
+ @Override
+ public boolean process(Param param, JsrCallable callable) throws InvalidSignatureException
+ {
+ if (super.process(param,callable))
+ {
+ // Found common roles
+ return true;
+ }
+
+ if (param.type.isAssignableFrom(ByteBuffer.class))
+ {
+ param.bind(Role.MESSAGE_BINARY);
+ callable.setDecoderClass(ByteBufferDecoder.class);
+ return true;
+ }
+
+ if (param.type.isAssignableFrom(byte[].class))
+ {
+ param.bind(Role.MESSAGE_BINARY);
+ callable.setDecoderClass(ByteArrayDecoder.class);
+ return true;
+ }
+
+ // Streaming
+ if (param.type.isAssignableFrom(InputStream.class))
+ {
+ assertPartialMessageSupportDisabled(param,callable);
+ param.bind(Role.MESSAGE_BINARY_STREAM);
+ // Streaming have no decoder
+ return true;
+ }
+
+ return false;
+ }
+}
diff --git a/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/annotations/JsrParamIdDecoder.java b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/annotations/JsrParamIdDecoder.java
new file mode 100644
index 0000000..8942eec
--- /dev/null
+++ b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/annotations/JsrParamIdDecoder.java
@@ -0,0 +1,78 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.annotations;
+
+import javax.websocket.Decoder;
+import javax.websocket.OnMessage;
+
+import org.eclipse.jetty.websocket.common.events.annotated.InvalidSignatureException;
+import org.eclipse.jetty.websocket.jsr356.annotations.Param.Role;
+import org.eclipse.jetty.websocket.jsr356.metadata.DecoderMetadata;
+
+/**
+ * Param handling for Text or Binary @{@link OnMessage} parameters declared as {@link Decoder}s
+ */
+public class JsrParamIdDecoder extends JsrParamIdOnMessage implements IJsrParamId
+{
+ private final DecoderMetadata metadata;
+
+ public JsrParamIdDecoder(DecoderMetadata metadata)
+ {
+ this.metadata = metadata;
+ }
+
+ @Override
+ public boolean process(Param param, JsrCallable callable) throws InvalidSignatureException
+ {
+ if (param.type.isAssignableFrom(metadata.getObjectType()))
+ {
+ assertPartialMessageSupportDisabled(param,callable);
+
+ switch (metadata.getMessageType())
+ {
+ case TEXT:
+ if (metadata.isStreamed())
+ {
+ param.bind(Role.MESSAGE_TEXT_STREAM);
+ }
+ else
+ {
+ param.bind(Role.MESSAGE_TEXT);
+ }
+ break;
+ case BINARY:
+ if (metadata.isStreamed())
+ {
+ param.bind(Role.MESSAGE_BINARY_STREAM);
+ }
+ else
+ {
+ param.bind(Role.MESSAGE_BINARY);
+ }
+ break;
+ case PONG:
+ param.bind(Role.MESSAGE_PONG);
+ break;
+ }
+ callable.setDecoderClass(metadata.getCoderClass());
+ return true;
+ }
+ return false;
+ }
+}
diff --git a/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/annotations/JsrParamIdOnClose.java b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/annotations/JsrParamIdOnClose.java
new file mode 100644
index 0000000..7b271a4
--- /dev/null
+++ b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/annotations/JsrParamIdOnClose.java
@@ -0,0 +1,50 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.annotations;
+
+import javax.websocket.CloseReason;
+import javax.websocket.OnClose;
+
+import org.eclipse.jetty.websocket.common.events.annotated.InvalidSignatureException;
+import org.eclipse.jetty.websocket.jsr356.annotations.Param.Role;
+
+/**
+ * Param handling for @{@link OnClose} parameters.
+ */
+public class JsrParamIdOnClose extends JsrParamIdBase implements IJsrParamId
+{
+ public static final IJsrParamId INSTANCE = new JsrParamIdOnClose();
+
+ @Override
+ public boolean process(Param param, JsrCallable callable) throws InvalidSignatureException
+ {
+ if (super.process(param,callable))
+ {
+ // Found common roles
+ return true;
+ }
+
+ if (param.type.isAssignableFrom(CloseReason.class))
+ {
+ param.bind(Role.CLOSE_REASON);
+ return true;
+ }
+ return false;
+ }
+}
diff --git a/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/annotations/JsrParamIdOnError.java b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/annotations/JsrParamIdOnError.java
new file mode 100644
index 0000000..765f7b4
--- /dev/null
+++ b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/annotations/JsrParamIdOnError.java
@@ -0,0 +1,49 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.annotations;
+
+import javax.websocket.OnError;
+
+import org.eclipse.jetty.websocket.common.events.annotated.InvalidSignatureException;
+import org.eclipse.jetty.websocket.jsr356.annotations.Param.Role;
+
+/**
+ * Param handling for @{@link OnError} parameters.
+ */
+public class JsrParamIdOnError extends JsrParamIdBase implements IJsrParamId
+{
+ public static final IJsrParamId INSTANCE = new JsrParamIdOnError();
+
+ @Override
+ public boolean process(Param param, JsrCallable callable) throws InvalidSignatureException
+ {
+ if (super.process(param,callable))
+ {
+ // Found common roles
+ return true;
+ }
+
+ if (param.type.isAssignableFrom(Throwable.class))
+ {
+ param.bind(Role.ERROR_CAUSE);
+ return true;
+ }
+ return false;
+ }
+}
diff --git a/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/annotations/JsrParamIdOnMessage.java b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/annotations/JsrParamIdOnMessage.java
new file mode 100644
index 0000000..c9bb86b
--- /dev/null
+++ b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/annotations/JsrParamIdOnMessage.java
@@ -0,0 +1,40 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.annotations;
+
+import org.eclipse.jetty.websocket.common.events.annotated.InvalidSignatureException;
+
+public abstract class JsrParamIdOnMessage extends JsrParamIdBase implements IJsrParamId
+{
+ protected void assertPartialMessageSupportDisabled(Param param, JsrCallable callable)
+ {
+ if (callable instanceof OnMessageCallable)
+ {
+ OnMessageCallable onmessage = (OnMessageCallable)callable;
+ if (onmessage.isPartialMessageSupported())
+ {
+ StringBuilder err = new StringBuilder();
+ err.append("Unable to support parameter type <");
+ err.append(param.type.getName()).append("> in conjunction with the partial message indicator boolean.");
+ err.append(" Only type <String> is supported with partial message boolean indicator.");
+ throw new InvalidSignatureException(err.toString());
+ }
+ }
+ }
+}
diff --git a/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/annotations/JsrParamIdOnOpen.java b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/annotations/JsrParamIdOnOpen.java
new file mode 100644
index 0000000..1340187
--- /dev/null
+++ b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/annotations/JsrParamIdOnOpen.java
@@ -0,0 +1,50 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.annotations;
+
+import javax.websocket.EndpointConfig;
+import javax.websocket.OnOpen;
+
+import org.eclipse.jetty.websocket.common.events.annotated.InvalidSignatureException;
+import org.eclipse.jetty.websocket.jsr356.annotations.Param.Role;
+
+/**
+ * Param handling for @{@link OnOpen} parameters.
+ */
+public class JsrParamIdOnOpen extends JsrParamIdBase implements IJsrParamId
+{
+ public static final IJsrParamId INSTANCE = new JsrParamIdOnOpen();
+
+ @Override
+ public boolean process(Param param, JsrCallable callable) throws InvalidSignatureException
+ {
+ if (super.process(param,callable))
+ {
+ // Found common roles
+ return true;
+ }
+
+ if (param.type.isAssignableFrom(EndpointConfig.class))
+ {
+ param.bind(Role.ENDPOINT_CONFIG);
+ return true;
+ }
+ return false;
+ }
+}
diff --git a/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/annotations/JsrParamIdPong.java b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/annotations/JsrParamIdPong.java
new file mode 100644
index 0000000..2f7d093
--- /dev/null
+++ b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/annotations/JsrParamIdPong.java
@@ -0,0 +1,49 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.annotations;
+
+import javax.websocket.PongMessage;
+
+import org.eclipse.jetty.websocket.common.events.annotated.InvalidSignatureException;
+import org.eclipse.jetty.websocket.jsr356.annotations.Param.Role;
+import org.eclipse.jetty.websocket.jsr356.decoders.PongMessageDecoder;
+
+public class JsrParamIdPong extends JsrParamIdOnMessage implements IJsrParamId
+{
+ public static final IJsrParamId INSTANCE = new JsrParamIdPong();
+
+ @Override
+ public boolean process(Param param, JsrCallable callable) throws InvalidSignatureException
+ {
+ if (super.process(param,callable))
+ {
+ // Found common roles
+ return true;
+ }
+
+ if (param.type.isAssignableFrom(PongMessage.class))
+ {
+ assertPartialMessageSupportDisabled(param,callable);
+ param.bind(Role.MESSAGE_PONG);
+ callable.setDecoderClass(PongMessageDecoder.class);
+ return true;
+ }
+ return false;
+ }
+}
diff --git a/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/annotations/JsrParamIdText.java b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/annotations/JsrParamIdText.java
new file mode 100644
index 0000000..d7eb8f9
--- /dev/null
+++ b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/annotations/JsrParamIdText.java
@@ -0,0 +1,160 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.annotations;
+
+import java.io.Reader;
+
+import javax.websocket.OnMessage;
+
+import org.eclipse.jetty.websocket.common.events.annotated.InvalidSignatureException;
+import org.eclipse.jetty.websocket.jsr356.annotations.Param.Role;
+import org.eclipse.jetty.websocket.jsr356.decoders.BooleanDecoder;
+import org.eclipse.jetty.websocket.jsr356.decoders.ByteDecoder;
+import org.eclipse.jetty.websocket.jsr356.decoders.CharacterDecoder;
+import org.eclipse.jetty.websocket.jsr356.decoders.DoubleDecoder;
+import org.eclipse.jetty.websocket.jsr356.decoders.FloatDecoder;
+import org.eclipse.jetty.websocket.jsr356.decoders.IntegerDecoder;
+import org.eclipse.jetty.websocket.jsr356.decoders.LongDecoder;
+import org.eclipse.jetty.websocket.jsr356.decoders.ReaderDecoder;
+import org.eclipse.jetty.websocket.jsr356.decoders.ShortDecoder;
+import org.eclipse.jetty.websocket.jsr356.decoders.StringDecoder;
+
+/**
+ * Param handling for static Text @{@link OnMessage} parameters
+ */
+public class JsrParamIdText extends JsrParamIdOnMessage implements IJsrParamId
+{
+ public static final IJsrParamId INSTANCE = new JsrParamIdText();
+
+ private boolean isMessageRoleAssigned(JsrCallable callable)
+ {
+ if (callable instanceof OnMessageCallable)
+ {
+ OnMessageCallable onmessage = (OnMessageCallable)callable;
+ return onmessage.isMessageRoleAssigned();
+ }
+ return false;
+ }
+
+ @Override
+ public boolean process(Param param, JsrCallable callable) throws InvalidSignatureException
+ {
+ if (super.process(param,callable))
+ {
+ // Found common roles
+ return true;
+ }
+
+ // String for whole message
+ if (param.type.isAssignableFrom(String.class))
+ {
+ param.bind(Role.MESSAGE_TEXT);
+ callable.setDecoderClass(StringDecoder.class);
+ return true;
+ }
+
+ // Java primitive or class equivalent to receive the whole message converted to that type
+ if (param.type.isAssignableFrom(Boolean.class))
+ {
+ assertPartialMessageSupportDisabled(param,callable);
+ param.bind(Role.MESSAGE_TEXT);
+ callable.setDecoderClass(BooleanDecoder.class);
+ return true;
+ }
+ if (param.type.isAssignableFrom(Byte.class) || (param.type == Byte.TYPE))
+ {
+ assertPartialMessageSupportDisabled(param,callable);
+ param.bind(Role.MESSAGE_TEXT);
+ callable.setDecoderClass(ByteDecoder.class);
+ return true;
+ }
+ if (param.type.isAssignableFrom(Character.class) || (param.type == Character.TYPE))
+ {
+ assertPartialMessageSupportDisabled(param,callable);
+ param.bind(Role.MESSAGE_TEXT);
+ callable.setDecoderClass(CharacterDecoder.class);
+ return true;
+ }
+ if (param.type.isAssignableFrom(Double.class) || (param.type == Double.TYPE))
+ {
+ assertPartialMessageSupportDisabled(param,callable);
+ param.bind(Role.MESSAGE_TEXT);
+ callable.setDecoderClass(DoubleDecoder.class);
+ return true;
+ }
+ if (param.type.isAssignableFrom(Float.class) || (param.type == Float.TYPE))
+ {
+ assertPartialMessageSupportDisabled(param,callable);
+ param.bind(Role.MESSAGE_TEXT);
+ callable.setDecoderClass(FloatDecoder.class);
+ return true;
+ }
+ if (param.type.isAssignableFrom(Integer.class) || (param.type == Integer.TYPE))
+ {
+ assertPartialMessageSupportDisabled(param,callable);
+ param.bind(Role.MESSAGE_TEXT);
+ callable.setDecoderClass(IntegerDecoder.class);
+ return true;
+ }
+ if (param.type.isAssignableFrom(Long.class) || (param.type == Long.TYPE))
+ {
+ assertPartialMessageSupportDisabled(param,callable);
+ param.bind(Role.MESSAGE_TEXT);
+ callable.setDecoderClass(LongDecoder.class);
+ return true;
+ }
+ if (param.type.isAssignableFrom(Short.class) || (param.type == Short.TYPE))
+ {
+ assertPartialMessageSupportDisabled(param,callable);
+ param.bind(Role.MESSAGE_TEXT);
+ callable.setDecoderClass(ShortDecoder.class);
+ return true;
+ }
+
+ // Streaming
+ if (param.type.isAssignableFrom(Reader.class))
+ {
+ assertPartialMessageSupportDisabled(param,callable);
+ param.bind(Role.MESSAGE_TEXT_STREAM);
+ callable.setDecoderClass(ReaderDecoder.class);
+ return true;
+ }
+
+ /*
+ * boolean primitive.
+ *
+ * can be used for either: 1) a boolean message type 2) a partial message indicator flag
+ */
+ if (param.type == Boolean.TYPE)
+ {
+ if (isMessageRoleAssigned(callable))
+ {
+ param.bind(Role.MESSAGE_PARTIAL_FLAG);
+ }
+ else
+ {
+ param.bind(Role.MESSAGE_TEXT);
+ callable.setDecoderClass(BooleanDecoder.class);
+ }
+ return true;
+ }
+
+ return false;
+ }
+}
diff --git a/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/annotations/OnCloseCallable.java b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/annotations/OnCloseCallable.java
new file mode 100644
index 0000000..f854aaa
--- /dev/null
+++ b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/annotations/OnCloseCallable.java
@@ -0,0 +1,90 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.annotations;
+
+import java.lang.reflect.Method;
+
+import javax.websocket.CloseReason;
+import javax.websocket.CloseReason.CloseCodes;
+import javax.websocket.Decoder;
+import javax.websocket.OnClose;
+
+import org.eclipse.jetty.websocket.common.CloseInfo;
+import org.eclipse.jetty.websocket.jsr356.JsrSession;
+import org.eclipse.jetty.websocket.jsr356.annotations.Param.Role;
+
+/**
+ * Callable for {@link OnClose} annotated methods
+ */
+public class OnCloseCallable extends JsrCallable
+{
+ private int idxCloseReason = -1;
+
+ public OnCloseCallable(Class<?> pojo, Method method)
+ {
+ super(pojo,method);
+ }
+
+ public OnCloseCallable(OnCloseCallable copy)
+ {
+ super(copy);
+ this.idxCloseReason = copy.idxCloseReason;
+ }
+
+ public void call(Object endpoint, CloseInfo close)
+ {
+ this.call(endpoint,close.getStatusCode(),close.getReason());
+ }
+
+ public void call(Object endpoint, CloseReason closeReason)
+ {
+ // Close Reason is an optional parameter
+ if (idxCloseReason >= 0)
+ {
+ // convert to javax.websocket.CloseReason
+ super.args[idxCloseReason] = closeReason;
+ }
+ super.call(endpoint,super.args);
+ }
+
+ public void call(Object endpoint, int statusCode, String reason)
+ {
+ // Close Reason is an optional parameter
+ if (idxCloseReason >= 0)
+ {
+ // convert to javax.websocket.CloseReason
+ CloseReason jsrclose = new CloseReason(CloseCodes.getCloseCode(statusCode),reason);
+ super.args[idxCloseReason] = jsrclose;
+ }
+ super.call(endpoint,super.args);
+ }
+
+ @Override
+ public void init(JsrSession session)
+ {
+ idxCloseReason = findIndexForRole(Role.CLOSE_REASON);
+ super.init(session);
+ }
+
+ @Override
+ public void setDecoderClass(Class<? extends Decoder> decoderClass)
+ {
+ /* ignore, not relevant for onClose */
+ }
+}
diff --git a/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/annotations/OnErrorCallable.java b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/annotations/OnErrorCallable.java
new file mode 100644
index 0000000..2f951e6
--- /dev/null
+++ b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/annotations/OnErrorCallable.java
@@ -0,0 +1,69 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.annotations;
+
+import java.lang.reflect.Method;
+
+import javax.websocket.Decoder;
+import javax.websocket.OnError;
+
+import org.eclipse.jetty.websocket.jsr356.JsrSession;
+
+/**
+ * Callable for {@link OnError} annotated methods
+ */
+public class OnErrorCallable extends JsrCallable
+{
+ private int idxThrowable = -1;
+
+ public OnErrorCallable(Class<?> pojo, Method method)
+ {
+ super(pojo,method);
+ }
+
+ public OnErrorCallable(OnErrorCallable copy)
+ {
+ super(copy);
+ this.idxThrowable = copy.idxThrowable;
+ }
+
+ public void call(Object endpoint, Throwable cause)
+ {
+ if (idxThrowable >= 0)
+ {
+ super.args[idxThrowable] = cause;
+ }
+ super.call(endpoint,super.args);
+ }
+
+ @Override
+ public void init(JsrSession session)
+ {
+ idxThrowable = findIndexForRole(Param.Role.ERROR_CAUSE);
+ assertRoleRequired(idxThrowable,"Throwable");
+ super.init(session);
+ }
+
+ @Override
+ public void setDecoderClass(Class<? extends Decoder> decoderClass)
+ {
+ /* ignore, not relevant for onClose */
+ }
+
+}
diff --git a/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/annotations/OnMessageBinaryCallable.java b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/annotations/OnMessageBinaryCallable.java
new file mode 100644
index 0000000..6d08d5e
--- /dev/null
+++ b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/annotations/OnMessageBinaryCallable.java
@@ -0,0 +1,74 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.annotations;
+
+import java.lang.reflect.Method;
+import java.nio.ByteBuffer;
+
+import javax.websocket.DecodeException;
+import javax.websocket.Decoder;
+import javax.websocket.OnMessage;
+
+import org.eclipse.jetty.websocket.jsr356.JsrSession;
+import org.eclipse.jetty.websocket.jsr356.annotations.Param.Role;
+
+/**
+ * Callable for {@link OnMessage} annotated methods with a whole or partial binary messages.
+ * <p>
+ * Not for use with {@link InputStream} based {@link OnMessage} method objects.
+ *
+ * @see Decoder.Binary
+ */
+public class OnMessageBinaryCallable extends OnMessageCallable
+{
+ private Decoder.Binary<?> binaryDecoder;
+
+ public OnMessageBinaryCallable(Class<?> pojo, Method method)
+ {
+ super(pojo,method);
+ }
+
+ /**
+ * Copy Constructor
+ */
+ public OnMessageBinaryCallable(OnMessageCallable copy)
+ {
+ super(copy);
+ }
+
+ public Object call(Object endpoint, ByteBuffer buf, boolean partialFlag) throws DecodeException
+ {
+ super.args[idxMessageObject] = binaryDecoder.decode(buf);
+ if (idxPartialMessageFlag >= 0)
+ {
+ super.args[idxPartialMessageFlag] = partialFlag;
+ }
+ return super.call(endpoint,super.args);
+ }
+
+ @Override
+ public void init(JsrSession session)
+ {
+ idxMessageObject = findIndexForRole(Role.MESSAGE_BINARY);
+ assertRoleRequired(idxMessageObject,"Binary Message Object");
+ super.init(session);
+ assertDecoderRequired();
+ binaryDecoder = (Decoder.Binary<?>)getDecoder();
+ }
+}
diff --git a/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/annotations/OnMessageBinaryStreamCallable.java b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/annotations/OnMessageBinaryStreamCallable.java
new file mode 100644
index 0000000..2c28f91
--- /dev/null
+++ b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/annotations/OnMessageBinaryStreamCallable.java
@@ -0,0 +1,69 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.annotations;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.lang.reflect.Method;
+
+import javax.websocket.DecodeException;
+import javax.websocket.Decoder;
+import javax.websocket.OnMessage;
+
+import org.eclipse.jetty.websocket.jsr356.JsrSession;
+import org.eclipse.jetty.websocket.jsr356.annotations.Param.Role;
+
+/**
+ * Callable for {@link OnMessage} annotated methods for {@link InputStream} based binary message objects
+ *
+ * @see Decoder.BinaryStream
+ */
+public class OnMessageBinaryStreamCallable extends OnMessageCallable
+{
+ private Decoder.BinaryStream<?> binaryDecoder;
+
+ public OnMessageBinaryStreamCallable(Class<?> pojo, Method method)
+ {
+ super(pojo,method);
+ }
+
+ /**
+ * Copy Constructor
+ */
+ public OnMessageBinaryStreamCallable(OnMessageCallable copy)
+ {
+ super(copy);
+ }
+
+ public Object call(Object endpoint, InputStream stream) throws DecodeException, IOException
+ {
+ super.args[idxMessageObject] = binaryDecoder.decode(stream);
+ return super.call(endpoint,super.args);
+ }
+
+ @Override
+ public void init(JsrSession session)
+ {
+ idxMessageObject = findIndexForRole(Role.MESSAGE_BINARY_STREAM);
+ assertRoleRequired(idxMessageObject,"Binary InputStream Message Object");
+ super.init(session);
+ assertDecoderRequired();
+ binaryDecoder = (Decoder.BinaryStream<?>)getDecoder();
+ }
+}
diff --git a/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/annotations/OnMessageCallable.java b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/annotations/OnMessageCallable.java
new file mode 100644
index 0000000..44f9fec
--- /dev/null
+++ b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/annotations/OnMessageCallable.java
@@ -0,0 +1,175 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.annotations;
+
+import java.lang.reflect.Method;
+
+import javax.websocket.Decoder;
+import javax.websocket.Encoder;
+
+import org.eclipse.jetty.websocket.common.events.annotated.InvalidSignatureException;
+import org.eclipse.jetty.websocket.common.util.ReflectUtils;
+import org.eclipse.jetty.websocket.jsr356.EncoderFactory;
+import org.eclipse.jetty.websocket.jsr356.InitException;
+import org.eclipse.jetty.websocket.jsr356.JsrSession;
+import org.eclipse.jetty.websocket.jsr356.annotations.Param.Role;
+
+public class OnMessageCallable extends JsrCallable
+{
+ protected final Class<?> returnType;
+ protected Encoder returnEncoder;
+ protected Class<? extends Decoder> decoderClass;
+ protected Decoder decoder;
+ protected int idxPartialMessageFlag = -1;
+ protected int idxMessageObject = -1;
+ protected boolean messageRoleAssigned = false;
+
+ public OnMessageCallable(Class<?> pojo, Method method)
+ {
+ super(pojo,method);
+ this.returnType = method.getReturnType();
+ }
+
+ public OnMessageCallable(OnMessageCallable copy)
+ {
+ super(copy);
+ this.returnType = copy.returnType;
+ this.decoderClass = copy.decoderClass;
+ this.decoder = copy.decoder;
+ this.idxPartialMessageFlag = copy.idxPartialMessageFlag;
+ this.idxMessageObject = copy.idxMessageObject;
+ }
+
+ protected void assertDecoderRequired()
+ {
+ if (getDecoder() == null)
+ {
+ StringBuilder err = new StringBuilder();
+ err.append("Unable to find a valid ");
+ err.append(Decoder.class.getName());
+ err.append(" for parameter #");
+ Param param = params[idxMessageObject];
+ err.append(param.index);
+ err.append(" [").append(param.type).append("] in method: ");
+ err.append(ReflectUtils.toString(pojo,method));
+ throw new InvalidSignatureException(err.toString());
+ }
+ }
+
+ private int findMessageObjectIndex()
+ {
+ int index = -1;
+
+ for (Param.Role role : Param.Role.getMessageRoles())
+ {
+ index = findIndexForRole(role);
+ if (index >= 0)
+ {
+ return index;
+ }
+ }
+
+ return -1;
+ }
+
+ public Decoder getDecoder()
+ {
+ return decoder;
+ }
+
+ public Class<? extends Decoder> getDecoderClass()
+ {
+ return decoderClass;
+ }
+
+ public Param getMessageObjectParam()
+ {
+ if (idxMessageObject < 0)
+ {
+ idxMessageObject = findMessageObjectIndex();
+
+ if (idxMessageObject < 0)
+ {
+ StringBuilder err = new StringBuilder();
+ err.append("A message type must be specified [TEXT, BINARY, DECODER, or PONG] : ");
+ err.append(ReflectUtils.toString(pojo,method));
+ throw new InvalidSignatureException(err.toString());
+ }
+ }
+
+ return super.params[idxMessageObject];
+ }
+
+ public Encoder getReturnEncoder()
+ {
+ return returnEncoder;
+ }
+
+ public Class<?> getReturnType()
+ {
+ return returnType;
+ }
+
+ @Override
+ public void init(JsrSession session)
+ {
+ super.init(session);
+ idxPartialMessageFlag = findIndexForRole(Role.MESSAGE_PARTIAL_FLAG);
+
+ EncoderFactory.Wrapper encoderWrapper = session.getEncoderFactory().getWrapperFor(returnType);
+ if (encoderWrapper != null)
+ {
+ this.returnEncoder = encoderWrapper.getEncoder();
+ }
+
+ if (decoderClass != null)
+ {
+ try
+ {
+ this.decoder = decoderClass.newInstance();
+ }
+ catch (InstantiationException | IllegalAccessException e)
+ {
+ throw new InitException("Unable to create decoder: " + decoderClass.getName(),e);
+ }
+ }
+ }
+
+ public boolean isMessageRoleAssigned()
+ {
+ return messageRoleAssigned;
+ }
+
+ public boolean isPartialMessageSupported()
+ {
+ return (idxPartialMessageFlag >= 0);
+ }
+
+ @Override
+ public void setDecoderClass(Class<? extends Decoder> decoderClass)
+ {
+ this.decoderClass = decoderClass;
+ messageRoleAssigned = true;
+ }
+
+ public void setPartialMessageFlag(Param param)
+ {
+ idxPartialMessageFlag = param.index;
+ }
+}
diff --git a/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/annotations/OnMessagePongCallable.java b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/annotations/OnMessagePongCallable.java
new file mode 100644
index 0000000..8ac8656
--- /dev/null
+++ b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/annotations/OnMessagePongCallable.java
@@ -0,0 +1,63 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.annotations;
+
+import java.lang.reflect.Method;
+import java.nio.ByteBuffer;
+
+import javax.websocket.DecodeException;
+import javax.websocket.OnMessage;
+import javax.websocket.PongMessage;
+
+import org.eclipse.jetty.websocket.jsr356.JsrPongMessage;
+import org.eclipse.jetty.websocket.jsr356.JsrSession;
+import org.eclipse.jetty.websocket.jsr356.annotations.Param.Role;
+
+/**
+ * Callable for {@link OnMessage} annotated methods with a {@link PongMessage} message object.
+ */
+public class OnMessagePongCallable extends OnMessageCallable
+{
+ public OnMessagePongCallable(Class<?> pojo, Method method)
+ {
+ super(pojo,method);
+ }
+
+ /**
+ * Copy Constructor
+ */
+ public OnMessagePongCallable(OnMessageCallable copy)
+ {
+ super(copy);
+ }
+
+ public Object call(Object endpoint, ByteBuffer buf) throws DecodeException
+ {
+ super.args[idxMessageObject] = new JsrPongMessage(buf);
+ return super.call(endpoint,super.args);
+ }
+
+ @Override
+ public void init(JsrSession session)
+ {
+ idxMessageObject = findIndexForRole(Role.MESSAGE_PONG);
+ assertRoleRequired(idxMessageObject,"Pong Message Object");
+ super.init(session);
+ }
+}
diff --git a/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/annotations/OnMessageTextCallable.java b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/annotations/OnMessageTextCallable.java
new file mode 100644
index 0000000..a71ea97
--- /dev/null
+++ b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/annotations/OnMessageTextCallable.java
@@ -0,0 +1,74 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.annotations;
+
+import java.io.Reader;
+import java.lang.reflect.Method;
+
+import javax.websocket.DecodeException;
+import javax.websocket.Decoder;
+import javax.websocket.OnMessage;
+
+import org.eclipse.jetty.websocket.jsr356.JsrSession;
+import org.eclipse.jetty.websocket.jsr356.annotations.Param.Role;
+
+/**
+ * Callable for {@link OnMessage} annotated methods with a whole or partial text messages.
+ * <p>
+ * Not for use with {@link Reader} based {@link OnMessage} method objects.
+ *
+ * @see Decoder.Text
+ */
+public class OnMessageTextCallable extends OnMessageCallable
+{
+ private Decoder.Text<?> textDecoder;
+
+ public OnMessageTextCallable(Class<?> pojo, Method method)
+ {
+ super(pojo,method);
+ }
+
+ /**
+ * Copy Constructor
+ */
+ public OnMessageTextCallable(OnMessageCallable copy)
+ {
+ super(copy);
+ }
+
+ public Object call(Object endpoint, String str, boolean partialFlag) throws DecodeException
+ {
+ super.args[idxMessageObject] = textDecoder.decode(str);
+ if (idxPartialMessageFlag >= 0)
+ {
+ super.args[idxPartialMessageFlag] = partialFlag;
+ }
+ return super.call(endpoint,super.args);
+ }
+
+ @Override
+ public void init(JsrSession session)
+ {
+ idxMessageObject = findIndexForRole(Role.MESSAGE_TEXT);
+ assertRoleRequired(idxMessageObject,"Text Message Object");
+ super.init(session);
+ assertDecoderRequired();
+ textDecoder = (Decoder.Text<?>)getDecoder();
+ }
+}
diff --git a/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/annotations/OnMessageTextStreamCallable.java b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/annotations/OnMessageTextStreamCallable.java
new file mode 100644
index 0000000..0cc5a2a
--- /dev/null
+++ b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/annotations/OnMessageTextStreamCallable.java
@@ -0,0 +1,69 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.annotations;
+
+import java.io.IOException;
+import java.io.Reader;
+import java.lang.reflect.Method;
+
+import javax.websocket.DecodeException;
+import javax.websocket.Decoder;
+import javax.websocket.OnMessage;
+
+import org.eclipse.jetty.websocket.jsr356.JsrSession;
+import org.eclipse.jetty.websocket.jsr356.annotations.Param.Role;
+
+/**
+ * Callable for {@link OnMessage} annotated methods for {@link Reader} based text message objects
+ *
+ * @see Decoder.TextStream
+ */
+public class OnMessageTextStreamCallable extends OnMessageCallable
+{
+ private Decoder.TextStream<?> textDecoder;
+
+ public OnMessageTextStreamCallable(Class<?> pojo, Method method)
+ {
+ super(pojo,method);
+ }
+
+ /**
+ * Copy Constructor
+ */
+ public OnMessageTextStreamCallable(OnMessageCallable copy)
+ {
+ super(copy);
+ }
+
+ public Object call(Object endpoint, Reader reader) throws DecodeException, IOException
+ {
+ super.args[idxMessageObject] = textDecoder.decode(reader);
+ return super.call(endpoint,super.args);
+ }
+
+ @Override
+ public void init(JsrSession session)
+ {
+ idxMessageObject = findIndexForRole(Role.MESSAGE_TEXT_STREAM);
+ assertRoleRequired(idxMessageObject,"Text Reader Message Object");
+ super.init(session);
+ assertDecoderRequired();
+ textDecoder = (Decoder.TextStream<?>)getDecoder();
+ }
+}
diff --git a/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/annotations/OnOpenCallable.java b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/annotations/OnOpenCallable.java
new file mode 100644
index 0000000..388bee6
--- /dev/null
+++ b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/annotations/OnOpenCallable.java
@@ -0,0 +1,70 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.annotations;
+
+import java.lang.reflect.Method;
+
+import javax.websocket.Decoder;
+import javax.websocket.EndpointConfig;
+import javax.websocket.OnOpen;
+
+import org.eclipse.jetty.websocket.jsr356.JsrSession;
+import org.eclipse.jetty.websocket.jsr356.annotations.Param.Role;
+
+/**
+ * Callable for {@link OnOpen} annotated methods
+ */
+public class OnOpenCallable extends JsrCallable
+{
+ private int idxEndpointConfig = -1;
+
+ public OnOpenCallable(Class<?> pojo, Method method)
+ {
+ super(pojo,method);
+ }
+
+ public OnOpenCallable(OnOpenCallable copy)
+ {
+ super(copy);
+ this.idxEndpointConfig = copy.idxEndpointConfig;
+ }
+
+ public void call(Object endpoint, EndpointConfig config)
+ {
+ // EndpointConfig is an optional parameter
+ if (idxEndpointConfig >= 0)
+ {
+ super.args[idxEndpointConfig] = config;
+ }
+ super.call(endpoint,super.args);
+ }
+
+ @Override
+ public void init(JsrSession session)
+ {
+ idxEndpointConfig = findIndexForRole(Role.ENDPOINT_CONFIG);
+ super.init(session);
+ }
+
+ @Override
+ public void setDecoderClass(Class<? extends Decoder> decoderClass)
+ {
+ /* ignore, not relevant for onClose */
+ }
+}
diff --git a/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/annotations/Param.java b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/annotations/Param.java
new file mode 100644
index 0000000..f5068ad
--- /dev/null
+++ b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/annotations/Param.java
@@ -0,0 +1,135 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.annotations;
+
+import java.lang.annotation.Annotation;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.eclipse.jetty.websocket.common.util.ReflectUtils;
+
+public class Param
+{
+ /**
+ * The various roles of the known parameters.
+ */
+ public static enum Role
+ {
+ SESSION,
+ ENDPOINT_CONFIG,
+ CLOSE_REASON,
+ ERROR_CAUSE,
+ MESSAGE_TEXT,
+ MESSAGE_TEXT_STREAM,
+ MESSAGE_BINARY,
+ MESSAGE_BINARY_STREAM,
+ MESSAGE_PONG,
+ MESSAGE_PARTIAL_FLAG,
+ PATH_PARAM;
+
+ private static Role[] messageRoles;
+
+ static
+ {
+ messageRoles = new Role[]
+ { MESSAGE_TEXT, MESSAGE_TEXT_STREAM, MESSAGE_BINARY, MESSAGE_BINARY_STREAM, MESSAGE_PONG, };
+ }
+
+ public static Role[] getMessageRoles()
+ {
+ return messageRoles;
+ }
+ }
+
+ public int index;
+ public Class<?> type;
+ private transient Map<Class<? extends Annotation>, Annotation> annotations;
+
+ /*
+ * The bound role for this parameter.
+ */
+ public Role role = null;
+ private String pathParamName = null;
+
+ public Param(int idx, Class<?> type, Annotation[] annos)
+ {
+ this.index = idx;
+ this.type = type;
+ if (annos != null)
+ {
+ this.annotations = new HashMap<>();
+ for (Annotation anno : annos)
+ {
+ this.annotations.put(anno.annotationType(),anno);
+ }
+ }
+ }
+
+ public void bind(Role role)
+ {
+ this.role = role;
+ }
+
+ @SuppressWarnings("unchecked")
+ public <A extends Annotation> A getAnnotation(Class<A> annotationClass)
+ {
+ if (this.annotations == null)
+ {
+ return null;
+ }
+
+ return (A)this.annotations.get(annotationClass);
+ }
+
+ public String getPathParamName()
+ {
+ return this.pathParamName;
+ }
+
+ public boolean isValid()
+ {
+ return this.role != null;
+ }
+
+ public void setPathParamName(String name)
+ {
+ this.pathParamName = name;
+ }
+
+ @Override
+ public String toString()
+ {
+ StringBuilder str = new StringBuilder();
+ str.append("Param[");
+ str.append("index=").append(index);
+ str.append(",type=").append(ReflectUtils.toShortName(type));
+ str.append(",role=").append(role);
+ if (pathParamName != null)
+ {
+ str.append(",pathParamName=").append(pathParamName);
+ }
+ str.append(']');
+ return str.toString();
+ }
+
+ public void unbind()
+ {
+ this.role = null;
+ }
+}
\ No newline at end of file
diff --git a/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/client/AnnotatedClientEndpointConfig.java b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/client/AnnotatedClientEndpointConfig.java
new file mode 100644
index 0000000..1175f3d
--- /dev/null
+++ b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/client/AnnotatedClientEndpointConfig.java
@@ -0,0 +1,112 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.client;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import javax.websocket.ClientEndpoint;
+import javax.websocket.ClientEndpointConfig;
+import javax.websocket.Decoder;
+import javax.websocket.Encoder;
+import javax.websocket.Extension;
+
+import org.eclipse.jetty.websocket.api.InvalidWebSocketException;
+
+public class AnnotatedClientEndpointConfig implements ClientEndpointConfig
+{
+ private final List<Class<? extends Decoder>> decoders;
+ private final List<Class<? extends Encoder>> encoders;
+ private final List<Extension> extensions;
+ private final List<String> preferredSubprotocols;
+ private final Configurator configurator;
+ private Map<String, Object> userProperties;
+
+ public AnnotatedClientEndpointConfig(ClientEndpoint anno)
+ {
+ this.decoders = Collections.unmodifiableList(Arrays.asList(anno.decoders()));
+ this.encoders = Collections.unmodifiableList(Arrays.asList(anno.encoders()));
+ this.preferredSubprotocols = Collections.unmodifiableList(Arrays.asList(anno.subprotocols()));
+
+ // no extensions declared in annotation
+ this.extensions = Collections.emptyList();
+ // no userProperties in annotation
+ this.userProperties = new HashMap<>();
+
+ if (anno.configurator() == null)
+ {
+ this.configurator = EmptyConfigurator.INSTANCE;
+ }
+ else
+ {
+ try
+ {
+ this.configurator = anno.configurator().newInstance();
+ }
+ catch (InstantiationException | IllegalAccessException e)
+ {
+ StringBuilder err = new StringBuilder();
+ err.append("Unable to instantiate ClientEndpoint.configurator() of ");
+ err.append(anno.configurator().getName());
+ err.append(" defined as annotation in ");
+ err.append(anno.getClass().getName());
+ throw new InvalidWebSocketException(err.toString(),e);
+ }
+ }
+ }
+
+ @Override
+ public Configurator getConfigurator()
+ {
+ return configurator;
+ }
+
+ @Override
+ public List<Class<? extends Decoder>> getDecoders()
+ {
+ return decoders;
+ }
+
+ @Override
+ public List<Class<? extends Encoder>> getEncoders()
+ {
+ return encoders;
+ }
+
+ @Override
+ public List<Extension> getExtensions()
+ {
+ return extensions;
+ }
+
+ @Override
+ public List<String> getPreferredSubprotocols()
+ {
+ return preferredSubprotocols;
+ }
+
+ @Override
+ public Map<String, Object> getUserProperties()
+ {
+ return userProperties;
+ }
+}
diff --git a/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/client/AnnotatedClientEndpointMetadata.java b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/client/AnnotatedClientEndpointMetadata.java
new file mode 100644
index 0000000..70f6d53
--- /dev/null
+++ b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/client/AnnotatedClientEndpointMetadata.java
@@ -0,0 +1,62 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.client;
+
+import javax.websocket.ClientEndpoint;
+import javax.websocket.ClientEndpointConfig;
+
+import org.eclipse.jetty.websocket.api.InvalidWebSocketException;
+import org.eclipse.jetty.websocket.jsr356.ClientContainer;
+import org.eclipse.jetty.websocket.jsr356.annotations.AnnotatedEndpointMetadata;
+
+public class AnnotatedClientEndpointMetadata extends AnnotatedEndpointMetadata<ClientEndpoint, ClientEndpointConfig>
+{
+ private final ClientEndpoint endpoint;
+ private final AnnotatedClientEndpointConfig config;
+
+ public AnnotatedClientEndpointMetadata(ClientContainer container, Class<?> websocket)
+ {
+ super(websocket);
+
+ ClientEndpoint anno = websocket.getAnnotation(ClientEndpoint.class);
+ if (anno == null)
+ {
+ throw new InvalidWebSocketException(String.format("Unsupported WebSocket object [%s], missing @%s annotation",websocket.getName(),
+ ClientEndpoint.class.getName()));
+ }
+
+ this.endpoint = anno;
+ this.config = new AnnotatedClientEndpointConfig(anno);
+
+ getDecoders().addAll(anno.decoders());
+ getEncoders().addAll(anno.encoders());
+ }
+
+ @Override
+ public ClientEndpoint getAnnotation()
+ {
+ return endpoint;
+ }
+
+ @Override
+ public ClientEndpointConfig getConfig()
+ {
+ return config;
+ }
+}
diff --git a/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/client/EmptyClientEndpointConfig.java b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/client/EmptyClientEndpointConfig.java
new file mode 100644
index 0000000..f3363d4
--- /dev/null
+++ b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/client/EmptyClientEndpointConfig.java
@@ -0,0 +1,85 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.client;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import javax.websocket.ClientEndpointConfig;
+import javax.websocket.Decoder;
+import javax.websocket.Encoder;
+import javax.websocket.Extension;
+
+public class EmptyClientEndpointConfig implements ClientEndpointConfig
+{
+ private final List<Class<? extends Decoder>> decoders;
+ private final List<Class<? extends Encoder>> encoders;
+ private final List<Extension> extensions;
+ private final List<String> preferredSubprotocols;
+ private final Configurator configurator;
+ private Map<String, Object> userProperties;
+
+ public EmptyClientEndpointConfig()
+ {
+ this.decoders = new ArrayList<>();
+ this.encoders = new ArrayList<>();
+ this.preferredSubprotocols = new ArrayList<>();
+ this.extensions = new ArrayList<>();
+ this.userProperties = new HashMap<>();
+ this.configurator = EmptyConfigurator.INSTANCE;
+ }
+
+ @Override
+ public Configurator getConfigurator()
+ {
+ return configurator;
+ }
+
+ @Override
+ public List<Class<? extends Decoder>> getDecoders()
+ {
+ return decoders;
+ }
+
+ @Override
+ public List<Class<? extends Encoder>> getEncoders()
+ {
+ return encoders;
+ }
+
+ @Override
+ public List<Extension> getExtensions()
+ {
+ return extensions;
+ }
+
+ @Override
+ public List<String> getPreferredSubprotocols()
+ {
+ return preferredSubprotocols;
+ }
+
+ @Override
+ public Map<String, Object> getUserProperties()
+ {
+ return userProperties;
+ }
+}
diff --git a/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/client/EmptyConfigurator.java b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/client/EmptyConfigurator.java
new file mode 100644
index 0000000..776ab89
--- /dev/null
+++ b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/client/EmptyConfigurator.java
@@ -0,0 +1,42 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.client;
+
+import java.util.List;
+import java.util.Map;
+
+import javax.websocket.ClientEndpointConfig;
+import javax.websocket.HandshakeResponse;
+
+public class EmptyConfigurator extends ClientEndpointConfig.Configurator
+{
+ public static final EmptyConfigurator INSTANCE = new EmptyConfigurator();
+
+ @Override
+ public void afterResponse(HandshakeResponse hr)
+ {
+ // do nothing
+ }
+
+ @Override
+ public void beforeRequest(Map<String, List<String>> headers)
+ {
+ // do nothing
+ }
+}
diff --git a/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/client/JsrClientEndpointImpl.java b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/client/JsrClientEndpointImpl.java
new file mode 100644
index 0000000..80b6b4b
--- /dev/null
+++ b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/client/JsrClientEndpointImpl.java
@@ -0,0 +1,72 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.client;
+
+import javax.websocket.ClientEndpoint;
+import javax.websocket.ClientEndpointConfig;
+import javax.websocket.DeploymentException;
+
+import org.eclipse.jetty.websocket.api.WebSocketPolicy;
+import org.eclipse.jetty.websocket.common.events.EventDriver;
+import org.eclipse.jetty.websocket.common.events.EventDriverImpl;
+import org.eclipse.jetty.websocket.jsr356.annotations.JsrEvents;
+import org.eclipse.jetty.websocket.jsr356.endpoints.EndpointInstance;
+import org.eclipse.jetty.websocket.jsr356.endpoints.JsrAnnotatedEventDriver;
+
+/**
+ * Event Driver for classes annotated with @{@link ClientEndpoint}
+ */
+public class JsrClientEndpointImpl implements EventDriverImpl
+{
+ @Override
+ public EventDriver create(Object websocket, WebSocketPolicy policy) throws DeploymentException
+ {
+ if (!(websocket instanceof EndpointInstance))
+ {
+ throw new IllegalStateException(String.format("Websocket %s must be an %s",websocket.getClass().getName(),EndpointInstance.class.getName()));
+ }
+
+ EndpointInstance ei = (EndpointInstance)websocket;
+ AnnotatedClientEndpointMetadata metadata = (AnnotatedClientEndpointMetadata)ei.getMetadata();
+ JsrEvents<ClientEndpoint, ClientEndpointConfig> events = new JsrEvents<>(metadata);
+
+ return new JsrAnnotatedEventDriver(policy,ei,events);
+ }
+
+ @Override
+ public String describeRule()
+ {
+ return "class is annotated with @" + ClientEndpoint.class.getName();
+ }
+
+ @Override
+ public boolean supports(Object websocket)
+ {
+ if (!(websocket instanceof EndpointInstance))
+ {
+ return false;
+ }
+
+ EndpointInstance ei = (EndpointInstance)websocket;
+ Object endpoint = ei.getEndpoint();
+
+ ClientEndpoint anno = endpoint.getClass().getAnnotation(ClientEndpoint.class);
+ return (anno != null);
+ }
+}
diff --git a/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/client/SimpleEndpointMetadata.java b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/client/SimpleEndpointMetadata.java
new file mode 100644
index 0000000..cdd0eda
--- /dev/null
+++ b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/client/SimpleEndpointMetadata.java
@@ -0,0 +1,60 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.client;
+
+import javax.websocket.Endpoint;
+
+import org.eclipse.jetty.websocket.jsr356.metadata.DecoderMetadataSet;
+import org.eclipse.jetty.websocket.jsr356.metadata.EncoderMetadataSet;
+import org.eclipse.jetty.websocket.jsr356.metadata.EndpointMetadata;
+
+/**
+ * Basic {@link EndpointMetadata} for an WebSocket that extends from {@link Endpoint}
+ */
+public class SimpleEndpointMetadata implements EndpointMetadata
+{
+ private final Class<?> endpointClass;
+ private DecoderMetadataSet decoders;
+ private EncoderMetadataSet encoders;
+
+ public SimpleEndpointMetadata(Class<? extends Endpoint> endpointClass)
+ {
+ this.endpointClass = endpointClass;
+ this.decoders = new DecoderMetadataSet();
+ this.encoders = new EncoderMetadataSet();
+ }
+
+ @Override
+ public DecoderMetadataSet getDecoders()
+ {
+ return decoders;
+ }
+
+ @Override
+ public EncoderMetadataSet getEncoders()
+ {
+ return encoders;
+ }
+
+ @Override
+ public Class<?> getEndpointClass()
+ {
+ return endpointClass;
+ }
+}
diff --git a/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/decoders/AbstractDecoder.java b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/decoders/AbstractDecoder.java
new file mode 100644
index 0000000..3dd9afc
--- /dev/null
+++ b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/decoders/AbstractDecoder.java
@@ -0,0 +1,35 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.decoders;
+
+import javax.websocket.Decoder;
+import javax.websocket.EndpointConfig;
+
+public abstract class AbstractDecoder implements Decoder
+{
+ @Override
+ public void destroy()
+ {
+ }
+
+ @Override
+ public void init(EndpointConfig config)
+ {
+ }
+}
diff --git a/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/decoders/BooleanDecoder.java b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/decoders/BooleanDecoder.java
new file mode 100644
index 0000000..ea87165
--- /dev/null
+++ b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/decoders/BooleanDecoder.java
@@ -0,0 +1,48 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.decoders;
+
+import javax.websocket.DecodeException;
+import javax.websocket.Decoder;
+
+/**
+ * Default implementation of the {@link Text} Message to {@link Boolean} decoder.
+ * <p>
+ * Note: delegates to {@link Boolean#parseBoolean(String)} and will only support "true" and "false" as boolean values.
+ */
+public class BooleanDecoder extends AbstractDecoder implements Decoder.Text<Boolean>
+{
+ public static final BooleanDecoder INSTANCE = new BooleanDecoder();
+
+ @Override
+ public Boolean decode(String s) throws DecodeException
+ {
+ return Boolean.parseBoolean(s);
+ }
+
+ @Override
+ public boolean willDecode(String s)
+ {
+ if (s == null)
+ {
+ return false;
+ }
+ return (s.equalsIgnoreCase("true") || s.equalsIgnoreCase("false"));
+ }
+}
diff --git a/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/decoders/ByteArrayDecoder.java b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/decoders/ByteArrayDecoder.java
new file mode 100644
index 0000000..71b62db
--- /dev/null
+++ b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/decoders/ByteArrayDecoder.java
@@ -0,0 +1,43 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.decoders;
+
+import java.nio.ByteBuffer;
+
+import javax.websocket.DecodeException;
+import javax.websocket.Decoder;
+
+import org.eclipse.jetty.util.BufferUtil;
+
+public class ByteArrayDecoder extends AbstractDecoder implements Decoder.Binary<byte[]>
+{
+ public static final ByteArrayDecoder INSTANCE = new ByteArrayDecoder();
+
+ @Override
+ public byte[] decode(ByteBuffer bytes) throws DecodeException
+ {
+ return BufferUtil.toArray(bytes);
+ }
+
+ @Override
+ public boolean willDecode(ByteBuffer bytes)
+ {
+ return true;
+ }
+}
diff --git a/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/decoders/ByteBufferDecoder.java b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/decoders/ByteBufferDecoder.java
new file mode 100644
index 0000000..d1fcb91
--- /dev/null
+++ b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/decoders/ByteBufferDecoder.java
@@ -0,0 +1,41 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.decoders;
+
+import java.nio.ByteBuffer;
+
+import javax.websocket.DecodeException;
+import javax.websocket.Decoder;
+
+public class ByteBufferDecoder extends AbstractDecoder implements Decoder.Binary<ByteBuffer>
+{
+ public static final ByteBufferDecoder INSTANCE = new ByteBufferDecoder();
+
+ @Override
+ public ByteBuffer decode(ByteBuffer bytes) throws DecodeException
+ {
+ return bytes;
+ }
+
+ @Override
+ public boolean willDecode(ByteBuffer bytes)
+ {
+ return true;
+ }
+}
diff --git a/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/decoders/ByteDecoder.java b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/decoders/ByteDecoder.java
new file mode 100644
index 0000000..def579e
--- /dev/null
+++ b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/decoders/ByteDecoder.java
@@ -0,0 +1,61 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.decoders;
+
+import javax.websocket.DecodeException;
+import javax.websocket.Decoder;
+
+/**
+ * Default implementation of the {@link Text} Message to {@link Byte} decoder
+ */
+public class ByteDecoder extends AbstractDecoder implements Decoder.Text<Byte>
+{
+ public static final ByteDecoder INSTANCE = new ByteDecoder();
+
+ @Override
+ public Byte decode(String s) throws DecodeException
+ {
+ try
+ {
+ return Byte.parseByte(s);
+ }
+ catch (NumberFormatException e)
+ {
+ throw new DecodeException(s,"Unable to parse Byte",e);
+ }
+ }
+
+ @Override
+ public boolean willDecode(String s)
+ {
+ if (s == null)
+ {
+ return false;
+ }
+ try
+ {
+ Byte.parseByte(s);
+ return true;
+ }
+ catch (NumberFormatException e)
+ {
+ return false;
+ }
+ }
+}
diff --git a/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/decoders/CharacterDecoder.java b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/decoders/CharacterDecoder.java
new file mode 100644
index 0000000..ddcebd1
--- /dev/null
+++ b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/decoders/CharacterDecoder.java
@@ -0,0 +1,51 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.decoders;
+
+import javax.websocket.DecodeException;
+import javax.websocket.Decoder;
+
+/**
+ * Default implementation of the {@link Text} Message to {@link Character} decoder
+ */
+public class CharacterDecoder extends AbstractDecoder implements Decoder.Text<Character>
+{
+ public static final CharacterDecoder INSTANCE = new CharacterDecoder();
+
+ @Override
+ public Character decode(String s) throws DecodeException
+ {
+ return Character.valueOf(s.charAt(0));
+ }
+
+ @Override
+ public boolean willDecode(String s)
+ {
+ if (s == null)
+ {
+ return false;
+ }
+ if (s.length() == 1)
+ {
+ return true;
+ }
+ // can only parse 1 character
+ return false;
+ }
+}
diff --git a/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/decoders/DoubleDecoder.java b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/decoders/DoubleDecoder.java
new file mode 100644
index 0000000..a9a002f
--- /dev/null
+++ b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/decoders/DoubleDecoder.java
@@ -0,0 +1,61 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.decoders;
+
+import javax.websocket.DecodeException;
+import javax.websocket.Decoder;
+
+/**
+ * Default implementation of the {@link Text} Message to {@link Double} to decoder
+ */
+public class DoubleDecoder extends AbstractDecoder implements Decoder.Text<Double>
+{
+ public static final DoubleDecoder INSTANCE = new DoubleDecoder();
+
+ @Override
+ public Double decode(String s) throws DecodeException
+ {
+ try
+ {
+ return Double.parseDouble(s);
+ }
+ catch (NumberFormatException e)
+ {
+ throw new DecodeException(s,"Unable to parse double",e);
+ }
+ }
+
+ @Override
+ public boolean willDecode(String s)
+ {
+ if (s == null)
+ {
+ return false;
+ }
+ try
+ {
+ Double.parseDouble(s);
+ return true;
+ }
+ catch (NumberFormatException e)
+ {
+ return false;
+ }
+ }
+}
diff --git a/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/decoders/FloatDecoder.java b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/decoders/FloatDecoder.java
new file mode 100644
index 0000000..d8e0790
--- /dev/null
+++ b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/decoders/FloatDecoder.java
@@ -0,0 +1,66 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.decoders;
+
+import javax.websocket.DecodeException;
+import javax.websocket.Decoder;
+
+/**
+ * Default implementation of the Text Message to {@link Float} decoder
+ */
+public class FloatDecoder extends AbstractDecoder implements Decoder.Text<Float>
+{
+ public static final FloatDecoder INSTANCE = new FloatDecoder();
+
+ @Override
+ public Float decode(String s) throws DecodeException
+ {
+ try
+ {
+ Float val = Float.parseFloat(s);
+ if (val.isNaN())
+ {
+ throw new DecodeException(s,"NaN");
+ }
+ return val;
+ }
+ catch (NumberFormatException e)
+ {
+ throw new DecodeException(s,"Unable to parse float",e);
+ }
+ }
+
+ @Override
+ public boolean willDecode(String s)
+ {
+ if (s == null)
+ {
+ return false;
+ }
+ try
+ {
+ Float val = Float.parseFloat(s);
+ return (!val.isNaN());
+ }
+ catch (NumberFormatException e)
+ {
+ return false;
+ }
+ }
+}
diff --git a/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/decoders/InputStreamDecoder.java b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/decoders/InputStreamDecoder.java
new file mode 100644
index 0000000..47f37bd
--- /dev/null
+++ b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/decoders/InputStreamDecoder.java
@@ -0,0 +1,45 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.decoders;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+import javax.websocket.DecodeException;
+import javax.websocket.Decoder;
+import javax.websocket.EndpointConfig;
+
+public class InputStreamDecoder implements Decoder.BinaryStream<InputStream>
+{
+ @Override
+ public InputStream decode(InputStream is) throws DecodeException, IOException
+ {
+ return is;
+ }
+
+ @Override
+ public void destroy()
+ {
+ }
+
+ @Override
+ public void init(EndpointConfig config)
+ {
+ }
+}
diff --git a/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/decoders/IntegerDecoder.java b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/decoders/IntegerDecoder.java
new file mode 100644
index 0000000..41d4f39
--- /dev/null
+++ b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/decoders/IntegerDecoder.java
@@ -0,0 +1,62 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.decoders;
+
+import javax.websocket.DecodeException;
+import javax.websocket.Decoder;
+
+/**
+ * Default implementation of the {@link Text} Message to {@link Integer} decoder
+ */
+public class IntegerDecoder extends AbstractDecoder implements Decoder.Text<Integer>
+{
+ public static final IntegerDecoder INSTANCE = new IntegerDecoder();
+
+ @Override
+ public Integer decode(String s) throws DecodeException
+ {
+ try
+ {
+ return Integer.parseInt(s);
+ }
+ catch (NumberFormatException e)
+ {
+ throw new DecodeException(s,"Unable to parse Integer",e);
+ }
+ }
+
+ @Override
+ public boolean willDecode(String s)
+ {
+ if (s == null)
+ {
+ return false;
+ }
+
+ try
+ {
+ Integer.parseInt(s);
+ return true;
+ }
+ catch (NumberFormatException e)
+ {
+ return false;
+ }
+ }
+}
diff --git a/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/decoders/LongDecoder.java b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/decoders/LongDecoder.java
new file mode 100644
index 0000000..f42901e
--- /dev/null
+++ b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/decoders/LongDecoder.java
@@ -0,0 +1,61 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.decoders;
+
+import javax.websocket.DecodeException;
+import javax.websocket.Decoder;
+
+/**
+ * Default implementation of the Text Message to {@link Long} decoder
+ */
+public class LongDecoder extends AbstractDecoder implements Decoder.Text<Long>
+{
+ public static final LongDecoder INSTANCE = new LongDecoder();
+
+ @Override
+ public Long decode(String s) throws DecodeException
+ {
+ try
+ {
+ return Long.parseLong(s);
+ }
+ catch (NumberFormatException e)
+ {
+ throw new DecodeException(s,"Unable to parse Long",e);
+ }
+ }
+
+ @Override
+ public boolean willDecode(String s)
+ {
+ if (s == null)
+ {
+ return false;
+ }
+ try
+ {
+ Long.parseLong(s);
+ return true;
+ }
+ catch (NumberFormatException e)
+ {
+ return false;
+ }
+ }
+}
diff --git a/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/decoders/PongMessageDecoder.java b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/decoders/PongMessageDecoder.java
new file mode 100644
index 0000000..c81454e
--- /dev/null
+++ b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/decoders/PongMessageDecoder.java
@@ -0,0 +1,61 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.decoders;
+
+import java.nio.ByteBuffer;
+
+import javax.websocket.DecodeException;
+import javax.websocket.Decoder;
+import javax.websocket.PongMessage;
+
+import org.eclipse.jetty.util.BufferUtil;
+
+public class PongMessageDecoder extends AbstractDecoder implements Decoder.Binary<PongMessage>
+{
+ private static class PongMsg implements PongMessage
+ {
+ private final ByteBuffer bytes;
+
+ public PongMsg(ByteBuffer buf)
+ {
+ int len = buf.remaining();
+ this.bytes = ByteBuffer.allocate(len);
+ BufferUtil.put(buf,this.bytes);
+ BufferUtil.flipToFlush(this.bytes,0);
+ }
+
+ @Override
+ public ByteBuffer getApplicationData()
+ {
+ return this.bytes;
+ }
+ }
+
+ @Override
+ public PongMessage decode(ByteBuffer bytes) throws DecodeException
+ {
+ return new PongMsg(bytes);
+ }
+
+ @Override
+ public boolean willDecode(ByteBuffer bytes)
+ {
+ return true;
+ }
+}
diff --git a/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/decoders/PrimitiveDecoderMetadataSet.java b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/decoders/PrimitiveDecoderMetadataSet.java
new file mode 100644
index 0000000..e746a76
--- /dev/null
+++ b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/decoders/PrimitiveDecoderMetadataSet.java
@@ -0,0 +1,70 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.decoders;
+
+import java.io.InputStream;
+import java.io.Reader;
+import java.nio.ByteBuffer;
+
+import org.eclipse.jetty.websocket.jsr356.MessageType;
+import org.eclipse.jetty.websocket.jsr356.metadata.DecoderMetadataSet;
+
+public class PrimitiveDecoderMetadataSet extends DecoderMetadataSet
+{
+ public static final DecoderMetadataSet INSTANCE = new PrimitiveDecoderMetadataSet();
+
+ public PrimitiveDecoderMetadataSet()
+ {
+ boolean streamed = false;
+ // TEXT based - Classes Based
+ MessageType msgType = MessageType.TEXT;
+ register(Boolean.class,BooleanDecoder.class,msgType,streamed);
+ register(Byte.class,ByteDecoder.class,msgType,streamed);
+ register(Character.class,CharacterDecoder.class,msgType,streamed);
+ register(Double.class,DoubleDecoder.class,msgType,streamed);
+ register(Float.class,FloatDecoder.class,msgType,streamed);
+ register(Integer.class,IntegerDecoder.class,msgType,streamed);
+ register(Long.class,LongDecoder.class,msgType,streamed);
+ register(Short.class,ShortDecoder.class,msgType,streamed);
+ register(String.class,StringDecoder.class,msgType,streamed);
+
+ // TEXT based - Primitive Types
+ msgType = MessageType.TEXT;
+ register(Boolean.TYPE,BooleanDecoder.class,msgType,streamed);
+ register(Byte.TYPE,ByteDecoder.class,msgType,streamed);
+ register(Character.TYPE,CharacterDecoder.class,msgType,streamed);
+ register(Double.TYPE,DoubleDecoder.class,msgType,streamed);
+ register(Float.TYPE,FloatDecoder.class,msgType,streamed);
+ register(Integer.TYPE,IntegerDecoder.class,msgType,streamed);
+ register(Long.TYPE,LongDecoder.class,msgType,streamed);
+ register(Short.TYPE,ShortDecoder.class,msgType,streamed);
+
+ // BINARY based
+ msgType = MessageType.BINARY;
+ register(ByteBuffer.class,ByteBufferDecoder.class,msgType,streamed);
+ register(byte[].class,ByteArrayDecoder.class,msgType,streamed);
+
+ // STREAMING based
+ streamed = true;
+ msgType = MessageType.TEXT;
+ register(Reader.class,ReaderDecoder.class,msgType,streamed);
+ msgType = MessageType.BINARY;
+ register(InputStream.class,InputStreamDecoder.class,msgType,streamed);
+ }
+}
diff --git a/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/decoders/ReaderDecoder.java b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/decoders/ReaderDecoder.java
new file mode 100644
index 0000000..2a36c88
--- /dev/null
+++ b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/decoders/ReaderDecoder.java
@@ -0,0 +1,45 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.decoders;
+
+import java.io.IOException;
+import java.io.Reader;
+
+import javax.websocket.DecodeException;
+import javax.websocket.Decoder;
+import javax.websocket.EndpointConfig;
+
+public class ReaderDecoder implements Decoder.TextStream<Reader>
+{
+ @Override
+ public Reader decode(Reader reader) throws DecodeException, IOException
+ {
+ return reader;
+ }
+
+ @Override
+ public void destroy()
+ {
+ }
+
+ @Override
+ public void init(EndpointConfig config)
+ {
+ }
+}
diff --git a/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/decoders/ShortDecoder.java b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/decoders/ShortDecoder.java
new file mode 100644
index 0000000..607fa30
--- /dev/null
+++ b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/decoders/ShortDecoder.java
@@ -0,0 +1,61 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.decoders;
+
+import javax.websocket.DecodeException;
+import javax.websocket.Decoder;
+
+/**
+ * Default implementation of the {@link Text} Message to {@link Short} decoder
+ */
+public class ShortDecoder extends AbstractDecoder implements Decoder.Text<Short>
+{
+ public static final ShortDecoder INSTANCE = new ShortDecoder();
+
+ @Override
+ public Short decode(String s) throws DecodeException
+ {
+ try
+ {
+ return Short.parseShort(s);
+ }
+ catch (NumberFormatException e)
+ {
+ throw new DecodeException(s,"Unable to parse Short",e);
+ }
+ }
+
+ @Override
+ public boolean willDecode(String s)
+ {
+ if (s == null)
+ {
+ return false;
+ }
+ try
+ {
+ Short.parseShort(s);
+ return true;
+ }
+ catch (NumberFormatException e)
+ {
+ return false;
+ }
+ }
+}
diff --git a/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/decoders/StringDecoder.java b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/decoders/StringDecoder.java
new file mode 100644
index 0000000..bdd75b0
--- /dev/null
+++ b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/decoders/StringDecoder.java
@@ -0,0 +1,42 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.decoders;
+
+import javax.websocket.DecodeException;
+import javax.websocket.Decoder;
+
+/**
+ * Default implementation of the {@link Text} Message to {@link String} decoder
+ */
+public class StringDecoder extends AbstractDecoder implements Decoder.Text<String>
+{
+ public static final StringDecoder INSTANCE = new StringDecoder();
+
+ @Override
+ public String decode(String s) throws DecodeException
+ {
+ return s;
+ }
+
+ @Override
+ public boolean willDecode(String s)
+ {
+ return true;
+ }
+}
diff --git a/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/encoders/AbstractEncoder.java b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/encoders/AbstractEncoder.java
new file mode 100644
index 0000000..93e4104
--- /dev/null
+++ b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/encoders/AbstractEncoder.java
@@ -0,0 +1,35 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.encoders;
+
+import javax.websocket.Encoder;
+import javax.websocket.EndpointConfig;
+
+public abstract class AbstractEncoder implements Encoder
+{
+ @Override
+ public void destroy()
+ {
+ }
+
+ @Override
+ public void init(EndpointConfig config)
+ {
+ }
+}
diff --git a/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/encoders/BooleanEncoder.java b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/encoders/BooleanEncoder.java
new file mode 100644
index 0000000..f2cf552
--- /dev/null
+++ b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/encoders/BooleanEncoder.java
@@ -0,0 +1,38 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.encoders;
+
+import javax.websocket.EncodeException;
+import javax.websocket.Encoder;
+
+/**
+ * Default encoder for {@link Boolean} to {@link Text} Message encoder
+ */
+public class BooleanEncoder extends AbstractEncoder implements Encoder.Text<Boolean>
+{
+ @Override
+ public String encode(Boolean object) throws EncodeException
+ {
+ if (object == null)
+ {
+ return null;
+ }
+ return object.toString();
+ }
+}
diff --git a/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/encoders/ByteEncoder.java b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/encoders/ByteEncoder.java
new file mode 100644
index 0000000..193bb5a
--- /dev/null
+++ b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/encoders/ByteEncoder.java
@@ -0,0 +1,38 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.encoders;
+
+import javax.websocket.EncodeException;
+import javax.websocket.Encoder;
+
+/**
+ * Default encoder for {@link Byte} to {@link Text} Message encoder
+ */
+public class ByteEncoder extends AbstractEncoder implements Encoder.Text<Byte>
+{
+ @Override
+ public String encode(Byte object) throws EncodeException
+ {
+ if (object == null)
+ {
+ return null;
+ }
+ return object.toString();
+ }
+}
diff --git a/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/encoders/CharacterEncoder.java b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/encoders/CharacterEncoder.java
new file mode 100644
index 0000000..1a0f1a2
--- /dev/null
+++ b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/encoders/CharacterEncoder.java
@@ -0,0 +1,38 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.encoders;
+
+import javax.websocket.EncodeException;
+import javax.websocket.Encoder;
+
+/**
+ * Default encoder for {@link Character} to {@link Text} Message encoder
+ */
+public class CharacterEncoder extends AbstractEncoder implements Encoder.Text<Character>
+{
+ @Override
+ public String encode(Character object) throws EncodeException
+ {
+ if (object == null)
+ {
+ return null;
+ }
+ return object.toString();
+ }
+}
diff --git a/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/encoders/DefaultBinaryEncoder.java b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/encoders/DefaultBinaryEncoder.java
new file mode 100644
index 0000000..77a4f89
--- /dev/null
+++ b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/encoders/DefaultBinaryEncoder.java
@@ -0,0 +1,33 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.encoders;
+
+import java.nio.ByteBuffer;
+
+import javax.websocket.EncodeException;
+import javax.websocket.Encoder;
+
+public class DefaultBinaryEncoder extends AbstractEncoder implements Encoder.Binary<ByteBuffer>
+{
+ @Override
+ public ByteBuffer encode(ByteBuffer message) throws EncodeException
+ {
+ return message;
+ }
+}
diff --git a/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/encoders/DefaultBinaryStreamEncoder.java b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/encoders/DefaultBinaryStreamEncoder.java
new file mode 100644
index 0000000..8eab691
--- /dev/null
+++ b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/encoders/DefaultBinaryStreamEncoder.java
@@ -0,0 +1,37 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.encoders;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.nio.ByteBuffer;
+
+import javax.websocket.EncodeException;
+import javax.websocket.Encoder;
+
+import org.eclipse.jetty.util.BufferUtil;
+
+public class DefaultBinaryStreamEncoder extends AbstractEncoder implements Encoder.BinaryStream<ByteBuffer>
+{
+ @Override
+ public void encode(ByteBuffer message, OutputStream out) throws EncodeException, IOException
+ {
+ BufferUtil.writeTo(message,out);
+ }
+}
diff --git a/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/encoders/DefaultTextEncoder.java b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/encoders/DefaultTextEncoder.java
new file mode 100644
index 0000000..e3e52a4
--- /dev/null
+++ b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/encoders/DefaultTextEncoder.java
@@ -0,0 +1,31 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.encoders;
+
+import javax.websocket.EncodeException;
+import javax.websocket.Encoder;
+
+public class DefaultTextEncoder extends AbstractEncoder implements Encoder.Text<String>
+{
+ @Override
+ public String encode(String message) throws EncodeException
+ {
+ return message;
+ }
+}
diff --git a/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/encoders/DefaultTextStreamEncoder.java b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/encoders/DefaultTextStreamEncoder.java
new file mode 100644
index 0000000..72ac319
--- /dev/null
+++ b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/encoders/DefaultTextStreamEncoder.java
@@ -0,0 +1,35 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.encoders;
+
+import java.io.IOException;
+import java.io.Writer;
+
+import javax.websocket.EncodeException;
+import javax.websocket.Encoder;
+
+public class DefaultTextStreamEncoder extends AbstractEncoder implements Encoder.TextStream<String>
+{
+ @Override
+ public void encode(String message, Writer writer) throws EncodeException, IOException
+ {
+ writer.append(message);
+ writer.flush();
+ }
+}
diff --git a/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/encoders/DoubleEncoder.java b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/encoders/DoubleEncoder.java
new file mode 100644
index 0000000..ab40aac
--- /dev/null
+++ b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/encoders/DoubleEncoder.java
@@ -0,0 +1,38 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.encoders;
+
+import javax.websocket.EncodeException;
+import javax.websocket.Encoder;
+
+/**
+ * Default encoder for {@link Double} to {@link Text} Message encoder
+ */
+public class DoubleEncoder extends AbstractEncoder implements Encoder.Text<Double>
+{
+ @Override
+ public String encode(Double object) throws EncodeException
+ {
+ if (object == null)
+ {
+ return null;
+ }
+ return object.toString();
+ }
+}
diff --git a/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/encoders/EncodeFailedFuture.java b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/encoders/EncodeFailedFuture.java
new file mode 100644
index 0000000..3841144
--- /dev/null
+++ b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/encoders/EncodeFailedFuture.java
@@ -0,0 +1,71 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.encoders;
+
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+import javax.websocket.Encoder;
+
+/**
+ * A <code>Future<Void></code> that is already failed as a result of an Encode error
+ */
+public class EncodeFailedFuture implements Future<Void>
+{
+ private final String msg;
+ private final Throwable cause;
+
+ public EncodeFailedFuture(Object data, Encoder encoder, Class<?> encoderType, Throwable cause)
+ {
+ this.msg = String.format("Unable to encode %s using %s as %s",data.getClass().getName(),encoder.getClass().getName(),encoderType.getName());
+ this.cause = cause;
+ }
+
+ @Override
+ public boolean cancel(boolean mayInterruptIfRunning)
+ {
+ return false;
+ }
+
+ @Override
+ public Void get() throws InterruptedException, ExecutionException
+ {
+ throw new ExecutionException(msg,cause);
+ }
+
+ @Override
+ public Void get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException
+ {
+ throw new ExecutionException(msg,cause);
+ }
+
+ @Override
+ public boolean isCancelled()
+ {
+ return false;
+ }
+
+ @Override
+ public boolean isDone()
+ {
+ return true;
+ }
+}
diff --git a/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/encoders/FloatEncoder.java b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/encoders/FloatEncoder.java
new file mode 100644
index 0000000..2cf4ead
--- /dev/null
+++ b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/encoders/FloatEncoder.java
@@ -0,0 +1,38 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.encoders;
+
+import javax.websocket.EncodeException;
+import javax.websocket.Encoder;
+
+/**
+ * Default encoder for {@link Float} to {@link Text} Message encoder
+ */
+public class FloatEncoder extends AbstractEncoder implements Encoder.Text<Float>
+{
+ @Override
+ public String encode(Float object) throws EncodeException
+ {
+ if (object == null)
+ {
+ return null;
+ }
+ return object.toString();
+ }
+}
diff --git a/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/encoders/IntegerEncoder.java b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/encoders/IntegerEncoder.java
new file mode 100644
index 0000000..fd4afe9
--- /dev/null
+++ b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/encoders/IntegerEncoder.java
@@ -0,0 +1,38 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.encoders;
+
+import javax.websocket.EncodeException;
+import javax.websocket.Encoder;
+
+/**
+ * Default encoder for {@link Integer} to {@link Text} Message encoder
+ */
+public class IntegerEncoder extends AbstractEncoder implements Encoder.Text<Integer>
+{
+ @Override
+ public String encode(Integer object) throws EncodeException
+ {
+ if (object == null)
+ {
+ return null;
+ }
+ return object.toString();
+ }
+}
diff --git a/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/encoders/LongEncoder.java b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/encoders/LongEncoder.java
new file mode 100644
index 0000000..6a5dfe7
--- /dev/null
+++ b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/encoders/LongEncoder.java
@@ -0,0 +1,38 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.encoders;
+
+import javax.websocket.EncodeException;
+import javax.websocket.Encoder;
+
+/**
+ * Default encoder for {@link Long} to {@link Text} Message encoder
+ */
+public class LongEncoder extends AbstractEncoder implements Encoder.Text<Long>
+{
+ @Override
+ public String encode(Long object) throws EncodeException
+ {
+ if (object == null)
+ {
+ return null;
+ }
+ return object.toString();
+ }
+}
diff --git a/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/encoders/PrimitiveEncoderMetadataSet.java b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/encoders/PrimitiveEncoderMetadataSet.java
new file mode 100644
index 0000000..cff6db0
--- /dev/null
+++ b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/encoders/PrimitiveEncoderMetadataSet.java
@@ -0,0 +1,60 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.encoders;
+
+import org.eclipse.jetty.websocket.jsr356.MessageType;
+import org.eclipse.jetty.websocket.jsr356.metadata.EncoderMetadataSet;
+
+public class PrimitiveEncoderMetadataSet extends EncoderMetadataSet
+{
+ public static final EncoderMetadataSet INSTANCE = new PrimitiveEncoderMetadataSet();
+
+ public PrimitiveEncoderMetadataSet()
+ {
+ boolean streamed = false;
+ // TEXT based - Classes Based
+ MessageType msgType = MessageType.TEXT;
+ register(Boolean.class,BooleanEncoder.class,msgType,streamed);
+ register(Byte.class,ByteEncoder.class,msgType,streamed);
+ register(Character.class,CharacterEncoder.class,msgType,streamed);
+ register(Double.class,DoubleEncoder.class,msgType,streamed);
+ register(Float.class,FloatEncoder.class,msgType,streamed);
+ register(Integer.class,IntegerEncoder.class,msgType,streamed);
+ register(Long.class,LongEncoder.class,msgType,streamed);
+ register(Short.class,ShortEncoder.class,msgType,streamed);
+ register(String.class,StringEncoder.class,msgType,streamed);
+
+ // TEXT based - Primitive Types
+ msgType = MessageType.TEXT;
+ register(Boolean.TYPE,BooleanEncoder.class,msgType,streamed);
+ register(Byte.TYPE,ByteEncoder.class,msgType,streamed);
+ register(Character.TYPE,CharacterEncoder.class,msgType,streamed);
+ register(Double.TYPE,DoubleEncoder.class,msgType,streamed);
+ register(Float.TYPE,FloatEncoder.class,msgType,streamed);
+ register(Integer.TYPE,IntegerEncoder.class,msgType,streamed);
+ register(Long.TYPE,LongEncoder.class,msgType,streamed);
+ register(Short.TYPE,ShortEncoder.class,msgType,streamed);
+
+ // BINARY based
+ msgType = MessageType.BINARY;
+ // FIXME register(ByteBuffer.class,ByteBufferEncoder.class,msgType,streamed);
+ // FIXME register(byte[].class,ByteArrayEncoder.class,msgType,streamed);
+
+ }
+}
diff --git a/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/encoders/ShortEncoder.java b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/encoders/ShortEncoder.java
new file mode 100644
index 0000000..14150df
--- /dev/null
+++ b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/encoders/ShortEncoder.java
@@ -0,0 +1,38 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.encoders;
+
+import javax.websocket.EncodeException;
+import javax.websocket.Encoder;
+
+/**
+ * Default encoder for {@link Short} to {@link Text} Message encoder
+ */
+public class ShortEncoder extends AbstractEncoder implements Encoder.Text<Short>
+{
+ @Override
+ public String encode(Short object) throws EncodeException
+ {
+ if (object == null)
+ {
+ return null;
+ }
+ return object.toString();
+ }
+}
diff --git a/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/encoders/StringEncoder.java b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/encoders/StringEncoder.java
new file mode 100644
index 0000000..ff7f443
--- /dev/null
+++ b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/encoders/StringEncoder.java
@@ -0,0 +1,34 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.encoders;
+
+import javax.websocket.EncodeException;
+import javax.websocket.Encoder;
+
+/**
+ * Default encoder for {@link String} to {@link Text} Message encoder
+ */
+public class StringEncoder extends AbstractEncoder implements Encoder.Text<String>
+{
+ @Override
+ public String encode(String object) throws EncodeException
+ {
+ return object;
+ }
+}
diff --git a/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/endpoints/AbstractJsrEventDriver.java b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/endpoints/AbstractJsrEventDriver.java
new file mode 100644
index 0000000..1252a3f
--- /dev/null
+++ b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/endpoints/AbstractJsrEventDriver.java
@@ -0,0 +1,113 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.endpoints;
+
+import javax.websocket.CloseReason;
+import javax.websocket.CloseReason.CloseCode;
+import javax.websocket.CloseReason.CloseCodes;
+import javax.websocket.EndpointConfig;
+import javax.websocket.Session;
+
+import org.eclipse.jetty.websocket.api.WebSocketPolicy;
+import org.eclipse.jetty.websocket.api.extensions.Frame;
+import org.eclipse.jetty.websocket.common.CloseInfo;
+import org.eclipse.jetty.websocket.common.WebSocketSession;
+import org.eclipse.jetty.websocket.common.events.AbstractEventDriver;
+import org.eclipse.jetty.websocket.common.events.EventDriver;
+import org.eclipse.jetty.websocket.jsr356.JsrSession;
+import org.eclipse.jetty.websocket.jsr356.metadata.EndpointMetadata;
+
+public abstract class AbstractJsrEventDriver extends AbstractEventDriver implements EventDriver
+{
+ protected final EndpointMetadata metadata;
+ protected final EndpointConfig config;
+ protected JsrSession jsrsession;
+ private boolean hasCloseBeenCalled = false;
+
+ public AbstractJsrEventDriver(WebSocketPolicy policy, EndpointInstance endpointInstance)
+ {
+ super(policy,endpointInstance.getEndpoint());
+ this.config = endpointInstance.getConfig();
+ this.metadata = endpointInstance.getMetadata();
+ }
+
+ public EndpointConfig getConfig()
+ {
+ return config;
+ }
+
+ public Session getJsrSession()
+ {
+ return this.jsrsession;
+ }
+
+ public EndpointMetadata getMetadata()
+ {
+ return metadata;
+ }
+
+ protected void init(JsrSession jsrsession)
+ {
+ }
+
+ @Override
+ public final void onClose(CloseInfo close)
+ {
+ if (hasCloseBeenCalled)
+ {
+ // avoid duplicate close events (possible when using harsh Session.disconnect())
+ return;
+ }
+ hasCloseBeenCalled = true;
+
+ CloseCode closecode = CloseCodes.getCloseCode(close.getStatusCode());
+ CloseReason closereason = new CloseReason(closecode,close.getReason());
+ onClose(closereason);
+ }
+
+ protected abstract void onClose(CloseReason closereason);
+
+ @Override
+ public void onFrame(Frame frame)
+ {
+ /* Ignored, not supported by JSR-356 */
+ }
+
+ @Override
+ public final void openSession(WebSocketSession session)
+ {
+ // Cast should be safe, as it was created by JsrSessionFactory
+ this.jsrsession = (JsrSession)session;
+
+ // Allow jsr session to init
+ this.jsrsession.init(config);
+
+ // Allow event driver to init itself
+ init(jsrsession);
+
+ // Allow end-user socket to adjust configuration
+ super.openSession(session);
+ }
+
+ public void setEndpointconfig(EndpointConfig endpointconfig)
+ {
+ throw new RuntimeException("Why are you reconfiguring the endpoint?");
+ // this.config = endpointconfig;
+ }
+}
diff --git a/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/endpoints/EndpointInstance.java b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/endpoints/EndpointInstance.java
new file mode 100644
index 0000000..3aa5575
--- /dev/null
+++ b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/endpoints/EndpointInstance.java
@@ -0,0 +1,58 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.endpoints;
+
+import javax.websocket.EndpointConfig;
+
+import org.eclipse.jetty.websocket.jsr356.metadata.EndpointMetadata;
+
+/**
+ * Associate a JSR Endpoint with its optional {@link EndpointConfig}
+ */
+public class EndpointInstance
+{
+ /** The instance of the Endpoint */
+ private final Object endpoint;
+ /** The instance specific configuration for the Endpoint */
+ private final EndpointConfig config;
+ /** The metadata for this endpoint */
+ private final EndpointMetadata metadata;
+
+ public EndpointInstance(Object endpoint, EndpointConfig config, EndpointMetadata metadata)
+ {
+ this.endpoint = endpoint;
+ this.config = config;
+ this.metadata = metadata;
+ }
+
+ public EndpointConfig getConfig()
+ {
+ return config;
+ }
+
+ public Object getEndpoint()
+ {
+ return endpoint;
+ }
+
+ public EndpointMetadata getMetadata()
+ {
+ return metadata;
+ }
+}
diff --git a/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/endpoints/JsrAnnotatedEventDriver.java b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/endpoints/JsrAnnotatedEventDriver.java
new file mode 100644
index 0000000..e05baac
--- /dev/null
+++ b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/endpoints/JsrAnnotatedEventDriver.java
@@ -0,0 +1,354 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.endpoints;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.Reader;
+import java.nio.ByteBuffer;
+import java.util.Map;
+
+import javax.websocket.CloseReason;
+import javax.websocket.DecodeException;
+import javax.websocket.MessageHandler;
+
+import org.eclipse.jetty.util.BufferUtil;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+import org.eclipse.jetty.websocket.api.WebSocketPolicy;
+import org.eclipse.jetty.websocket.api.extensions.Frame;
+import org.eclipse.jetty.websocket.common.events.EventDriver;
+import org.eclipse.jetty.websocket.common.message.MessageInputStream;
+import org.eclipse.jetty.websocket.common.message.MessageReader;
+import org.eclipse.jetty.websocket.common.message.SimpleBinaryMessage;
+import org.eclipse.jetty.websocket.common.message.SimpleTextMessage;
+import org.eclipse.jetty.websocket.jsr356.JsrSession;
+import org.eclipse.jetty.websocket.jsr356.annotations.JsrEvents;
+
+/**
+ * Base implementation for JSR-356 Annotated event drivers.
+ */
+public class JsrAnnotatedEventDriver extends AbstractJsrEventDriver implements EventDriver
+{
+ private static final Logger LOG = Log.getLogger(JsrAnnotatedEventDriver.class);
+ private final JsrEvents<?, ?> events;
+
+ public JsrAnnotatedEventDriver(WebSocketPolicy policy, EndpointInstance endpointInstance, JsrEvents<?, ?> events)
+ {
+ super(policy,endpointInstance);
+ this.events = events;
+ }
+
+ @Override
+ protected void init(JsrSession jsrsession)
+ {
+ this.events.init(jsrsession);
+ }
+
+ /**
+ * Entry point for all incoming binary frames.
+ */
+ @Override
+ public void onBinaryFrame(ByteBuffer buffer, boolean fin) throws IOException
+ {
+ if (LOG.isDebugEnabled())
+ {
+ LOG.debug("onBinaryFrame({}, {})",BufferUtil.toDetailString(buffer),fin);
+ LOG.debug("events.onBinary={}",events.hasBinary());
+ LOG.debug("events.onBinaryStream={}",events.hasBinaryStream());
+ }
+ boolean handled = false;
+
+ if (events.hasBinary())
+ {
+ handled = true;
+ if (events.isBinaryPartialSupported())
+ {
+ LOG.debug("Partial Binary Message: fin={}",fin);
+ // Partial Message Support (does not use messageAppender)
+ try
+ {
+ events.callBinary(jsrsession.getAsyncRemote(),websocket,buffer,fin);
+ }
+ catch (DecodeException e)
+ {
+ onFatalError(e);
+ }
+ return;
+ }
+ else
+ {
+ // Whole Message Support
+ if (activeMessage == null)
+ {
+ LOG.debug("Whole Binary Message");
+ activeMessage = new SimpleBinaryMessage(this);
+ }
+ }
+ }
+
+ if (events.hasBinaryStream())
+ {
+ handled = true;
+ // Streaming Message Support
+ if (activeMessage == null)
+ {
+ LOG.debug("Binary Message InputStream");
+ final MessageInputStream stream = new MessageInputStream(session.getConnection());
+ activeMessage = stream;
+
+ // Always dispatch streaming read to another thread.
+ dispatch(new Runnable()
+ {
+ @Override
+ public void run()
+ {
+ try
+ {
+ events.callBinaryStream(jsrsession.getAsyncRemote(),websocket,stream);
+ }
+ catch (DecodeException | IOException e)
+ {
+ onFatalError(e);
+ }
+ }
+ });
+ }
+ }
+
+ LOG.debug("handled = {}",handled);
+
+ // Process any active MessageAppender
+ if (handled && (activeMessage != null))
+ {
+ appendMessage(buffer,fin);
+ }
+ }
+
+ /**
+ * Entry point for binary frames destined for {@link MessageHandler#Whole}
+ */
+ @Override
+ public void onBinaryMessage(byte[] data)
+ {
+ if (data == null)
+ {
+ return;
+ }
+
+ ByteBuffer buf = ByteBuffer.wrap(data);
+
+ if (LOG.isDebugEnabled())
+ {
+ LOG.debug("onBinaryMessage({})",BufferUtil.toDetailString(buf));
+ }
+
+ try
+ {
+ // FIN is always true here
+ events.callBinary(jsrsession.getAsyncRemote(),websocket,buf,true);
+ }
+ catch (DecodeException e)
+ {
+ onFatalError(e);
+ }
+ }
+
+ @Override
+ protected void onClose(CloseReason closereason)
+ {
+ events.callClose(websocket,closereason);
+ }
+
+ @Override
+ public void onConnect()
+ {
+ events.callOpen(websocket,config);
+ }
+
+ @Override
+ public void onError(Throwable cause)
+ {
+ events.callError(websocket,cause);
+ }
+
+ private void onFatalError(Throwable t)
+ {
+ onError(t);
+ // TODO: close connection?
+ }
+
+ @Override
+ public void onFrame(Frame frame)
+ {
+ /* Ignored in JSR-356 */
+ }
+
+ @Override
+ public void onInputStream(InputStream stream)
+ {
+ try
+ {
+ events.callBinaryStream(jsrsession.getAsyncRemote(),websocket,stream);
+ }
+ catch (DecodeException | IOException e)
+ {
+ onFatalError(e);
+ }
+ }
+
+ @Override
+ public void onPong(ByteBuffer buffer)
+ {
+ try
+ {
+ events.callPong(jsrsession.getAsyncRemote(),websocket,buffer);
+ }
+ catch (DecodeException | IOException e)
+ {
+ onFatalError(e);
+ }
+ }
+
+ @Override
+ public void onReader(Reader reader)
+ {
+ try
+ {
+ events.callTextStream(jsrsession.getAsyncRemote(),websocket,reader);
+ }
+ catch (DecodeException | IOException e)
+ {
+ onFatalError(e);
+ }
+ }
+
+ /**
+ * Entry point for all incoming text frames.
+ */
+ @Override
+ public void onTextFrame(ByteBuffer buffer, boolean fin) throws IOException
+ {
+ if (LOG.isDebugEnabled())
+ {
+ LOG.debug("onTextFrame({}, {})",BufferUtil.toDetailString(buffer),fin);
+ LOG.debug("events.hasText={}",events.hasText());
+ LOG.debug("events.hasTextStream={}",events.hasTextStream());
+ }
+
+ boolean handled = false;
+
+ if (events.hasText())
+ {
+ handled = true;
+ if (events.isTextPartialSupported())
+ {
+ LOG.debug("Partial Text Message: fin={}",fin);
+ // Partial Message Support (does not use messageAppender)
+ try
+ {
+ String text = BufferUtil.toUTF8String(buffer);
+ events.callText(jsrsession.getAsyncRemote(),websocket,text,fin);
+ }
+ catch (DecodeException e)
+ {
+ onFatalError(e);
+ }
+ return;
+ }
+ else
+ {
+ // Whole Message Support
+ if (activeMessage == null)
+ {
+ LOG.debug("Whole Text Message");
+ activeMessage = new SimpleTextMessage(this);
+ }
+ }
+ }
+
+ if (events.hasTextStream())
+ {
+ handled = true;
+ // Streaming Message Support
+ if (activeMessage == null)
+ {
+ LOG.debug("Text Message Writer");
+
+ final MessageReader stream = new MessageReader(new MessageInputStream(session.getConnection()));
+ activeMessage = stream;
+
+ // Always dispatch streaming read to another thread.
+ dispatch(new Runnable()
+ {
+ @Override
+ public void run()
+ {
+ try
+ {
+ events.callTextStream(jsrsession.getAsyncRemote(),websocket,stream);
+ }
+ catch (DecodeException | IOException e)
+ {
+ onFatalError(e);
+ }
+ }
+ });
+ }
+ }
+
+ LOG.debug("handled = {}",handled);
+
+ // Process any active MessageAppender
+ if (handled && (activeMessage != null))
+ {
+ appendMessage(buffer,fin);
+ }
+ }
+
+ /**
+ * Entry point for whole text messages
+ */
+ @Override
+ public void onTextMessage(String message)
+ {
+ LOG.debug("onText({})",message);
+
+ try
+ {
+ // FIN is always true here
+ events.callText(jsrsession.getAsyncRemote(),websocket,message,true);
+ }
+ catch (DecodeException e)
+ {
+ onFatalError(e);
+ }
+ }
+
+ public void setRequestParameters(Map<String, String> requestParameters)
+ {
+ events.setRequestParameters(requestParameters);
+ }
+
+ @Override
+ public String toString()
+ {
+ return String.format("%s[websocket=%s]",this.getClass().getSimpleName(),websocket);
+ }
+}
diff --git a/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/endpoints/JsrEndpointEventDriver.java b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/endpoints/JsrEndpointEventDriver.java
new file mode 100644
index 0000000..f3e7cb3
--- /dev/null
+++ b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/endpoints/JsrEndpointEventDriver.java
@@ -0,0 +1,212 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.endpoints;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.Reader;
+import java.nio.ByteBuffer;
+
+import javax.websocket.CloseReason;
+import javax.websocket.Endpoint;
+import javax.websocket.MessageHandler;
+import javax.websocket.MessageHandler.Whole;
+
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+import org.eclipse.jetty.websocket.api.WebSocketPolicy;
+import org.eclipse.jetty.websocket.api.extensions.Frame;
+import org.eclipse.jetty.websocket.common.events.EventDriver;
+import org.eclipse.jetty.websocket.common.message.MessageInputStream;
+import org.eclipse.jetty.websocket.common.message.MessageReader;
+import org.eclipse.jetty.websocket.jsr356.MessageHandlerWrapper;
+import org.eclipse.jetty.websocket.jsr356.MessageType;
+import org.eclipse.jetty.websocket.jsr356.messages.BinaryPartialMessage;
+import org.eclipse.jetty.websocket.jsr356.messages.BinaryWholeMessage;
+import org.eclipse.jetty.websocket.jsr356.messages.TextPartialMessage;
+import org.eclipse.jetty.websocket.jsr356.messages.TextWholeMessage;
+
+/**
+ * EventDriver for websocket that extend from {@link javax.websocket.Endpoint}
+ */
+public class JsrEndpointEventDriver extends AbstractJsrEventDriver implements EventDriver
+{
+ private static final Logger LOG = Log.getLogger(JsrEndpointEventDriver.class);
+
+ private final Endpoint endpoint;
+
+ public JsrEndpointEventDriver(WebSocketPolicy policy, EndpointInstance endpointInstance)
+ {
+ super(policy,endpointInstance);
+ this.endpoint = (Endpoint)endpointInstance.getEndpoint();
+ }
+
+ @Override
+ public void onBinaryFrame(ByteBuffer buffer, boolean fin) throws IOException
+ {
+ if (activeMessage == null)
+ {
+ final MessageHandlerWrapper wrapper = jsrsession.getMessageHandlerWrapper(MessageType.BINARY);
+ if (wrapper == null)
+ {
+ LOG.debug("No BINARY MessageHandler declared");
+ return;
+ }
+ if (wrapper.wantsPartialMessages())
+ {
+ activeMessage = new BinaryPartialMessage(wrapper);
+ }
+ else if (wrapper.wantsStreams())
+ {
+ final MessageInputStream stream = new MessageInputStream(session.getConnection());
+ activeMessage = stream;
+ dispatch(new Runnable()
+ {
+ @SuppressWarnings("unchecked")
+ @Override
+ public void run()
+ {
+ MessageHandler.Whole<InputStream> handler = (Whole<InputStream>)wrapper.getHandler();
+ handler.onMessage(stream);
+ }
+ });
+ }
+ else
+ {
+ activeMessage = new BinaryWholeMessage(this,wrapper);
+ }
+ }
+
+ activeMessage.appendMessage(buffer,fin);
+
+ if (fin)
+ {
+ activeMessage.messageComplete();
+ activeMessage = null;
+ }
+ }
+
+ @Override
+ public void onBinaryMessage(byte[] data)
+ {
+ /* Ignored, handled by BinaryWholeMessage */
+ }
+
+ @Override
+ protected void onClose(CloseReason closereason)
+ {
+ endpoint.onClose(this.jsrsession,closereason);
+ }
+
+ @Override
+ public void onConnect()
+ {
+ LOG.debug("onConnect({}, {})",jsrsession,config);
+ try
+ {
+ endpoint.onOpen(jsrsession,config);
+ }
+ catch (Throwable t)
+ {
+ LOG.warn("Uncaught exception",t);
+ }
+ }
+
+ @Override
+ public void onError(Throwable cause)
+ {
+ endpoint.onError(jsrsession,cause);
+ }
+
+ @Override
+ public void onFrame(Frame frame)
+ {
+ /* Ignored, not supported by JSR-356 */
+ }
+
+ @Override
+ public void onInputStream(InputStream stream)
+ {
+ /* Ignored, handled by BinaryStreamMessage */
+ }
+
+ @Override
+ public void onReader(Reader reader)
+ {
+ /* Ignored, handled by TextStreamMessage */
+ }
+
+ @Override
+ public void onTextFrame(ByteBuffer buffer, boolean fin) throws IOException
+ {
+ if (activeMessage == null)
+ {
+ final MessageHandlerWrapper wrapper = jsrsession.getMessageHandlerWrapper(MessageType.TEXT);
+ if (wrapper == null)
+ {
+ LOG.debug("No TEXT MessageHandler declared");
+ return;
+ }
+ if (wrapper.wantsPartialMessages())
+ {
+ activeMessage = new TextPartialMessage(wrapper);
+ }
+ else if (wrapper.wantsStreams())
+ {
+ final MessageReader stream = new MessageReader(new MessageInputStream(session.getConnection()));
+ activeMessage = stream;
+
+ dispatch(new Runnable()
+ {
+ @SuppressWarnings("unchecked")
+ @Override
+ public void run()
+ {
+ MessageHandler.Whole<Reader> handler = (Whole<Reader>)wrapper.getHandler();
+ handler.onMessage(stream);
+ }
+ });
+ }
+ else
+ {
+ activeMessage = new TextWholeMessage(this,wrapper);
+ }
+ }
+
+ activeMessage.appendMessage(buffer,fin);
+
+ if (fin)
+ {
+ activeMessage.messageComplete();
+ activeMessage = null;
+ }
+ }
+
+ @Override
+ public void onTextMessage(String message)
+ {
+ /* Ignored, handled by TextWholeMessage */
+ }
+
+ @Override
+ public String toString()
+ {
+ return String.format("%s[%s]",JsrEndpointEventDriver.class.getSimpleName(),endpoint.getClass().getName());
+ }
+}
diff --git a/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/endpoints/JsrEndpointImpl.java b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/endpoints/JsrEndpointImpl.java
new file mode 100644
index 0000000..f14a650
--- /dev/null
+++ b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/endpoints/JsrEndpointImpl.java
@@ -0,0 +1,57 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.endpoints;
+
+import org.eclipse.jetty.websocket.api.WebSocketPolicy;
+import org.eclipse.jetty.websocket.common.events.EventDriver;
+import org.eclipse.jetty.websocket.common.events.EventDriverImpl;
+
+public class JsrEndpointImpl implements EventDriverImpl
+{
+ @Override
+ public EventDriver create(Object websocket, WebSocketPolicy policy)
+ {
+ if (!(websocket instanceof EndpointInstance))
+ {
+ throw new IllegalStateException(String.format("Websocket %s must be an %s",websocket.getClass().getName(),EndpointInstance.class.getName()));
+ }
+
+ return new JsrEndpointEventDriver(policy,(EndpointInstance)websocket);
+ }
+
+ @Override
+ public String describeRule()
+ {
+ return "class extends " + javax.websocket.Endpoint.class.getName();
+ }
+
+ @Override
+ public boolean supports(Object websocket)
+ {
+ if (!(websocket instanceof EndpointInstance))
+ {
+ return false;
+ }
+
+ EndpointInstance ei = (EndpointInstance)websocket;
+ Object endpoint = ei.getEndpoint();
+
+ return (endpoint instanceof javax.websocket.Endpoint);
+ }
+}
diff --git a/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/endpoints/JsrEventDriverFactory.java b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/endpoints/JsrEventDriverFactory.java
new file mode 100644
index 0000000..ce95a04
--- /dev/null
+++ b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/endpoints/JsrEventDriverFactory.java
@@ -0,0 +1,52 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.endpoints;
+
+import org.eclipse.jetty.websocket.api.WebSocketPolicy;
+import org.eclipse.jetty.websocket.common.events.EventDriverFactory;
+import org.eclipse.jetty.websocket.jsr356.client.JsrClientEndpointImpl;
+
+public class JsrEventDriverFactory extends EventDriverFactory
+{
+ public JsrEventDriverFactory(WebSocketPolicy policy)
+ {
+ super(policy);
+
+ clearImplementations();
+ // Classes that extend javax.websocket.Endpoint
+ addImplementation(new JsrEndpointImpl());
+ // Classes annotated with @javax.websocket.ClientEndpoint
+ addImplementation(new JsrClientEndpointImpl());
+ }
+
+ /**
+ * Unwrap ConfiguredEndpoint for end-user.
+ */
+ @Override
+ protected String getClassName(Object websocket)
+ {
+ if (websocket instanceof EndpointInstance)
+ {
+ EndpointInstance ce = (EndpointInstance)websocket;
+ return ce.getEndpoint().getClass().getName();
+ }
+
+ return websocket.getClass().getName();
+ }
+}
diff --git a/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/messages/BinaryPartialMessage.java b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/messages/BinaryPartialMessage.java
new file mode 100644
index 0000000..3255343
--- /dev/null
+++ b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/messages/BinaryPartialMessage.java
@@ -0,0 +1,77 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.messages;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+
+import javax.websocket.MessageHandler;
+import javax.websocket.MessageHandler.Partial;
+
+import org.eclipse.jetty.util.BufferUtil;
+import org.eclipse.jetty.websocket.common.message.MessageAppender;
+import org.eclipse.jetty.websocket.jsr356.MessageHandlerWrapper;
+
+public class BinaryPartialMessage implements MessageAppender
+{
+ private final MessageHandlerWrapper msgWrapper;
+ private final MessageHandler.Partial<Object> partialHandler;
+
+ @SuppressWarnings("unchecked")
+ public BinaryPartialMessage(MessageHandlerWrapper wrapper)
+ {
+ this.msgWrapper = wrapper;
+ this.partialHandler = (Partial<Object>)wrapper.getHandler();
+ }
+
+ @Override
+ public void appendMessage(ByteBuffer payload, boolean isLast) throws IOException
+ {
+ // No decoders for Partial messages per JSR-356 (PFD1 spec)
+
+ // Supported Partial<> Type #1: ByteBuffer
+ if (msgWrapper.isMessageType(ByteBuffer.class))
+ {
+ partialHandler.onMessage(payload.slice(),isLast);
+ return;
+ }
+
+ // Supported Partial<> Type #2: byte[]
+ if (msgWrapper.isMessageType(byte[].class))
+ {
+ partialHandler.onMessage(BufferUtil.toArray(payload),isLast);
+ return;
+ }
+
+ StringBuilder err = new StringBuilder();
+ err.append(msgWrapper.getHandler().getClass());
+ err.append(" does not implement an expected ");
+ err.append(MessageHandler.Partial.class.getName());
+ err.append(" of type ");
+ err.append(ByteBuffer.class.getName());
+ err.append(" or byte[]");
+ throw new IllegalStateException(err.toString());
+ }
+
+ @Override
+ public void messageComplete()
+ {
+ /* nothing to do here */
+ }
+}
diff --git a/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/messages/BinaryWholeMessage.java b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/messages/BinaryWholeMessage.java
new file mode 100644
index 0000000..b1da55b
--- /dev/null
+++ b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/messages/BinaryWholeMessage.java
@@ -0,0 +1,67 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.messages;
+
+import javax.websocket.DecodeException;
+import javax.websocket.Decoder;
+import javax.websocket.Decoder.Binary;
+import javax.websocket.MessageHandler;
+import javax.websocket.MessageHandler.Whole;
+
+import org.eclipse.jetty.util.BufferUtil;
+import org.eclipse.jetty.websocket.api.WebSocketException;
+import org.eclipse.jetty.websocket.common.events.EventDriver;
+import org.eclipse.jetty.websocket.common.message.SimpleBinaryMessage;
+import org.eclipse.jetty.websocket.jsr356.DecoderFactory;
+import org.eclipse.jetty.websocket.jsr356.MessageHandlerWrapper;
+
+public class BinaryWholeMessage extends SimpleBinaryMessage
+{
+ private final MessageHandlerWrapper msgWrapper;
+ private final MessageHandler.Whole<Object> wholeHandler;
+
+ @SuppressWarnings("unchecked")
+ public BinaryWholeMessage(EventDriver onEvent, MessageHandlerWrapper wrapper)
+ {
+ super(onEvent);
+ this.msgWrapper = wrapper;
+ this.wholeHandler = (Whole<Object>)wrapper.getHandler();
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public void messageComplete()
+ {
+ super.finished = true;
+
+ byte data[] = out.toByteArray();
+
+ DecoderFactory.Wrapper decoder = msgWrapper.getDecoder();
+ Decoder.Binary<Object> binaryDecoder = (Binary<Object>)decoder.getDecoder();
+ try
+ {
+ Object obj = binaryDecoder.decode(BufferUtil.toBuffer(data));
+ wholeHandler.onMessage(obj);
+ }
+ catch (DecodeException e)
+ {
+ throw new WebSocketException("Unable to decode binary data",e);
+ }
+ }
+}
diff --git a/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/messages/SendHandlerWriteCallback.java b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/messages/SendHandlerWriteCallback.java
new file mode 100644
index 0000000..b33f64c
--- /dev/null
+++ b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/messages/SendHandlerWriteCallback.java
@@ -0,0 +1,46 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.messages;
+
+import javax.websocket.SendHandler;
+import javax.websocket.SendResult;
+
+import org.eclipse.jetty.websocket.api.WriteCallback;
+
+public class SendHandlerWriteCallback implements WriteCallback
+{
+ private final SendHandler sendHandler;
+
+ public SendHandlerWriteCallback(SendHandler sendHandler)
+ {
+ this.sendHandler = sendHandler;
+ }
+
+ @Override
+ public void writeFailed(Throwable x)
+ {
+ sendHandler.onResult(new SendResult(x));
+ }
+
+ @Override
+ public void writeSuccess()
+ {
+ sendHandler.onResult(new SendResult());
+ }
+}
diff --git a/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/messages/TextPartialMessage.java b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/messages/TextPartialMessage.java
new file mode 100644
index 0000000..45e3cc8
--- /dev/null
+++ b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/messages/TextPartialMessage.java
@@ -0,0 +1,56 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.messages;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+
+import javax.websocket.MessageHandler;
+import javax.websocket.MessageHandler.Partial;
+
+import org.eclipse.jetty.util.BufferUtil;
+import org.eclipse.jetty.websocket.common.message.MessageAppender;
+import org.eclipse.jetty.websocket.jsr356.MessageHandlerWrapper;
+
+public class TextPartialMessage implements MessageAppender
+{
+ @SuppressWarnings("unused")
+ private final MessageHandlerWrapper msgWrapper;
+ private final MessageHandler.Partial<String> partialHandler;
+
+ @SuppressWarnings("unchecked")
+ public TextPartialMessage(MessageHandlerWrapper wrapper)
+ {
+ this.msgWrapper = wrapper;
+ this.partialHandler = (Partial<String>)wrapper.getHandler();
+ }
+
+ @Override
+ public void appendMessage(ByteBuffer payload, boolean isLast) throws IOException
+ {
+ // No decoders for Partial messages per JSR-356 (PFD1 spec)
+ partialHandler.onMessage(BufferUtil.toUTF8String(payload.slice()),isLast);
+ }
+
+ @Override
+ public void messageComplete()
+ {
+ /* nothing to do here */
+ }
+}
diff --git a/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/messages/TextWholeMessage.java b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/messages/TextWholeMessage.java
new file mode 100644
index 0000000..f98be92
--- /dev/null
+++ b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/messages/TextWholeMessage.java
@@ -0,0 +1,63 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.messages;
+
+import javax.websocket.DecodeException;
+import javax.websocket.Decoder;
+import javax.websocket.MessageHandler;
+import javax.websocket.MessageHandler.Whole;
+
+import org.eclipse.jetty.websocket.api.WebSocketException;
+import org.eclipse.jetty.websocket.common.events.EventDriver;
+import org.eclipse.jetty.websocket.common.message.SimpleTextMessage;
+import org.eclipse.jetty.websocket.jsr356.DecoderFactory;
+import org.eclipse.jetty.websocket.jsr356.MessageHandlerWrapper;
+
+public class TextWholeMessage extends SimpleTextMessage
+{
+ private final MessageHandlerWrapper msgWrapper;
+ private final MessageHandler.Whole<Object> wholeHandler;
+
+ @SuppressWarnings("unchecked")
+ public TextWholeMessage(EventDriver onEvent, MessageHandlerWrapper wrapper)
+ {
+ super(onEvent);
+ this.msgWrapper = wrapper;
+ this.wholeHandler = (Whole<Object>)wrapper.getHandler();
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public void messageComplete()
+ {
+ finished = true;
+
+ DecoderFactory.Wrapper decoder = msgWrapper.getDecoder();
+ Decoder.Text<Object> textDecoder = (Decoder.Text<Object>)decoder.getDecoder();
+ try
+ {
+ Object obj = textDecoder.decode(utf.toString());
+ wholeHandler.onMessage(obj);
+ }
+ catch (DecodeException e)
+ {
+ throw new WebSocketException("Unable to decode text data",e);
+ }
+ }
+}
diff --git a/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/metadata/CoderMetadata.java b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/metadata/CoderMetadata.java
new file mode 100644
index 0000000..07f3bd5
--- /dev/null
+++ b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/metadata/CoderMetadata.java
@@ -0,0 +1,70 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.metadata;
+
+import javax.websocket.Decoder;
+import javax.websocket.Encoder;
+
+import org.eclipse.jetty.websocket.jsr356.MessageType;
+
+/**
+ * The immutable base metadata for a coder ({@link Decoder} or {@link Encoder}
+ *
+ * @param <T>
+ * the specific type of coder ({@link Decoder} or {@link Encoder}
+ */
+public abstract class CoderMetadata<T>
+{
+ /** The class for the Coder */
+ private final Class<? extends T> coderClass;
+ /** The Class that the Decoder declares it decodes */
+ private final Class<?> objType;
+ /** The Basic type of message the decoder handles */
+ private final MessageType messageType;
+ /** Flag indicating if Decoder is for streaming (or not) */
+ private final boolean streamed;
+
+ public CoderMetadata(Class<? extends T> coderClass, Class<?> objType, MessageType messageType, boolean streamed)
+ {
+ this.objType = objType;
+ this.coderClass = coderClass;
+ this.messageType = messageType;
+ this.streamed = streamed;
+ }
+
+ public Class<? extends T> getCoderClass()
+ {
+ return this.coderClass;
+ }
+
+ public MessageType getMessageType()
+ {
+ return messageType;
+ }
+
+ public Class<?> getObjectType()
+ {
+ return objType;
+ }
+
+ public boolean isStreamed()
+ {
+ return streamed;
+ }
+}
diff --git a/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/metadata/CoderMetadataSet.java b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/metadata/CoderMetadataSet.java
new file mode 100644
index 0000000..9835c05
--- /dev/null
+++ b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/metadata/CoderMetadataSet.java
@@ -0,0 +1,235 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.metadata;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+import javax.websocket.Decoder;
+import javax.websocket.Encoder;
+
+import org.eclipse.jetty.websocket.api.InvalidWebSocketException;
+
+/**
+ * An durable collection of {@link CoderMetadata}.
+ * <p>
+ * This is a write-only collection, and cannot be modified once initialized.
+ *
+ * @param <T>
+ * The type of coder ({@link Decoder} or {@link Encoder}
+ * @param <M>
+ * The metadata for the coder
+ */
+public abstract class CoderMetadataSet<T, M extends CoderMetadata<T>> implements Iterable<M>
+{
+ /**
+ * Collection of metadatas
+ */
+ private final List<M> metadatas;
+ /**
+ * Collection of declared Coder classes
+ */
+ private final List<Class<? extends T>> coders;
+ /**
+ * Mapping of supported Type to metadata list index
+ */
+ private final Map<Class<?>, Integer> typeMap;
+ /**
+ * Mapping of Coder class to list of supported metadata
+ */
+ private final Map<Class<? extends T>, List<Integer>> implMap;
+
+ protected CoderMetadataSet()
+ {
+ metadatas = new ArrayList<>();
+ coders = new ArrayList<>();
+ typeMap = new ConcurrentHashMap<>();
+ implMap = new ConcurrentHashMap<>();
+ }
+
+ public void add(Class<? extends T> coder)
+ {
+ List<M> metadatas = discover(coder);
+ trackMetadata(metadatas);
+ }
+
+ public List<M> addAll(Class<? extends T>[] coders)
+ {
+ List<M> metadatas = new ArrayList<>();
+
+ for (Class<? extends T> coder : coders)
+ {
+ metadatas.addAll(discover(coder));
+ }
+
+ trackMetadata(metadatas);
+ return metadatas;
+ }
+
+ public List<M> addAll(List<Class<? extends T>> coders)
+ {
+ List<M> metadatas = new ArrayList<>();
+
+ for (Class<? extends T> coder : coders)
+ {
+ metadatas.addAll(discover(coder));
+ }
+
+ trackMetadata(metadatas);
+ return metadatas;
+ }
+
+ /**
+ * Coder Specific discovery of Metadata for a specific coder.
+ *
+ * @param coder
+ * the coder to discover metadata in.
+ * @return the list of metadata discovered
+ * @throws InvalidWebSocketException
+ * if unable to discover some metadata. Sucha as: a duplicate {@link CoderMetadata#getObjectType()} encountered, , or if unable to find the
+ * concrete generic class reference for the coder, or if the provided coder is not valid per spec.
+ */
+ protected abstract List<M> discover(Class<? extends T> coder);
+
+ public Class<? extends T> getCoder(Class<?> type)
+ {
+ M metadata = getMetadataByType(type);
+ if (metadata == null)
+ {
+ return null;
+ }
+ return metadata.getCoderClass();
+ }
+
+ public List<Class<? extends T>> getList()
+ {
+ return coders;
+ }
+
+ public List<M> getMetadataByImplementation(Class<? extends T> clazz)
+ {
+ List<Integer> indexes = implMap.get(clazz);
+ if (indexes == null)
+ {
+ return null;
+ }
+ List<M> ret = new ArrayList<>();
+ for (Integer idx : indexes)
+ {
+ ret.add(metadatas.get(idx));
+ }
+ return ret;
+ }
+
+ public M getMetadataByType(Class<?> type)
+ {
+ Integer idx = typeMap.get(type);
+ if (idx == null)
+ {
+ return null;
+ }
+ return metadatas.get(idx);
+ }
+
+ @Override
+ public Iterator<M> iterator()
+ {
+ return metadatas.iterator();
+ }
+
+ @Override
+ public String toString()
+ {
+ StringBuilder builder = new StringBuilder();
+ builder.append(this.getClass().getSimpleName());
+ builder.append("[metadatas=");
+ builder.append(metadatas.size());
+ builder.append(",coders=");
+ builder.append(coders.size());
+ builder.append("]");
+ return builder.toString();
+ }
+
+ protected void trackMetadata(List<M> metadatas)
+ {
+ for (M metadata : metadatas)
+ {
+ trackMetadata(metadata);
+ }
+ }
+
+ protected void trackMetadata(M metadata)
+ {
+ synchronized (metadatas)
+ {
+ // Validate
+ boolean duplicate = false;
+
+ // Is this metadata already declared?
+ if (metadatas.contains(metadata))
+ {
+ duplicate = true;
+ }
+
+ // Is this type already declared?
+ Class<?> type = metadata.getObjectType();
+ if (typeMap.containsKey(type))
+ {
+ duplicate = true;
+ }
+
+ if (duplicate)
+ {
+ StringBuilder err = new StringBuilder();
+ err.append("Duplicate decoder for type: ");
+ err.append(type);
+ err.append(" (class ").append(metadata.getCoderClass().getName());
+
+ // Get prior one
+ M dup = getMetadataByType(type);
+ err.append(" duplicates ");
+ err.append(dup.getCoderClass().getName());
+ err.append(")");
+ throw new IllegalStateException(err.toString());
+ }
+
+ // Track
+ Class<? extends T> coderClass = metadata.getCoderClass();
+ int newidx = metadatas.size();
+ metadatas.add(metadata);
+ coders.add(coderClass);
+ typeMap.put(type,newidx);
+
+ List<Integer> indexes = implMap.get(coderClass);
+ if (indexes == null)
+ {
+ indexes = new ArrayList<>();
+ }
+ if (indexes.contains(newidx))
+ {
+ // possible duplicate, TODO: how?
+ }
+ indexes.add(newidx);
+ implMap.put(coderClass,indexes);
+ }
+ }
+}
diff --git a/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/metadata/DecoderMetadata.java b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/metadata/DecoderMetadata.java
new file mode 100644
index 0000000..3c01df0
--- /dev/null
+++ b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/metadata/DecoderMetadata.java
@@ -0,0 +1,34 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.metadata;
+
+import javax.websocket.Decoder;
+
+import org.eclipse.jetty.websocket.jsr356.MessageType;
+
+/**
+ * Immutable Metadata for a {@link Decoder}
+ */
+public class DecoderMetadata extends CoderMetadata<Decoder>
+{
+ public DecoderMetadata(Class<? extends Decoder> coderClass, Class<?> objType, MessageType messageType, boolean streamed)
+ {
+ super(coderClass,objType,messageType,streamed);
+ }
+}
\ No newline at end of file
diff --git a/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/metadata/DecoderMetadataSet.java b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/metadata/DecoderMetadataSet.java
new file mode 100644
index 0000000..0307b3c
--- /dev/null
+++ b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/metadata/DecoderMetadataSet.java
@@ -0,0 +1,92 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.metadata;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.websocket.Decoder;
+
+import org.eclipse.jetty.websocket.api.InvalidWebSocketException;
+import org.eclipse.jetty.websocket.common.events.annotated.InvalidSignatureException;
+import org.eclipse.jetty.websocket.common.util.ReflectUtils;
+import org.eclipse.jetty.websocket.jsr356.MessageType;
+
+public class DecoderMetadataSet extends CoderMetadataSet<Decoder, DecoderMetadata>
+{
+ @Override
+ protected List<DecoderMetadata> discover(Class<? extends Decoder> decoder)
+ {
+ List<DecoderMetadata> metadatas = new ArrayList<>();
+
+ if (Decoder.Binary.class.isAssignableFrom(decoder))
+ {
+ Class<?> objType = getDecoderType(decoder,Decoder.Binary.class);
+ metadatas.add(new DecoderMetadata(decoder,objType,MessageType.BINARY,false));
+ }
+ if (Decoder.BinaryStream.class.isAssignableFrom(decoder))
+ {
+ Class<?> objType = getDecoderType(decoder,Decoder.BinaryStream.class);
+ metadatas.add(new DecoderMetadata(decoder,objType,MessageType.BINARY,true));
+ }
+ if (Decoder.Text.class.isAssignableFrom(decoder))
+ {
+ Class<?> objType = getDecoderType(decoder,Decoder.Text.class);
+ metadatas.add(new DecoderMetadata(decoder,objType,MessageType.TEXT,false));
+ }
+ if (Decoder.TextStream.class.isAssignableFrom(decoder))
+ {
+ Class<?> objType = getDecoderType(decoder,Decoder.TextStream.class);
+ metadatas.add(new DecoderMetadata(decoder,objType,MessageType.TEXT,true));
+ }
+
+ if (!ReflectUtils.isDefaultConstructable(decoder))
+ {
+ throw new InvalidSignatureException("Decoder must have public, no-args constructor: " + decoder.getName());
+ }
+
+ if (metadatas.size() <= 0)
+ {
+ throw new InvalidSignatureException("Not a valid Decoder class: " + decoder.getName());
+ }
+
+ return metadatas;
+ }
+
+ private Class<?> getDecoderType(Class<? extends Decoder> decoder, Class<?> interfaceClass)
+ {
+ Class<?> decoderClass = ReflectUtils.findGenericClassFor(decoder,interfaceClass);
+ if (decoderClass == null)
+ {
+ StringBuilder err = new StringBuilder();
+ err.append("Invalid type declared for interface ");
+ err.append(interfaceClass.getName());
+ err.append(" on class ");
+ err.append(decoder);
+ throw new InvalidWebSocketException(err.toString());
+ }
+ return decoderClass;
+ }
+
+ protected final void register(Class<?> type, Class<? extends Decoder> decoder, MessageType msgType, boolean streamed)
+ {
+ DecoderMetadata metadata = new DecoderMetadata(decoder,type,msgType,streamed);
+ trackMetadata(metadata);
+ }
+}
diff --git a/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/metadata/DuplicateCoderException.java b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/metadata/DuplicateCoderException.java
new file mode 100644
index 0000000..3dcdbb2
--- /dev/null
+++ b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/metadata/DuplicateCoderException.java
@@ -0,0 +1,42 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.metadata;
+
+import javax.websocket.Decoder;
+import javax.websocket.Encoder;
+
+import org.eclipse.jetty.websocket.api.InvalidWebSocketException;
+
+/**
+ * Thrown when a duplicate coder is encountered when attempting to identify a Endpoint's metadata ( {@link Decoder} or {@link Encoder})
+ */
+public class DuplicateCoderException extends InvalidWebSocketException
+{
+ private static final long serialVersionUID = -3049181444035417170L;
+
+ public DuplicateCoderException(String message)
+ {
+ super(message);
+ }
+
+ public DuplicateCoderException(String message, Throwable cause)
+ {
+ super(message,cause);
+ }
+}
diff --git a/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/metadata/EncoderMetadata.java b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/metadata/EncoderMetadata.java
new file mode 100644
index 0000000..8a43564
--- /dev/null
+++ b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/metadata/EncoderMetadata.java
@@ -0,0 +1,34 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.metadata;
+
+import javax.websocket.Encoder;
+
+import org.eclipse.jetty.websocket.jsr356.MessageType;
+
+/**
+ * Immutable Metadata for a {@link Encoder}
+ */
+public class EncoderMetadata extends CoderMetadata<Encoder>
+{
+ public EncoderMetadata(Class<? extends Encoder> coderClass, Class<?> objType, MessageType messageType, boolean streamed)
+ {
+ super(coderClass,objType,messageType,streamed);
+ }
+}
\ No newline at end of file
diff --git a/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/metadata/EncoderMetadataSet.java b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/metadata/EncoderMetadataSet.java
new file mode 100644
index 0000000..5f1b279
--- /dev/null
+++ b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/metadata/EncoderMetadataSet.java
@@ -0,0 +1,92 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.metadata;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.websocket.Encoder;
+
+import org.eclipse.jetty.websocket.api.InvalidWebSocketException;
+import org.eclipse.jetty.websocket.common.events.annotated.InvalidSignatureException;
+import org.eclipse.jetty.websocket.common.util.ReflectUtils;
+import org.eclipse.jetty.websocket.jsr356.MessageType;
+
+public class EncoderMetadataSet extends CoderMetadataSet<Encoder, EncoderMetadata>
+{
+ @Override
+ protected List<EncoderMetadata> discover(Class<? extends Encoder> encoder)
+ {
+ List<EncoderMetadata> metadatas = new ArrayList<>();
+
+ if (Encoder.Binary.class.isAssignableFrom(encoder))
+ {
+ Class<?> objType = getEncoderType(encoder,Encoder.Binary.class);
+ metadatas.add(new EncoderMetadata(encoder,objType,MessageType.BINARY,false));
+ }
+ if (Encoder.BinaryStream.class.isAssignableFrom(encoder))
+ {
+ Class<?> objType = getEncoderType(encoder,Encoder.BinaryStream.class);
+ metadatas.add(new EncoderMetadata(encoder,objType,MessageType.BINARY,true));
+ }
+ if (Encoder.Text.class.isAssignableFrom(encoder))
+ {
+ Class<?> objType = getEncoderType(encoder,Encoder.Text.class);
+ metadatas.add(new EncoderMetadata(encoder,objType,MessageType.TEXT,false));
+ }
+ if (Encoder.TextStream.class.isAssignableFrom(encoder))
+ {
+ Class<?> objType = getEncoderType(encoder,Encoder.TextStream.class);
+ metadatas.add(new EncoderMetadata(encoder,objType,MessageType.TEXT,true));
+ }
+
+ if (!ReflectUtils.isDefaultConstructable(encoder))
+ {
+ throw new InvalidSignatureException("Encoder must have public, no-args constructor: " + encoder.getName());
+ }
+
+ if (metadatas.size() <= 0)
+ {
+ throw new InvalidSignatureException("Not a valid Encoder class: " + encoder.getName() + " implements no " + Encoder.class.getName() + " interfaces");
+ }
+
+ return metadatas;
+ }
+
+ private Class<?> getEncoderType(Class<? extends Encoder> encoder, Class<?> interfaceClass)
+ {
+ Class<?> decoderClass = ReflectUtils.findGenericClassFor(encoder,interfaceClass);
+ if (decoderClass == null)
+ {
+ StringBuilder err = new StringBuilder();
+ err.append("Invalid type declared for interface ");
+ err.append(interfaceClass.getName());
+ err.append(" on class ");
+ err.append(encoder);
+ throw new InvalidWebSocketException(err.toString());
+ }
+ return decoderClass;
+ }
+
+ protected final void register(Class<?> type, Class<? extends Encoder> encoder, MessageType msgType, boolean streamed)
+ {
+ EncoderMetadata metadata = new EncoderMetadata(encoder,type,msgType,streamed);
+ trackMetadata(metadata);
+ }
+}
diff --git a/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/metadata/EndpointMetadata.java b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/metadata/EndpointMetadata.java
new file mode 100644
index 0000000..a6ba012
--- /dev/null
+++ b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/metadata/EndpointMetadata.java
@@ -0,0 +1,28 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.metadata;
+
+public interface EndpointMetadata
+{
+ public DecoderMetadataSet getDecoders();
+
+ public EncoderMetadataSet getEncoders();
+
+ public Class<?> getEndpointClass();
+}
diff --git a/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/metadata/MessageHandlerMetadata.java b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/metadata/MessageHandlerMetadata.java
new file mode 100644
index 0000000..e5cdc82
--- /dev/null
+++ b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/metadata/MessageHandlerMetadata.java
@@ -0,0 +1,71 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.metadata;
+
+import javax.websocket.MessageHandler;
+
+/**
+ * An immutable metadata for a {@link MessageHandler}, representing a single interface on a message handling class.
+ * <p>
+ * A message handling class can contain more than 1 valid {@link MessageHandler} interface, this will result in multiple {@link MessageHandlerMetadata}
+ * instances, each tracking one of the {@link MessageHandler} interfaces declared.
+ */
+public class MessageHandlerMetadata
+{
+ /**
+ * The implemented MessageHandler class.
+ * <p>
+ * Commonly a end-user provided class, with 1 or more implemented {@link MessageHandler} interfaces
+ */
+ private final Class<? extends MessageHandler> handlerClass;
+ /**
+ * Indicator if this is a {@link MessageHandler.Partial} or {@link MessageHandler.Whole} interface.
+ * <p>
+ * True for MessageHandler.Partial, other wise its a MessageHandler.Whole
+ */
+ private final boolean isPartialSupported;
+ /**
+ * The class type that this specific interface's generic implements.
+ * <p>
+ * Or said another way, the first parameter type on this interface's onMessage() method.
+ */
+ private final Class<?> messageClass;
+
+ public MessageHandlerMetadata(Class<? extends MessageHandler> handlerClass, Class<?> messageClass, boolean partial)
+ {
+ this.handlerClass = handlerClass;
+ this.isPartialSupported = partial;
+ this.messageClass = messageClass;
+ }
+
+ public Class<? extends MessageHandler> getHandlerClass()
+ {
+ return handlerClass;
+ }
+
+ public Class<?> getMessageClass()
+ {
+ return messageClass;
+ }
+
+ public boolean isPartialSupported()
+ {
+ return isPartialSupported;
+ }
+}
diff --git a/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/utils/Primitives.java b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/utils/Primitives.java
new file mode 100644
index 0000000..f72be7a
--- /dev/null
+++ b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/utils/Primitives.java
@@ -0,0 +1,77 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.utils;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+
+public class Primitives
+{
+ private static final Map<Class<?>, Class<?>> PRIMITIVE_CLASS_MAP;
+ private static final Map<Class<?>, Class<?>> CLASS_PRIMITIVE_MAP;
+
+ static
+ {
+ Map<Class<?>, Class<?>> primitives = new HashMap<>();
+
+ // Map of classes to primitive types
+ primitives.put(Boolean.class,Boolean.TYPE);
+ primitives.put(Byte.class,Byte.TYPE);
+ primitives.put(Character.class,Character.TYPE);
+ primitives.put(Double.class,Double.TYPE);
+ primitives.put(Float.class,Float.TYPE);
+ primitives.put(Integer.class,Integer.TYPE);
+ primitives.put(Long.class,Long.TYPE);
+ primitives.put(Short.class,Short.TYPE);
+ primitives.put(Void.class,Void.TYPE);
+
+ CLASS_PRIMITIVE_MAP = Collections.unmodifiableMap(primitives);
+
+ // Map of primitive types to classes
+ Map<Class<?>, Class<?>> types = new HashMap<>();
+ for (Map.Entry<Class<?>, Class<?>> classEntry : primitives.entrySet())
+ {
+ types.put(classEntry.getValue(),classEntry.getKey());
+ }
+
+ PRIMITIVE_CLASS_MAP = Collections.unmodifiableMap(types);
+ }
+
+ public static Class<?> getPrimitiveClass(Class<?> primitiveType)
+ {
+ return PRIMITIVE_CLASS_MAP.get(primitiveType);
+ }
+
+ public static Set<Class<?>> getPrimitiveClasses()
+ {
+ return CLASS_PRIMITIVE_MAP.keySet();
+ }
+
+ public static Set<Class<?>> getPrimitives()
+ {
+ return PRIMITIVE_CLASS_MAP.keySet();
+ }
+
+ public static Class<?> getPrimitiveType(Class<?> primitiveClass)
+ {
+ return CLASS_PRIMITIVE_MAP.get(primitiveClass);
+ }
+}
diff --git a/jetty-websocket/javax-websocket-client-impl/src/main/resources/META-INF/services/javax.websocket.ContainerProvider b/jetty-websocket/javax-websocket-client-impl/src/main/resources/META-INF/services/javax.websocket.ContainerProvider
new file mode 100644
index 0000000..e6dffe7
--- /dev/null
+++ b/jetty-websocket/javax-websocket-client-impl/src/main/resources/META-INF/services/javax.websocket.ContainerProvider
@@ -0,0 +1 @@
+org.eclipse.jetty.websocket.jsr356.JettyClientContainerProvider
\ No newline at end of file
diff --git a/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/AnnotatedEchoClient.java b/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/AnnotatedEchoClient.java
new file mode 100644
index 0000000..56bdcd9
--- /dev/null
+++ b/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/AnnotatedEchoClient.java
@@ -0,0 +1,60 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356;
+
+import java.io.IOException;
+
+import javax.websocket.ClientEndpoint;
+import javax.websocket.CloseReason;
+import javax.websocket.OnMessage;
+import javax.websocket.OnOpen;
+import javax.websocket.Session;
+
+@ClientEndpoint
+public class AnnotatedEchoClient
+{
+ private Session session = null;
+ public CloseReason close = null;
+ public MessageQueue messageQueue = new MessageQueue();
+
+ public void onClose(CloseReason close)
+ {
+ this.close = close;
+ }
+
+ @OnMessage
+ public void onMessage(String message)
+ {
+ this.messageQueue.offer(message);
+ }
+
+ @OnOpen
+ public void onOpen(Session session)
+ {
+ this.session = session;
+ }
+
+ public void sendText(String text) throws IOException
+ {
+ if (session != null)
+ {
+ session.getBasicRemote().sendText(text);
+ }
+ }
+}
diff --git a/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/AnnotatedEchoTest.java b/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/AnnotatedEchoTest.java
new file mode 100644
index 0000000..01d9940
--- /dev/null
+++ b/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/AnnotatedEchoTest.java
@@ -0,0 +1,89 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356;
+
+import java.net.URI;
+import java.util.concurrent.TimeUnit;
+
+import javax.websocket.ContainerProvider;
+import javax.websocket.Session;
+import javax.websocket.WebSocketContainer;
+
+import org.eclipse.jetty.server.Server;
+import org.eclipse.jetty.server.ServerConnector;
+import org.eclipse.jetty.server.handler.ContextHandler;
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+public class AnnotatedEchoTest
+{
+ private static Server server;
+ private static EchoHandler handler;
+ private static URI serverUri;
+
+ @BeforeClass
+ public static void startServer() throws Exception
+ {
+ server = new Server();
+ ServerConnector connector = new ServerConnector(server);
+ server.addConnector(connector);
+
+ handler = new EchoHandler();
+
+ ContextHandler context = new ContextHandler();
+ context.setContextPath("/");
+ context.setHandler(handler);
+ server.setHandler(context);
+
+ // Start Server
+ server.start();
+
+ String host = connector.getHost();
+ if (host == null)
+ {
+ host = "localhost";
+ }
+ int port = connector.getLocalPort();
+ serverUri = new URI(String.format("ws://%s:%d/",host,port));
+ }
+
+ @AfterClass
+ public static void stopServer()
+ {
+ try
+ {
+ server.stop();
+ }
+ catch (Exception e)
+ {
+ e.printStackTrace(System.err);
+ }
+ }
+
+ @Test
+ public void testEcho() throws Exception
+ {
+ WebSocketContainer container = ContainerProvider.getWebSocketContainer();
+ AnnotatedEchoClient echoer = new AnnotatedEchoClient();
+ Session session = container.connectToServer(echoer,serverUri);
+ session.getBasicRemote().sendText("Echo");
+ echoer.messageQueue.awaitMessages(1,1000,TimeUnit.MILLISECONDS);
+ }
+}
diff --git a/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/ConfiguratorTest.java b/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/ConfiguratorTest.java
new file mode 100644
index 0000000..1a1d55d
--- /dev/null
+++ b/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/ConfiguratorTest.java
@@ -0,0 +1,133 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356;
+
+import static org.hamcrest.Matchers.*;
+
+import java.net.URI;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+
+import javax.websocket.ClientEndpointConfig;
+import javax.websocket.ContainerProvider;
+import javax.websocket.HandshakeResponse;
+import javax.websocket.Session;
+import javax.websocket.WebSocketContainer;
+
+import org.eclipse.jetty.server.Server;
+import org.eclipse.jetty.server.ServerConnector;
+import org.eclipse.jetty.server.handler.ContextHandler;
+import org.junit.AfterClass;
+import org.junit.Assert;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+/**
+ * Tests of {@link ClientEndpointConfig.Configurator}
+ */
+public class ConfiguratorTest
+{
+ public class TrackingConfigurator extends ClientEndpointConfig.Configurator
+ {
+ public HandshakeResponse response;
+ public Map<String, List<String>> request;
+
+ @Override
+ public void afterResponse(HandshakeResponse hr)
+ {
+ this.response = hr;
+ }
+
+ @Override
+ public void beforeRequest(Map<String, List<String>> headers)
+ {
+ this.request = headers;
+ }
+ }
+
+ private static Server server;
+ private static EchoHandler handler;
+ private static URI serverUri;
+
+ @BeforeClass
+ public static void startServer() throws Exception
+ {
+ server = new Server();
+ ServerConnector connector = new ServerConnector(server);
+ server.addConnector(connector);
+
+ handler = new EchoHandler();
+
+ ContextHandler context = new ContextHandler();
+ context.setContextPath("/");
+ context.setHandler(handler);
+ server.setHandler(context);
+
+ // Start Server
+ server.start();
+
+ String host = connector.getHost();
+ if (host == null)
+ {
+ host = "localhost";
+ }
+ int port = connector.getLocalPort();
+ serverUri = new URI(String.format("ws://%s:%d/",host,port));
+ }
+
+ @AfterClass
+ public static void stopServer()
+ {
+ try
+ {
+ server.stop();
+ }
+ catch (Exception e)
+ {
+ e.printStackTrace(System.err);
+ }
+ }
+
+ @Test
+ public void testEndpointHandshakeInfo() throws Exception
+ {
+ WebSocketContainer container = ContainerProvider.getWebSocketContainer();
+ EndpointEchoClient echoer = new EndpointEchoClient();
+
+ // Build Config
+ ClientEndpointConfig.Builder cfgbldr = ClientEndpointConfig.Builder.create();
+ TrackingConfigurator configurator = new TrackingConfigurator();
+ cfgbldr.configurator(configurator);
+ ClientEndpointConfig config = cfgbldr.build();
+
+ // Connect
+ Session session = container.connectToServer(echoer,config,serverUri);
+
+ // Send Simple Message
+ session.getBasicRemote().sendText("Echo");
+
+ // Wait for echo
+ echoer.textCapture.messageQueue.awaitMessages(1,1000,TimeUnit.MILLISECONDS);
+
+ // Validate client side configurator use
+ Assert.assertThat("configurator.request",configurator.request,notNullValue());
+ Assert.assertThat("configurator.response",configurator.response,notNullValue());
+ }
+}
diff --git a/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/DecoderFactoryTest.java b/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/DecoderFactoryTest.java
new file mode 100644
index 0000000..c6cb679
--- /dev/null
+++ b/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/DecoderFactoryTest.java
@@ -0,0 +1,107 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356;
+
+import static org.hamcrest.Matchers.*;
+
+import java.nio.ByteBuffer;
+import java.util.Date;
+
+import javax.websocket.Decoder;
+
+import org.eclipse.jetty.websocket.jsr356.decoders.ByteArrayDecoder;
+import org.eclipse.jetty.websocket.jsr356.decoders.ByteBufferDecoder;
+import org.eclipse.jetty.websocket.jsr356.decoders.DateDecoder;
+import org.eclipse.jetty.websocket.jsr356.decoders.IntegerDecoder;
+import org.eclipse.jetty.websocket.jsr356.decoders.LongDecoder;
+import org.eclipse.jetty.websocket.jsr356.decoders.PrimitiveDecoderMetadataSet;
+import org.eclipse.jetty.websocket.jsr356.decoders.StringDecoder;
+import org.eclipse.jetty.websocket.jsr356.metadata.DecoderMetadata;
+import org.eclipse.jetty.websocket.jsr356.metadata.DecoderMetadataSet;
+import org.eclipse.jetty.websocket.jsr356.samples.Fruit;
+import org.eclipse.jetty.websocket.jsr356.samples.FruitDecoder;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+
+public class DecoderFactoryTest
+{
+ private DecoderMetadataSet metadatas;
+ private DecoderFactory factory;
+
+ private void assertMetadataFor(Class<?> type, Class<? extends Decoder> expectedDecoderClass, MessageType expectedType)
+ {
+ DecoderMetadata metadata = factory.getMetadataFor(type);
+ Assert.assertEquals("metadata.coderClass",metadata.getCoderClass(),expectedDecoderClass);
+ Assert.assertThat("metadata.messageType",metadata.getMessageType(),is(expectedType));
+ Assert.assertEquals("metadata.objectType",metadata.getObjectType(),type);
+ }
+
+ @Before
+ public void initDecoderFactory()
+ {
+ DecoderFactory primitivesFactory = new DecoderFactory(PrimitiveDecoderMetadataSet.INSTANCE);
+ metadatas = new DecoderMetadataSet();
+ factory = new DecoderFactory(metadatas,primitivesFactory);
+ }
+
+ @Test
+ public void testGetMetadataForByteArray()
+ {
+ assertMetadataFor(byte[].class,ByteArrayDecoder.class,MessageType.BINARY);
+ }
+
+ @Test
+ public void testGetMetadataForByteBuffer()
+ {
+ assertMetadataFor(ByteBuffer.class,ByteBufferDecoder.class,MessageType.BINARY);
+ }
+
+ @Test
+ public void testGetMetadataForDate()
+ {
+ metadatas.add(DateDecoder.class);
+ assertMetadataFor(Date.class,DateDecoder.class,MessageType.TEXT);
+ }
+
+ @Test
+ public void testGetMetadataForFruit()
+ {
+ metadatas.add(FruitDecoder.class);
+ assertMetadataFor(Fruit.class,FruitDecoder.class,MessageType.TEXT);
+ }
+
+ @Test
+ public void testGetMetadataForInteger()
+ {
+ assertMetadataFor(Integer.TYPE,IntegerDecoder.class,MessageType.TEXT);
+ }
+
+ @Test
+ public void testGetMetadataForLong()
+ {
+ assertMetadataFor(Long.TYPE,LongDecoder.class,MessageType.TEXT);
+ }
+
+ @Test
+ public void testGetStringDecoder()
+ {
+ assertMetadataFor(String.class,StringDecoder.class,MessageType.TEXT);
+ }
+}
diff --git a/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/EchoCaptureHandler.java b/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/EchoCaptureHandler.java
new file mode 100644
index 0000000..9d414f0
--- /dev/null
+++ b/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/EchoCaptureHandler.java
@@ -0,0 +1,32 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356;
+
+import javax.websocket.MessageHandler;
+
+public class EchoCaptureHandler implements MessageHandler.Whole<String>
+{
+ public MessageQueue messageQueue = new MessageQueue();
+
+ @Override
+ public void onMessage(String message)
+ {
+ messageQueue.offer(message);
+ }
+}
diff --git a/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/EchoHandler.java b/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/EchoHandler.java
new file mode 100644
index 0000000..3afac77
--- /dev/null
+++ b/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/EchoHandler.java
@@ -0,0 +1,42 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356;
+
+import org.eclipse.jetty.websocket.server.WebSocketHandler;
+import org.eclipse.jetty.websocket.servlet.ServletUpgradeRequest;
+import org.eclipse.jetty.websocket.servlet.ServletUpgradeResponse;
+import org.eclipse.jetty.websocket.servlet.WebSocketCreator;
+import org.eclipse.jetty.websocket.servlet.WebSocketServletFactory;
+
+public class EchoHandler extends WebSocketHandler implements WebSocketCreator
+{
+ public JettyEchoSocket socket = new JettyEchoSocket();
+
+ @Override
+ public void configure(WebSocketServletFactory factory)
+ {
+ factory.setCreator(this);
+ }
+
+ @Override
+ public Object createWebSocket(ServletUpgradeRequest req, ServletUpgradeResponse resp)
+ {
+ return socket;
+ }
+}
diff --git a/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/EncoderFactoryTest.java b/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/EncoderFactoryTest.java
new file mode 100644
index 0000000..3cce94a
--- /dev/null
+++ b/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/EncoderFactoryTest.java
@@ -0,0 +1,86 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356;
+
+import static org.hamcrest.Matchers.*;
+
+import javax.websocket.Encoder;
+
+import org.eclipse.jetty.websocket.jsr356.encoders.IntegerEncoder;
+import org.eclipse.jetty.websocket.jsr356.encoders.LongEncoder;
+import org.eclipse.jetty.websocket.jsr356.encoders.PrimitiveEncoderMetadataSet;
+import org.eclipse.jetty.websocket.jsr356.metadata.EncoderMetadata;
+import org.eclipse.jetty.websocket.jsr356.metadata.EncoderMetadataSet;
+import org.eclipse.jetty.websocket.jsr356.samples.Fruit;
+import org.eclipse.jetty.websocket.jsr356.samples.FruitBinaryEncoder;
+import org.eclipse.jetty.websocket.jsr356.samples.FruitTextEncoder;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * Tests against the Encoders class
+ */
+public class EncoderFactoryTest
+{
+ private EncoderMetadataSet metadatas;
+ private EncoderFactory factory;
+
+ private void assertMetadataFor(Class<?> type, Class<? extends Encoder> expectedEncoderClass, MessageType expectedType)
+ {
+ EncoderMetadata metadata = factory.getMetadataFor(type);
+ Assert.assertEquals("metadata.coderClass",metadata.getCoderClass(),expectedEncoderClass);
+ Assert.assertThat("metadata.messageType",metadata.getMessageType(),is(expectedType));
+ Assert.assertEquals("metadata.objectType",metadata.getObjectType(),type);
+ }
+
+ @Before
+ public void initEncoderFactory()
+ {
+ EncoderFactory primitivesFactory = new EncoderFactory(PrimitiveEncoderMetadataSet.INSTANCE);
+ metadatas = new EncoderMetadataSet();
+ factory = new EncoderFactory(metadatas,primitivesFactory);
+ }
+
+ @Test
+ public void testGetMetadataForFruitBinary()
+ {
+ metadatas.add(FruitBinaryEncoder.class);
+ assertMetadataFor(Fruit.class,FruitBinaryEncoder.class,MessageType.BINARY);
+ }
+
+ @Test
+ public void testGetMetadataForFruitText()
+ {
+ metadatas.add(FruitTextEncoder.class);
+ assertMetadataFor(Fruit.class,FruitTextEncoder.class,MessageType.TEXT);
+ }
+
+ @Test
+ public void testGetMetadataForInteger()
+ {
+ assertMetadataFor(Integer.TYPE,IntegerEncoder.class,MessageType.TEXT);
+ }
+
+ @Test
+ public void testGetMetadataForLong()
+ {
+ assertMetadataFor(Long.TYPE,LongEncoder.class,MessageType.TEXT);
+ }
+}
diff --git a/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/EndpointEchoClient.java b/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/EndpointEchoClient.java
new file mode 100644
index 0000000..1337b50
--- /dev/null
+++ b/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/EndpointEchoClient.java
@@ -0,0 +1,66 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356;
+
+import static org.hamcrest.Matchers.*;
+
+import java.io.IOException;
+
+import javax.websocket.CloseReason;
+import javax.websocket.Endpoint;
+import javax.websocket.EndpointConfig;
+import javax.websocket.Session;
+
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+import org.junit.Assert;
+
+/**
+ * Basic Echo Client from extended Endpoint
+ */
+public class EndpointEchoClient extends Endpoint
+{
+ private static final Logger LOG = Log.getLogger(EndpointEchoClient.class);
+ private Session session = null;
+ private CloseReason close = null;
+ public EchoCaptureHandler textCapture = new EchoCaptureHandler();
+
+ public CloseReason getClose()
+ {
+ return close;
+ }
+
+ @Override
+ public void onOpen(Session session, EndpointConfig config)
+ {
+ LOG.debug("onOpen({}, {})",session,config);
+ this.session = session;
+ Assert.assertThat("Session is required",session,notNullValue());
+ Assert.assertThat("EndpointConfig is required",config,notNullValue());
+ this.session.addMessageHandler(textCapture);
+ }
+
+ public void sendText(String text) throws IOException
+ {
+ if (session != null)
+ {
+ session.getBasicRemote().sendText(text);
+ }
+ }
+}
diff --git a/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/EndpointEchoTest.java b/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/EndpointEchoTest.java
new file mode 100644
index 0000000..f2eceee
--- /dev/null
+++ b/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/EndpointEchoTest.java
@@ -0,0 +1,140 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356;
+
+import static org.hamcrest.Matchers.*;
+
+import java.net.URI;
+import java.util.concurrent.TimeUnit;
+
+import javax.websocket.ContainerProvider;
+import javax.websocket.Session;
+import javax.websocket.WebSocketContainer;
+
+import org.eclipse.jetty.server.Server;
+import org.eclipse.jetty.server.ServerConnector;
+import org.eclipse.jetty.server.handler.ContextHandler;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+import org.eclipse.jetty.websocket.jsr356.samples.EchoStringEndpoint;
+import org.junit.AfterClass;
+import org.junit.Assert;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+public class EndpointEchoTest
+{
+ private static final Logger LOG = Log.getLogger(EndpointEchoTest.class);
+ private static Server server;
+ private static EchoHandler handler;
+ private static URI serverUri;
+
+ @BeforeClass
+ public static void startServer() throws Exception
+ {
+ server = new Server();
+ ServerConnector connector = new ServerConnector(server);
+ server.addConnector(connector);
+
+ handler = new EchoHandler();
+
+ ContextHandler context = new ContextHandler();
+ context.setContextPath("/");
+ context.setHandler(handler);
+ server.setHandler(context);
+
+ // Start Server
+ server.start();
+
+ String host = connector.getHost();
+ if (host == null)
+ {
+ host = "localhost";
+ }
+ int port = connector.getLocalPort();
+ serverUri = new URI(String.format("ws://%s:%d/",host,port));
+ }
+
+ @AfterClass
+ public static void stopServer()
+ {
+ try
+ {
+ server.stop();
+ }
+ catch (Exception e)
+ {
+ e.printStackTrace(System.err);
+ }
+ }
+
+ @Test
+ public void testBasicEchoInstance() throws Exception
+ {
+ WebSocketContainer container = ContainerProvider.getWebSocketContainer();
+ EndpointEchoClient echoer = new EndpointEchoClient();
+ Assert.assertThat(echoer,instanceOf(javax.websocket.Endpoint.class));
+ // Issue connect using instance of class that extends Endpoint
+ Session session = container.connectToServer(echoer,serverUri);
+ LOG.debug("Client Connected: {}",session);
+ session.getBasicRemote().sendText("Echo");
+ LOG.debug("Client Message Sent");
+ echoer.textCapture.messageQueue.awaitMessages(1,1000,TimeUnit.MILLISECONDS);
+ }
+
+ @Test
+ public void testBasicEchoClassref() throws Exception
+ {
+ WebSocketContainer container = ContainerProvider.getWebSocketContainer();
+ // Issue connect using class reference (class extends Endpoint)
+ Session session = container.connectToServer(EndpointEchoClient.class,serverUri);
+ LOG.debug("Client Connected: {}",session);
+ session.getBasicRemote().sendText("Echo");
+ LOG.debug("Client Message Sent");
+ // TODO: figure out echo verification.
+ // echoer.textCapture.messageQueue.awaitMessages(1,1000,TimeUnit.MILLISECONDS);
+ }
+
+ @Test
+ public void testAbstractEchoInstance() throws Exception
+ {
+ WebSocketContainer container = ContainerProvider.getWebSocketContainer();
+ EchoStringEndpoint echoer = new EchoStringEndpoint();
+ Assert.assertThat(echoer,instanceOf(javax.websocket.Endpoint.class));
+ // Issue connect using instance of class that extends abstract that extends Endpoint
+ Session session = container.connectToServer(echoer,serverUri);
+ LOG.debug("Client Connected: {}",session);
+ session.getBasicRemote().sendText("Echo");
+ LOG.debug("Client Message Sent");
+ echoer.messageQueue.awaitMessages(1,1000,TimeUnit.MILLISECONDS);
+ }
+
+ @Test
+ public void testAbstractEchoClassref() throws Exception
+ {
+ WebSocketContainer container = ContainerProvider.getWebSocketContainer();
+ // Issue connect using class reference (class that extends abstract that extends Endpoint)
+ Session session = container.connectToServer(EchoStringEndpoint.class,serverUri);
+ LOG.debug("Client Connected: {}",session);
+ session.getBasicRemote().sendText("Echo");
+ LOG.debug("Client Message Sent");
+ // TODO: figure out echo verification.
+ // echoer.messageQueue.awaitMessages(1,1000,TimeUnit.MILLISECONDS);
+ }
+}
diff --git a/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/JettyEchoSocket.java b/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/JettyEchoSocket.java
new file mode 100644
index 0000000..b186a78
--- /dev/null
+++ b/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/JettyEchoSocket.java
@@ -0,0 +1,50 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356;
+
+import org.eclipse.jetty.util.BufferUtil;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+import org.eclipse.jetty.websocket.api.WebSocketAdapter;
+
+/**
+ * Jetty Echo Socket. using Jetty techniques.
+ */
+public class JettyEchoSocket extends WebSocketAdapter
+{
+ private static final Logger LOG = Log.getLogger(JettyEchoSocket.class);
+
+ @Override
+ public void onWebSocketBinary(byte[] payload, int offset, int len)
+ {
+ getRemote().sendBytesByFuture(BufferUtil.toBuffer(payload,offset,len));
+ }
+
+ @Override
+ public void onWebSocketError(Throwable cause)
+ {
+ LOG.warn(cause);
+ }
+
+ @Override
+ public void onWebSocketText(String message)
+ {
+ getRemote().sendStringByFuture(message);
+ }
+}
diff --git a/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/JsrSessionTest.java b/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/JsrSessionTest.java
new file mode 100644
index 0000000..426e42d
--- /dev/null
+++ b/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/JsrSessionTest.java
@@ -0,0 +1,116 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356;
+
+import static org.hamcrest.Matchers.*;
+
+import java.net.URI;
+import java.nio.ByteBuffer;
+
+import javax.websocket.ClientEndpointConfig;
+import javax.websocket.DeploymentException;
+import javax.websocket.MessageHandler;
+
+import org.eclipse.jetty.websocket.api.WebSocketPolicy;
+import org.eclipse.jetty.websocket.common.events.EventDriver;
+import org.eclipse.jetty.websocket.jsr356.client.EmptyClientEndpointConfig;
+import org.eclipse.jetty.websocket.jsr356.client.SimpleEndpointMetadata;
+import org.eclipse.jetty.websocket.jsr356.endpoints.EndpointInstance;
+import org.eclipse.jetty.websocket.jsr356.endpoints.JsrEndpointEventDriver;
+import org.eclipse.jetty.websocket.jsr356.handlers.ByteArrayWholeHandler;
+import org.eclipse.jetty.websocket.jsr356.handlers.ByteBufferPartialHandler;
+import org.eclipse.jetty.websocket.jsr356.handlers.LongMessageHandler;
+import org.eclipse.jetty.websocket.jsr356.handlers.StringWholeHandler;
+import org.eclipse.jetty.websocket.jsr356.samples.DummyConnection;
+import org.eclipse.jetty.websocket.jsr356.samples.DummyEndpoint;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+
+public class JsrSessionTest
+{
+ private ClientContainer container;
+ private JsrSession session;
+
+ @Before
+ public void initSession()
+ {
+ container = new ClientContainer();
+ String id = JsrSessionTest.class.getSimpleName();
+ URI requestURI = URI.create("ws://localhost/" + id);
+ WebSocketPolicy policy = WebSocketPolicy.newClientPolicy();
+ ClientEndpointConfig config = new EmptyClientEndpointConfig();
+ DummyEndpoint websocket = new DummyEndpoint();
+ SimpleEndpointMetadata metadata = new SimpleEndpointMetadata(websocket.getClass());
+ // Executor executor = null;
+
+ EndpointInstance ei = new EndpointInstance(websocket,config,metadata);
+
+ EventDriver driver = new JsrEndpointEventDriver(policy,ei);
+ DummyConnection connection = new DummyConnection();
+ session = new JsrSession(requestURI,driver,connection,container,id);
+ }
+
+ @Test
+ public void testMessageHandlerBinary() throws DeploymentException
+ {
+ session.addMessageHandler(new ByteBufferPartialHandler());
+ MessageHandlerWrapper wrapper = session.getMessageHandlerWrapper(MessageType.BINARY);
+ Assert.assertThat("Binary Handler",wrapper.getHandler(),instanceOf(ByteBufferPartialHandler.class));
+ Assert.assertEquals("Message Class",wrapper.getMetadata().getMessageClass(),ByteBuffer.class);
+ }
+
+ @Test
+ public void testMessageHandlerBoth() throws DeploymentException
+ {
+ session.addMessageHandler(new StringWholeHandler());
+ session.addMessageHandler(new ByteArrayWholeHandler());
+ MessageHandlerWrapper wrapper = session.getMessageHandlerWrapper(MessageType.TEXT);
+ Assert.assertThat("Text Handler",wrapper.getHandler(),instanceOf(StringWholeHandler.class));
+ Assert.assertEquals("Message Class",wrapper.getMetadata().getMessageClass(),String.class);
+ wrapper = session.getMessageHandlerWrapper(MessageType.BINARY);
+ Assert.assertThat("Binary Handler",wrapper.getHandler(),instanceOf(ByteArrayWholeHandler.class));
+ Assert.assertEquals("Message Class",wrapper.getMetadata().getMessageClass(),byte[].class);
+ }
+
+ @Test
+ public void testMessageHandlerReplaceTextHandler() throws DeploymentException
+ {
+ MessageHandler oldText = new StringWholeHandler();
+ session.addMessageHandler(oldText); // add a TEXT handler
+ session.addMessageHandler(new ByteArrayWholeHandler()); // add BINARY handler
+ session.removeMessageHandler(oldText); // remove original TEXT handler
+ session.addMessageHandler(new LongMessageHandler()); // add new TEXT handler
+ MessageHandlerWrapper wrapper = session.getMessageHandlerWrapper(MessageType.BINARY);
+ Assert.assertThat("Binary Handler",wrapper.getHandler(),instanceOf(ByteArrayWholeHandler.class));
+ Assert.assertEquals("Message Class",wrapper.getMetadata().getMessageClass(),byte[].class);
+ wrapper = session.getMessageHandlerWrapper(MessageType.TEXT);
+ Assert.assertThat("Text Handler",wrapper.getHandler(),instanceOf(LongMessageHandler.class));
+ Assert.assertEquals("Message Class",wrapper.getMetadata().getMessageClass(),Long.class);
+ }
+
+ @Test
+ public void testMessageHandlerText() throws DeploymentException
+ {
+ session.addMessageHandler(new StringWholeHandler());
+ MessageHandlerWrapper wrapper = session.getMessageHandlerWrapper(MessageType.TEXT);
+ Assert.assertThat("Text Handler",wrapper.getHandler(),instanceOf(StringWholeHandler.class));
+ Assert.assertEquals("Message Class",wrapper.getMetadata().getMessageClass(),String.class);
+ }
+}
diff --git a/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/MessageHandlerFactoryTest.java b/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/MessageHandlerFactoryTest.java
new file mode 100644
index 0000000..6f06ae8
--- /dev/null
+++ b/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/MessageHandlerFactoryTest.java
@@ -0,0 +1,76 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356;
+
+import static org.hamcrest.Matchers.*;
+
+import java.lang.reflect.Type;
+import java.util.List;
+
+import javax.websocket.DeploymentException;
+
+import org.eclipse.jetty.websocket.jsr356.decoders.PrimitiveDecoderMetadataSet;
+import org.eclipse.jetty.websocket.jsr356.handlers.ByteArrayPartialHandler;
+import org.eclipse.jetty.websocket.jsr356.handlers.StringPartialHandler;
+import org.eclipse.jetty.websocket.jsr356.metadata.DecoderMetadata;
+import org.eclipse.jetty.websocket.jsr356.metadata.DecoderMetadataSet;
+import org.eclipse.jetty.websocket.jsr356.metadata.MessageHandlerMetadata;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+
+public class MessageHandlerFactoryTest
+{
+ private MessageHandlerFactory factory;
+ private DecoderMetadataSet metadatas;
+ private DecoderFactory decoders;
+
+ @Before
+ public void init() throws DeploymentException
+ {
+ DecoderFactory primitivesFactory = new DecoderFactory(PrimitiveDecoderMetadataSet.INSTANCE);
+ metadatas = new DecoderMetadataSet();
+ decoders = new DecoderFactory(metadatas,primitivesFactory);
+ factory = new MessageHandlerFactory();
+ }
+
+ @Test
+ public void testByteArrayPartial() throws DeploymentException
+ {
+ List<MessageHandlerMetadata> metadatas = factory.getMetadata(ByteArrayPartialHandler.class);
+ Assert.assertThat("Metadata.list.size",metadatas.size(),is(1));
+
+ MessageHandlerMetadata handlerMetadata = metadatas.get(0);
+ DecoderMetadata decoderMetadata = decoders.getMetadataFor(handlerMetadata.getMessageClass());
+ Assert.assertThat("Message Type",decoderMetadata.getMessageType(),is(MessageType.BINARY));
+ Assert.assertThat("Message Class",handlerMetadata.getMessageClass(),is((Type)byte[].class));
+ }
+
+ @Test
+ public void testStringPartial() throws DeploymentException
+ {
+ List<MessageHandlerMetadata> metadatas = factory.getMetadata(StringPartialHandler.class);
+ Assert.assertThat("Metadata.list.size",metadatas.size(),is(1));
+
+ MessageHandlerMetadata handlerMetadata = metadatas.get(0);
+ DecoderMetadata decoderMetadata = decoders.getMetadataFor(handlerMetadata.getMessageClass());
+ Assert.assertThat("Message Type",decoderMetadata.getMessageType(),is(MessageType.TEXT));
+ Assert.assertThat("Message Class",handlerMetadata.getMessageClass(),is((Type)String.class));
+ }
+}
diff --git a/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/MessageQueue.java b/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/MessageQueue.java
new file mode 100644
index 0000000..b1dd649
--- /dev/null
+++ b/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/MessageQueue.java
@@ -0,0 +1,56 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356;
+
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+import org.eclipse.jetty.util.BlockingArrayQueue;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+
+public class MessageQueue extends BlockingArrayQueue<String>
+{
+ private static final Logger LOG = Log.getLogger(MessageQueue.class);
+
+ public void awaitMessages(int expectedMessageCount, int timeoutDuration, TimeUnit timeoutUnit) throws TimeoutException
+ {
+ long msDur = TimeUnit.MILLISECONDS.convert(timeoutDuration,timeoutUnit);
+ long now = System.currentTimeMillis();
+ long expireOn = now + msDur;
+ LOG.debug("Await Message.. Now: {} - expireOn: {} ({} ms)",now,expireOn,msDur);
+
+ while (this.size() < expectedMessageCount)
+ {
+ try
+ {
+ TimeUnit.MILLISECONDS.sleep(20);
+ }
+ catch (InterruptedException gnore)
+ {
+ /* ignore */
+ }
+ if (!LOG.isDebugEnabled() && (System.currentTimeMillis() > expireOn))
+ {
+ throw new TimeoutException(String.format("Timeout reading all %d expected messages. (managed to only read %d messages)",expectedMessageCount,
+ this.size()));
+ }
+ }
+ }
+}
diff --git a/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/annotations/DateTextSocket.java b/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/annotations/DateTextSocket.java
new file mode 100644
index 0000000..a07b2dd
--- /dev/null
+++ b/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/annotations/DateTextSocket.java
@@ -0,0 +1,57 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.annotations;
+
+import java.io.IOException;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+
+import javax.websocket.ClientEndpoint;
+import javax.websocket.OnMessage;
+import javax.websocket.OnOpen;
+import javax.websocket.Session;
+
+import org.eclipse.jetty.websocket.jsr356.decoders.DateDecoder;
+
+@ClientEndpoint(decoders =
+{ DateDecoder.class })
+public class DateTextSocket
+{
+ private Session session;
+
+ @OnMessage
+ public void onMessage(Date d) throws IOException
+ {
+ if (d == null)
+ {
+ session.getAsyncRemote().sendText("Error: Date is null");
+ }
+ else
+ {
+ String msg = SimpleDateFormat.getDateInstance(SimpleDateFormat.SHORT).format(d);
+ session.getAsyncRemote().sendText(msg);
+ }
+ }
+
+ @OnOpen
+ public void onOpen(Session session)
+ {
+ this.session = session;
+ }
+}
diff --git a/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/annotations/JsrParamIdDecoderTest.java b/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/annotations/JsrParamIdDecoderTest.java
new file mode 100644
index 0000000..6316918
--- /dev/null
+++ b/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/annotations/JsrParamIdDecoderTest.java
@@ -0,0 +1,57 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.annotations;
+
+import static org.hamcrest.Matchers.*;
+
+import java.lang.reflect.Method;
+import java.util.Date;
+
+import org.eclipse.jetty.websocket.jsr356.MessageType;
+import org.eclipse.jetty.websocket.jsr356.decoders.DateDecoder;
+import org.eclipse.jetty.websocket.jsr356.metadata.DecoderMetadata;
+import org.junit.Assert;
+import org.junit.Test;
+
+public class JsrParamIdDecoderTest
+{
+ private JsrCallable getOnMessageCallableFrom(Class<?> clazz, String methodName)
+ {
+ for (Method method : clazz.getMethods())
+ {
+ if (method.getName().equals(methodName))
+ {
+ return new OnMessageCallable(clazz,method);
+ }
+ }
+ return null;
+ }
+
+ @Test
+ public void testMatchDateDecoder()
+ {
+ DecoderMetadata metadata = new DecoderMetadata(DateDecoder.class,Date.class,MessageType.TEXT,false);
+ JsrParamIdDecoder paramId = new JsrParamIdDecoder(metadata);
+
+ JsrCallable callable = getOnMessageCallableFrom(DateTextSocket.class,"onMessage");
+ Param param = new Param(0,Date.class,null);
+
+ Assert.assertThat("Match for Decoder",paramId.process(param,callable),is(true));
+ }
+}
diff --git a/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/decoders/BadDualDecoder.java b/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/decoders/BadDualDecoder.java
new file mode 100644
index 0000000..6e3520a
--- /dev/null
+++ b/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/decoders/BadDualDecoder.java
@@ -0,0 +1,122 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.decoders;
+
+import java.nio.BufferUnderflowException;
+import java.nio.ByteBuffer;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import javax.websocket.DecodeException;
+import javax.websocket.Decoder;
+import javax.websocket.EndpointConfig;
+
+import org.eclipse.jetty.util.BufferUtil;
+import org.eclipse.jetty.websocket.jsr356.samples.Fruit;
+import org.eclipse.jetty.websocket.jsr356.samples.FruitBinaryEncoder;
+
+/**
+ * Intentionally bad example of attempting to decode the same object to different message formats.
+ */
+public class BadDualDecoder implements Decoder.Text<Fruit>, Decoder.Binary<Fruit>
+{
+ @Override
+ public Fruit decode(ByteBuffer bytes) throws DecodeException
+ {
+ try
+ {
+ int id = bytes.get(bytes.position());
+ if (id != FruitBinaryEncoder.FRUIT_ID_BYTE)
+ {
+ // not a binary fruit object
+ throw new DecodeException(bytes,"Not an encoded Binary Fruit object");
+ }
+
+ Fruit fruit = new Fruit();
+ fruit.name = getUTF8String(bytes);
+ fruit.color = getUTF8String(bytes);
+ return fruit;
+ }
+ catch (BufferUnderflowException e)
+ {
+ throw new DecodeException(bytes,"Unable to read Fruit from binary message",e);
+ }
+ }
+
+ @Override
+ public Fruit decode(String s) throws DecodeException
+ {
+ Pattern pat = Pattern.compile("([^|]*)|([^|]*)");
+ Matcher mat = pat.matcher(s);
+ if (!mat.find())
+ {
+ throw new DecodeException(s,"Unable to find Fruit reference encoded in text message");
+ }
+
+ Fruit fruit = new Fruit();
+ fruit.name = mat.group(1);
+ fruit.color = mat.group(2);
+
+ return fruit;
+ }
+
+ @Override
+ public void destroy()
+ {
+ }
+
+ private String getUTF8String(ByteBuffer buf)
+ {
+ int strLen = buf.getInt();
+ ByteBuffer slice = buf.slice();
+ slice.limit(slice.position() + strLen);
+ String str = BufferUtil.toUTF8String(slice);
+ buf.position(buf.position() + strLen);
+ return str;
+ }
+
+ @Override
+ public void init(EndpointConfig config)
+ {
+ }
+
+ @Override
+ public boolean willDecode(ByteBuffer bytes)
+ {
+ if (bytes == null)
+ {
+ return false;
+ }
+ int id = bytes.get(bytes.position());
+ return (id != FruitBinaryEncoder.FRUIT_ID_BYTE);
+ }
+
+ @Override
+ public boolean willDecode(String s)
+ {
+ if (s == null)
+ {
+ return false;
+ }
+
+ Pattern pat = Pattern.compile("([^|]*)|([^|]*)");
+ Matcher mat = pat.matcher(s);
+ return (mat.find());
+ }
+}
diff --git a/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/decoders/DateDecoder.java b/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/decoders/DateDecoder.java
new file mode 100644
index 0000000..8191aaf
--- /dev/null
+++ b/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/decoders/DateDecoder.java
@@ -0,0 +1,62 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.decoders;
+
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+
+import javax.websocket.DecodeException;
+import javax.websocket.Decoder;
+import javax.websocket.EndpointConfig;
+
+/**
+ * Decode Date
+ */
+public class DateDecoder implements Decoder.Text<Date>
+{
+ @Override
+ public Date decode(String s) throws DecodeException
+ {
+ try
+ {
+ return new SimpleDateFormat("yyyy.MM.dd").parse(s);
+ }
+ catch (ParseException e)
+ {
+ throw new DecodeException(s,e.getMessage(),e);
+ }
+ }
+
+ @Override
+ public void destroy()
+ {
+ }
+
+ @Override
+ public void init(EndpointConfig config)
+ {
+ }
+
+ @Override
+ public boolean willDecode(String s)
+ {
+ return true;
+ }
+}
diff --git a/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/decoders/DateTimeDecoder.java b/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/decoders/DateTimeDecoder.java
new file mode 100644
index 0000000..f901983
--- /dev/null
+++ b/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/decoders/DateTimeDecoder.java
@@ -0,0 +1,62 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.decoders;
+
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+
+import javax.websocket.DecodeException;
+import javax.websocket.Decoder;
+import javax.websocket.EndpointConfig;
+
+/**
+ * Decode Date and Time
+ */
+public class DateTimeDecoder implements Decoder.Text<Date>
+{
+ @Override
+ public Date decode(String s) throws DecodeException
+ {
+ try
+ {
+ return new SimpleDateFormat("yyyy.MM.dd G 'at' HH:mm:ss z").parse(s);
+ }
+ catch (ParseException e)
+ {
+ throw new DecodeException(s,e.getMessage(),e);
+ }
+ }
+
+ @Override
+ public void destroy()
+ {
+ }
+
+ @Override
+ public void init(EndpointConfig config)
+ {
+ }
+
+ @Override
+ public boolean willDecode(String s)
+ {
+ return true;
+ }
+}
diff --git a/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/decoders/IntegerDecoderTest.java b/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/decoders/IntegerDecoderTest.java
new file mode 100644
index 0000000..822ffe8
--- /dev/null
+++ b/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/decoders/IntegerDecoderTest.java
@@ -0,0 +1,37 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.decoders;
+
+import static org.hamcrest.Matchers.*;
+
+import javax.websocket.DecodeException;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+public class IntegerDecoderTest
+{
+ @Test
+ public void testDecode() throws DecodeException
+ {
+ IntegerDecoder decoder = new IntegerDecoder();
+ Integer val = decoder.decode("123");
+ Assert.assertThat("Decoded value",val,is(123));
+ }
+}
diff --git a/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/decoders/PrimitiveDecoderMetadataSetTest.java b/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/decoders/PrimitiveDecoderMetadataSetTest.java
new file mode 100644
index 0000000..97bc271
--- /dev/null
+++ b/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/decoders/PrimitiveDecoderMetadataSetTest.java
@@ -0,0 +1,53 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.decoders;
+
+import static org.hamcrest.Matchers.*;
+
+import javax.websocket.Decoder;
+
+import org.eclipse.jetty.websocket.jsr356.MessageType;
+import org.eclipse.jetty.websocket.jsr356.metadata.DecoderMetadata;
+import org.junit.Assert;
+import org.junit.Test;
+
+public class PrimitiveDecoderMetadataSetTest
+{
+ private void assertClassEquals(String msg, Class<?> actual, Class<?> expected)
+ {
+ Assert.assertThat(msg,actual.getName(),is(expected.getName()));
+ }
+
+ private void assertDecoderType(Class<? extends Decoder> expectedDecoder, MessageType expectedMsgType, Class<?> type)
+ {
+ PrimitiveDecoderMetadataSet primitives = new PrimitiveDecoderMetadataSet();
+ DecoderMetadata metadata = primitives.getMetadataByType(type);
+ String prefix = String.format("Metadata By Type [%s]",type.getName());
+ Assert.assertThat(prefix,metadata,notNullValue());
+
+ assertClassEquals(prefix + ".coderClass",metadata.getCoderClass(),expectedDecoder);
+ Assert.assertThat(prefix + ".messageType",metadata.getMessageType(),is(expectedMsgType));
+ }
+
+ @Test
+ public void testGetByteArray()
+ {
+ assertDecoderType(ByteArrayDecoder.class,MessageType.BINARY,byte[].class);
+ }
+}
diff --git a/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/decoders/TimeDecoder.java b/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/decoders/TimeDecoder.java
new file mode 100644
index 0000000..1f3a28e
--- /dev/null
+++ b/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/decoders/TimeDecoder.java
@@ -0,0 +1,62 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.decoders;
+
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+
+import javax.websocket.DecodeException;
+import javax.websocket.Decoder;
+import javax.websocket.EndpointConfig;
+
+/**
+ * Decode Time
+ */
+public class TimeDecoder implements Decoder.Text<Date>
+{
+ @Override
+ public Date decode(String s) throws DecodeException
+ {
+ try
+ {
+ return new SimpleDateFormat("HH:mm:ss z").parse(s);
+ }
+ catch (ParseException e)
+ {
+ throw new DecodeException(s,e.getMessage(),e);
+ }
+ }
+
+ @Override
+ public void destroy()
+ {
+ }
+
+ @Override
+ public void init(EndpointConfig config)
+ {
+ }
+
+ @Override
+ public boolean willDecode(String s)
+ {
+ return true;
+ }
+}
diff --git a/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/decoders/ValidDualDecoder.java b/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/decoders/ValidDualDecoder.java
new file mode 100644
index 0000000..1454d57
--- /dev/null
+++ b/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/decoders/ValidDualDecoder.java
@@ -0,0 +1,65 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.decoders;
+
+import java.nio.ByteBuffer;
+
+import javax.websocket.DecodeException;
+import javax.websocket.Decoder;
+import javax.websocket.EndpointConfig;
+
+/**
+ * Example of a valid decoder impl declaring 2 decoders.
+ */
+public class ValidDualDecoder implements Decoder.Text<Integer>, Decoder.Binary<Long>
+{
+ @Override
+ public Long decode(ByteBuffer bytes) throws DecodeException
+ {
+ return bytes.getLong();
+ }
+
+ @Override
+ public Integer decode(String s) throws DecodeException
+ {
+ return Integer.parseInt(s);
+ }
+
+ @Override
+ public void destroy()
+ {
+ }
+
+ @Override
+ public void init(EndpointConfig config)
+ {
+ }
+
+ @Override
+ public boolean willDecode(ByteBuffer bytes)
+ {
+ return true;
+ }
+
+ @Override
+ public boolean willDecode(String s)
+ {
+ return true;
+ }
+}
diff --git a/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/encoders/BadDualEncoder.java b/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/encoders/BadDualEncoder.java
new file mode 100644
index 0000000..81149ab
--- /dev/null
+++ b/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/encoders/BadDualEncoder.java
@@ -0,0 +1,54 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.encoders;
+
+import java.io.IOException;
+import java.io.Writer;
+
+import javax.websocket.EncodeException;
+import javax.websocket.Encoder;
+import javax.websocket.EndpointConfig;
+
+/**
+ * Intentionally bad example of attempting to encode the same object for different message types.
+ */
+public class BadDualEncoder implements Encoder.Text<Integer>, Encoder.TextStream<Integer>
+{
+ @Override
+ public void destroy()
+ {
+ }
+
+ @Override
+ public String encode(Integer object) throws EncodeException
+ {
+ return Integer.toString(object);
+ }
+
+ @Override
+ public void encode(Integer object, Writer writer) throws EncodeException, IOException
+ {
+ writer.write(Integer.toString(object));
+ }
+
+ @Override
+ public void init(EndpointConfig config)
+ {
+ }
+}
diff --git a/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/encoders/DateEncoder.java b/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/encoders/DateEncoder.java
new file mode 100644
index 0000000..cc7b664
--- /dev/null
+++ b/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/encoders/DateEncoder.java
@@ -0,0 +1,48 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.encoders;
+
+import java.text.SimpleDateFormat;
+import java.util.Date;
+
+import javax.websocket.EncodeException;
+import javax.websocket.Encoder;
+import javax.websocket.EndpointConfig;
+
+/**
+ * Encode Date
+ */
+public class DateEncoder implements Encoder.Text<Date>
+{
+ @Override
+ public void destroy()
+ {
+ }
+
+ @Override
+ public String encode(Date object) throws EncodeException
+ {
+ return new SimpleDateFormat("yyyy.MM.dd").format(object);
+ }
+
+ @Override
+ public void init(EndpointConfig config)
+ {
+ }
+}
diff --git a/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/encoders/DateTimeEncoder.java b/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/encoders/DateTimeEncoder.java
new file mode 100644
index 0000000..4a684a8
--- /dev/null
+++ b/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/encoders/DateTimeEncoder.java
@@ -0,0 +1,48 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.encoders;
+
+import java.text.SimpleDateFormat;
+import java.util.Date;
+
+import javax.websocket.EncodeException;
+import javax.websocket.Encoder;
+import javax.websocket.EndpointConfig;
+
+/**
+ * Encode Date
+ */
+public class DateTimeEncoder implements Encoder.Text<Date>
+{
+ @Override
+ public void destroy()
+ {
+ }
+
+ @Override
+ public String encode(Date object) throws EncodeException
+ {
+ return new SimpleDateFormat("yyyy.MM.dd G 'at' HH:mm:ss z").format(object);
+ }
+
+ @Override
+ public void init(EndpointConfig config)
+ {
+ }
+}
diff --git a/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/encoders/DualEncoder.java b/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/encoders/DualEncoder.java
new file mode 100644
index 0000000..7475020
--- /dev/null
+++ b/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/encoders/DualEncoder.java
@@ -0,0 +1,58 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.encoders;
+
+import java.io.IOException;
+import java.io.Writer;
+
+import javax.websocket.EncodeException;
+import javax.websocket.Encoder;
+import javax.websocket.EndpointConfig;
+
+import org.eclipse.jetty.websocket.jsr356.samples.Fruit;
+
+/**
+ * Intentionally bad example of attempting to decode the same object to different message formats.
+ */
+public class DualEncoder implements Encoder.Text<Fruit>, Encoder.TextStream<Fruit>
+{
+ @Override
+ public void destroy()
+ {
+ }
+
+ @Override
+ public String encode(Fruit fruit) throws EncodeException
+ {
+ return String.format("%s|%s",fruit.name,fruit.color);
+ }
+
+ @Override
+ public void encode(Fruit fruit, Writer writer) throws EncodeException, IOException
+ {
+ writer.write(fruit.name);
+ writer.write('|');
+ writer.write(fruit.color);
+ }
+
+ @Override
+ public void init(EndpointConfig config)
+ {
+ }
+}
diff --git a/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/encoders/TimeEncoder.java b/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/encoders/TimeEncoder.java
new file mode 100644
index 0000000..fc316da
--- /dev/null
+++ b/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/encoders/TimeEncoder.java
@@ -0,0 +1,48 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.encoders;
+
+import java.text.SimpleDateFormat;
+import java.util.Date;
+
+import javax.websocket.EncodeException;
+import javax.websocket.Encoder;
+import javax.websocket.EndpointConfig;
+
+/**
+ * Encode Time
+ */
+public class TimeEncoder implements Encoder.Text<Date>
+{
+ @Override
+ public void destroy()
+ {
+ }
+
+ @Override
+ public String encode(Date object) throws EncodeException
+ {
+ return new SimpleDateFormat("HH:mm:ss z").format(object);
+ }
+
+ @Override
+ public void init(EndpointConfig config)
+ {
+ }
+}
diff --git a/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/encoders/ValidDualEncoder.java b/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/encoders/ValidDualEncoder.java
new file mode 100644
index 0000000..7df3678
--- /dev/null
+++ b/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/encoders/ValidDualEncoder.java
@@ -0,0 +1,64 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.encoders;
+
+import java.io.IOException;
+import java.io.OutputStream;
+
+import javax.websocket.EncodeException;
+import javax.websocket.Encoder;
+import javax.websocket.EndpointConfig;
+
+/**
+ * Example of a valid encoder impl declaring 2 encoders.
+ */
+public class ValidDualEncoder implements Encoder.Text<Integer>, Encoder.BinaryStream<Long>
+{
+ @Override
+ public void destroy()
+ {
+ }
+
+ @Override
+ public String encode(Integer object) throws EncodeException
+ {
+ return Integer.toString(object);
+ }
+
+ @Override
+ public void encode(Long object, OutputStream os) throws EncodeException, IOException
+ {
+ byte b[] = new byte[8];
+ long v = object;
+ b[0] = (byte)(v >>> 56);
+ b[1] = (byte)(v >>> 48);
+ b[2] = (byte)(v >>> 40);
+ b[3] = (byte)(v >>> 32);
+ b[4] = (byte)(v >>> 24);
+ b[5] = (byte)(v >>> 16);
+ b[6] = (byte)(v >>> 8);
+ b[7] = (byte)(v >>> 0);
+ os.write(b,0,8);
+ }
+
+ @Override
+ public void init(EndpointConfig config)
+ {
+ }
+}
diff --git a/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/endpoints/ClientAnnotatedEndpointScanner_GoodSignaturesTest.java b/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/endpoints/ClientAnnotatedEndpointScanner_GoodSignaturesTest.java
new file mode 100644
index 0000000..47bfe04
--- /dev/null
+++ b/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/endpoints/ClientAnnotatedEndpointScanner_GoodSignaturesTest.java
@@ -0,0 +1,162 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.endpoints;
+
+import static org.hamcrest.Matchers.*;
+
+import java.lang.reflect.Field;
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+import javax.websocket.ClientEndpoint;
+import javax.websocket.ClientEndpointConfig;
+import javax.websocket.CloseReason;
+import javax.websocket.PongMessage;
+import javax.websocket.Session;
+
+import org.eclipse.jetty.websocket.jsr356.ClientContainer;
+import org.eclipse.jetty.websocket.jsr356.annotations.AnnotatedEndpointMetadata;
+import org.eclipse.jetty.websocket.jsr356.annotations.AnnotatedEndpointScanner;
+import org.eclipse.jetty.websocket.jsr356.annotations.JsrCallable;
+import org.eclipse.jetty.websocket.jsr356.client.AnnotatedClientEndpointMetadata;
+import org.eclipse.jetty.websocket.jsr356.endpoints.samples.BasicBinaryMessageByteBufferSocket;
+import org.eclipse.jetty.websocket.jsr356.endpoints.samples.BasicErrorSessionSocket;
+import org.eclipse.jetty.websocket.jsr356.endpoints.samples.BasicErrorSessionThrowableSocket;
+import org.eclipse.jetty.websocket.jsr356.endpoints.samples.BasicErrorSocket;
+import org.eclipse.jetty.websocket.jsr356.endpoints.samples.BasicErrorThrowableSessionSocket;
+import org.eclipse.jetty.websocket.jsr356.endpoints.samples.BasicErrorThrowableSocket;
+import org.eclipse.jetty.websocket.jsr356.endpoints.samples.BasicOpenSessionSocket;
+import org.eclipse.jetty.websocket.jsr356.endpoints.samples.BasicOpenSocket;
+import org.eclipse.jetty.websocket.jsr356.endpoints.samples.BasicPongMessageSocket;
+import org.eclipse.jetty.websocket.jsr356.endpoints.samples.BasicTextMessageStringSocket;
+import org.eclipse.jetty.websocket.jsr356.endpoints.samples.close.CloseReasonSessionSocket;
+import org.eclipse.jetty.websocket.jsr356.endpoints.samples.close.CloseReasonSocket;
+import org.eclipse.jetty.websocket.jsr356.endpoints.samples.close.CloseSessionReasonSocket;
+import org.eclipse.jetty.websocket.jsr356.endpoints.samples.close.CloseSocket;
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+/**
+ * Test {@link JsrAnnotatedClientScanner} against various valid, simple, 1 method annotated classes with valid signatures.
+ */
+@RunWith(Parameterized.class)
+public class ClientAnnotatedEndpointScanner_GoodSignaturesTest
+{
+ public static class Case
+ {
+ public static void add(List<Case[]> data, Class<?> pojo, Field metadataField, Class<?>... expectedParams)
+ {
+ data.add(new Case[]
+ { new Case(pojo,metadataField,expectedParams) });
+ }
+
+ // The websocket pojo to test against
+ Class<?> pojo;
+ // The JsrAnnotatedMetadata field that should be populated
+ Field metadataField;
+ // The expected parameters for the Callable found by the scanner
+ Class<?> expectedParameters[];
+
+ public Case(Class<?> pojo, Field metadataField, Class<?>... expectedParams)
+ {
+ this.pojo = pojo;
+ this.metadataField = metadataField;
+ this.expectedParameters = expectedParams;
+ }
+ }
+
+ private static ClientContainer container = new ClientContainer();
+
+ @Parameters
+ public static Collection<Case[]> data() throws Exception
+ {
+ List<Case[]> data = new ArrayList<>();
+ Field fOpen = findFieldRef(AnnotatedEndpointMetadata.class,"onOpen");
+ Field fClose = findFieldRef(AnnotatedEndpointMetadata.class,"onClose");
+ Field fError = findFieldRef(AnnotatedEndpointMetadata.class,"onError");
+ Field fText = findFieldRef(AnnotatedEndpointMetadata.class,"onText");
+ Field fBinary = findFieldRef(AnnotatedEndpointMetadata.class,"onBinary");
+ Field fPong = findFieldRef(AnnotatedEndpointMetadata.class,"onPong");
+
+ // @formatter:off
+ // -- Open Events
+ Case.add(data, BasicOpenSocket.class, fOpen);
+ Case.add(data, BasicOpenSessionSocket.class, fOpen, Session.class);
+ // -- Close Events
+ Case.add(data, CloseSocket.class, fClose);
+ Case.add(data, CloseReasonSocket.class, fClose, CloseReason.class);
+ Case.add(data, CloseReasonSessionSocket.class, fClose, CloseReason.class, Session.class);
+ Case.add(data, CloseSessionReasonSocket.class, fClose, Session.class, CloseReason.class);
+ // -- Error Events
+ Case.add(data, BasicErrorSocket.class, fError);
+ Case.add(data, BasicErrorSessionSocket.class, fError, Session.class);
+ Case.add(data, BasicErrorSessionThrowableSocket.class, fError, Session.class, Throwable.class);
+ Case.add(data, BasicErrorThrowableSocket.class, fError, Throwable.class);
+ Case.add(data, BasicErrorThrowableSessionSocket.class, fError, Throwable.class, Session.class);
+ // -- Text Events
+ Case.add(data, BasicTextMessageStringSocket.class, fText, String.class);
+ // -- Binary Events
+ Case.add(data, BasicBinaryMessageByteBufferSocket.class, fBinary, ByteBuffer.class);
+ // -- Pong Events
+ Case.add(data, BasicPongMessageSocket.class, fPong, PongMessage.class);
+ // @formatter:on
+
+ // TODO: validate return types
+
+ return data;
+ }
+
+ private static Field findFieldRef(Class<?> clazz, String fldName) throws Exception
+ {
+ return clazz.getField(fldName);
+ }
+
+ private Case testcase;
+
+ public ClientAnnotatedEndpointScanner_GoodSignaturesTest(Case testcase)
+ {
+ this.testcase = testcase;
+ }
+
+ @Test
+ public void testScan_Basic() throws Exception
+ {
+ AnnotatedClientEndpointMetadata metadata = new AnnotatedClientEndpointMetadata(container,testcase.pojo);
+ AnnotatedEndpointScanner<ClientEndpoint, ClientEndpointConfig> scanner = new AnnotatedEndpointScanner<>(metadata);
+ scanner.scan();
+
+ Assert.assertThat("Metadata",metadata,notNullValue());
+
+ JsrCallable cm = (JsrCallable)testcase.metadataField.get(metadata);
+ Assert.assertThat(testcase.metadataField.toString(),cm,notNullValue());
+ int len = testcase.expectedParameters.length;
+ for (int i = 0; i < len; i++)
+ {
+ Class<?> expectedParam = testcase.expectedParameters[i];
+ Class<?> actualParam = cm.getParamTypes()[i];
+
+ Assert.assertTrue("Parameter[" + i + "] - expected:[" + expectedParam + "], actual:[" + actualParam + "]",actualParam.equals(expectedParam));
+ }
+ }
+}
diff --git a/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/endpoints/ClientAnnotatedEndpointScanner_InvalidSignaturesTest.java b/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/endpoints/ClientAnnotatedEndpointScanner_InvalidSignaturesTest.java
new file mode 100644
index 0000000..d467e84
--- /dev/null
+++ b/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/endpoints/ClientAnnotatedEndpointScanner_InvalidSignaturesTest.java
@@ -0,0 +1,113 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.endpoints;
+
+import static org.hamcrest.Matchers.*;
+
+import java.lang.annotation.Annotation;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+import javax.websocket.ClientEndpoint;
+import javax.websocket.ClientEndpointConfig;
+import javax.websocket.DeploymentException;
+import javax.websocket.OnClose;
+import javax.websocket.OnError;
+import javax.websocket.OnOpen;
+
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+import org.eclipse.jetty.websocket.common.events.annotated.InvalidSignatureException;
+import org.eclipse.jetty.websocket.jsr356.ClientContainer;
+import org.eclipse.jetty.websocket.jsr356.annotations.AnnotatedEndpointScanner;
+import org.eclipse.jetty.websocket.jsr356.client.AnnotatedClientEndpointMetadata;
+import org.eclipse.jetty.websocket.jsr356.endpoints.samples.InvalidCloseIntSocket;
+import org.eclipse.jetty.websocket.jsr356.endpoints.samples.InvalidErrorErrorSocket;
+import org.eclipse.jetty.websocket.jsr356.endpoints.samples.InvalidErrorExceptionSocket;
+import org.eclipse.jetty.websocket.jsr356.endpoints.samples.InvalidErrorIntSocket;
+import org.eclipse.jetty.websocket.jsr356.endpoints.samples.InvalidOpenCloseReasonSocket;
+import org.eclipse.jetty.websocket.jsr356.endpoints.samples.InvalidOpenIntSocket;
+import org.eclipse.jetty.websocket.jsr356.endpoints.samples.InvalidOpenSessionIntSocket;
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+/**
+ * Test {@link JsrAnnotatedClientScanner} against various simple, single method annotated classes with invalid signatures.
+ */
+@RunWith(Parameterized.class)
+public class ClientAnnotatedEndpointScanner_InvalidSignaturesTest
+{
+ private static final Logger LOG = Log.getLogger(ClientAnnotatedEndpointScanner_InvalidSignaturesTest.class);
+ private static ClientContainer container = new ClientContainer();
+
+ @Parameters
+ public static Collection<Class<?>[]> data()
+ {
+ List<Class<?>[]> data = new ArrayList<>();
+
+ // @formatter:off
+ data.add(new Class<?>[]{ InvalidCloseIntSocket.class, OnClose.class });
+ data.add(new Class<?>[]{ InvalidErrorErrorSocket.class, OnError.class });
+ data.add(new Class<?>[]{ InvalidErrorExceptionSocket.class, OnError.class });
+ data.add(new Class<?>[]{ InvalidErrorIntSocket.class, OnError.class });
+ data.add(new Class<?>[]{ InvalidOpenCloseReasonSocket.class, OnOpen.class });
+ data.add(new Class<?>[]{ InvalidOpenIntSocket.class, OnOpen.class });
+ data.add(new Class<?>[]{ InvalidOpenSessionIntSocket.class, OnOpen.class });
+ // @formatter:on
+
+ // TODO: invalid return types
+ // TODO: static methods
+ // TODO: private or protected methods
+ // TODO: abstract methods
+
+ return data;
+ }
+
+ // The pojo to test
+ private Class<?> pojo;
+ // The annotation class expected to be mentioned in the error message
+ private Class<? extends Annotation> expectedAnnoClass;
+
+ public ClientAnnotatedEndpointScanner_InvalidSignaturesTest(Class<?> pojo, Class<? extends Annotation> expectedAnnotation)
+ {
+ this.pojo = pojo;
+ this.expectedAnnoClass = expectedAnnotation;
+ }
+
+ @Test
+ public void testScan_InvalidSignature() throws DeploymentException
+ {
+ AnnotatedClientEndpointMetadata metadata = new AnnotatedClientEndpointMetadata(container,pojo);
+ AnnotatedEndpointScanner<ClientEndpoint, ClientEndpointConfig> scanner = new AnnotatedEndpointScanner<>(metadata);
+ try
+ {
+ scanner.scan();
+ Assert.fail("Expected " + InvalidSignatureException.class + " with message that references " + expectedAnnoClass + " annotation");
+ }
+ catch (InvalidSignatureException e)
+ {
+ LOG.debug("{}:{}",e.getClass(),e.getMessage());
+ Assert.assertThat("Message",e.getMessage(),containsString(expectedAnnoClass.getSimpleName()));
+ }
+ }
+}
diff --git a/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/endpoints/OnCloseTest.java b/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/endpoints/OnCloseTest.java
new file mode 100644
index 0000000..8d5958c
--- /dev/null
+++ b/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/endpoints/OnCloseTest.java
@@ -0,0 +1,126 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.endpoints;
+
+import static org.hamcrest.Matchers.*;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+import javax.websocket.ClientEndpoint;
+import javax.websocket.ClientEndpointConfig;
+
+import org.eclipse.jetty.toolchain.test.EventQueue;
+import org.eclipse.jetty.websocket.api.StatusCode;
+import org.eclipse.jetty.websocket.api.WebSocketPolicy;
+import org.eclipse.jetty.websocket.common.CloseInfo;
+import org.eclipse.jetty.websocket.common.events.EventDriver;
+import org.eclipse.jetty.websocket.jsr356.ClientContainer;
+import org.eclipse.jetty.websocket.jsr356.annotations.AnnotatedEndpointScanner;
+import org.eclipse.jetty.websocket.jsr356.annotations.JsrEvents;
+import org.eclipse.jetty.websocket.jsr356.client.AnnotatedClientEndpointMetadata;
+import org.eclipse.jetty.websocket.jsr356.endpoints.samples.close.CloseEndpointConfigSocket;
+import org.eclipse.jetty.websocket.jsr356.endpoints.samples.close.CloseReasonSessionSocket;
+import org.eclipse.jetty.websocket.jsr356.endpoints.samples.close.CloseReasonSocket;
+import org.eclipse.jetty.websocket.jsr356.endpoints.samples.close.CloseSessionReasonSocket;
+import org.eclipse.jetty.websocket.jsr356.endpoints.samples.close.CloseSessionSocket;
+import org.eclipse.jetty.websocket.jsr356.endpoints.samples.close.CloseSocket;
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class OnCloseTest
+{
+ private static class Case
+ {
+ public static Case add(List<Case[]> data, Class<?> closeClass)
+ {
+ Case tcase = new Case();
+ tcase.closeClass = closeClass;
+ data.add(new Case[]
+ { tcase });
+ return tcase;
+ }
+
+ Class<?> closeClass;
+ String expectedCloseEvent;
+
+ public Case expect(String expectedEvent)
+ {
+ this.expectedCloseEvent = expectedEvent;
+ return this;
+ }
+ }
+
+ private static ClientContainer container = new ClientContainer();
+
+ @Parameters
+ public static Collection<Case[]> data() throws Exception
+ {
+ List<Case[]> data = new ArrayList<>();
+
+ Case.add(data,CloseSocket.class).expect("onClose()");
+ Case.add(data,CloseReasonSocket.class).expect("onClose(CloseReason)");
+ Case.add(data,CloseSessionSocket.class).expect("onClose(Session)");
+ Case.add(data,CloseReasonSessionSocket.class).expect("onClose(CloseReason,Session)");
+ Case.add(data,CloseSessionReasonSocket.class).expect("onClose(Session,CloseReason)");
+ Case.add(data,CloseEndpointConfigSocket.class).expect("onClose(EndpointConfig)");
+
+ return data;
+ }
+
+ private final Case testcase;
+
+ public OnCloseTest(Case testcase)
+ {
+ this.testcase = testcase;
+ System.err.printf("Testing @OnClose for %s%n",testcase.closeClass.getName());
+ }
+
+ @Test
+ public void testOnCloseCall() throws Exception
+ {
+ // Scan annotations
+ AnnotatedClientEndpointMetadata metadata = new AnnotatedClientEndpointMetadata(container,testcase.closeClass);
+ AnnotatedEndpointScanner<ClientEndpoint, ClientEndpointConfig> scanner = new AnnotatedEndpointScanner<>(metadata);
+ scanner.scan();
+
+ // Build up EventDriver
+ WebSocketPolicy policy = WebSocketPolicy.newClientPolicy();
+ ClientEndpointConfig config = metadata.getConfig();
+ TrackingSocket endpoint = (TrackingSocket)testcase.closeClass.newInstance();
+ EndpointInstance ei = new EndpointInstance(endpoint,config,metadata);
+ JsrEvents<ClientEndpoint, ClientEndpointConfig> jsrevents = new JsrEvents<>(metadata);
+
+ EventDriver driver = new JsrAnnotatedEventDriver(policy,ei,jsrevents);
+
+ // Execute onClose call
+ driver.onClose(new CloseInfo(StatusCode.NORMAL,"normal"));
+
+ // Test captured event
+ EventQueue<String> events = endpoint.eventQueue;
+ Assert.assertThat("Number of Events Captured",events.size(),is(1));
+ String closeEvent = events.poll();
+ Assert.assertThat("Close Event",closeEvent,is(testcase.expectedCloseEvent));
+ }
+}
diff --git a/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/endpoints/TrackingSocket.java b/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/endpoints/TrackingSocket.java
new file mode 100644
index 0000000..953ab3e
--- /dev/null
+++ b/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/endpoints/TrackingSocket.java
@@ -0,0 +1,125 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.endpoints;
+
+import static org.hamcrest.Matchers.*;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+import javax.websocket.CloseReason;
+import javax.websocket.CloseReason.CloseCode;
+
+import org.eclipse.jetty.toolchain.test.EventQueue;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+import org.junit.Assert;
+
+/**
+ * Abstract base socket used for tracking state and events within the socket for testing reasons.
+ */
+public abstract class TrackingSocket
+{
+ private static final Logger LOG = Log.getLogger(TrackingSocket.class);
+
+ public CloseReason closeReason;
+ public EventQueue<String> eventQueue = new EventQueue<String>();
+ public EventQueue<Throwable> errorQueue = new EventQueue<>();
+ public CountDownLatch openLatch = new CountDownLatch(1);
+ public CountDownLatch closeLatch = new CountDownLatch(1);
+ public CountDownLatch dataLatch = new CountDownLatch(1);
+
+ protected void addError(Throwable t)
+ {
+ LOG.warn(t);
+ errorQueue.add(t);
+ }
+
+ protected void addEvent(String format, Object... args)
+ {
+ eventQueue.add(String.format(format,args));
+ }
+
+ public void assertClose(CloseCode expectedCode, String expectedReason) throws InterruptedException
+ {
+ assertCloseCode(expectedCode);
+ assertCloseReason(expectedReason);
+ }
+
+ public void assertCloseCode(CloseCode expectedCode) throws InterruptedException
+ {
+ Assert.assertThat("Was Closed",closeLatch.await(50,TimeUnit.MILLISECONDS),is(true));
+ Assert.assertThat("CloseReason",closeReason,notNullValue());
+ Assert.assertThat("Close Code",closeReason.getCloseCode(),is(expectedCode));
+ }
+
+ private void assertCloseReason(String expectedReason)
+ {
+ Assert.assertThat("Close Reason",closeReason.getReasonPhrase(),is(expectedReason));
+ }
+
+ public void assertEvent(String expected)
+ {
+ String actual = eventQueue.poll();
+ Assert.assertEquals("Event",expected,actual);
+ }
+
+ public void assertIsOpen() throws InterruptedException
+ {
+ assertWasOpened();
+ assertNotClosed();
+ }
+
+ public void assertNotClosed()
+ {
+ Assert.assertThat("Closed Latch",closeLatch.getCount(),greaterThanOrEqualTo(1L));
+ }
+
+ public void assertNotOpened()
+ {
+ Assert.assertThat("Open Latch",openLatch.getCount(),greaterThanOrEqualTo(1L));
+ }
+
+ public void assertWasOpened() throws InterruptedException
+ {
+ Assert.assertThat("Was Opened",openLatch.await(500,TimeUnit.MILLISECONDS),is(true));
+ }
+
+ public void clear()
+ {
+ eventQueue.clear();
+ errorQueue.clear();
+ }
+
+ public void waitForClose(int timeoutDuration, TimeUnit timeoutUnit) throws InterruptedException
+ {
+ Assert.assertThat("Client Socket Closed",closeLatch.await(timeoutDuration,timeoutUnit),is(true));
+ }
+
+ public void waitForConnected(int timeoutDuration, TimeUnit timeoutUnit) throws InterruptedException
+ {
+ Assert.assertThat("Client Socket Connected",openLatch.await(timeoutDuration,timeoutUnit),is(true));
+ }
+
+ public void waitForData(int timeoutDuration, TimeUnit timeoutUnit) throws InterruptedException
+ {
+ LOG.debug("Waiting for message");
+ Assert.assertThat("Data Received",dataLatch.await(timeoutDuration,timeoutUnit),is(true));
+ }
+}
diff --git a/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/endpoints/samples/BasicBinaryMessageByteBufferSocket.java b/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/endpoints/samples/BasicBinaryMessageByteBufferSocket.java
new file mode 100644
index 0000000..8516b49
--- /dev/null
+++ b/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/endpoints/samples/BasicBinaryMessageByteBufferSocket.java
@@ -0,0 +1,37 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.endpoints.samples;
+
+import java.nio.ByteBuffer;
+
+import javax.websocket.ClientEndpoint;
+import javax.websocket.OnMessage;
+
+import org.eclipse.jetty.websocket.jsr356.endpoints.TrackingSocket;
+
+@ClientEndpoint
+public class BasicBinaryMessageByteBufferSocket extends TrackingSocket
+{
+ @OnMessage
+ public void onBinary(ByteBuffer data)
+ {
+ addEvent("onBinary(%s)",data);
+ dataLatch.countDown();
+ }
+}
diff --git a/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/endpoints/samples/BasicErrorSessionSocket.java b/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/endpoints/samples/BasicErrorSessionSocket.java
new file mode 100644
index 0000000..5629359
--- /dev/null
+++ b/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/endpoints/samples/BasicErrorSessionSocket.java
@@ -0,0 +1,35 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.endpoints.samples;
+
+import javax.websocket.ClientEndpoint;
+import javax.websocket.OnError;
+import javax.websocket.Session;
+
+import org.eclipse.jetty.websocket.jsr356.endpoints.TrackingSocket;
+
+@ClientEndpoint
+public class BasicErrorSessionSocket extends TrackingSocket
+{
+ @OnError
+ public void onError(Session session)
+ {
+ addEvent("onError(%s)",session);
+ }
+}
diff --git a/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/endpoints/samples/BasicErrorSessionThrowableSocket.java b/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/endpoints/samples/BasicErrorSessionThrowableSocket.java
new file mode 100644
index 0000000..79fa681
--- /dev/null
+++ b/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/endpoints/samples/BasicErrorSessionThrowableSocket.java
@@ -0,0 +1,36 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.endpoints.samples;
+
+import javax.websocket.ClientEndpoint;
+import javax.websocket.OnError;
+import javax.websocket.Session;
+
+import org.eclipse.jetty.websocket.jsr356.endpoints.TrackingSocket;
+
+@ClientEndpoint
+public class BasicErrorSessionThrowableSocket extends TrackingSocket
+{
+ @OnError
+ public void onError(Session session, Throwable t)
+ {
+ addEvent("onError(%s,%s)",session,t);
+ addError(t);
+ }
+}
diff --git a/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/endpoints/samples/BasicErrorSocket.java b/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/endpoints/samples/BasicErrorSocket.java
new file mode 100644
index 0000000..7aade74
--- /dev/null
+++ b/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/endpoints/samples/BasicErrorSocket.java
@@ -0,0 +1,34 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.endpoints.samples;
+
+import javax.websocket.ClientEndpoint;
+import javax.websocket.OnError;
+
+import org.eclipse.jetty.websocket.jsr356.endpoints.TrackingSocket;
+
+@ClientEndpoint
+public class BasicErrorSocket extends TrackingSocket
+{
+ @OnError
+ public void onError()
+ {
+ addEvent("onError()");
+ }
+}
diff --git a/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/endpoints/samples/BasicErrorThrowableSessionSocket.java b/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/endpoints/samples/BasicErrorThrowableSessionSocket.java
new file mode 100644
index 0000000..4381df5
--- /dev/null
+++ b/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/endpoints/samples/BasicErrorThrowableSessionSocket.java
@@ -0,0 +1,36 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.endpoints.samples;
+
+import javax.websocket.ClientEndpoint;
+import javax.websocket.OnError;
+import javax.websocket.Session;
+
+import org.eclipse.jetty.websocket.jsr356.endpoints.TrackingSocket;
+
+@ClientEndpoint
+public class BasicErrorThrowableSessionSocket extends TrackingSocket
+{
+ @OnError
+ public void onError(Throwable t, Session session)
+ {
+ addEvent("onError(%s,%s)",t,session);
+ addError(t);
+ }
+}
diff --git a/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/endpoints/samples/BasicErrorThrowableSocket.java b/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/endpoints/samples/BasicErrorThrowableSocket.java
new file mode 100644
index 0000000..aa71e20
--- /dev/null
+++ b/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/endpoints/samples/BasicErrorThrowableSocket.java
@@ -0,0 +1,35 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.endpoints.samples;
+
+import javax.websocket.ClientEndpoint;
+import javax.websocket.OnError;
+
+import org.eclipse.jetty.websocket.jsr356.endpoints.TrackingSocket;
+
+@ClientEndpoint
+public class BasicErrorThrowableSocket extends TrackingSocket
+{
+ @OnError
+ public void onError(Throwable t)
+ {
+ addEvent("onError(%s)",t);
+ addError(t);
+ }
+}
diff --git a/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/endpoints/samples/BasicOpenCloseSessionSocket.java b/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/endpoints/samples/BasicOpenCloseSessionSocket.java
new file mode 100644
index 0000000..63fc766
--- /dev/null
+++ b/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/endpoints/samples/BasicOpenCloseSessionSocket.java
@@ -0,0 +1,46 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.endpoints.samples;
+
+import javax.websocket.ClientEndpoint;
+import javax.websocket.CloseReason;
+import javax.websocket.OnClose;
+import javax.websocket.OnOpen;
+import javax.websocket.Session;
+
+import org.eclipse.jetty.websocket.jsr356.endpoints.TrackingSocket;
+
+@ClientEndpoint
+public class BasicOpenCloseSessionSocket extends TrackingSocket
+{
+ @OnClose
+ public void onClose(CloseReason close, Session session)
+ {
+ addEvent("onClose(%s, %s)",close,session);
+ this.closeReason = close;
+ closeLatch.countDown();
+ }
+
+ @OnOpen
+ public void onOpen(Session session)
+ {
+ addEvent("onOpen(%s)",session);
+ openLatch.countDown();
+ }
+}
diff --git a/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/endpoints/samples/BasicOpenCloseSocket.java b/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/endpoints/samples/BasicOpenCloseSocket.java
new file mode 100644
index 0000000..ffb8208
--- /dev/null
+++ b/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/endpoints/samples/BasicOpenCloseSocket.java
@@ -0,0 +1,41 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.endpoints.samples;
+
+import javax.websocket.ClientEndpoint;
+import javax.websocket.CloseReason;
+import javax.websocket.OnClose;
+import javax.websocket.OnOpen;
+
+import org.eclipse.jetty.websocket.jsr356.endpoints.TrackingSocket;
+
+@ClientEndpoint
+public class BasicOpenCloseSocket extends TrackingSocket
+{
+ @OnOpen
+ public void onOpen() {
+ openLatch.countDown();
+ }
+
+ @OnClose
+ public void onClose(CloseReason close) {
+ this.closeReason = close;
+ closeLatch.countDown();
+ }
+}
diff --git a/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/endpoints/samples/BasicOpenSessionSocket.java b/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/endpoints/samples/BasicOpenSessionSocket.java
new file mode 100644
index 0000000..fb87077
--- /dev/null
+++ b/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/endpoints/samples/BasicOpenSessionSocket.java
@@ -0,0 +1,35 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.endpoints.samples;
+
+import javax.websocket.ClientEndpoint;
+import javax.websocket.OnOpen;
+import javax.websocket.Session;
+
+import org.eclipse.jetty.websocket.jsr356.endpoints.TrackingSocket;
+
+@ClientEndpoint
+public class BasicOpenSessionSocket extends TrackingSocket
+{
+ @OnOpen
+ public void onOpen(Session session)
+ {
+ openLatch.countDown();
+ }
+}
diff --git a/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/endpoints/samples/BasicOpenSocket.java b/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/endpoints/samples/BasicOpenSocket.java
new file mode 100644
index 0000000..6db95f8
--- /dev/null
+++ b/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/endpoints/samples/BasicOpenSocket.java
@@ -0,0 +1,34 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.endpoints.samples;
+
+import javax.websocket.ClientEndpoint;
+import javax.websocket.OnOpen;
+
+import org.eclipse.jetty.websocket.jsr356.endpoints.TrackingSocket;
+
+@ClientEndpoint
+public class BasicOpenSocket extends TrackingSocket
+{
+ @OnOpen
+ public void onOpen()
+ {
+ openLatch.countDown();
+ }
+}
diff --git a/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/endpoints/samples/BasicPongMessageSocket.java b/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/endpoints/samples/BasicPongMessageSocket.java
new file mode 100644
index 0000000..07dd394
--- /dev/null
+++ b/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/endpoints/samples/BasicPongMessageSocket.java
@@ -0,0 +1,36 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.endpoints.samples;
+
+import javax.websocket.ClientEndpoint;
+import javax.websocket.OnMessage;
+import javax.websocket.PongMessage;
+
+import org.eclipse.jetty.websocket.jsr356.endpoints.TrackingSocket;
+
+@ClientEndpoint
+public class BasicPongMessageSocket extends TrackingSocket
+{
+ @OnMessage
+ public void onPong(PongMessage pong)
+ {
+ addEvent("onPong(%s)",pong);
+ dataLatch.countDown();
+ }
+}
diff --git a/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/endpoints/samples/BasicTextMessageStringSocket.java b/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/endpoints/samples/BasicTextMessageStringSocket.java
new file mode 100644
index 0000000..019aa6d
--- /dev/null
+++ b/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/endpoints/samples/BasicTextMessageStringSocket.java
@@ -0,0 +1,35 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.endpoints.samples;
+
+import javax.websocket.ClientEndpoint;
+import javax.websocket.OnMessage;
+
+import org.eclipse.jetty.websocket.jsr356.endpoints.TrackingSocket;
+
+@ClientEndpoint
+public class BasicTextMessageStringSocket extends TrackingSocket
+{
+ @OnMessage
+ public void onText(String message)
+ {
+ addEvent("onText(%s)",message);
+ dataLatch.countDown();
+ }
+}
diff --git a/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/endpoints/samples/InvalidCloseIntSocket.java b/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/endpoints/samples/InvalidCloseIntSocket.java
new file mode 100644
index 0000000..01f68ab
--- /dev/null
+++ b/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/endpoints/samples/InvalidCloseIntSocket.java
@@ -0,0 +1,37 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.endpoints.samples;
+
+import javax.websocket.ClientEndpoint;
+import javax.websocket.OnClose;
+
+import org.eclipse.jetty.websocket.jsr356.endpoints.TrackingSocket;
+
+@ClientEndpoint
+public class InvalidCloseIntSocket extends TrackingSocket
+{
+ /**
+ * Invalid Close Method Declaration (parameter type int)
+ */
+ @OnClose
+ public void onClose(int statusCode)
+ {
+ closeLatch.countDown();
+ }
+}
diff --git a/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/endpoints/samples/InvalidErrorErrorSocket.java b/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/endpoints/samples/InvalidErrorErrorSocket.java
new file mode 100644
index 0000000..7ed4ad7
--- /dev/null
+++ b/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/endpoints/samples/InvalidErrorErrorSocket.java
@@ -0,0 +1,37 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.endpoints.samples;
+
+import javax.websocket.ClientEndpoint;
+import javax.websocket.OnError;
+
+import org.eclipse.jetty.websocket.jsr356.endpoints.TrackingSocket;
+
+@ClientEndpoint
+public class InvalidErrorErrorSocket extends TrackingSocket
+{
+ /**
+ * Invalid Error Method Declaration (parameter type Error)
+ */
+ @OnError
+ public void onError(Error error)
+ {
+ /* no impl */
+ }
+}
diff --git a/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/endpoints/samples/InvalidErrorExceptionSocket.java b/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/endpoints/samples/InvalidErrorExceptionSocket.java
new file mode 100644
index 0000000..a63d334
--- /dev/null
+++ b/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/endpoints/samples/InvalidErrorExceptionSocket.java
@@ -0,0 +1,37 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.endpoints.samples;
+
+import javax.websocket.ClientEndpoint;
+import javax.websocket.OnError;
+
+import org.eclipse.jetty.websocket.jsr356.endpoints.TrackingSocket;
+
+@ClientEndpoint
+public class InvalidErrorExceptionSocket extends TrackingSocket
+{
+ /**
+ * Invalid Error Method Declaration (parameter type Exception)
+ */
+ @OnError
+ public void onError(Exception e)
+ {
+ /* no impl */
+ }
+}
diff --git a/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/endpoints/samples/InvalidErrorIntSocket.java b/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/endpoints/samples/InvalidErrorIntSocket.java
new file mode 100644
index 0000000..03e5d22
--- /dev/null
+++ b/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/endpoints/samples/InvalidErrorIntSocket.java
@@ -0,0 +1,37 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.endpoints.samples;
+
+import javax.websocket.ClientEndpoint;
+import javax.websocket.OnError;
+
+import org.eclipse.jetty.websocket.jsr356.endpoints.TrackingSocket;
+
+@ClientEndpoint
+public class InvalidErrorIntSocket extends TrackingSocket
+{
+ /**
+ * Invalid Error Method Declaration (parameter type int)
+ */
+ @OnError
+ public void onError(int errorCount)
+ {
+ /* no impl */
+ }
+}
diff --git a/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/endpoints/samples/InvalidOpenCloseReasonSocket.java b/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/endpoints/samples/InvalidOpenCloseReasonSocket.java
new file mode 100644
index 0000000..9692ecf
--- /dev/null
+++ b/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/endpoints/samples/InvalidOpenCloseReasonSocket.java
@@ -0,0 +1,38 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.endpoints.samples;
+
+import javax.websocket.ClientEndpoint;
+import javax.websocket.CloseReason;
+import javax.websocket.OnOpen;
+
+import org.eclipse.jetty.websocket.jsr356.endpoints.TrackingSocket;
+
+@ClientEndpoint
+public class InvalidOpenCloseReasonSocket extends TrackingSocket
+{
+ /**
+ * Invalid Open Method Declaration (parameter type CloseReason)
+ */
+ @OnOpen
+ public void onOpen(CloseReason reason)
+ {
+ openLatch.countDown();
+ }
+}
diff --git a/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/endpoints/samples/InvalidOpenIntSocket.java b/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/endpoints/samples/InvalidOpenIntSocket.java
new file mode 100644
index 0000000..675f2e7
--- /dev/null
+++ b/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/endpoints/samples/InvalidOpenIntSocket.java
@@ -0,0 +1,37 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.endpoints.samples;
+
+import javax.websocket.ClientEndpoint;
+import javax.websocket.OnOpen;
+
+import org.eclipse.jetty.websocket.jsr356.endpoints.TrackingSocket;
+
+@ClientEndpoint
+public class InvalidOpenIntSocket extends TrackingSocket
+{
+ /**
+ * Invalid Open Method Declaration (parameter type int)
+ */
+ @OnOpen
+ public void onOpen(int value)
+ {
+ openLatch.countDown();
+ }
+}
diff --git a/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/endpoints/samples/InvalidOpenSessionIntSocket.java b/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/endpoints/samples/InvalidOpenSessionIntSocket.java
new file mode 100644
index 0000000..8f21c75
--- /dev/null
+++ b/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/endpoints/samples/InvalidOpenSessionIntSocket.java
@@ -0,0 +1,38 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.endpoints.samples;
+
+import javax.websocket.ClientEndpoint;
+import javax.websocket.OnOpen;
+import javax.websocket.Session;
+
+import org.eclipse.jetty.websocket.jsr356.endpoints.TrackingSocket;
+
+@ClientEndpoint
+public class InvalidOpenSessionIntSocket extends TrackingSocket
+{
+ /**
+ * Invalid Open Method Declaration (parameter of type int)
+ */
+ @OnOpen
+ public void onOpen(Session session, int count)
+ {
+ openLatch.countDown();
+ }
+}
diff --git a/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/endpoints/samples/close/CloseEndpointConfigSocket.java b/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/endpoints/samples/close/CloseEndpointConfigSocket.java
new file mode 100644
index 0000000..64aba6c
--- /dev/null
+++ b/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/endpoints/samples/close/CloseEndpointConfigSocket.java
@@ -0,0 +1,36 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.endpoints.samples.close;
+
+import javax.websocket.ClientEndpoint;
+import javax.websocket.EndpointConfig;
+import javax.websocket.OnClose;
+
+import org.eclipse.jetty.websocket.jsr356.endpoints.TrackingSocket;
+
+@ClientEndpoint
+public class CloseEndpointConfigSocket extends TrackingSocket
+{
+ @OnClose
+ public void onClose(EndpointConfig config)
+ {
+ addEvent("onClose(EndpointConfig)");
+ closeLatch.countDown();
+ }
+}
diff --git a/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/endpoints/samples/close/CloseReasonSessionSocket.java b/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/endpoints/samples/close/CloseReasonSessionSocket.java
new file mode 100644
index 0000000..caf8fe3
--- /dev/null
+++ b/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/endpoints/samples/close/CloseReasonSessionSocket.java
@@ -0,0 +1,37 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.endpoints.samples.close;
+
+import javax.websocket.ClientEndpoint;
+import javax.websocket.CloseReason;
+import javax.websocket.OnClose;
+import javax.websocket.Session;
+
+import org.eclipse.jetty.websocket.jsr356.endpoints.TrackingSocket;
+
+@ClientEndpoint
+public class CloseReasonSessionSocket extends TrackingSocket
+{
+ @OnClose
+ public void onClose(CloseReason reason, Session session)
+ {
+ addEvent("onClose(CloseReason,Session)");
+ closeLatch.countDown();
+ }
+}
diff --git a/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/endpoints/samples/close/CloseReasonSocket.java b/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/endpoints/samples/close/CloseReasonSocket.java
new file mode 100644
index 0000000..2b84946
--- /dev/null
+++ b/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/endpoints/samples/close/CloseReasonSocket.java
@@ -0,0 +1,36 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.endpoints.samples.close;
+
+import javax.websocket.ClientEndpoint;
+import javax.websocket.CloseReason;
+import javax.websocket.OnClose;
+
+import org.eclipse.jetty.websocket.jsr356.endpoints.TrackingSocket;
+
+@ClientEndpoint
+public class CloseReasonSocket extends TrackingSocket
+{
+ @OnClose
+ public void onClose(CloseReason reason)
+ {
+ addEvent("onClose(CloseReason)");
+ closeLatch.countDown();
+ }
+}
diff --git a/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/endpoints/samples/close/CloseSessionReasonSocket.java b/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/endpoints/samples/close/CloseSessionReasonSocket.java
new file mode 100644
index 0000000..ee7439f
--- /dev/null
+++ b/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/endpoints/samples/close/CloseSessionReasonSocket.java
@@ -0,0 +1,37 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.endpoints.samples.close;
+
+import javax.websocket.ClientEndpoint;
+import javax.websocket.CloseReason;
+import javax.websocket.OnClose;
+import javax.websocket.Session;
+
+import org.eclipse.jetty.websocket.jsr356.endpoints.TrackingSocket;
+
+@ClientEndpoint
+public class CloseSessionReasonSocket extends TrackingSocket
+{
+ @OnClose
+ public void onClose(Session session, CloseReason reason)
+ {
+ addEvent("onClose(Session,CloseReason)");
+ closeLatch.countDown();
+ }
+}
diff --git a/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/endpoints/samples/close/CloseSessionSocket.java b/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/endpoints/samples/close/CloseSessionSocket.java
new file mode 100644
index 0000000..599deaa
--- /dev/null
+++ b/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/endpoints/samples/close/CloseSessionSocket.java
@@ -0,0 +1,36 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.endpoints.samples.close;
+
+import javax.websocket.ClientEndpoint;
+import javax.websocket.OnClose;
+import javax.websocket.Session;
+
+import org.eclipse.jetty.websocket.jsr356.endpoints.TrackingSocket;
+
+@ClientEndpoint
+public class CloseSessionSocket extends TrackingSocket
+{
+ @OnClose
+ public void onClose(Session session)
+ {
+ addEvent("onClose(Session)");
+ closeLatch.countDown();
+ }
+}
diff --git a/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/endpoints/samples/close/CloseSocket.java b/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/endpoints/samples/close/CloseSocket.java
new file mode 100644
index 0000000..e184022
--- /dev/null
+++ b/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/endpoints/samples/close/CloseSocket.java
@@ -0,0 +1,35 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.endpoints.samples.close;
+
+import javax.websocket.ClientEndpoint;
+import javax.websocket.OnClose;
+
+import org.eclipse.jetty.websocket.jsr356.endpoints.TrackingSocket;
+
+@ClientEndpoint
+public class CloseSocket extends TrackingSocket
+{
+ @OnClose
+ public void onClose()
+ {
+ addEvent("onClose()");
+ closeLatch.countDown();
+ }
+}
diff --git a/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/handlers/BaseMessageHandler.java b/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/handlers/BaseMessageHandler.java
new file mode 100644
index 0000000..a025f64
--- /dev/null
+++ b/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/handlers/BaseMessageHandler.java
@@ -0,0 +1,30 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.handlers;
+
+import javax.websocket.MessageHandler;
+
+public class BaseMessageHandler implements MessageHandler.Whole<String>
+{
+ @Override
+ public void onMessage(String message)
+ {
+ // TODO Auto-generated method stub
+ }
+}
diff --git a/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/handlers/ByteArrayPartialHandler.java b/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/handlers/ByteArrayPartialHandler.java
new file mode 100644
index 0000000..ca56ac2
--- /dev/null
+++ b/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/handlers/ByteArrayPartialHandler.java
@@ -0,0 +1,30 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.handlers;
+
+import javax.websocket.MessageHandler;
+
+public class ByteArrayPartialHandler implements MessageHandler.Partial<byte[]>
+{
+ @Override
+ public void onMessage(byte[] partialMessage, boolean last)
+ {
+ // TODO Auto-generated method stub
+ }
+}
diff --git a/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/handlers/ByteArrayWholeHandler.java b/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/handlers/ByteArrayWholeHandler.java
new file mode 100644
index 0000000..1cd68b8
--- /dev/null
+++ b/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/handlers/ByteArrayWholeHandler.java
@@ -0,0 +1,30 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.handlers;
+
+import javax.websocket.MessageHandler;
+
+public class ByteArrayWholeHandler implements MessageHandler.Whole<byte[]>
+{
+ @Override
+ public void onMessage(byte[] message)
+ {
+ // TODO Auto-generated method stub
+ }
+}
diff --git a/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/handlers/ByteBufferPartialHandler.java b/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/handlers/ByteBufferPartialHandler.java
new file mode 100644
index 0000000..7d70ed9
--- /dev/null
+++ b/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/handlers/ByteBufferPartialHandler.java
@@ -0,0 +1,32 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.handlers;
+
+import java.nio.ByteBuffer;
+
+import javax.websocket.MessageHandler;
+
+public class ByteBufferPartialHandler implements MessageHandler.Partial<ByteBuffer>
+{
+ @Override
+ public void onMessage(ByteBuffer partialMessage, boolean last)
+ {
+ // TODO Auto-generated method stub
+ }
+}
diff --git a/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/handlers/ByteBufferWholeHandler.java b/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/handlers/ByteBufferWholeHandler.java
new file mode 100644
index 0000000..1fb628f
--- /dev/null
+++ b/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/handlers/ByteBufferWholeHandler.java
@@ -0,0 +1,32 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.handlers;
+
+import java.nio.ByteBuffer;
+
+import javax.websocket.MessageHandler;
+
+public class ByteBufferWholeHandler implements MessageHandler.Whole<ByteBuffer>
+{
+ @Override
+ public void onMessage(ByteBuffer message)
+ {
+ // TODO Auto-generated method stub
+ }
+}
diff --git a/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/handlers/ComboMessageHandler.java b/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/handlers/ComboMessageHandler.java
new file mode 100644
index 0000000..4dea474
--- /dev/null
+++ b/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/handlers/ComboMessageHandler.java
@@ -0,0 +1,41 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.handlers;
+
+import java.nio.ByteBuffer;
+
+import javax.websocket.MessageHandler;
+
+/**
+ * A particularly annoying type of MessageHandler. One defining 2 implementations.
+ */
+public class ComboMessageHandler implements MessageHandler.Whole<String>, MessageHandler.Partial<ByteBuffer>
+{
+ @Override
+ public void onMessage(ByteBuffer partialMessage, boolean last)
+ {
+ // TODO Auto-generated method stub
+ }
+
+ @Override
+ public void onMessage(String message)
+ {
+ // TODO Auto-generated method stub
+ }
+}
diff --git a/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/handlers/ExtendedMessageHandler.java b/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/handlers/ExtendedMessageHandler.java
new file mode 100644
index 0000000..c10fbee
--- /dev/null
+++ b/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/handlers/ExtendedMessageHandler.java
@@ -0,0 +1,32 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.handlers;
+
+import java.nio.ByteBuffer;
+
+import javax.websocket.MessageHandler;
+
+public class ExtendedMessageHandler extends BaseMessageHandler implements MessageHandler.Partial<ByteBuffer>
+{
+ @Override
+ public void onMessage(ByteBuffer partialMessage, boolean last)
+ {
+ // TODO Auto-generated method stub
+ }
+}
diff --git a/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/handlers/InputStreamWholeHandler.java b/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/handlers/InputStreamWholeHandler.java
new file mode 100644
index 0000000..b2c73d4
--- /dev/null
+++ b/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/handlers/InputStreamWholeHandler.java
@@ -0,0 +1,32 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.handlers;
+
+import java.io.InputStream;
+
+import javax.websocket.MessageHandler;
+
+public class InputStreamWholeHandler implements MessageHandler.Whole<InputStream>
+{
+ @Override
+ public void onMessage(InputStream stream)
+ {
+ // TODO Auto-generated method stub
+ }
+}
diff --git a/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/handlers/LongMessageHandler.java b/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/handlers/LongMessageHandler.java
new file mode 100644
index 0000000..de9c637
--- /dev/null
+++ b/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/handlers/LongMessageHandler.java
@@ -0,0 +1,29 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.handlers;
+
+import javax.websocket.MessageHandler;
+
+public class LongMessageHandler implements MessageHandler.Whole<Long>
+{
+ @Override
+ public void onMessage(Long message)
+ {
+ }
+}
diff --git a/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/handlers/ReaderWholeHandler.java b/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/handlers/ReaderWholeHandler.java
new file mode 100644
index 0000000..ccb9324
--- /dev/null
+++ b/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/handlers/ReaderWholeHandler.java
@@ -0,0 +1,32 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.handlers;
+
+import java.io.Reader;
+
+import javax.websocket.MessageHandler;
+
+public class ReaderWholeHandler implements MessageHandler.Whole<Reader>
+{
+ @Override
+ public void onMessage(Reader reader)
+ {
+ // TODO Auto-generated method stub
+ }
+}
diff --git a/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/handlers/StringPartialHandler.java b/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/handlers/StringPartialHandler.java
new file mode 100644
index 0000000..549a65a
--- /dev/null
+++ b/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/handlers/StringPartialHandler.java
@@ -0,0 +1,30 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.handlers;
+
+import javax.websocket.MessageHandler;
+
+public class StringPartialHandler implements MessageHandler.Partial<String>
+{
+ @Override
+ public void onMessage(String partialMessage, boolean last)
+ {
+ // TODO Auto-generated method stub
+ }
+}
diff --git a/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/handlers/StringWholeHandler.java b/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/handlers/StringWholeHandler.java
new file mode 100644
index 0000000..4a8fd75
--- /dev/null
+++ b/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/handlers/StringWholeHandler.java
@@ -0,0 +1,30 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.handlers;
+
+import javax.websocket.MessageHandler;
+
+public class StringWholeHandler implements MessageHandler.Whole<String>
+{
+ @Override
+ public void onMessage(String message)
+ {
+ // TODO Auto-generated method stub
+ }
+}
diff --git a/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/metadata/DecoderMetadataSetTest.java b/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/metadata/DecoderMetadataSetTest.java
new file mode 100644
index 0000000..5a0c05b
--- /dev/null
+++ b/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/metadata/DecoderMetadataSetTest.java
@@ -0,0 +1,132 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.metadata;
+
+import static org.hamcrest.Matchers.*;
+
+import java.util.List;
+
+import javax.websocket.Decoder;
+
+import org.eclipse.jetty.websocket.jsr356.MessageType;
+import org.eclipse.jetty.websocket.jsr356.decoders.BadDualDecoder;
+import org.eclipse.jetty.websocket.jsr356.decoders.DateDecoder;
+import org.eclipse.jetty.websocket.jsr356.decoders.IntegerDecoder;
+import org.eclipse.jetty.websocket.jsr356.decoders.TimeDecoder;
+import org.eclipse.jetty.websocket.jsr356.decoders.ValidDualDecoder;
+import org.junit.Assert;
+import org.junit.Test;
+
+public class DecoderMetadataSetTest
+{
+ private void assertMetadata(CoderMetadata<?> metadata, Class<?> expectedType, Class<?> expectedCoder, MessageType expectedMessageType)
+ {
+ Assert.assertEquals("metadata.coderClass",expectedCoder,metadata.getCoderClass());
+ Assert.assertThat("metadata.messageType",metadata.getMessageType(),is(expectedMessageType));
+ Assert.assertEquals("metadata.objectType",expectedType,metadata.getObjectType());
+ }
+
+ @Test
+ public void testAddBadDualDecoders()
+ {
+ try
+ {
+ DecoderMetadataSet coders = new DecoderMetadataSet();
+
+ // has duplicated support for the same target Type
+ coders.add(BadDualDecoder.class);
+ Assert.fail("Should have thrown IllegalStateException for attempting to register Decoders with duplicate implementation");
+ }
+ catch (IllegalStateException e)
+ {
+ Assert.assertThat(e.getMessage(),containsString("Duplicate"));
+ }
+ }
+
+ @Test
+ public void testAddDuplicate()
+ {
+ DecoderMetadataSet coders = new DecoderMetadataSet();
+
+ // Add DateDecoder (decodes java.util.Date)
+ coders.add(DateDecoder.class);
+
+ try
+ {
+ // Add TimeDecoder (which also wants to decode java.util.Date)
+ coders.add(TimeDecoder.class);
+ Assert.fail("Should have thrown IllegalStateException for attempting to register Decoders with duplicate implementation");
+ }
+ catch (IllegalStateException e)
+ {
+ Assert.assertThat(e.getMessage(),containsString("Duplicate"));
+ }
+ }
+
+ @Test
+ public void testAddGetCoder()
+ {
+ DecoderMetadataSet coders = new DecoderMetadataSet();
+
+ coders.add(IntegerDecoder.class);
+ Class<? extends Decoder> actualClazz = coders.getCoder(Integer.class);
+ Assert.assertEquals("Coder Class",IntegerDecoder.class,actualClazz);
+ }
+
+ @Test
+ public void testAddGetMetadataByImpl()
+ {
+ DecoderMetadataSet coders = new DecoderMetadataSet();
+
+ coders.add(IntegerDecoder.class);
+ List<DecoderMetadata> metadatas = coders.getMetadataByImplementation(IntegerDecoder.class);
+ Assert.assertThat("Metadatas (by impl) count",metadatas.size(),is(1));
+ DecoderMetadata metadata = metadatas.get(0);
+ assertMetadata(metadata,Integer.class,IntegerDecoder.class,MessageType.TEXT);
+ }
+
+ @Test
+ public void testAddGetMetadataByType()
+ {
+ DecoderMetadataSet coders = new DecoderMetadataSet();
+
+ coders.add(IntegerDecoder.class);
+ DecoderMetadata metadata = coders.getMetadataByType(Integer.class);
+ assertMetadata(metadata,Integer.class,IntegerDecoder.class,MessageType.TEXT);
+ }
+
+ @Test
+ public void testAddValidDualDecoders()
+ {
+ DecoderMetadataSet coders = new DecoderMetadataSet();
+
+ coders.add(ValidDualDecoder.class);
+
+ List<Class<? extends Decoder>> decodersList = coders.getList();
+ Assert.assertThat("Decoder List",decodersList,notNullValue());
+ Assert.assertThat("Decoder List count",decodersList.size(),is(2));
+
+ DecoderMetadata metadata;
+ metadata = coders.getMetadataByType(Integer.class);
+ assertMetadata(metadata,Integer.class,ValidDualDecoder.class,MessageType.TEXT);
+
+ metadata = coders.getMetadataByType(Long.class);
+ assertMetadata(metadata,Long.class,ValidDualDecoder.class,MessageType.BINARY);
+ }
+}
diff --git a/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/metadata/EncoderMetadataSetTest.java b/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/metadata/EncoderMetadataSetTest.java
new file mode 100644
index 0000000..48b58ff
--- /dev/null
+++ b/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/metadata/EncoderMetadataSetTest.java
@@ -0,0 +1,132 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.metadata;
+
+import static org.hamcrest.Matchers.*;
+
+import java.util.List;
+
+import javax.websocket.Encoder;
+
+import org.eclipse.jetty.websocket.jsr356.MessageType;
+import org.eclipse.jetty.websocket.jsr356.encoders.BadDualEncoder;
+import org.eclipse.jetty.websocket.jsr356.encoders.DateEncoder;
+import org.eclipse.jetty.websocket.jsr356.encoders.IntegerEncoder;
+import org.eclipse.jetty.websocket.jsr356.encoders.TimeEncoder;
+import org.eclipse.jetty.websocket.jsr356.encoders.ValidDualEncoder;
+import org.junit.Assert;
+import org.junit.Test;
+
+public class EncoderMetadataSetTest
+{
+ private void assertMetadata(CoderMetadata<?> metadata, Class<?> expectedType, Class<?> expectedCoder, MessageType expectedMessageType)
+ {
+ Assert.assertEquals("metadata.coderClass",expectedCoder,metadata.getCoderClass());
+ Assert.assertThat("metadata.messageType",metadata.getMessageType(),is(expectedMessageType));
+ Assert.assertEquals("metadata.objectType",expectedType,metadata.getObjectType());
+ }
+
+ @Test
+ public void testAddBadDualEncoders()
+ {
+ try
+ {
+ EncoderMetadataSet coders = new EncoderMetadataSet();
+
+ // has duplicated support for the same target Type
+ coders.add(BadDualEncoder.class);
+ Assert.fail("Should have thrown IllegalStateException for attempting to register Encoders with duplicate implementation");
+ }
+ catch (IllegalStateException e)
+ {
+ Assert.assertThat(e.getMessage(),containsString("Duplicate"));
+ }
+ }
+
+ @Test
+ public void testAddDuplicate()
+ {
+ EncoderMetadataSet coders = new EncoderMetadataSet();
+
+ // Add DateEncoder (decodes java.util.Date)
+ coders.add(DateEncoder.class);
+
+ try
+ {
+ // Add TimeEncoder (which also wants to decode java.util.Date)
+ coders.add(TimeEncoder.class);
+ Assert.fail("Should have thrown IllegalStateException for attempting to register Encoders with duplicate implementation");
+ }
+ catch (IllegalStateException e)
+ {
+ Assert.assertThat(e.getMessage(),containsString("Duplicate"));
+ }
+ }
+
+ @Test
+ public void testAddGetCoder()
+ {
+ EncoderMetadataSet coders = new EncoderMetadataSet();
+
+ coders.add(IntegerEncoder.class);
+ Class<? extends Encoder> actualClazz = coders.getCoder(Integer.class);
+ Assert.assertEquals("Coder Class",IntegerEncoder.class,actualClazz);
+ }
+
+ @Test
+ public void testAddGetMetadataByImpl()
+ {
+ EncoderMetadataSet coders = new EncoderMetadataSet();
+
+ coders.add(IntegerEncoder.class);
+ List<EncoderMetadata> metadatas = coders.getMetadataByImplementation(IntegerEncoder.class);
+ Assert.assertThat("Metadatas (by impl) count",metadatas.size(),is(1));
+ EncoderMetadata metadata = metadatas.get(0);
+ assertMetadata(metadata,Integer.class,IntegerEncoder.class,MessageType.TEXT);
+ }
+
+ @Test
+ public void testAddGetMetadataByType()
+ {
+ EncoderMetadataSet coders = new EncoderMetadataSet();
+
+ coders.add(IntegerEncoder.class);
+ EncoderMetadata metadata = coders.getMetadataByType(Integer.class);
+ assertMetadata(metadata,Integer.class,IntegerEncoder.class,MessageType.TEXT);
+ }
+
+ @Test
+ public void testAddValidDualEncoders()
+ {
+ EncoderMetadataSet coders = new EncoderMetadataSet();
+
+ coders.add(ValidDualEncoder.class);
+
+ List<Class<? extends Encoder>> EncodersList = coders.getList();
+ Assert.assertThat("Encoder List",EncodersList,notNullValue());
+ Assert.assertThat("Encoder List count",EncodersList.size(),is(2));
+
+ EncoderMetadata metadata;
+ metadata = coders.getMetadataByType(Integer.class);
+ assertMetadata(metadata,Integer.class,ValidDualEncoder.class,MessageType.TEXT);
+
+ metadata = coders.getMetadataByType(Long.class);
+ assertMetadata(metadata,Long.class,ValidDualEncoder.class,MessageType.BINARY);
+ }
+}
diff --git a/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/samples/AbstractStringEndpoint.java b/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/samples/AbstractStringEndpoint.java
new file mode 100644
index 0000000..dec7740
--- /dev/null
+++ b/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/samples/AbstractStringEndpoint.java
@@ -0,0 +1,58 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.samples;
+
+import javax.websocket.CloseReason;
+import javax.websocket.Endpoint;
+import javax.websocket.EndpointConfig;
+import javax.websocket.MessageHandler;
+import javax.websocket.Session;
+
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+
+/**
+ * Base Abstract Class.
+ */
+public abstract class AbstractStringEndpoint extends Endpoint implements MessageHandler.Whole<String>
+{
+ private static final Logger LOG = Log.getLogger(AbstractStringEndpoint.class);
+ protected Session session;
+ protected EndpointConfig config;
+
+ @Override
+ public void onOpen(Session session, EndpointConfig config)
+ {
+ LOG.debug("onOpen({}, {})",session,config);
+ session.addMessageHandler(this);
+ this.session = session;
+ this.config = config;
+ }
+
+ public void onClose(Session session, CloseReason closeReason)
+ {
+ LOG.debug("onClose({}, {})",session,closeReason);
+ this.session = null;
+ }
+
+ public void onError(Session session, Throwable thr)
+ {
+ LOG.warn("onError()",thr);
+ }
+}
diff --git a/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/samples/DummyConnection.java b/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/samples/DummyConnection.java
new file mode 100644
index 0000000..3ca26bf
--- /dev/null
+++ b/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/samples/DummyConnection.java
@@ -0,0 +1,155 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.samples;
+
+import java.net.InetSocketAddress;
+import java.util.concurrent.Executor;
+
+import org.eclipse.jetty.io.ByteBufferPool;
+import org.eclipse.jetty.websocket.api.SuspendToken;
+import org.eclipse.jetty.websocket.api.WebSocketPolicy;
+import org.eclipse.jetty.websocket.api.WriteCallback;
+import org.eclipse.jetty.websocket.api.extensions.Frame;
+import org.eclipse.jetty.websocket.api.extensions.IncomingFrames;
+import org.eclipse.jetty.websocket.common.LogicalConnection;
+import org.eclipse.jetty.websocket.common.WebSocketSession;
+import org.eclipse.jetty.websocket.common.io.IOState;
+
+public class DummyConnection implements LogicalConnection
+{
+ private IOState iostate;
+
+ public DummyConnection()
+ {
+ this.iostate = new IOState();
+ }
+
+ @Override
+ public void close()
+ {
+ }
+
+ @Override
+ public void close(int statusCode, String reason)
+ {
+ }
+
+ @Override
+ public void disconnect()
+ {
+ }
+
+ @Override
+ public ByteBufferPool getBufferPool()
+ {
+ return null;
+ }
+
+ @Override
+ public Executor getExecutor()
+ {
+ return null;
+ }
+
+ @Override
+ public long getIdleTimeout()
+ {
+ return 0;
+ }
+
+ @Override
+ public IOState getIOState()
+ {
+ return this.iostate;
+ }
+
+ @Override
+ public InetSocketAddress getLocalAddress()
+ {
+ return null;
+ }
+
+ @Override
+ public long getMaxIdleTimeout()
+ {
+ return 0;
+ }
+
+ @Override
+ public WebSocketPolicy getPolicy()
+ {
+ return null;
+ }
+
+ @Override
+ public InetSocketAddress getRemoteAddress()
+ {
+ // TODO Auto-generated method stub
+ return null;
+ }
+
+ @Override
+ public WebSocketSession getSession()
+ {
+ return null;
+ }
+
+ @Override
+ public boolean isOpen()
+ {
+ return false;
+ }
+
+ @Override
+ public boolean isReading()
+ {
+ return false;
+ }
+
+ @Override
+ public void outgoingFrame(Frame frame, WriteCallback callback)
+ {
+ }
+
+ @Override
+ public void resume()
+ {
+ }
+
+ @Override
+ public void setMaxIdleTimeout(long ms)
+ {
+ }
+
+ @Override
+ public void setNextIncomingFrames(IncomingFrames incoming)
+ {
+ }
+
+ @Override
+ public void setSession(WebSocketSession session)
+ {
+ }
+
+ @Override
+ public SuspendToken suspend()
+ {
+ return null;
+ }
+}
diff --git a/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/samples/DummyEndpoint.java b/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/samples/DummyEndpoint.java
new file mode 100644
index 0000000..558c300
--- /dev/null
+++ b/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/samples/DummyEndpoint.java
@@ -0,0 +1,32 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.samples;
+
+import javax.websocket.Endpoint;
+import javax.websocket.EndpointConfig;
+import javax.websocket.Session;
+
+public class DummyEndpoint extends Endpoint
+{
+ @Override
+ public void onOpen(Session session, EndpointConfig config)
+ {
+ /* do nothing */
+ }
+}
diff --git a/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/samples/EchoStringEndpoint.java b/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/samples/EchoStringEndpoint.java
new file mode 100644
index 0000000..d52d4a9
--- /dev/null
+++ b/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/samples/EchoStringEndpoint.java
@@ -0,0 +1,36 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.samples;
+
+import org.eclipse.jetty.websocket.jsr356.MessageQueue;
+
+/**
+ * Legitimate structure for an Endpoint
+ */
+public class EchoStringEndpoint extends AbstractStringEndpoint
+{
+ public MessageQueue messageQueue = new MessageQueue();
+
+ @Override
+ public void onMessage(String message)
+ {
+ messageQueue.offer(message);
+ session.getAsyncRemote().sendText(message);
+ }
+}
diff --git a/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/samples/ExtDecoder.java b/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/samples/ExtDecoder.java
new file mode 100644
index 0000000..8073105
--- /dev/null
+++ b/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/samples/ExtDecoder.java
@@ -0,0 +1,29 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.samples;
+
+import javax.websocket.Decoder;
+
+/**
+ * Testing scenario of an extended Decoder interface
+ */
+public interface ExtDecoder<T> extends Decoder.Text<T>
+{
+ void setId(String id);
+}
diff --git a/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/samples/Fruit.java b/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/samples/Fruit.java
new file mode 100644
index 0000000..47b59f8
--- /dev/null
+++ b/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/samples/Fruit.java
@@ -0,0 +1,25 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.samples;
+
+public class Fruit
+{
+ public String name;
+ public String color;
+}
diff --git a/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/samples/FruitBinaryEncoder.java b/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/samples/FruitBinaryEncoder.java
new file mode 100644
index 0000000..6cd6f79
--- /dev/null
+++ b/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/samples/FruitBinaryEncoder.java
@@ -0,0 +1,68 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.samples;
+
+import java.nio.ByteBuffer;
+import java.nio.charset.Charset;
+
+import javax.websocket.EncodeException;
+import javax.websocket.Encoder;
+import javax.websocket.EndpointConfig;
+
+import org.eclipse.jetty.util.BufferUtil;
+
+public class FruitBinaryEncoder implements Encoder.Binary<Fruit>
+{
+ public static final byte FRUIT_ID_BYTE = (byte)0xAF;
+ // the number of bytes to store a string (1 int)
+ public static final int STRLEN_STORAGE = 4;
+
+ @Override
+ public void destroy()
+ {
+ }
+
+ @Override
+ public ByteBuffer encode(Fruit fruit) throws EncodeException
+ {
+ int len = 1; // id byte
+ len += STRLEN_STORAGE + fruit.name.length();
+ len += STRLEN_STORAGE + fruit.color.length();
+
+ ByteBuffer buf = ByteBuffer.allocate(len + 64);
+ buf.flip();
+ buf.put(FRUIT_ID_BYTE);
+ putString(buf,fruit.name);
+ putString(buf,fruit.color);
+ buf.flip();
+
+ return buf;
+ }
+
+ @Override
+ public void init(EndpointConfig config)
+ {
+ }
+
+ private void putString(ByteBuffer buf, String str)
+ {
+ buf.putInt(str.length());
+ BufferUtil.toBuffer(str,Charset.forName("UTF-8"));
+ }
+}
diff --git a/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/samples/FruitDecoder.java b/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/samples/FruitDecoder.java
new file mode 100644
index 0000000..b3fbb92
--- /dev/null
+++ b/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/samples/FruitDecoder.java
@@ -0,0 +1,82 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.samples;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import javax.websocket.DecodeException;
+import javax.websocket.EndpointConfig;
+
+public class FruitDecoder implements ExtDecoder<Fruit>
+{
+ private String id;
+
+ @Override
+ public Fruit decode(String s) throws DecodeException
+ {
+ Pattern pat = Pattern.compile("([^|]*)|([^|]*)");
+ Matcher mat = pat.matcher(s);
+ if (!mat.find())
+ {
+ throw new DecodeException(s,"Unable to find Fruit reference encoded in text message");
+ }
+
+ Fruit fruit = new Fruit();
+ fruit.name = mat.group(1);
+ fruit.color = mat.group(2);
+
+ return fruit;
+ }
+
+ @Override
+ public void destroy()
+ {
+ }
+
+ @Override
+ public void init(EndpointConfig config)
+ {
+ }
+
+ @Override
+ public void setId(String id)
+ {
+ this.id = id;
+ }
+
+ @Override
+ public String toString()
+ {
+ return "SecondDecoder[id=" + id + "]";
+ }
+
+ @Override
+ public boolean willDecode(String s)
+ {
+ if (s == null)
+ {
+ return false;
+ }
+
+ Pattern pat = Pattern.compile("([^|]*)|([^|]*)");
+ Matcher mat = pat.matcher(s);
+ return (mat.find());
+ }
+}
diff --git a/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/samples/FruitTextEncoder.java b/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/samples/FruitTextEncoder.java
new file mode 100644
index 0000000..edc69ea
--- /dev/null
+++ b/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/samples/FruitTextEncoder.java
@@ -0,0 +1,42 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.samples;
+
+import javax.websocket.EncodeException;
+import javax.websocket.Encoder;
+import javax.websocket.EndpointConfig;
+
+public class FruitTextEncoder implements Encoder.Text<Fruit>
+{
+ @Override
+ public void destroy()
+ {
+ }
+
+ @Override
+ public String encode(Fruit fruit) throws EncodeException
+ {
+ return String.format("%s|%s",fruit.name,fruit.color);
+ }
+
+ @Override
+ public void init(EndpointConfig config)
+ {
+ }
+}
diff --git a/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/samples/IntSocket.java b/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/samples/IntSocket.java
new file mode 100644
index 0000000..5c55ad2
--- /dev/null
+++ b/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/samples/IntSocket.java
@@ -0,0 +1,46 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.samples;
+
+import java.io.IOException;
+
+import javax.websocket.ClientEndpoint;
+import javax.websocket.EncodeException;
+import javax.websocket.OnMessage;
+import javax.websocket.Session;
+
+import org.eclipse.jetty.websocket.jsr356.decoders.BadDualDecoder;
+
+@ClientEndpoint(decoders =
+{ BadDualDecoder.class })
+public class IntSocket
+{
+ @OnMessage
+ public void onInt(Session session, int value)
+ {
+ try
+ {
+ session.getBasicRemote().sendObject(value);
+ }
+ catch (IOException | EncodeException e)
+ {
+ e.printStackTrace();
+ }
+ }
+}
diff --git a/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/utils/ReflectUtilsTest.java b/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/utils/ReflectUtilsTest.java
new file mode 100644
index 0000000..f8e040b
--- /dev/null
+++ b/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/utils/ReflectUtilsTest.java
@@ -0,0 +1,138 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.utils;
+
+import static org.hamcrest.Matchers.*;
+
+import org.eclipse.jetty.websocket.common.util.ReflectUtils;
+import org.junit.Assert;
+import org.junit.Test;
+
+public class ReflectUtilsTest
+{
+ public static interface Fruit<T>
+ {
+ }
+
+ public static interface Color<T>
+ {
+ }
+
+ public static interface Food<T> extends Fruit<T>
+ {
+ }
+
+ public static abstract class Apple<T extends Object> implements Fruit<T>, Color<String>
+ {
+ }
+
+ public static abstract class Cherry<A extends Object, B extends Number> implements Fruit<A>, Color<B>
+ {
+ }
+
+ public static abstract class Banana implements Fruit<String>, Color<String>
+ {
+ }
+
+ public static class Washington<Z extends Number, X extends Object> extends Cherry<X, Z>
+ {
+ }
+
+ public static class Rainier extends Washington<Float, Short>
+ {
+ }
+
+ public static class Pizza implements Food<Integer>
+ {
+ }
+
+ public static class Cavendish extends Banana
+ {
+ }
+
+ public static class GrannySmith extends Apple<Long>
+ {
+ }
+
+ public static class Pear implements Fruit<String>, Color<Double>
+ {
+ }
+
+ public static class Kiwi implements Fruit<Character>
+ {
+ }
+
+ @Test
+ public void testFindGeneric_PearFruit()
+ {
+ assertFindGenericClass(Pear.class,Fruit.class,String.class);
+ }
+
+ @Test
+ public void testFindGeneric_PizzaFruit()
+ {
+ assertFindGenericClass(Pizza.class,Fruit.class,Integer.class);
+ }
+
+ @Test
+ public void testFindGeneric_KiwiFruit()
+ {
+ assertFindGenericClass(Kiwi.class,Fruit.class,Character.class);
+ }
+
+ @Test
+ public void testFindGeneric_PearColor()
+ {
+ assertFindGenericClass(Pear.class,Color.class,Double.class);
+ }
+
+ @Test
+ public void testFindGeneric_GrannySmithFruit()
+ {
+ assertFindGenericClass(GrannySmith.class,Fruit.class,Long.class);
+ }
+
+ @Test
+ public void testFindGeneric_CavendishFruit()
+ {
+ assertFindGenericClass(Cavendish.class,Fruit.class,String.class);
+ }
+
+ @Test
+ public void testFindGeneric_RainierFruit()
+ {
+ assertFindGenericClass(Rainier.class,Fruit.class,Short.class);
+ }
+
+ @Test
+ public void testFindGeneric_WashingtonFruit()
+ {
+ // Washington does not have a concrete implementation
+ // of the Fruit interface, this should return null
+ Class<?> impl = ReflectUtils.findGenericClassFor(Washington.class,Fruit.class);
+ Assert.assertThat("Washington -> Fruit implementation",impl,nullValue());
+ }
+
+ private void assertFindGenericClass(Class<?> baseClass, Class<?> ifaceClass, Class<?> expectedClass)
+ {
+ Class<?> foundClass = ReflectUtils.findGenericClassFor(baseClass,ifaceClass);
+ String msg = String.format("Expecting %s<%s> found on %s",ifaceClass.getName(),expectedClass.getName(),baseClass.getName());
+ Assert.assertEquals(msg,expectedClass,foundClass);
+ }
+}
diff --git a/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/utils/TypeTree.java b/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/utils/TypeTree.java
new file mode 100644
index 0000000..648ebd4
--- /dev/null
+++ b/jetty-websocket/javax-websocket-client-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/utils/TypeTree.java
@@ -0,0 +1,120 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.utils;
+
+import java.lang.reflect.GenericArrayType;
+import java.lang.reflect.ParameterizedType;
+import java.lang.reflect.Type;
+import java.lang.reflect.TypeVariable;
+import java.lang.reflect.WildcardType;
+
+import org.eclipse.jetty.websocket.common.util.ReflectUtils;
+
+public class TypeTree
+{
+ public static void dumpTree(String indent, Type type)
+ {
+ if ((type == null) || (type == Object.class))
+ {
+ return;
+ }
+
+ if (type instanceof Class<?>)
+ {
+ Class<?> ctype = (Class<?>)type;
+ System.out.printf("%s (Class) = %s%n",indent,ctype.getName());
+
+ String name = ctype.getName();
+ if (name.startsWith("java.lang.") || name.startsWith("java.io."))
+ {
+ // filter away standard classes from tree (otherwise it will go on infinitely)
+ return;
+ }
+
+ Type superType = ctype.getGenericSuperclass();
+ dumpTree(indent + ".genericSuperClass()",superType);
+
+ Type[] ifaces = ctype.getGenericInterfaces();
+ if ((ifaces != null) && (ifaces.length > 0))
+ {
+ // System.out.printf("%s.genericInterfaces[].length = %d%n",indent,ifaces.length);
+ for (int i = 0; i < ifaces.length; i++)
+ {
+ Type iface = ifaces[i];
+ dumpTree(indent + ".genericInterfaces[" + i + "]",iface);
+ }
+ }
+
+ TypeVariable<?>[] typeParams = ctype.getTypeParameters();
+ if ((typeParams != null) && (typeParams.length > 0))
+ {
+ // System.out.printf("%s.typeParameters[].length = %d%n",indent,typeParams.length);
+ for (int i = 0; i < typeParams.length; i++)
+ {
+ TypeVariable<?> typeParam = typeParams[i];
+ dumpTree(indent + ".typeParameters[" + i + "]",typeParam);
+ }
+ }
+ return;
+ }
+
+ if (type instanceof ParameterizedType)
+ {
+ ParameterizedType ptype = (ParameterizedType)type;
+ System.out.printf("%s (ParameterizedType) = %s%n",indent,ReflectUtils.toShortName(ptype));
+ // dumpTree(indent + ".ownerType()",ptype.getOwnerType());
+ dumpTree(indent + ".rawType(" + ReflectUtils.toShortName(ptype.getRawType()) + ")",ptype.getRawType());
+ Type args[] = ptype.getActualTypeArguments();
+ if (args != null)
+ {
+ System.out.printf("%s.actualTypeArguments[].length = %d%n",indent,args.length);
+ for (int i = 0; i < args.length; i++)
+ {
+ Type arg = args[i];
+ dumpTree(indent + ".actualTypeArguments[" + i + "]",arg);
+ }
+ }
+ return;
+ }
+
+ if (type instanceof GenericArrayType)
+ {
+ GenericArrayType gtype = (GenericArrayType)type;
+ System.out.printf("%s (GenericArrayType) = %s%n",indent,gtype);
+ return;
+ }
+
+ if (type instanceof TypeVariable<?>)
+ {
+ TypeVariable<?> tvar = (TypeVariable<?>)type;
+ System.out.printf("%s (TypeVariable) = %s%n",indent,tvar);
+ System.out.printf("%s.getName() = %s%n",indent,tvar.getName());
+ System.out.printf("%s.getGenericDeclaration() = %s%n",indent,tvar.getGenericDeclaration());
+ return;
+ }
+
+ if (type instanceof WildcardType)
+ {
+ System.out.printf("%s (WildcardType) = %s%n",indent,type);
+ return;
+ }
+
+ System.out.printf("%s (?) = %s%n",indent,type);
+ }
+}
diff --git a/jetty-websocket/javax-websocket-client-impl/src/test/resources/jetty-logging.properties b/jetty-websocket/javax-websocket-client-impl/src/test/resources/jetty-logging.properties
new file mode 100644
index 0000000..6c5baae
--- /dev/null
+++ b/jetty-websocket/javax-websocket-client-impl/src/test/resources/jetty-logging.properties
@@ -0,0 +1,5 @@
+org.eclipse.jetty.util.log.class=org.eclipse.jetty.util.log.StdErrLog
+org.eclipse.jetty.LEVEL=WARN
+
+# org.eclipse.jetty.websocket.LEVEL=WARN
+# org.eclipse.jetty.websocket.jsr356.LEVEL=DEBUG
diff --git a/jetty-websocket/javax-websocket-server-impl/pom.xml b/jetty-websocket/javax-websocket-server-impl/pom.xml
new file mode 100644
index 0000000..20ae245
--- /dev/null
+++ b/jetty-websocket/javax-websocket-server-impl/pom.xml
@@ -0,0 +1,67 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+ <parent>
+ <groupId>org.eclipse.jetty.websocket</groupId>
+ <artifactId>websocket-parent</artifactId>
+ <version>9.1.0-SNAPSHOT</version>
+ </parent>
+
+ <modelVersion>4.0.0</modelVersion>
+ <artifactId>javax-websocket-server-impl</artifactId>
+ <name>Jetty :: Websocket :: javax.websocket.server :: Server Implementation</name>
+
+ <properties>
+ <bundle-symbolic-name>${project.groupId}.javax.websocket.server</bundle-symbolic-name>
+ </properties>
+
+ <dependencies>
+ <dependency>
+ <groupId>org.eclipse.jetty</groupId>
+ <artifactId>jetty-annotations</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.eclipse.jetty.websocket</groupId>
+ <artifactId>javax-websocket-client-impl</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.eclipse.jetty.websocket</groupId>
+ <artifactId>websocket-server</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>javax.websocket</groupId>
+ <artifactId>javax.websocket-api</artifactId>
+ <version>1.0</version>
+ </dependency>
+ <dependency>
+ <groupId>org.eclipse.jetty.toolchain</groupId>
+ <artifactId>jetty-test-helper</artifactId>
+ <scope>test</scope>
+ </dependency>
+ </dependencies>
+
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-assembly-plugin</artifactId>
+ <executions>
+ <execution>
+ <phase>package</phase>
+ <goals>
+ <goal>single</goal>
+ </goals>
+ <configuration>
+ <descriptorRefs>
+ <descriptorRef>config</descriptorRef>
+ </descriptorRefs>
+ </configuration>
+ </execution>
+ </executions>
+ </plugin>
+ </plugins>
+ </build>
+</project>
diff --git a/jetty-websocket/javax-websocket-server-impl/src/main/config/etc/jetty-websockets.xml b/jetty-websocket/javax-websocket-server-impl/src/main/config/etc/jetty-websockets.xml
new file mode 100644
index 0000000..190a58e
--- /dev/null
+++ b/jetty-websocket/javax-websocket-server-impl/src/main/config/etc/jetty-websockets.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0"?>
+<!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "http://www.eclipse.org/jetty/configure_9_0.dtd">
+
+<Configure id="Server" class="org.eclipse.jetty.server.Server">
+ <!-- =========================================================== -->
+ <!-- Add javax.websocket Configuring classes to all webapps for this Server -->
+ <!-- =========================================================== -->
+ <Call class="org.eclipse.jetty.webapp.Configuration$ClassList" name="setServerDefault">
+ <Arg><Ref refid="Server" /></Arg>
+ <Call name="addBefore">
+ <Arg name="beforeClass">org.eclipse.jetty.annotations.AnnotationConfiguration</Arg>
+ <Arg>
+ <Array type="String">
+ <Item>org.eclipse.jetty.websocket.jsr356.server.WebSocketConfiguration</Item>
+ </Array>
+ </Arg>
+ </Call>
+ </Call>
+
+</Configure>
diff --git a/jetty-websocket/javax-websocket-server-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/server/AnnotatedServerEndpointConfig.java b/jetty-websocket/javax-websocket-server-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/server/AnnotatedServerEndpointConfig.java
new file mode 100644
index 0000000..ed33f39
--- /dev/null
+++ b/jetty-websocket/javax-websocket-server-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/server/AnnotatedServerEndpointConfig.java
@@ -0,0 +1,182 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.server;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import javax.websocket.Decoder;
+import javax.websocket.DeploymentException;
+import javax.websocket.Encoder;
+import javax.websocket.Extension;
+import javax.websocket.server.ServerEndpoint;
+import javax.websocket.server.ServerEndpointConfig;
+
+public class AnnotatedServerEndpointConfig implements ServerEndpointConfig
+{
+ private final Class<?> endpointClass;
+ private final String path;
+ private final List<Class<? extends Decoder>> decoders;
+ private final List<Class<? extends Encoder>> encoders;
+ private final Configurator configurator;
+ private final List<String> subprotocols;
+
+ private Map<String, Object> userProperties;
+ private List<Extension> extensions;
+
+ public AnnotatedServerEndpointConfig(Class<?> endpointClass, ServerEndpoint anno) throws DeploymentException
+ {
+ this(endpointClass,anno,null);
+ }
+
+ public AnnotatedServerEndpointConfig(Class<?> endpointClass, ServerEndpoint anno, ServerEndpointConfig baseConfig) throws DeploymentException
+ {
+ List<Class<? extends Decoder>> compositeDecoders = new ArrayList<>();
+ List<Class<? extends Encoder>> compositeEncoders = new ArrayList<>();
+ List<String> compositeSubProtocols = new ArrayList<>();
+
+ Configurator configr = null;
+
+ // Copy from base config
+ if (baseConfig != null)
+ {
+ compositeDecoders.addAll(baseConfig.getDecoders());
+ compositeEncoders.addAll(baseConfig.getEncoders());
+ compositeSubProtocols.addAll(baseConfig.getSubprotocols());
+ configr = baseConfig.getConfigurator();
+ }
+
+ // now add from annotations
+ compositeDecoders.addAll(Arrays.asList(anno.decoders()));
+ compositeEncoders.addAll(Arrays.asList(anno.encoders()));
+ compositeSubProtocols.addAll(Arrays.asList(anno.subprotocols()));
+
+ // Create unmodifiable lists
+ this.decoders = Collections.unmodifiableList(compositeDecoders);
+ this.encoders = Collections.unmodifiableList(compositeEncoders);
+ this.subprotocols = Collections.unmodifiableList(compositeSubProtocols);
+
+ // supplied by init lifecycle
+ this.extensions = new ArrayList<>();
+ this.path = anno.value();
+ this.endpointClass = endpointClass;
+ // no userProperties in annotation
+ this.userProperties = new HashMap<>();
+
+ if (anno.configurator() == null)
+ {
+ if (configr != null)
+ {
+ this.configurator = configr;
+ }
+ else
+ {
+ this.configurator = BasicServerEndpointConfigurator.INSTANCE;
+ }
+ }
+ else
+ {
+ try
+ {
+ this.configurator = anno.configurator().newInstance();
+ }
+ catch (InstantiationException | IllegalAccessException e)
+ {
+ StringBuilder err = new StringBuilder();
+ err.append("Unable to instantiate ClientEndpoint.configurator() of ");
+ err.append(anno.configurator().getName());
+ err.append(" defined as annotation in ");
+ err.append(anno.getClass().getName());
+ throw new DeploymentException(err.toString(),e);
+ }
+ }
+ }
+
+ @Override
+ public Configurator getConfigurator()
+ {
+ return configurator;
+ }
+
+ @Override
+ public List<Class<? extends Decoder>> getDecoders()
+ {
+ return decoders;
+ }
+
+ @Override
+ public List<Class<? extends Encoder>> getEncoders()
+ {
+ return encoders;
+ }
+
+ @Override
+ public Class<?> getEndpointClass()
+ {
+ return endpointClass;
+ }
+
+ @Override
+ public List<Extension> getExtensions()
+ {
+ return extensions;
+ }
+
+ @Override
+ public String getPath()
+ {
+ return path;
+ }
+
+ @Override
+ public List<String> getSubprotocols()
+ {
+ return subprotocols;
+ }
+
+ @Override
+ public Map<String, Object> getUserProperties()
+ {
+ return userProperties;
+ }
+
+ @Override
+ public String toString()
+ {
+ StringBuilder builder = new StringBuilder();
+ builder.append("AnnotatedServerEndpointConfig[endpointClass=");
+ builder.append(endpointClass);
+ builder.append(",path=");
+ builder.append(path);
+ builder.append(",decoders=");
+ builder.append(decoders);
+ builder.append(",encoders=");
+ builder.append(encoders);
+ builder.append(",subprotocols=");
+ builder.append(subprotocols);
+ builder.append(",extensions=");
+ builder.append(extensions);
+ builder.append("]");
+ return builder.toString();
+ }
+}
diff --git a/jetty-websocket/javax-websocket-server-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/server/AnnotatedServerEndpointMetadata.java b/jetty-websocket/javax-websocket-server-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/server/AnnotatedServerEndpointMetadata.java
new file mode 100644
index 0000000..da9d88b
--- /dev/null
+++ b/jetty-websocket/javax-websocket-server-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/server/AnnotatedServerEndpointMetadata.java
@@ -0,0 +1,108 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.server;
+
+import java.util.LinkedList;
+
+import javax.websocket.DeploymentException;
+import javax.websocket.server.ServerEndpoint;
+import javax.websocket.server.ServerEndpointConfig;
+
+import org.eclipse.jetty.websocket.api.InvalidWebSocketException;
+import org.eclipse.jetty.websocket.jsr356.annotations.AnnotatedEndpointMetadata;
+import org.eclipse.jetty.websocket.jsr356.annotations.IJsrParamId;
+
+public class AnnotatedServerEndpointMetadata extends AnnotatedEndpointMetadata<ServerEndpoint,ServerEndpointConfig> implements ServerEndpointMetadata
+{
+ private final ServerEndpoint endpoint;
+ private final AnnotatedServerEndpointConfig config;
+
+ protected AnnotatedServerEndpointMetadata(ServerContainer container, Class<?> websocket, ServerEndpointConfig baseConfig) throws DeploymentException
+ {
+ super(websocket);
+
+ ServerEndpoint anno = websocket.getAnnotation(ServerEndpoint.class);
+ if (anno == null)
+ {
+ throw new InvalidWebSocketException("Unsupported WebSocket object, missing @" + ServerEndpoint.class + " annotation");
+ }
+
+ this.endpoint = anno;
+ this.config = new AnnotatedServerEndpointConfig(websocket,anno,baseConfig);
+
+ getDecoders().addAll(anno.decoders());
+ getEncoders().addAll(anno.encoders());
+ }
+
+ @Override
+ public void customizeParamsOnClose(LinkedList<IJsrParamId> params)
+ {
+ super.customizeParamsOnClose(params);
+ params.addFirst(JsrPathParamId.INSTANCE);
+ }
+
+ @Override
+ public void customizeParamsOnError(LinkedList<IJsrParamId> params)
+ {
+ super.customizeParamsOnError(params);
+ params.addFirst(JsrPathParamId.INSTANCE);
+ }
+
+ @Override
+ public void customizeParamsOnOpen(LinkedList<IJsrParamId> params)
+ {
+ super.customizeParamsOnOpen(params);
+ params.addFirst(JsrPathParamId.INSTANCE);
+ }
+
+ @Override
+ public void customizeParamsOnMessage(LinkedList<IJsrParamId> params)
+ {
+ super.customizeParamsOnMessage(params);
+ params.addFirst(JsrPathParamId.INSTANCE);
+ }
+
+ @Override
+ public ServerEndpoint getAnnotation()
+ {
+ return endpoint;
+ }
+
+ public AnnotatedServerEndpointConfig getConfig()
+ {
+ return config;
+ }
+
+ public String getPath()
+ {
+ return config.getPath();
+ }
+
+ @Override
+ public String toString()
+ {
+ StringBuilder builder = new StringBuilder();
+ builder.append("AnnotatedServerEndpointMetadata[endpoint=");
+ builder.append(endpoint);
+ builder.append(",config=");
+ builder.append(config);
+ builder.append("]");
+ return builder.toString();
+ }
+}
diff --git a/jetty-websocket/javax-websocket-server-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/server/BasicServerEndpointConfig.java b/jetty-websocket/javax-websocket-server-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/server/BasicServerEndpointConfig.java
new file mode 100644
index 0000000..c77b01f
--- /dev/null
+++ b/jetty-websocket/javax-websocket-server-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/server/BasicServerEndpointConfig.java
@@ -0,0 +1,122 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.server;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import javax.websocket.Decoder;
+import javax.websocket.Encoder;
+import javax.websocket.Extension;
+import javax.websocket.server.ServerEndpointConfig;
+
+public class BasicServerEndpointConfig implements ServerEndpointConfig
+{
+ private final List<Class<? extends Decoder>> decoders;
+ private final List<Class<? extends Encoder>> encoders;
+ private final List<Extension> extensions;
+ private final List<String> subprotocols;
+ private final Configurator configurator;
+ private final Class<?> endpointClass;
+ private final String path;
+ private Map<String, Object> userProperties;
+
+ public BasicServerEndpointConfig(Class<?> endpointClass, String path)
+ {
+ this.endpointClass = endpointClass;
+ this.path = path;
+
+ this.decoders = new ArrayList<>();
+ this.encoders = new ArrayList<>();
+ this.subprotocols = new ArrayList<>();
+ this.extensions = new ArrayList<>();
+ this.userProperties = new HashMap<>();
+ this.configurator = BasicServerEndpointConfigurator.INSTANCE;
+ }
+
+ public BasicServerEndpointConfig(ServerEndpointConfig copy)
+ {
+ this.endpointClass = copy.getEndpointClass();
+ this.path = copy.getPath();
+
+ this.decoders = new ArrayList<>(copy.getDecoders());
+ this.encoders = new ArrayList<>(copy.getEncoders());
+ this.subprotocols = new ArrayList<>(copy.getSubprotocols());
+ this.extensions = new ArrayList<>(copy.getExtensions());
+ this.userProperties = new HashMap<>(copy.getUserProperties());
+ if (copy.getConfigurator() != null)
+ {
+ this.configurator = copy.getConfigurator();
+ }
+ else
+ {
+ this.configurator = BasicServerEndpointConfigurator.INSTANCE;
+ }
+ }
+
+ @Override
+ public List<Class<? extends Encoder>> getEncoders()
+ {
+ return encoders;
+ }
+
+ @Override
+ public List<Class<? extends Decoder>> getDecoders()
+ {
+ return decoders;
+ }
+
+ @Override
+ public Map<String, Object> getUserProperties()
+ {
+ return userProperties;
+ }
+
+ @Override
+ public Class<?> getEndpointClass()
+ {
+ return endpointClass;
+ }
+
+ @Override
+ public String getPath()
+ {
+ return path;
+ }
+
+ @Override
+ public List<String> getSubprotocols()
+ {
+ return subprotocols;
+ }
+
+ @Override
+ public List<Extension> getExtensions()
+ {
+ return extensions;
+ }
+
+ @Override
+ public Configurator getConfigurator()
+ {
+ return configurator;
+ }
+}
diff --git a/jetty-websocket/javax-websocket-server-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/server/BasicServerEndpointConfigurator.java b/jetty-websocket/javax-websocket-server-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/server/BasicServerEndpointConfigurator.java
new file mode 100644
index 0000000..d95c469
--- /dev/null
+++ b/jetty-websocket/javax-websocket-server-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/server/BasicServerEndpointConfigurator.java
@@ -0,0 +1,82 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.server;
+
+import java.util.List;
+
+import javax.websocket.Extension;
+import javax.websocket.HandshakeResponse;
+import javax.websocket.server.HandshakeRequest;
+import javax.websocket.server.ServerEndpointConfig;
+import javax.websocket.server.ServerEndpointConfig.Configurator;
+
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+
+public class BasicServerEndpointConfigurator extends Configurator
+{
+ private static final Logger LOG = Log.getLogger(BasicServerEndpointConfigurator.class);
+ public static final Configurator INSTANCE = new BasicServerEndpointConfigurator();
+
+ @Override
+ public boolean checkOrigin(String originHeaderValue)
+ {
+ return true;
+ }
+
+ @Override
+ public <T> T getEndpointInstance(Class<T> endpointClass) throws InstantiationException
+ {
+ LOG.debug(".getEndpointInstance({})",endpointClass);
+ try
+ {
+ return endpointClass.newInstance();
+ }
+ catch (IllegalAccessException e)
+ {
+ throw new InstantiationException(String.format("%s: %s",e.getClass().getName(),e.getMessage()));
+ }
+ }
+
+ @Override
+ public List<Extension> getNegotiatedExtensions(List<Extension> installed, List<Extension> requested)
+ {
+ /* do nothing */
+ return null;
+ }
+
+ @Override
+ public String getNegotiatedSubprotocol(List<String> supported, List<String> requested)
+ {
+ for (String possible : requested)
+ {
+ if (supported.contains(possible))
+ {
+ return possible;
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public void modifyHandshake(ServerEndpointConfig sec, HandshakeRequest request, HandshakeResponse response)
+ {
+ /* do nothing */
+ }
+}
diff --git a/jetty-websocket/javax-websocket-server-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/server/JsrCreator.java b/jetty-websocket/javax-websocket-server-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/server/JsrCreator.java
new file mode 100644
index 0000000..0bde7da
--- /dev/null
+++ b/jetty-websocket/javax-websocket-server-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/server/JsrCreator.java
@@ -0,0 +1,109 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.server;
+
+import java.io.IOException;
+import java.util.List;
+
+import javax.websocket.server.ServerEndpointConfig;
+
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+import org.eclipse.jetty.websocket.jsr356.endpoints.EndpointInstance;
+import org.eclipse.jetty.websocket.jsr356.server.pathmap.WebSocketPathSpec;
+import org.eclipse.jetty.websocket.server.pathmap.PathSpec;
+import org.eclipse.jetty.websocket.servlet.ServletUpgradeRequest;
+import org.eclipse.jetty.websocket.servlet.ServletUpgradeResponse;
+import org.eclipse.jetty.websocket.servlet.WebSocketCreator;
+
+public class JsrCreator implements WebSocketCreator
+{
+ private static final Logger LOG = Log.getLogger(JsrCreator.class);
+ private final ServerEndpointMetadata metadata;
+
+ public JsrCreator(ServerEndpointMetadata metadata)
+ {
+ this.metadata = metadata;
+ }
+
+ @Override
+ public Object createWebSocket(ServletUpgradeRequest req, ServletUpgradeResponse resp)
+ {
+ JsrHandshakeRequest hsreq = new JsrHandshakeRequest(req);
+ JsrHandshakeResponse hsresp = new JsrHandshakeResponse(resp);
+
+ ServerEndpointConfig config = metadata.getConfig();
+
+ ServerEndpointConfig.Configurator configurator = config.getConfigurator();
+
+ // modify handshake
+ configurator.modifyHandshake(config,hsreq,hsresp);
+
+ // check origin
+ if (!configurator.checkOrigin(req.getOrigin()))
+ {
+ try
+ {
+ resp.sendForbidden("Origin mismatch");
+ }
+ catch (IOException e)
+ {
+ LOG.debug("Unable to send error response",e);
+ }
+ return null;
+ }
+
+ // deal with sub protocols
+ List<String> supported = config.getSubprotocols();
+ List<String> requested = req.getSubProtocols();
+ String subprotocol = configurator.getNegotiatedSubprotocol(supported,requested);
+ if (subprotocol != null)
+ {
+ resp.setAcceptedSubProtocol(subprotocol);
+ }
+
+ // create endpoint class
+ try
+ {
+ Class<?> endpointClass = config.getEndpointClass();
+ Object endpoint = config.getConfigurator().getEndpointInstance(endpointClass);
+ PathSpec pathSpec = hsreq.getRequestPathSpec();
+ if (pathSpec instanceof WebSocketPathSpec)
+ {
+ // We have a PathParam path spec
+ WebSocketPathSpec wspathSpec = (WebSocketPathSpec)pathSpec;
+ String requestPath = req.getRequestPath();
+ // Wrap the config with the path spec information
+ config = new PathParamServerEndpointConfig(config,wspathSpec,requestPath);
+ }
+ return new EndpointInstance(endpoint,config,metadata);
+ }
+ catch (InstantiationException e)
+ {
+ LOG.debug("Unable to create websocket: " + config.getEndpointClass().getName(),e);
+ return null;
+ }
+ }
+
+ @Override
+ public String toString()
+ {
+ return String.format("%s[metadata=%s]",this.getClass().getName(),metadata);
+ }
+}
diff --git a/jetty-websocket/javax-websocket-server-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/server/JsrHandshakeRequest.java b/jetty-websocket/javax-websocket-server-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/server/JsrHandshakeRequest.java
new file mode 100644
index 0000000..f004efb
--- /dev/null
+++ b/jetty-websocket/javax-websocket-server-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/server/JsrHandshakeRequest.java
@@ -0,0 +1,86 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.server;
+
+import java.net.URI;
+import java.security.Principal;
+import java.util.List;
+import java.util.Map;
+
+import javax.websocket.server.HandshakeRequest;
+
+import org.eclipse.jetty.websocket.server.pathmap.PathSpec;
+import org.eclipse.jetty.websocket.servlet.ServletUpgradeRequest;
+
+public class JsrHandshakeRequest implements HandshakeRequest
+{
+ private final ServletUpgradeRequest request;
+
+ public JsrHandshakeRequest(ServletUpgradeRequest req)
+ {
+ this.request = req;
+ }
+
+ @Override
+ public Map<String, List<String>> getHeaders()
+ {
+ return request.getHeaders();
+ }
+
+ @Override
+ public Object getHttpSession()
+ {
+ return request.getSession();
+ }
+
+ @Override
+ public Map<String, List<String>> getParameterMap()
+ {
+ return request.getParameterMap();
+ }
+
+ @Override
+ public String getQueryString()
+ {
+ return request.getQueryString();
+ }
+
+ public PathSpec getRequestPathSpec()
+ {
+ return (PathSpec)request.getServletAttribute(PathSpec.class.getName());
+ }
+
+ @Override
+ public URI getRequestURI()
+ {
+ return request.getRequestURI();
+ }
+
+ @Override
+ public Principal getUserPrincipal()
+ {
+ return request.getUserPrincipal();
+ }
+
+ @Override
+ public boolean isUserInRole(String role)
+ {
+ return request.isUserInRole(role);
+ }
+}
diff --git a/jetty-websocket/javax-websocket-server-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/server/JsrHandshakeResponse.java b/jetty-websocket/javax-websocket-server-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/server/JsrHandshakeResponse.java
new file mode 100644
index 0000000..0eaf62f
--- /dev/null
+++ b/jetty-websocket/javax-websocket-server-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/server/JsrHandshakeResponse.java
@@ -0,0 +1,42 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.server;
+
+import java.util.List;
+import java.util.Map;
+
+import javax.websocket.HandshakeResponse;
+
+import org.eclipse.jetty.websocket.api.UpgradeResponse;
+
+public class JsrHandshakeResponse implements HandshakeResponse
+{
+ private final UpgradeResponse response;
+
+ public JsrHandshakeResponse(UpgradeResponse resp)
+ {
+ this.response = resp;
+ }
+
+ @Override
+ public Map<String, List<String>> getHeaders()
+ {
+ return response.getHeaders();
+ }
+}
diff --git a/jetty-websocket/javax-websocket-server-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/server/JsrPathParamId.java b/jetty-websocket/javax-websocket-server-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/server/JsrPathParamId.java
new file mode 100644
index 0000000..bc7ddac
--- /dev/null
+++ b/jetty-websocket/javax-websocket-server-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/server/JsrPathParamId.java
@@ -0,0 +1,49 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.server;
+
+import javax.websocket.server.PathParam;
+
+import org.eclipse.jetty.websocket.common.events.annotated.InvalidSignatureException;
+import org.eclipse.jetty.websocket.jsr356.annotations.IJsrParamId;
+import org.eclipse.jetty.websocket.jsr356.annotations.JsrCallable;
+import org.eclipse.jetty.websocket.jsr356.annotations.Param;
+import org.eclipse.jetty.websocket.jsr356.annotations.Param.Role;
+
+/**
+ * Param handling for static parameters annotated with @{@link PathParam}
+ */
+public class JsrPathParamId implements IJsrParamId
+{
+ public static final IJsrParamId INSTANCE = new JsrPathParamId();
+
+ @Override
+ public boolean process(Param param, JsrCallable callable) throws InvalidSignatureException
+ {
+ PathParam pathparam = param.getAnnotation(PathParam.class);
+ if(pathparam != null)
+ {
+ param.bind(Role.PATH_PARAM);
+ param.setPathParamName(pathparam.value());
+ return true;
+ }
+
+ return false;
+ }
+}
diff --git a/jetty-websocket/javax-websocket-server-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/server/JsrServerEndpointImpl.java b/jetty-websocket/javax-websocket-server-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/server/JsrServerEndpointImpl.java
new file mode 100644
index 0000000..6020ab4
--- /dev/null
+++ b/jetty-websocket/javax-websocket-server-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/server/JsrServerEndpointImpl.java
@@ -0,0 +1,79 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.server;
+
+import javax.websocket.server.ServerEndpoint;
+import javax.websocket.server.ServerEndpointConfig;
+
+import org.eclipse.jetty.websocket.api.WebSocketPolicy;
+import org.eclipse.jetty.websocket.common.events.EventDriver;
+import org.eclipse.jetty.websocket.common.events.EventDriverImpl;
+import org.eclipse.jetty.websocket.jsr356.annotations.JsrEvents;
+import org.eclipse.jetty.websocket.jsr356.endpoints.EndpointInstance;
+import org.eclipse.jetty.websocket.jsr356.endpoints.JsrAnnotatedEventDriver;
+
+/**
+ * Event Driver for classes annotated with @{@link ServerEndpoint}
+ */
+public class JsrServerEndpointImpl implements EventDriverImpl
+{
+ @Override
+ public EventDriver create(Object websocket, WebSocketPolicy policy) throws Throwable
+ {
+ if (!(websocket instanceof EndpointInstance))
+ {
+ throw new IllegalStateException(String.format("Websocket %s must be an %s",websocket.getClass().getName(),EndpointInstance.class.getName()));
+ }
+
+ EndpointInstance ei = (EndpointInstance)websocket;
+ AnnotatedServerEndpointMetadata metadata = (AnnotatedServerEndpointMetadata)ei.getMetadata();
+ JsrEvents<ServerEndpoint, ServerEndpointConfig> events = new JsrEvents<>(metadata);
+ JsrAnnotatedEventDriver driver = new JsrAnnotatedEventDriver(policy,ei,events);
+
+ ServerEndpointConfig config = (ServerEndpointConfig)ei.getConfig();
+ if (config instanceof PathParamServerEndpointConfig)
+ {
+ PathParamServerEndpointConfig ppconfig = (PathParamServerEndpointConfig)config;
+ driver.setRequestParameters(ppconfig.getPathParamMap());
+ }
+
+ return driver;
+ }
+
+ @Override
+ public String describeRule()
+ {
+ return "class is annotated with @" + ServerEndpoint.class.getName();
+ }
+
+ @Override
+ public boolean supports(Object websocket)
+ {
+ if (!(websocket instanceof EndpointInstance))
+ {
+ return false;
+ }
+
+ EndpointInstance ei = (EndpointInstance)websocket;
+ Object endpoint = ei.getEndpoint();
+
+ ServerEndpoint anno = endpoint.getClass().getAnnotation(ServerEndpoint.class);
+ return (anno != null);
+ }
+}
diff --git a/jetty-websocket/javax-websocket-server-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/server/PathParamServerEndpointConfig.java b/jetty-websocket/javax-websocket-server-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/server/PathParamServerEndpointConfig.java
new file mode 100644
index 0000000..758b606
--- /dev/null
+++ b/jetty-websocket/javax-websocket-server-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/server/PathParamServerEndpointConfig.java
@@ -0,0 +1,51 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.server;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.websocket.server.ServerEndpointConfig;
+
+import org.eclipse.jetty.websocket.jsr356.server.pathmap.WebSocketPathSpec;
+
+/**
+ * Wrapper for a {@link ServerEndpointConfig} where there PathParm information from the incoming request.
+ */
+public class PathParamServerEndpointConfig extends BasicServerEndpointConfig implements ServerEndpointConfig
+{
+ private final Map<String, String> pathParamMap;
+
+ public PathParamServerEndpointConfig(ServerEndpointConfig config, WebSocketPathSpec pathSpec, String requestPath)
+ {
+ super(config);
+
+ Map<String, String> pathMap = pathSpec.getPathParams(requestPath);
+ pathParamMap = new HashMap<String, String>();
+ if (pathMap != null)
+ {
+ pathParamMap.putAll(pathMap);
+ }
+ }
+
+ public Map<String, String> getPathParamMap()
+ {
+ return pathParamMap;
+ }
+}
diff --git a/jetty-websocket/javax-websocket-server-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/server/ServerContainer.java b/jetty-websocket/javax-websocket-server-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/server/ServerContainer.java
new file mode 100644
index 0000000..e10026d
--- /dev/null
+++ b/jetty-websocket/javax-websocket-server-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/server/ServerContainer.java
@@ -0,0 +1,201 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.server;
+
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import javax.websocket.DeploymentException;
+import javax.websocket.Endpoint;
+import javax.websocket.server.ServerEndpoint;
+import javax.websocket.server.ServerEndpointConfig;
+
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+import org.eclipse.jetty.websocket.common.events.EventDriverFactory;
+import org.eclipse.jetty.websocket.jsr356.ClientContainer;
+import org.eclipse.jetty.websocket.jsr356.JsrSessionFactory;
+import org.eclipse.jetty.websocket.jsr356.annotations.AnnotatedEndpointScanner;
+import org.eclipse.jetty.websocket.jsr356.endpoints.EndpointInstance;
+import org.eclipse.jetty.websocket.jsr356.endpoints.JsrEndpointImpl;
+import org.eclipse.jetty.websocket.jsr356.metadata.EndpointMetadata;
+import org.eclipse.jetty.websocket.jsr356.server.pathmap.WebSocketPathSpec;
+import org.eclipse.jetty.websocket.server.MappedWebSocketCreator;
+import org.eclipse.jetty.websocket.server.WebSocketServerFactory;
+
+public class ServerContainer extends ClientContainer implements javax.websocket.server.ServerContainer
+{
+ private static final Logger LOG = Log.getLogger(ServerContainer.class);
+
+ private final MappedWebSocketCreator mappedCreator;
+ private final WebSocketServerFactory webSocketServerFactory;
+ private final Map<Class<?>, ServerEndpointMetadata> endpointServerMetadataCache = new ConcurrentHashMap<>();
+
+ public ServerContainer(MappedWebSocketCreator creator, WebSocketServerFactory factory)
+ {
+ super();
+ this.mappedCreator = creator;
+ this.webSocketServerFactory = factory;
+ EventDriverFactory eventDriverFactory = this.webSocketServerFactory.getEventDriverFactory();
+ eventDriverFactory.addImplementation(new JsrServerEndpointImpl());
+ eventDriverFactory.addImplementation(new JsrEndpointImpl());
+ this.webSocketServerFactory.addSessionFactory(new JsrSessionFactory(this));
+ }
+
+ @Override
+ protected void doStop() throws Exception
+ {
+ endpointServerMetadataCache.clear();
+ super.doStop();
+ }
+
+ public EndpointInstance newClientEndpointInstance(Object endpoint, ServerEndpointConfig config, String path)
+ {
+ EndpointMetadata metadata = getClientEndpointMetadata(endpoint.getClass());
+ ServerEndpointConfig cec = config;
+ if (config == null)
+ {
+ if (metadata instanceof AnnotatedServerEndpointMetadata)
+ {
+ cec = ((AnnotatedServerEndpointMetadata)metadata).getConfig();
+ }
+ else
+ {
+ cec = new BasicServerEndpointConfig(endpoint.getClass(),path);
+ }
+ }
+ return new EndpointInstance(endpoint,cec,metadata);
+ }
+
+ @Override
+ public void addEndpoint(Class<?> endpointClass) throws DeploymentException
+ {
+ ServerEndpointMetadata metadata = getServerEndpointMetadata(endpointClass,null);
+ addEndpoint(metadata);
+ }
+
+ public void addEndpoint(ServerEndpointMetadata metadata) throws DeploymentException
+ {
+ JsrCreator creator = new JsrCreator(metadata);
+ mappedCreator.addMapping(new WebSocketPathSpec(metadata.getPath()),creator);
+ }
+
+ @Override
+ public void addEndpoint(ServerEndpointConfig config) throws DeploymentException
+ {
+ LOG.debug("addEndpoint({})",config);
+ ServerEndpointMetadata metadata = getServerEndpointMetadata(config.getEndpointClass(),config);
+ addEndpoint(metadata);
+ }
+
+ public ServerEndpointMetadata getServerEndpointMetadata(Class<?> endpoint, ServerEndpointConfig config) throws DeploymentException
+ {
+ synchronized (endpointServerMetadataCache)
+ {
+ ServerEndpointMetadata metadata = endpointServerMetadataCache.get(endpoint);
+ if (metadata != null)
+ {
+ return metadata;
+ }
+
+ ServerEndpoint anno = endpoint.getAnnotation(ServerEndpoint.class);
+ if (anno != null)
+ {
+ // Annotated takes precedence here
+ AnnotatedServerEndpointMetadata ametadata = new AnnotatedServerEndpointMetadata(this,endpoint,config);
+ AnnotatedEndpointScanner<ServerEndpoint,ServerEndpointConfig> scanner = new AnnotatedEndpointScanner<>(ametadata);
+ metadata = ametadata;
+ scanner.scan();
+ }
+ else if (Endpoint.class.isAssignableFrom(endpoint))
+ {
+ // extends Endpoint
+ @SuppressWarnings("unchecked")
+ Class<? extends Endpoint> eendpoint = (Class<? extends Endpoint>)endpoint;
+ metadata = new SimpleServerEndpointMetadata(eendpoint,config);
+ }
+ else
+ {
+ StringBuilder err = new StringBuilder();
+ err.append("Not a recognized websocket [");
+ err.append(endpoint.getName());
+ err.append("] does not extend @").append(ServerEndpoint.class.getName());
+ err.append(" or extend from ").append(Endpoint.class.getName());
+ throw new DeploymentException("Unable to identify as valid Endpoint: " + endpoint);
+ }
+
+ endpointServerMetadataCache.put(endpoint,metadata);
+
+ return metadata;
+ }
+ }
+
+ @Override
+ public long getDefaultAsyncSendTimeout()
+ {
+ return webSocketServerFactory.getPolicy().getAsyncWriteTimeout();
+ }
+
+ @Override
+ public int getDefaultMaxBinaryMessageBufferSize()
+ {
+ return webSocketServerFactory.getPolicy().getMaxBinaryMessageSize();
+ }
+
+ @Override
+ public long getDefaultMaxSessionIdleTimeout()
+ {
+ return webSocketServerFactory.getPolicy().getIdleTimeout();
+ }
+
+ @Override
+ public int getDefaultMaxTextMessageBufferSize()
+ {
+ return webSocketServerFactory.getPolicy().getMaxTextMessageSize();
+ }
+
+ @Override
+ public void setAsyncSendTimeout(long ms)
+ {
+ webSocketServerFactory.getPolicy().setAsyncWriteTimeout(ms);
+ }
+
+ @Override
+ public void setDefaultMaxBinaryMessageBufferSize(int max)
+ {
+ // overall message limit (used in non-streaming)
+ webSocketServerFactory.getPolicy().setMaxBinaryMessageSize(max);
+ // incoming streaming buffer size
+ webSocketServerFactory.getPolicy().setMaxBinaryMessageBufferSize(max);
+ }
+
+ @Override
+ public void setDefaultMaxSessionIdleTimeout(long ms)
+ {
+ webSocketServerFactory.getPolicy().setIdleTimeout(ms);
+ }
+
+ @Override
+ public void setDefaultMaxTextMessageBufferSize(int max)
+ {
+ // overall message limit (used in non-streaming)
+ webSocketServerFactory.getPolicy().setMaxTextMessageSize(max);
+ // incoming streaming buffer size
+ webSocketServerFactory.getPolicy().setMaxTextMessageBufferSize(max);
+ }
+}
diff --git a/jetty-websocket/javax-websocket-server-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/server/ServerEndpointMetadata.java b/jetty-websocket/javax-websocket-server-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/server/ServerEndpointMetadata.java
new file mode 100644
index 0000000..25e98c5
--- /dev/null
+++ b/jetty-websocket/javax-websocket-server-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/server/ServerEndpointMetadata.java
@@ -0,0 +1,30 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.server;
+
+import javax.websocket.server.ServerEndpointConfig;
+
+import org.eclipse.jetty.websocket.jsr356.metadata.EndpointMetadata;
+
+public interface ServerEndpointMetadata extends EndpointMetadata
+{
+ ServerEndpointConfig getConfig();
+
+ public String getPath();
+}
diff --git a/jetty-websocket/javax-websocket-server-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/server/SimpleServerEndpointMetadata.java b/jetty-websocket/javax-websocket-server-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/server/SimpleServerEndpointMetadata.java
new file mode 100644
index 0000000..5864268
--- /dev/null
+++ b/jetty-websocket/javax-websocket-server-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/server/SimpleServerEndpointMetadata.java
@@ -0,0 +1,52 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.server;
+
+import javax.websocket.Endpoint;
+import javax.websocket.server.ServerEndpointConfig;
+
+import org.eclipse.jetty.websocket.jsr356.client.SimpleEndpointMetadata;
+
+public class SimpleServerEndpointMetadata extends SimpleEndpointMetadata implements ServerEndpointMetadata
+{
+ private final ServerEndpointConfig config;
+
+ public SimpleServerEndpointMetadata(Class<? extends Endpoint> endpointClass, ServerEndpointConfig config)
+ {
+ super(endpointClass);
+ this.config = config;
+ if (this.config != null)
+ {
+ getDecoders().addAll(config.getDecoders());
+ getEncoders().addAll(config.getEncoders());
+ }
+ }
+
+ @Override
+ public ServerEndpointConfig getConfig()
+ {
+ return config;
+ }
+
+ @Override
+ public String getPath()
+ {
+ return config.getPath();
+ }
+}
diff --git a/jetty-websocket/javax-websocket-server-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/server/WebSocketConfiguration.java b/jetty-websocket/javax-websocket-server-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/server/WebSocketConfiguration.java
new file mode 100644
index 0000000..4ecc8fc
--- /dev/null
+++ b/jetty-websocket/javax-websocket-server-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/server/WebSocketConfiguration.java
@@ -0,0 +1,71 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.server;
+
+import org.eclipse.jetty.annotations.AnnotationConfiguration;
+import org.eclipse.jetty.servlet.ServletContextHandler;
+import org.eclipse.jetty.webapp.AbstractConfiguration;
+import org.eclipse.jetty.webapp.Configuration;
+import org.eclipse.jetty.webapp.WebAppContext;
+import org.eclipse.jetty.websocket.jsr356.server.deploy.DiscoveredEndpoints;
+import org.eclipse.jetty.websocket.jsr356.server.deploy.ServerEndpointAnnotationHandler;
+import org.eclipse.jetty.websocket.server.WebSocketUpgradeFilter;
+
+/**
+ * WebSocket Server Configuration component
+ */
+public class WebSocketConfiguration extends AbstractConfiguration
+{
+ public static ServerContainer configureContext(ServletContextHandler context)
+ {
+ WebSocketUpgradeFilter filter = WebSocketUpgradeFilter.configureContext(context);
+
+ // Create the Jetty ServerContainer implementation
+ ServerContainer jettyContainer = new ServerContainer(filter,filter.getFactory());
+ context.addBean(jettyContainer);
+
+ // Store a reference to the ServerContainer per javax.websocket spec 1.0 final section 6.4 Programmatic Server Deployment
+ context.setAttribute(javax.websocket.server.ServerContainer.class.getName(),jettyContainer);
+
+ // Store reference to DiscoveredEndpoints
+ context.setAttribute(DiscoveredEndpoints.class.getName(),new DiscoveredEndpoints());
+
+ return jettyContainer;
+ }
+
+ @Override
+ public void configure(WebAppContext context) throws Exception
+ {
+ WebSocketConfiguration.configureContext(context);
+ }
+
+ @Override
+ public void preConfigure(WebAppContext context) throws Exception
+ {
+ // Add the annotation scanning handlers (if annotation scanning enabled)
+ for (Configuration config : context.getConfigurations())
+ {
+ if (config instanceof AnnotationConfiguration)
+ {
+ AnnotationConfiguration annocfg = (AnnotationConfiguration)config;
+ annocfg.addDiscoverableAnnotationHandler(new ServerEndpointAnnotationHandler(context));
+ }
+ }
+ }
+}
diff --git a/jetty-websocket/javax-websocket-server-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/server/deploy/DiscoveredEndpoints.java b/jetty-websocket/javax-websocket-server-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/server/deploy/DiscoveredEndpoints.java
new file mode 100644
index 0000000..65cf840
--- /dev/null
+++ b/jetty-websocket/javax-websocket-server-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/server/deploy/DiscoveredEndpoints.java
@@ -0,0 +1,158 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.server.deploy;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.util.HashSet;
+import java.util.Set;
+
+import javax.websocket.Endpoint;
+
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+
+/**
+ * Tracking for Discovered Endpoints.
+ * <p>
+ * This is a collection of endpoints, grouped by type (by Annotation or by extending Endpoint). Along with some better error messages for conflicting endpoints.
+ */
+public class DiscoveredEndpoints
+{
+ private static class LocatedClass
+ {
+ private Class<?> clazz;
+
+ private URI location;
+ public LocatedClass(Class<?> clazz)
+ {
+ this.clazz = clazz;
+ this.location = getArchiveURI(clazz);
+ }
+
+ @Override
+ public String toString()
+ {
+ StringBuilder builder = new StringBuilder();
+ builder.append("LocatedClas[");
+ builder.append(clazz.getName());
+ builder.append("]");
+ return builder.toString();
+ }
+ }
+
+ private static final Logger LOG = Log.getLogger(DiscoveredEndpoints.class);
+
+ public static URI getArchiveURI(Class<?> clazz)
+ {
+ String resourceName = clazz.getName().replace('.','/') + ".class";
+ URL classUrl = clazz.getClassLoader().getResource(resourceName);
+ if (classUrl == null)
+ {
+ // is this even possible at this point?
+ return null;
+ }
+ try
+ {
+ URI uri = classUrl.toURI();
+ String scheme = uri.getScheme();
+ if (scheme.equalsIgnoreCase("jar"))
+ {
+ String ssp = uri.getSchemeSpecificPart();
+ int idx = ssp.indexOf("!/");
+ if (idx >= 0)
+ {
+ ssp = ssp.substring(0,idx);
+ }
+ return URI.create(ssp);
+ }
+ }
+ catch (URISyntaxException e)
+ {
+ LOG.warn("Class reference not a valid URI? " + classUrl,e);
+ }
+
+ return null;
+ }
+ private Set<LocatedClass> extendedEndpoints;
+
+ private Set<LocatedClass> annotatedEndpoints;
+
+ public DiscoveredEndpoints()
+ {
+ extendedEndpoints = new HashSet<>();
+ annotatedEndpoints = new HashSet<>();
+ }
+
+ public void addAnnotatedEndpoint(Class<?> endpoint)
+ {
+ this.annotatedEndpoints.add(new LocatedClass(endpoint));
+ }
+
+ public void addExtendedEndpoint(Class<? extends Endpoint> endpoint)
+ {
+ this.extendedEndpoints.add(new LocatedClass(endpoint));
+ }
+
+ public Set<Class<?>> getAnnotatedEndpoints()
+ {
+ Set<Class<?>> endpoints = new HashSet<>();
+ for (LocatedClass lc : annotatedEndpoints)
+ {
+ endpoints.add(lc.clazz);
+ }
+ return endpoints;
+ }
+
+ public void getArchiveSpecificAnnnotatedEndpoints(URI archiveURI, Set<Class<?>> archiveSpecificEndpoints)
+ {
+ for (LocatedClass lc : annotatedEndpoints)
+ {
+ if ((archiveURI == null) || lc.location.getPath().equals(archiveURI.getPath()))
+ {
+ archiveSpecificEndpoints.add(lc.clazz);
+ }
+ }
+ }
+
+ @SuppressWarnings("unchecked")
+ public void getArchiveSpecificExtendedEndpoints(URI archiveURI, Set<Class<? extends Endpoint>> archiveSpecificEndpoints)
+ {
+ for (LocatedClass lc : extendedEndpoints)
+ {
+ if ((archiveURI == null) || (lc.location.getPath().equals(archiveURI.getPath()) && Endpoint.class.isAssignableFrom(lc.clazz)))
+ {
+ archiveSpecificEndpoints.add((Class<? extends Endpoint>)lc.clazz);
+ }
+ }
+ }
+
+ @Override
+ public String toString()
+ {
+ StringBuilder builder = new StringBuilder();
+ builder.append("DiscoveredEndpoints [extendedEndpoints=");
+ builder.append(extendedEndpoints);
+ builder.append(", annotatedEndpoints=");
+ builder.append(annotatedEndpoints);
+ builder.append("]");
+ return builder.toString();
+ }
+}
diff --git a/jetty-websocket/javax-websocket-server-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/server/deploy/ServerApplicationConfigListener.java b/jetty-websocket/javax-websocket-server-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/server/deploy/ServerApplicationConfigListener.java
new file mode 100644
index 0000000..5b82dff
--- /dev/null
+++ b/jetty-websocket/javax-websocket-server-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/server/deploy/ServerApplicationConfigListener.java
@@ -0,0 +1,157 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.server.deploy;
+
+import java.net.URI;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+import javax.servlet.ServletContainerInitializer;
+import javax.servlet.ServletContext;
+import javax.servlet.ServletException;
+import javax.servlet.annotation.HandlesTypes;
+import javax.websocket.DeploymentException;
+import javax.websocket.Endpoint;
+import javax.websocket.server.ServerApplicationConfig;
+import javax.websocket.server.ServerEndpointConfig;
+
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+import org.eclipse.jetty.websocket.jsr356.server.ServerContainer;
+import org.eclipse.jetty.websocket.server.WebSocketUpgradeFilter;
+
+@HandlesTypes(
+{ ServerApplicationConfig.class, Endpoint.class })
+public class ServerApplicationConfigListener implements ServletContainerInitializer
+{
+ private static final Logger LOG = Log.getLogger(ServerApplicationConfigListener.class);
+
+ @Override
+ public void onStartup(Set<Class<?>> c, ServletContext ctx) throws ServletException
+ {
+ WebSocketUpgradeFilter filter = (WebSocketUpgradeFilter)ctx.getAttribute(WebSocketUpgradeFilter.class.getName());
+ if (filter == null)
+ {
+ LOG.warn("Required attribute not available: " + WebSocketUpgradeFilter.class.getName());
+ return;
+ }
+
+ DiscoveredEndpoints discovered = (DiscoveredEndpoints)ctx.getAttribute(DiscoveredEndpoints.class.getName());
+ if (discovered == null)
+ {
+ LOG.warn("Required attribute not available: " + DiscoveredEndpoints.class.getName());
+ return;
+ }
+
+ LOG.debug("Found {} classes",c.size());
+ LOG.debug("Discovered: {}",discovered);
+
+ // First add all of the Endpoints
+ addEndpoints(c,discovered);
+
+ // Now process the ServerApplicationConfig entries
+ ServerContainer container = (ServerContainer)ctx.getAttribute(javax.websocket.server.ServerContainer.class.getName());
+ Set<Class<? extends Endpoint>> archiveSpecificExtendEndpoints = new HashSet<>();
+ Set<Class<?>> archiveSpecificAnnotatedEndpoints = new HashSet<>();
+ List<Class<? extends ServerApplicationConfig>> serverAppConfigs = filterServerApplicationConfigs(c);
+
+ if(serverAppConfigs.size() >= 1) {
+ for (Class<? extends ServerApplicationConfig> clazz : filterServerApplicationConfigs(c))
+ {
+ LOG.debug("Found ServerApplicationConfig: {}",clazz);
+ try
+ {
+ ServerApplicationConfig config = (ServerApplicationConfig)clazz.newInstance();
+ URI archiveURI = DiscoveredEndpoints.getArchiveURI(clazz);
+ archiveSpecificExtendEndpoints.clear();
+ archiveSpecificAnnotatedEndpoints.clear();
+ discovered.getArchiveSpecificExtendedEndpoints(archiveURI,archiveSpecificExtendEndpoints);
+ discovered.getArchiveSpecificAnnnotatedEndpoints(archiveURI,archiveSpecificAnnotatedEndpoints);
+
+ Set<ServerEndpointConfig> seconfigs = config.getEndpointConfigs(archiveSpecificExtendEndpoints);
+ if (seconfigs != null)
+ {
+ for (ServerEndpointConfig sec : seconfigs)
+ {
+ container.addEndpoint(sec);
+ }
+ }
+ Set<Class<?>> annotatedClasses = config.getAnnotatedEndpointClasses(archiveSpecificAnnotatedEndpoints);
+ if (annotatedClasses != null)
+ {
+ for (Class<?> annotatedClass : annotatedClasses)
+ {
+ container.addEndpoint(annotatedClass);
+ }
+ }
+ }
+ catch (InstantiationException | IllegalAccessException e)
+ {
+ throw new ServletException("Unable to instantiate: " + clazz.getName(),e);
+ }
+ catch (DeploymentException e)
+ {
+ throw new ServletException(e);
+ }
+ }
+ } else {
+ // Default behavior (no ServerApplicationConfigs found)
+ // Note: it is impossible to determine path of "extends Endpoint" discovered classes
+ for(Class<?> annotatedClass: discovered.getAnnotatedEndpoints())
+ {
+ try
+ {
+ container.addEndpoint(annotatedClass);
+ }
+ catch (DeploymentException e)
+ {
+ throw new ServletException(e);
+ }
+ }
+ }
+ }
+
+ @SuppressWarnings("unchecked")
+ private List<Class<? extends ServerApplicationConfig>> filterServerApplicationConfigs(Set<Class<?>> c)
+ {
+ List<Class<? extends ServerApplicationConfig>> configs = new ArrayList<>();
+ for (Class<?> clazz : c)
+ {
+ if (ServerApplicationConfig.class.isAssignableFrom(clazz))
+ {
+ configs.add((Class<? extends ServerApplicationConfig>)clazz);
+ }
+ }
+ return configs;
+ }
+
+ @SuppressWarnings("unchecked")
+ private void addEndpoints(Set<Class<?>> c, DiscoveredEndpoints discovered)
+ {
+ for (Class<?> clazz : c)
+ {
+ if (Endpoint.class.isAssignableFrom(clazz))
+ {
+ discovered.addExtendedEndpoint((Class<? extends Endpoint>)clazz);
+ }
+ }
+ }
+}
diff --git a/jetty-websocket/javax-websocket-server-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/server/deploy/ServerEndpointAnnotation.java b/jetty-websocket/javax-websocket-server-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/server/deploy/ServerEndpointAnnotation.java
new file mode 100644
index 0000000..a0ba33c
--- /dev/null
+++ b/jetty-websocket/javax-websocket-server-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/server/deploy/ServerEndpointAnnotation.java
@@ -0,0 +1,64 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.server.deploy;
+
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+import org.eclipse.jetty.util.resource.Resource;
+import org.eclipse.jetty.webapp.DiscoveredAnnotation;
+import org.eclipse.jetty.webapp.WebAppContext;
+
+/**
+ * Once an Annotated Server Endpoint is discovered, add it to the list of
+ * discovered Annotated Endpoints.
+ */
+public class ServerEndpointAnnotation extends DiscoveredAnnotation
+{
+ private static final Logger LOG = Log.getLogger(ServerEndpointAnnotation.class);
+
+ public ServerEndpointAnnotation(WebAppContext context, String className)
+ {
+ super(context,className);
+ }
+
+ public ServerEndpointAnnotation(WebAppContext context, String className, Resource resource)
+ {
+ super(context,className,resource);
+ }
+
+ @Override
+ public void apply()
+ {
+ Class<?> clazz = getTargetClass();
+
+ if (clazz == null)
+ {
+ LOG.warn(_className + " cannot be loaded");
+ return;
+ }
+
+ DiscoveredEndpoints discovered = (DiscoveredEndpoints)_context.getAttribute(DiscoveredEndpoints.class.getName());
+ if(discovered == null) {
+ LOG.warn("Context attribute not found: " + DiscoveredEndpoints.class.getName());
+ return;
+ }
+
+ discovered.addAnnotatedEndpoint(clazz);
+ }
+}
diff --git a/jetty-websocket/javax-websocket-server-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/server/deploy/ServerEndpointAnnotationHandler.java b/jetty-websocket/javax-websocket-server-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/server/deploy/ServerEndpointAnnotationHandler.java
new file mode 100644
index 0000000..7b97671
--- /dev/null
+++ b/jetty-websocket/javax-websocket-server-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/server/deploy/ServerEndpointAnnotationHandler.java
@@ -0,0 +1,88 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.server.deploy;
+
+import java.util.List;
+
+import javax.websocket.server.ServerEndpoint;
+
+import org.eclipse.jetty.annotations.AbstractDiscoverableAnnotationHandler;
+import org.eclipse.jetty.annotations.AnnotationParser.Value;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+import org.eclipse.jetty.webapp.DiscoveredAnnotation;
+import org.eclipse.jetty.webapp.WebAppContext;
+
+/**
+ * Processing for @{@link ServerEndpoint} annotations during Web App Annotation Scanning
+ */
+public class ServerEndpointAnnotationHandler extends AbstractDiscoverableAnnotationHandler
+{
+ private static final String ANNOTATION_NAME = "javax.websocket.server.ServerEndpoint";
+ private static final Logger LOG = Log.getLogger(ServerEndpointAnnotationHandler.class);
+
+ public ServerEndpointAnnotationHandler(WebAppContext context)
+ {
+ super(context);
+ }
+
+ public ServerEndpointAnnotationHandler(WebAppContext context, List<DiscoveredAnnotation> list)
+ {
+ super(context,list);
+ }
+
+ @Override
+ public String getAnnotationName()
+ {
+ return ANNOTATION_NAME;
+ }
+
+ @Override
+ public void handleClass(String className, int version, int access, String signature, String superName, String[] interfaces, String annotationName,
+ List<Value> values)
+ {
+ if (LOG.isDebugEnabled())
+ {
+ LOG.debug("handleClass: {}, {}, {}",className,annotationName,values);
+ }
+
+ if (!ANNOTATION_NAME.equals(annotationName))
+ {
+ // Not the one we are interested in
+ return;
+ }
+
+ ServerEndpointAnnotation annotation = new ServerEndpointAnnotation(_context,className,_resource);
+ addAnnotation(annotation);
+ }
+
+ @Override
+ public void handleField(String className, String fieldName, int access, String fieldType, String signature, Object value, String annotation,
+ List<Value> values)
+ {
+ /* @ServerEndpoint annotation not supported for fields */
+ }
+
+ @Override
+ public void handleMethod(String className, String methodName, int access, String desc, String signature, String[] exceptions, String annotation,
+ List<Value> values)
+ {
+ /* @ServerEndpoint annotation not supported for methods */
+ }
+}
diff --git a/jetty-websocket/javax-websocket-server-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/server/pathmap/WebSocketPathSpec.java b/jetty-websocket/javax-websocket-server-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/server/pathmap/WebSocketPathSpec.java
new file mode 100644
index 0000000..42c76dc
--- /dev/null
+++ b/jetty-websocket/javax-websocket-server-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/server/pathmap/WebSocketPathSpec.java
@@ -0,0 +1,240 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.server.pathmap;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import javax.websocket.server.PathParam;
+import javax.websocket.server.ServerEndpoint;
+
+import org.eclipse.jetty.websocket.server.pathmap.PathSpecGroup;
+import org.eclipse.jetty.websocket.server.pathmap.RegexPathSpec;
+
+/**
+ * PathSpec for WebSocket @{@link ServerEndpoint} declarations with support for URI templates and @{@link PathParam} annotations
+ */
+public class WebSocketPathSpec extends RegexPathSpec
+{
+ private static final Pattern VARIABLE_PATTERN = Pattern.compile("\\{(.*)\\}");
+ private static final Pattern VALID_VARIABLE_NAME = Pattern.compile("[a-zA-Z0-9._-]+");
+ private static final Set<String> FORBIDDEN_SEGMENTS;
+
+ static
+ {
+ FORBIDDEN_SEGMENTS = new HashSet<>();
+ FORBIDDEN_SEGMENTS.add("/./");
+ FORBIDDEN_SEGMENTS.add("/../");
+ FORBIDDEN_SEGMENTS.add("//");
+ }
+
+ private String variables[];
+
+ public WebSocketPathSpec(String pathParamSpec)
+ {
+ super();
+ Objects.requireNonNull(pathParamSpec,"Path Param Spec cannot be null");
+
+ if ("".equals(pathParamSpec) || "/".equals(pathParamSpec))
+ {
+ super.pathSpec = "/";
+ super.pattern = Pattern.compile("^/$");
+ super.pathDepth = 1;
+ this.specLength = 1;
+ this.variables = new String[0];
+ this.group = PathSpecGroup.EXACT;
+ return;
+ }
+
+ if (pathParamSpec.charAt(0) != '/')
+ {
+ // path specs must start with '/'
+ StringBuilder err = new StringBuilder();
+ err.append("Syntax Error: path spec \"");
+ err.append(pathParamSpec);
+ err.append("\" must start with '/'");
+ throw new IllegalArgumentException(err.toString());
+ }
+
+ for (String forbidden : FORBIDDEN_SEGMENTS)
+ {
+ if (pathParamSpec.contains(forbidden))
+ {
+ StringBuilder err = new StringBuilder();
+ err.append("Syntax Error: segment ");
+ err.append(forbidden);
+ err.append(" is forbidden in path spec: ");
+ err.append(pathParamSpec);
+ throw new IllegalArgumentException(err.toString());
+ }
+ }
+
+ this.pathSpec = pathParamSpec;
+
+ StringBuilder regex = new StringBuilder();
+ regex.append('^');
+
+ List<String> varNames = new ArrayList<>();
+ // split up into path segments (ignoring the first slash that will always be empty)
+ String segments[] = pathParamSpec.substring(1).split("/");
+ char segmentSignature[] = new char[segments.length];
+ this.pathDepth = segments.length;
+ for (int i = 0; i < segments.length; i++)
+ {
+ String segment = segments[i];
+ Matcher mat = VARIABLE_PATTERN.matcher(segment);
+
+ if (mat.matches())
+ {
+ // entire path segment is a variable.
+ String variable = mat.group(1);
+ if (varNames.contains(variable))
+ {
+ // duplicate variable names
+ StringBuilder err = new StringBuilder();
+ err.append("Syntax Error: variable ");
+ err.append(variable);
+ err.append(" is duplicated in path spec: ");
+ err.append(pathParamSpec);
+ throw new IllegalArgumentException(err.toString());
+ }
+ else if (VALID_VARIABLE_NAME.matcher(variable).matches())
+ {
+ segmentSignature[i] = 'v'; // variable
+ // valid variable name
+ varNames.add(variable);
+ // build regex
+ regex.append("/([^/]+)");
+ }
+ else
+ {
+ // invalid variable name
+ StringBuilder err = new StringBuilder();
+ err.append("Syntax Error: variable {");
+ err.append(variable);
+ err.append("} an invalid variable name: ");
+ err.append(pathParamSpec);
+ throw new IllegalArgumentException(err.toString());
+ }
+ }
+ else if (mat.find(0))
+ {
+ // variable exists as partial segment
+ StringBuilder err = new StringBuilder();
+ err.append("Syntax Error: variable ");
+ err.append(mat.group());
+ err.append(" must exist as entire path segment: ");
+ err.append(pathParamSpec);
+ throw new IllegalArgumentException(err.toString());
+ }
+ else if ((segment.indexOf('{') >= 0) || (segment.indexOf('}') >= 0))
+ {
+ // variable is split with a path separator
+ StringBuilder err = new StringBuilder();
+ err.append("Syntax Error: invalid path segment /");
+ err.append(segment);
+ err.append("/ variable declaration incomplete: ");
+ err.append(pathParamSpec);
+ throw new IllegalArgumentException(err.toString());
+ }
+ else if (segment.indexOf('*') >= 0)
+ {
+ // glob segment
+ StringBuilder err = new StringBuilder();
+ err.append("Syntax Error: path segment /");
+ err.append(segment);
+ err.append("/ contains a wildcard symbol: ");
+ err.append(pathParamSpec);
+ throw new IllegalArgumentException(err.toString());
+ }
+ else
+ {
+ // valid path segment
+ segmentSignature[i] = 'e'; // exact
+ // build regex
+ regex.append('/').append(segment);
+ }
+ }
+
+ regex.append('$');
+
+ this.pattern = Pattern.compile(regex.toString());
+
+ int varcount = varNames.size();
+ this.variables = varNames.toArray(new String[varcount]);
+
+ // Convert signature to group
+ String sig = String.valueOf(segmentSignature);
+
+ if (Pattern.matches("^e*$",sig))
+ {
+ this.group = PathSpecGroup.EXACT;
+ }
+ else if (Pattern.matches("^e*v+",sig))
+ {
+ this.group = PathSpecGroup.PREFIX_GLOB;
+ }
+ else if (Pattern.matches("^v+e+",sig))
+ {
+ this.group = PathSpecGroup.SUFFIX_GLOB;
+ }
+ else
+ {
+ this.group = PathSpecGroup.MIDDLE_GLOB;
+ }
+ }
+
+ public Map<String, String> getPathParams(String path)
+ {
+ Matcher matcher = getMatcher(path);
+ if (matcher.matches())
+ {
+ if (group == PathSpecGroup.EXACT)
+ {
+ return Collections.emptyMap();
+ }
+ Map<String, String> ret = new HashMap<>();
+ int groupCount = matcher.groupCount();
+ for (int i = 1; i <= groupCount; i++)
+ {
+ ret.put(this.variables[i - 1],matcher.group(i));
+ }
+ return ret;
+ }
+ return null;
+ }
+
+ public int getVariableCount()
+ {
+ return variables.length;
+ }
+
+ public String[] getVariables()
+ {
+ return this.variables;
+ }
+}
diff --git a/jetty-websocket/javax-websocket-server-impl/src/main/resources/META-INF/services/javax.servlet.ServletContainerInitializer b/jetty-websocket/javax-websocket-server-impl/src/main/resources/META-INF/services/javax.servlet.ServletContainerInitializer
new file mode 100644
index 0000000..fd1c3cb
--- /dev/null
+++ b/jetty-websocket/javax-websocket-server-impl/src/main/resources/META-INF/services/javax.servlet.ServletContainerInitializer
@@ -0,0 +1 @@
+org.eclipse.jetty.websocket.jsr356.server.deploy.ServerApplicationConfigListener
\ No newline at end of file
diff --git a/jetty-websocket/javax-websocket-server-impl/src/main/resources/META-INF/services/javax.websocket.server.ServerEndpointConfig$Configurator b/jetty-websocket/javax-websocket-server-impl/src/main/resources/META-INF/services/javax.websocket.server.ServerEndpointConfig$Configurator
new file mode 100644
index 0000000..57e3ef3
--- /dev/null
+++ b/jetty-websocket/javax-websocket-server-impl/src/main/resources/META-INF/services/javax.websocket.server.ServerEndpointConfig$Configurator
@@ -0,0 +1 @@
+org.eclipse.jetty.websocket.jsr356.server.BasicServerEndpointConfigurator
\ No newline at end of file
diff --git a/jetty-websocket/javax-websocket-server-impl/src/test/java/examples/GetHttpSessionConfigurator.java b/jetty-websocket/javax-websocket-server-impl/src/test/java/examples/GetHttpSessionConfigurator.java
new file mode 100644
index 0000000..1eff040
--- /dev/null
+++ b/jetty-websocket/javax-websocket-server-impl/src/test/java/examples/GetHttpSessionConfigurator.java
@@ -0,0 +1,34 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package examples;
+
+import javax.servlet.http.HttpSession;
+import javax.websocket.HandshakeResponse;
+import javax.websocket.server.HandshakeRequest;
+import javax.websocket.server.ServerEndpointConfig;
+
+public class GetHttpSessionConfigurator extends ServerEndpointConfig.Configurator
+{
+ @Override
+ public void modifyHandshake(ServerEndpointConfig config, HandshakeRequest request, HandshakeResponse response)
+ {
+ HttpSession httpSession = (HttpSession)request.getHttpSession();
+ config.getUserProperties().put(HttpSession.class.getName(),httpSession);
+ }
+}
diff --git a/jetty-websocket/javax-websocket-server-impl/src/test/java/examples/GetHttpSessionSocket.java b/jetty-websocket/javax-websocket-server-impl/src/test/java/examples/GetHttpSessionSocket.java
new file mode 100644
index 0000000..9873c9a
--- /dev/null
+++ b/jetty-websocket/javax-websocket-server-impl/src/test/java/examples/GetHttpSessionSocket.java
@@ -0,0 +1,46 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package examples;
+
+import java.io.IOException;
+
+import javax.servlet.http.HttpSession;
+import javax.websocket.EndpointConfig;
+import javax.websocket.OnMessage;
+import javax.websocket.OnOpen;
+import javax.websocket.Session;
+import javax.websocket.server.ServerEndpoint;
+
+@ServerEndpoint(value = "/example", configurator = GetHttpSessionConfigurator.class)
+public class GetHttpSessionSocket
+{
+ private Session wsSession;
+ private HttpSession httpSession;
+
+ @OnOpen
+ public void open(Session session, EndpointConfig config) {
+ this.wsSession = session;
+ this.httpSession = (HttpSession)config.getUserProperties().get(HttpSession.class.getName());
+ }
+
+ @OnMessage
+ public void echo(String msg) throws IOException {
+ wsSession.getBasicRemote().sendText(msg);
+ }
+}
diff --git a/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/BasicAnnotatedTest.java b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/BasicAnnotatedTest.java
new file mode 100644
index 0000000..1b5f535
--- /dev/null
+++ b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/BasicAnnotatedTest.java
@@ -0,0 +1,81 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.server;
+
+import java.net.URI;
+import java.util.Queue;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+
+import org.eclipse.jetty.toolchain.test.TestingDir;
+import org.eclipse.jetty.webapp.WebAppContext;
+import org.eclipse.jetty.websocket.api.Session;
+import org.eclipse.jetty.websocket.client.WebSocketClient;
+import org.eclipse.jetty.websocket.jsr356.server.samples.echo.BasicEchoSocket;
+import org.junit.Assert;
+import org.junit.Rule;
+import org.junit.Test;
+
+/**
+ * Example of an annotated echo server discovered via annotation scanning.
+ */
+public class BasicAnnotatedTest
+{
+ @Rule
+ public TestingDir testdir = new TestingDir();
+
+ @Test
+ public void testEcho() throws Exception
+ {
+ WSServer wsb = new WSServer(testdir,"app");
+ wsb.createWebInf();
+ wsb.copyEndpoint(BasicEchoSocket.class);
+
+ try
+ {
+ wsb.start();
+ URI uri = wsb.getServerBaseURI();
+
+ WebAppContext webapp = wsb.createWebAppContext();
+ wsb.deployWebapp(webapp);
+ // wsb.dump();
+
+ WebSocketClient client = new WebSocketClient();
+ try
+ {
+ client.start();
+ JettyEchoSocket clientEcho = new JettyEchoSocket();
+ Future<Session> foo = client.connect(clientEcho,uri.resolve("echo"));
+ // wait for connect
+ foo.get(1,TimeUnit.SECONDS);
+ clientEcho.sendMessage("Hello World");
+ Queue<String> msgs = clientEcho.awaitMessages(1);
+ Assert.assertEquals("Expected message","Hello World",msgs.poll());
+ }
+ finally
+ {
+ client.stop();
+ }
+ }
+ finally
+ {
+ wsb.stop();
+ }
+ }
+}
diff --git a/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/BasicEndpointTest.java b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/BasicEndpointTest.java
new file mode 100644
index 0000000..e06c551
--- /dev/null
+++ b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/BasicEndpointTest.java
@@ -0,0 +1,89 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.server;
+
+import java.net.URI;
+import java.util.Queue;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+
+import javax.websocket.Endpoint;
+import javax.websocket.server.ServerContainer;
+
+import org.eclipse.jetty.toolchain.test.TestingDir;
+import org.eclipse.jetty.webapp.WebAppContext;
+import org.eclipse.jetty.websocket.api.Session;
+import org.eclipse.jetty.websocket.client.WebSocketClient;
+import org.eclipse.jetty.websocket.jsr356.server.samples.echo.BasicEchoEndpoint;
+import org.eclipse.jetty.websocket.jsr356.server.samples.echo.BasicEchoEndpointConfigContextListener;
+import org.junit.Assert;
+import org.junit.Rule;
+import org.junit.Test;
+
+/**
+ * Example of an {@link Endpoint} extended echo server added programmatically via the
+ * {@link ServerContainer#addEndpoint(javax.websocket.server.ServerEndpointConfig)}
+ */
+public class BasicEndpointTest
+{
+ @Rule
+ public TestingDir testdir = new TestingDir();
+
+ @Test
+ public void testEcho() throws Exception
+ {
+ WSServer wsb = new WSServer(testdir,"app");
+ wsb.copyWebInf("basic-echo-endpoint-config-web.xml");
+ // the endpoint (extends javax.websocket.Endpoint)
+ wsb.copyClass(BasicEchoEndpoint.class);
+ // the configuration (adds the endpoint)
+ wsb.copyClass(BasicEchoEndpointConfigContextListener.class);
+
+ try
+ {
+ wsb.start();
+ URI uri = wsb.getServerBaseURI();
+
+ WebAppContext webapp = wsb.createWebAppContext();
+ wsb.deployWebapp(webapp);
+ // wsb.dump();
+
+ WebSocketClient client = new WebSocketClient();
+ try
+ {
+ client.start();
+ JettyEchoSocket clientEcho = new JettyEchoSocket();
+ Future<Session> future = client.connect(clientEcho,uri.resolve("echo"));
+ // wait for connect
+ future.get(1,TimeUnit.SECONDS);
+ clientEcho.sendMessage("Hello World");
+ Queue<String> msgs = clientEcho.awaitMessages(1);
+ Assert.assertEquals("Expected message","Hello World",msgs.poll());
+ }
+ finally
+ {
+ client.stop();
+ }
+ }
+ finally
+ {
+ wsb.stop();
+ }
+ }
+}
diff --git a/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/DummyCreator.java b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/DummyCreator.java
new file mode 100644
index 0000000..ac56098
--- /dev/null
+++ b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/DummyCreator.java
@@ -0,0 +1,39 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.server;
+
+import org.eclipse.jetty.websocket.server.MappedWebSocketCreator;
+import org.eclipse.jetty.websocket.server.pathmap.PathMappings;
+import org.eclipse.jetty.websocket.server.pathmap.PathSpec;
+import org.eclipse.jetty.websocket.servlet.WebSocketCreator;
+
+public class DummyCreator implements MappedWebSocketCreator
+{
+ @Override
+ public void addMapping(PathSpec spec, WebSocketCreator creator)
+ {
+ /* do nothing */
+ }
+
+ @Override
+ public PathMappings<WebSocketCreator> getMappings()
+ {
+ return null;
+ }
+}
diff --git a/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/EchoCase.java b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/EchoCase.java
new file mode 100644
index 0000000..679522d
--- /dev/null
+++ b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/EchoCase.java
@@ -0,0 +1,151 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.server;
+
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.websocket.server.ServerEndpoint;
+
+public class EchoCase
+{
+ public static class PartialBinary
+ {
+ ByteBuffer part;
+
+ boolean fin;
+ public PartialBinary(ByteBuffer part, boolean fin)
+ {
+ this.part = part;
+ this.fin = fin;
+ }
+ }
+
+ public static class PartialText
+ {
+ String part;
+
+ boolean fin;
+ public PartialText(String part, boolean fin)
+ {
+ this.part = part;
+ this.fin = fin;
+ }
+ }
+
+ public static EchoCase add(List<EchoCase[]> data, Class<?> serverPojo)
+ {
+ EchoCase ecase = new EchoCase();
+ ecase.serverPojo = serverPojo;
+ data.add(new EchoCase[]
+ { ecase });
+ ServerEndpoint endpoint = serverPojo.getAnnotation(ServerEndpoint.class);
+ ecase.path = endpoint.value();
+ return ecase;
+ }
+
+ public static EchoCase add(List<EchoCase[]> data, Class<?> serverPojo, String path)
+ {
+ EchoCase ecase = new EchoCase();
+ ecase.serverPojo = serverPojo;
+ ecase.path = path;
+ data.add(new EchoCase[]
+ { ecase });
+ return ecase;
+ }
+
+ // The websocket server pojo to test against
+ public Class<?> serverPojo;
+ // The (relative) URL path to hit
+ public String path;
+ // The messages to transmit
+ public List<Object> messages = new ArrayList<>();
+ // The expected Strings (that are echoed back)
+ public List<String> expectedStrings = new ArrayList<>();
+
+ public EchoCase addMessage(Object msg)
+ {
+ messages.add(msg);
+ return this;
+ }
+
+ public EchoCase addSplitMessage(ByteBuffer... parts)
+ {
+ int len = parts.length;
+ for (int i = 0; i < len; i++)
+ {
+ addMessage(new PartialBinary(parts[i],(i == (len-1))));
+ }
+ return this;
+ }
+
+ public EchoCase addSplitMessage(String... parts)
+ {
+ int len = parts.length;
+ for (int i = 0; i < len; i++)
+ {
+ addMessage(new PartialText(parts[i],(i == (len-1))));
+ }
+ return this;
+ }
+
+ public EchoCase expect(String message)
+ {
+ expectedStrings.add(message);
+ return this;
+ }
+
+ public EchoCase requestPath(String path)
+ {
+ this.path = path;
+ return this;
+ }
+
+ @Override
+ public String toString()
+ {
+ StringBuilder str = new StringBuilder();
+ str.append("EchoCase['");
+ str.append(path);
+ str.append("',").append(serverPojo.getName());
+ str.append(",messages[").append(messages.size());
+ str.append("]=");
+ boolean delim = false;
+ for (Object msg : messages)
+ {
+ if (delim)
+ {
+ str.append(",");
+ }
+ if (msg instanceof String)
+ {
+ str.append("'").append(msg).append("'");
+ }
+ else
+ {
+ str.append("(").append(msg.getClass().getName()).append(")");
+ str.append(msg);
+ }
+ delim = true;
+ }
+ str.append("]");
+ return str.toString();
+ }
+}
\ No newline at end of file
diff --git a/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/EchoClientSocket.java b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/EchoClientSocket.java
new file mode 100644
index 0000000..f974bc0
--- /dev/null
+++ b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/EchoClientSocket.java
@@ -0,0 +1,84 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.server;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+
+import javax.websocket.ClientEndpoint;
+import javax.websocket.CloseReason;
+import javax.websocket.EncodeException;
+import javax.websocket.OnClose;
+import javax.websocket.OnError;
+import javax.websocket.OnMessage;
+import javax.websocket.OnOpen;
+import javax.websocket.Session;
+
+@ClientEndpoint
+public class EchoClientSocket extends TrackingSocket
+{
+ private Session session;
+
+ public void close() throws IOException
+ {
+ this.session.close(new CloseReason(CloseReason.CloseCodes.NORMAL_CLOSURE,"Test Complete"));
+ }
+
+ @OnClose
+ public void onClose(CloseReason close)
+ {
+ this.session = null;
+ super.closeReason = close;
+ super.closeLatch.countDown();
+ }
+
+ @OnError
+ public void onError(Throwable t)
+ {
+ addError(t);
+ }
+
+ @OnOpen
+ public void onOpen(Session session)
+ {
+ this.session = session;
+ openLatch.countDown();
+ }
+
+ @OnMessage
+ public void onText(String text)
+ {
+ addEvent(text);
+ }
+
+ public void sendObject(Object obj) throws IOException, EncodeException
+ {
+ session.getBasicRemote().sendObject(obj);
+ }
+
+ public void sendPartialBinary(ByteBuffer part, boolean fin) throws IOException
+ {
+ session.getBasicRemote().sendBinary(part,fin);
+ }
+
+ public void sendPartialText(String part, boolean fin) throws IOException
+ {
+ session.getBasicRemote().sendText(part,fin);
+ }
+}
diff --git a/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/EchoTest.java b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/EchoTest.java
new file mode 100644
index 0000000..d81b4ad
--- /dev/null
+++ b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/EchoTest.java
@@ -0,0 +1,303 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.server;
+
+import static org.hamcrest.Matchers.*;
+
+import java.io.File;
+import java.net.URI;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+import javax.websocket.ContainerProvider;
+import javax.websocket.WebSocketContainer;
+
+import org.eclipse.jetty.toolchain.test.EventQueue;
+import org.eclipse.jetty.toolchain.test.MavenTestingUtils;
+import org.eclipse.jetty.webapp.WebAppContext;
+import org.eclipse.jetty.websocket.jsr356.server.EchoCase.PartialBinary;
+import org.eclipse.jetty.websocket.jsr356.server.EchoCase.PartialText;
+import org.eclipse.jetty.websocket.jsr356.server.samples.partial.PartialTextSessionSocket;
+import org.eclipse.jetty.websocket.jsr356.server.samples.partial.PartialTextSocket;
+import org.eclipse.jetty.websocket.jsr356.server.samples.primitives.BooleanObjectTextSocket;
+import org.eclipse.jetty.websocket.jsr356.server.samples.primitives.BooleanTextSocket;
+import org.eclipse.jetty.websocket.jsr356.server.samples.primitives.ByteObjectTextSocket;
+import org.eclipse.jetty.websocket.jsr356.server.samples.primitives.ByteTextSocket;
+import org.eclipse.jetty.websocket.jsr356.server.samples.primitives.CharTextSocket;
+import org.eclipse.jetty.websocket.jsr356.server.samples.primitives.CharacterObjectTextSocket;
+import org.eclipse.jetty.websocket.jsr356.server.samples.primitives.DoubleObjectTextSocket;
+import org.eclipse.jetty.websocket.jsr356.server.samples.primitives.DoubleTextSocket;
+import org.eclipse.jetty.websocket.jsr356.server.samples.primitives.FloatObjectTextSocket;
+import org.eclipse.jetty.websocket.jsr356.server.samples.primitives.FloatTextSocket;
+import org.eclipse.jetty.websocket.jsr356.server.samples.primitives.IntParamTextSocket;
+import org.eclipse.jetty.websocket.jsr356.server.samples.primitives.IntTextSocket;
+import org.eclipse.jetty.websocket.jsr356.server.samples.primitives.IntegerObjectTextSocket;
+import org.eclipse.jetty.websocket.jsr356.server.samples.primitives.LongObjectTextSocket;
+import org.eclipse.jetty.websocket.jsr356.server.samples.primitives.LongTextSocket;
+import org.eclipse.jetty.websocket.jsr356.server.samples.primitives.ShortObjectTextSocket;
+import org.eclipse.jetty.websocket.jsr356.server.samples.primitives.ShortTextSocket;
+import org.eclipse.jetty.websocket.jsr356.server.samples.streaming.ReaderParamSocket;
+import org.eclipse.jetty.websocket.jsr356.server.samples.streaming.ReaderSocket;
+import org.eclipse.jetty.websocket.jsr356.server.samples.streaming.StringReturnReaderParamSocket;
+import org.junit.AfterClass;
+import org.junit.Assert;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class EchoTest
+{
+ private static final List<EchoCase[]> TESTCASES = new ArrayList<>();
+
+ private static WSServer server;
+ private static URI serverUri;
+ private static WebSocketContainer client;
+
+ static
+ {
+ EchoCase.add(TESTCASES,BooleanTextSocket.class).addMessage(true).expect("true");
+ EchoCase.add(TESTCASES,BooleanTextSocket.class).addMessage(false).expect("false");
+ EchoCase.add(TESTCASES,BooleanTextSocket.class).addMessage(Boolean.TRUE).expect("true");
+ EchoCase.add(TESTCASES,BooleanTextSocket.class).addMessage(Boolean.FALSE).expect("false");
+ EchoCase.add(TESTCASES,BooleanTextSocket.class).addMessage("true").expect("true");
+ EchoCase.add(TESTCASES,BooleanTextSocket.class).addMessage("TRUe").expect("true");
+ EchoCase.add(TESTCASES,BooleanTextSocket.class).addMessage("Apple").expect("false");
+ EchoCase.add(TESTCASES,BooleanTextSocket.class).addMessage("false").expect("false");
+
+ EchoCase.add(TESTCASES,BooleanObjectTextSocket.class).addMessage(true).expect("true");
+ EchoCase.add(TESTCASES,BooleanObjectTextSocket.class).addMessage(false).expect("false");
+ EchoCase.add(TESTCASES,BooleanObjectTextSocket.class).addMessage(Boolean.TRUE).expect("true");
+ EchoCase.add(TESTCASES,BooleanObjectTextSocket.class).addMessage(Boolean.FALSE).expect("false");
+ EchoCase.add(TESTCASES,BooleanObjectTextSocket.class).addMessage("true").expect("true");
+ EchoCase.add(TESTCASES,BooleanObjectTextSocket.class).addMessage("false").expect("false");
+ EchoCase.add(TESTCASES,BooleanObjectTextSocket.class).addMessage("FaLsE").expect("false");
+
+ EchoCase.add(TESTCASES,ByteTextSocket.class).addMessage((byte)88).expect("0x58");
+ EchoCase.add(TESTCASES,ByteTextSocket.class).addMessage((byte)101).expect("0x65");
+ EchoCase.add(TESTCASES,ByteTextSocket.class).addMessage((byte)202).expect("0xCA");
+ EchoCase.add(TESTCASES,ByteTextSocket.class).addMessage(Byte.valueOf((byte)33)).expect("0x21");
+ EchoCase.add(TESTCASES,ByteTextSocket.class).addMessage(Byte.valueOf((byte)131)).expect("0x83");
+ EchoCase.add(TESTCASES,ByteTextSocket.class).addMessage(Byte.valueOf((byte)232)).expect("0xE8");
+
+ EchoCase.add(TESTCASES,ByteObjectTextSocket.class).addMessage((byte)88).expect("0x58");
+ EchoCase.add(TESTCASES,ByteObjectTextSocket.class).addMessage((byte)101).expect("0x65");
+ EchoCase.add(TESTCASES,ByteObjectTextSocket.class).addMessage((byte)202).expect("0xCA");
+ EchoCase.add(TESTCASES,ByteObjectTextSocket.class).addMessage(Byte.valueOf((byte)33)).expect("0x21");
+ EchoCase.add(TESTCASES,ByteObjectTextSocket.class).addMessage(Byte.valueOf((byte)131)).expect("0x83");
+ EchoCase.add(TESTCASES,ByteObjectTextSocket.class).addMessage(Byte.valueOf((byte)232)).expect("0xE8");
+
+ EchoCase.add(TESTCASES,CharTextSocket.class).addMessage((char)40).expect("(");
+ EchoCase.add(TESTCASES,CharTextSocket.class).addMessage((char)106).expect("j");
+ EchoCase.add(TESTCASES,CharTextSocket.class).addMessage((char)126).expect("~");
+ EchoCase.add(TESTCASES,CharTextSocket.class).addMessage(Character.valueOf((char)41)).expect(")");
+ EchoCase.add(TESTCASES,CharTextSocket.class).addMessage(Character.valueOf((char)74)).expect("J");
+ EchoCase.add(TESTCASES,CharTextSocket.class).addMessage(Character.valueOf((char)64)).expect("@");
+
+ EchoCase.add(TESTCASES,CharacterObjectTextSocket.class).addMessage((char)40).expect("(");
+ EchoCase.add(TESTCASES,CharacterObjectTextSocket.class).addMessage((char)106).expect("j");
+ EchoCase.add(TESTCASES,CharacterObjectTextSocket.class).addMessage((char)126).expect("~");
+ EchoCase.add(TESTCASES,CharacterObjectTextSocket.class).addMessage("E").expect("E");
+ EchoCase.add(TESTCASES,CharacterObjectTextSocket.class).addMessage(Character.valueOf((char)41)).expect(")");
+ EchoCase.add(TESTCASES,CharacterObjectTextSocket.class).addMessage(Character.valueOf((char)74)).expect("J");
+ EchoCase.add(TESTCASES,CharacterObjectTextSocket.class).addMessage(Character.valueOf((char)64)).expect("@");
+
+ EchoCase.add(TESTCASES,DoubleTextSocket.class).addMessage((double)3.1459).expect("3.1459");
+ EchoCase.add(TESTCASES,DoubleTextSocket.class).addMessage((double)123.456).expect("123.4560");
+ EchoCase.add(TESTCASES,DoubleTextSocket.class).addMessage(Double.valueOf(55)).expect("55.0000");
+ EchoCase.add(TESTCASES,DoubleTextSocket.class).addMessage(Double.valueOf(1.0E8)).expect("100000000.0000");
+ EchoCase.add(TESTCASES,DoubleTextSocket.class).addMessage("42").expect("42.0000");
+ EchoCase.add(TESTCASES,DoubleTextSocket.class).addMessage(".123").expect("0.1230");
+
+ EchoCase.add(TESTCASES,DoubleObjectTextSocket.class).addMessage((double)3.1459).expect("3.1459");
+ EchoCase.add(TESTCASES,DoubleObjectTextSocket.class).addMessage((double)123.456).expect("123.4560");
+ EchoCase.add(TESTCASES,DoubleObjectTextSocket.class).addMessage(Double.valueOf(55)).expect("55.0000");
+ EchoCase.add(TESTCASES,DoubleObjectTextSocket.class).addMessage(Double.valueOf(1.0E8)).expect("100000000.0000");
+ EchoCase.add(TESTCASES,DoubleObjectTextSocket.class).addMessage("42").expect("42.0000");
+ EchoCase.add(TESTCASES,DoubleObjectTextSocket.class).addMessage(".123").expect("0.1230");
+
+ EchoCase.add(TESTCASES,FloatTextSocket.class).addMessage((float)3.1459).expect("3.1459");
+ EchoCase.add(TESTCASES,FloatTextSocket.class).addMessage((float)123.456).expect("123.4560");
+ EchoCase.add(TESTCASES,FloatTextSocket.class).addMessage(Float.valueOf(55)).expect("55.0000");
+ EchoCase.add(TESTCASES,FloatTextSocket.class).addMessage(Float.valueOf(1.0E8f)).expect("100000000.0000");
+ EchoCase.add(TESTCASES,FloatTextSocket.class).addMessage("42").expect("42.0000");
+ EchoCase.add(TESTCASES,FloatTextSocket.class).addMessage(".123").expect("0.1230");
+ EchoCase.add(TESTCASES,FloatTextSocket.class).addMessage("50505E-6").expect("0.0505");
+
+ EchoCase.add(TESTCASES,FloatObjectTextSocket.class).addMessage((float)3.1459).expect("3.1459");
+ EchoCase.add(TESTCASES,FloatObjectTextSocket.class).addMessage((float)123.456).expect("123.4560");
+ EchoCase.add(TESTCASES,FloatObjectTextSocket.class).addMessage(Float.valueOf(55)).expect("55.0000");
+ EchoCase.add(TESTCASES,FloatObjectTextSocket.class).addMessage(Float.valueOf(1.0E8f)).expect("100000000.0000");
+ EchoCase.add(TESTCASES,FloatObjectTextSocket.class).addMessage("42").expect("42.0000");
+ EchoCase.add(TESTCASES,FloatObjectTextSocket.class).addMessage(".123").expect("0.1230");
+ EchoCase.add(TESTCASES,FloatObjectTextSocket.class).addMessage("50505E-6").expect("0.0505");
+
+ EchoCase.add(TESTCASES,IntTextSocket.class).addMessage((int)8).expect("8");
+ EchoCase.add(TESTCASES,IntTextSocket.class).addMessage((int)22).expect("22");
+ EchoCase.add(TESTCASES,IntTextSocket.class).addMessage("12345678").expect("12345678");
+
+ EchoCase.add(TESTCASES,IntegerObjectTextSocket.class).addMessage((int)8).expect("8");
+ EchoCase.add(TESTCASES,IntegerObjectTextSocket.class).addMessage((int)22).expect("22");
+ EchoCase.add(TESTCASES,IntegerObjectTextSocket.class).addMessage("12345678").expect("12345678");
+
+ EchoCase.add(TESTCASES,LongTextSocket.class).addMessage((int)789).expect("789");
+ EchoCase.add(TESTCASES,LongTextSocket.class).addMessage((long)123456L).expect("123456");
+ EchoCase.add(TESTCASES,LongTextSocket.class).addMessage(-456).expect("-456");
+
+ EchoCase.add(TESTCASES,LongObjectTextSocket.class).addMessage((int)789).expect("789");
+ EchoCase.add(TESTCASES,LongObjectTextSocket.class).addMessage((long)123456L).expect("123456");
+ EchoCase.add(TESTCASES,LongObjectTextSocket.class).addMessage(-234).expect("-234");
+
+ EchoCase.add(TESTCASES,ShortTextSocket.class).addMessage((int)4).expect("4");
+ EchoCase.add(TESTCASES,ShortTextSocket.class).addMessage((long)987).expect("987");
+ EchoCase.add(TESTCASES,ShortTextSocket.class).addMessage("32001").expect("32001");
+
+ EchoCase.add(TESTCASES,ShortObjectTextSocket.class).addMessage((int)4).expect("4");
+ EchoCase.add(TESTCASES,ShortObjectTextSocket.class).addMessage((int)987).expect("987");
+ EchoCase.add(TESTCASES,ShortObjectTextSocket.class).addMessage(-32001L).expect("-32001");
+
+ // PathParam based
+ EchoCase.add(TESTCASES,IntParamTextSocket.class).requestPath("/echo/primitives/integer/params/5678").addMessage(1234).expect("1234|5678");
+
+ // Reader based
+ EchoCase.add(TESTCASES,ReaderSocket.class).addMessage("Hello World").expect("Hello World");
+ EchoCase.add(TESTCASES,ReaderParamSocket.class).requestPath("/echo/streaming/readerparam/OhNo").addMessage("Hello World").expect("Hello World|OhNo");
+ EchoCase.add(TESTCASES,StringReturnReaderParamSocket.class).requestPath("/echo/streaming/readerparam2/OhMy").addMessage("Hello World")
+ .expect("Hello World|OhMy");
+
+ // Partial message based
+ EchoCase.add(TESTCASES,PartialTextSocket.class)
+ .addSplitMessage("Saved"," by ","zero")
+ .expect("('Saved',false)(' by ',false)('zero',true)");
+ EchoCase.add(TESTCASES,PartialTextSessionSocket.class)
+ .addSplitMessage("Built"," for"," the"," future")
+ .expect("('Built',false)(' for',false)(' the',false)(' future',true)");
+ }
+
+ @BeforeClass
+ public static void startServer() throws Exception
+ {
+ File testdir = MavenTestingUtils.getTargetTestingDir(EchoTest.class.getName());
+ server = new WSServer(testdir,"app");
+ server.copyWebInf("empty-web.xml");
+
+ for (EchoCase cases[] : TESTCASES)
+ {
+ for (EchoCase ecase : cases)
+ {
+ server.copyClass(ecase.serverPojo);
+ }
+ }
+
+ server.start();
+ serverUri = server.getServerBaseURI();
+
+ WebAppContext webapp = server.createWebAppContext();
+ server.deployWebapp(webapp);
+ server.dump();
+ }
+
+ @BeforeClass
+ public static void startClient() throws Exception
+ {
+ client = ContainerProvider.getWebSocketContainer();
+ }
+
+ @AfterClass
+ public static void stopServer()
+ {
+ server.stop();
+ }
+
+ @Parameters
+ public static Collection<EchoCase[]> data() throws Exception
+ {
+ return TESTCASES;
+ }
+
+ private EchoCase testcase;
+
+ public EchoTest(EchoCase testcase)
+ {
+ this.testcase = testcase;
+ System.err.println(testcase);
+ }
+
+ @Test
+ public void testEcho() throws Exception
+ {
+ EchoClientSocket socket = new EchoClientSocket();
+ URI toUri = serverUri.resolve(testcase.path.substring(1));
+
+ try
+ {
+ // Connect
+ client.connectToServer(socket,toUri);
+ socket.waitForConnected(2,TimeUnit.SECONDS);
+
+ // Send Messages
+ int messageCount = 0;
+ for (Object msg : testcase.messages)
+ {
+ if (msg instanceof PartialText)
+ {
+ PartialText pt = (PartialText)msg;
+ socket.sendPartialText(pt.part,pt.fin);
+ if (pt.fin)
+ {
+ messageCount++;
+ }
+ }
+ else if (msg instanceof PartialBinary)
+ {
+ PartialBinary pb = (PartialBinary)msg;
+ socket.sendPartialBinary(pb.part,pb.fin);
+ if (pb.fin)
+ {
+ messageCount++;
+ }
+ }
+ else
+ {
+ socket.sendObject(msg);
+ messageCount++;
+ }
+ }
+
+ // Collect Responses
+ EventQueue<String> received = socket.eventQueue;
+ received.awaitEventCount(messageCount,3,TimeUnit.SECONDS);
+
+ // Validate Responses
+ for (String expected : testcase.expectedStrings)
+ {
+ Assert.assertThat("Received Echo Responses",received,contains(expected));
+ }
+ }
+ finally
+ {
+ // Close
+ socket.close();
+ }
+ }
+}
diff --git a/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/JettyEchoSocket.java b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/JettyEchoSocket.java
new file mode 100644
index 0000000..978bd70
--- /dev/null
+++ b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/JettyEchoSocket.java
@@ -0,0 +1,87 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.server;
+
+import java.util.Queue;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+import org.eclipse.jetty.toolchain.test.EventQueue;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+import org.eclipse.jetty.websocket.api.RemoteEndpoint;
+import org.eclipse.jetty.websocket.api.Session;
+import org.eclipse.jetty.websocket.api.annotations.OnWebSocketClose;
+import org.eclipse.jetty.websocket.api.annotations.OnWebSocketConnect;
+import org.eclipse.jetty.websocket.api.annotations.OnWebSocketError;
+import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage;
+import org.eclipse.jetty.websocket.api.annotations.WebSocket;
+
+/**
+ * This is a Jetty API version of a websocket.
+ * <p>
+ * This is used a a client socket during the server tests.
+ */
+@WebSocket
+public class JettyEchoSocket
+{
+ private static final Logger LOG = Log.getLogger(JettyEchoSocket.class);
+ @SuppressWarnings("unused")
+ private Session session;
+ private RemoteEndpoint remote;
+ private EventQueue<String> incomingMessages = new EventQueue<>();
+
+ public Queue<String> awaitMessages(int expected) throws TimeoutException, InterruptedException
+ {
+ incomingMessages.awaitEventCount(expected,2,TimeUnit.SECONDS);
+ return incomingMessages;
+ }
+
+ @OnWebSocketClose
+ public void onClose(int code, String reason)
+ {
+ session = null;
+ remote = null;
+ }
+
+ @OnWebSocketError
+ public void onError(Throwable t)
+ {
+ LOG.warn(t);
+ }
+
+ @OnWebSocketMessage
+ public void onMessage(String msg)
+ {
+ incomingMessages.add(msg);
+ remote.sendStringByFuture(msg);
+ }
+
+ @OnWebSocketConnect
+ public void onOpen(Session session)
+ {
+ this.session = session;
+ this.remote = session.getRemote();
+ }
+
+ public void sendMessage(String msg)
+ {
+ remote.sendStringByFuture(msg);
+ }
+}
diff --git a/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/JettyServerEndpointConfiguratorTest.java b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/JettyServerEndpointConfiguratorTest.java
new file mode 100644
index 0000000..91b43a4
--- /dev/null
+++ b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/JettyServerEndpointConfiguratorTest.java
@@ -0,0 +1,51 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.server;
+
+import static org.hamcrest.Matchers.*;
+import static org.junit.Assert.*;
+
+import java.util.Iterator;
+import java.util.ServiceLoader;
+
+import javax.websocket.server.ServerEndpointConfig;
+
+import org.junit.Test;
+
+/**
+ * Test the JettyServerEndpointConfigurator impl.
+ */
+public class JettyServerEndpointConfiguratorTest
+{
+ @Test
+ public void testServiceLoader()
+ {
+ System.out.printf("Service Name: %s%n",ServerEndpointConfig.Configurator.class.getName());
+
+ ServiceLoader<ServerEndpointConfig.Configurator> loader = ServiceLoader.load(javax.websocket.server.ServerEndpointConfig.Configurator.class);
+ assertThat("loader",loader,notNullValue());
+ Iterator<ServerEndpointConfig.Configurator> iter = loader.iterator();
+ assertThat("loader.iterator",iter,notNullValue());
+ assertThat("loader.iterator.hasNext",iter.hasNext(),is(true));
+
+ ServerEndpointConfig.Configurator configr = iter.next();
+ assertThat("Configurator",configr,notNullValue());
+ assertThat("COnfigurator type",configr,instanceOf(BasicServerEndpointConfigurator.class));
+ }
+}
diff --git a/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/LargeAnnotatedTest.java b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/LargeAnnotatedTest.java
new file mode 100644
index 0000000..8fe0fc2
--- /dev/null
+++ b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/LargeAnnotatedTest.java
@@ -0,0 +1,88 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.server;
+
+import java.net.URI;
+import java.util.Arrays;
+import java.util.Queue;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+
+import org.eclipse.jetty.toolchain.test.TestingDir;
+import org.eclipse.jetty.util.StringUtil;
+import org.eclipse.jetty.webapp.WebAppContext;
+import org.eclipse.jetty.websocket.api.Session;
+import org.eclipse.jetty.websocket.client.WebSocketClient;
+import org.eclipse.jetty.websocket.jsr356.server.samples.echo.LargeEchoConfiguredSocket;
+import org.junit.Assert;
+import org.junit.Rule;
+import org.junit.Test;
+
+/**
+ * Test Echo of Large messages, targeting the {@link javax.websocket.Session#setMaxTextMessageBufferSize(int)} functionality
+ */
+public class LargeAnnotatedTest
+{
+ @Rule
+ public TestingDir testdir = new TestingDir();
+
+ @Test
+ public void testEcho() throws Exception
+ {
+ WSServer wsb = new WSServer(testdir,"app");
+ wsb.createWebInf();
+ wsb.copyEndpoint(LargeEchoConfiguredSocket.class);
+
+ try
+ {
+ wsb.start();
+ URI uri = wsb.getServerBaseURI();
+
+ WebAppContext webapp = wsb.createWebAppContext();
+ wsb.deployWebapp(webapp);
+ // wsb.dump();
+
+ WebSocketClient client = new WebSocketClient();
+ try
+ {
+ client.getPolicy().setMaxTextMessageSize(128*1024);
+ client.start();
+ JettyEchoSocket clientEcho = new JettyEchoSocket();
+ Future<Session> foo = client.connect(clientEcho,uri.resolve("echo/large"));
+ // wait for connect
+ foo.get(1,TimeUnit.SECONDS);
+ // The message size should be bigger than default, but smaller than the limit that LargeEchoSocket specifies
+ byte txt[] = new byte[100 * 1024];
+ Arrays.fill(txt,(byte)'o');
+ String msg = new String(txt,StringUtil.__UTF8_CHARSET);
+ clientEcho.sendMessage(msg);
+ Queue<String> msgs = clientEcho.awaitMessages(1);
+ Assert.assertEquals("Expected message",msg,msgs.poll());
+ }
+ finally
+ {
+ client.stop();
+ }
+ }
+ finally
+ {
+ wsb.stop();
+ }
+ }
+}
diff --git a/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/LargeContainerTest.java b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/LargeContainerTest.java
new file mode 100644
index 0000000..fb1564d
--- /dev/null
+++ b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/LargeContainerTest.java
@@ -0,0 +1,88 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.server;
+
+import java.net.URI;
+import java.util.Arrays;
+import java.util.Queue;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+
+import org.eclipse.jetty.toolchain.test.TestingDir;
+import org.eclipse.jetty.util.StringUtil;
+import org.eclipse.jetty.webapp.WebAppContext;
+import org.eclipse.jetty.websocket.api.Session;
+import org.eclipse.jetty.websocket.client.WebSocketClient;
+import org.eclipse.jetty.websocket.jsr356.server.samples.echo.LargeEchoDefaultSocket;
+import org.junit.Assert;
+import org.junit.Rule;
+import org.junit.Test;
+
+/**
+ * Test Echo of Large messages, targeting the {@link javax.websocket.WebSocketContainer#setDefaultMaxTextMessageBufferSize(int)} functionality
+ */
+public class LargeContainerTest
+{
+ @Rule
+ public TestingDir testdir = new TestingDir();
+
+ @Test
+ public void testEcho() throws Exception
+ {
+ WSServer wsb = new WSServer(testdir,"app");
+ wsb.copyWebInf("large-echo-config-web.xml");
+ wsb.copyEndpoint(LargeEchoDefaultSocket.class);
+
+ try
+ {
+ wsb.start();
+ URI uri = wsb.getServerBaseURI();
+
+ WebAppContext webapp = wsb.createWebAppContext();
+ wsb.deployWebapp(webapp);
+ // wsb.dump();
+
+ WebSocketClient client = new WebSocketClient();
+ try
+ {
+ client.getPolicy().setMaxTextMessageSize(128*1024);
+ client.start();
+ JettyEchoSocket clientEcho = new JettyEchoSocket();
+ Future<Session> foo = client.connect(clientEcho,uri.resolve("echo/large"));
+ // wait for connect
+ foo.get(1,TimeUnit.SECONDS);
+ // The message size should be bigger than default, but smaller than the limit that LargeEchoSocket specifies
+ byte txt[] = new byte[100 * 1024];
+ Arrays.fill(txt,(byte)'o');
+ String msg = new String(txt,StringUtil.__UTF8_CHARSET);
+ clientEcho.sendMessage(msg);
+ Queue<String> msgs = clientEcho.awaitMessages(1);
+ Assert.assertEquals("Expected message",msg,msgs.poll());
+ }
+ finally
+ {
+ client.stop();
+ }
+ }
+ finally
+ {
+ wsb.stop();
+ }
+ }
+}
diff --git a/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/OnMessageReturnTest.java b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/OnMessageReturnTest.java
new file mode 100644
index 0000000..36f3862
--- /dev/null
+++ b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/OnMessageReturnTest.java
@@ -0,0 +1,79 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.server;
+
+import java.net.URI;
+import java.util.Queue;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+
+import org.eclipse.jetty.toolchain.test.TestingDir;
+import org.eclipse.jetty.webapp.WebAppContext;
+import org.eclipse.jetty.websocket.api.Session;
+import org.eclipse.jetty.websocket.client.WebSocketClient;
+import org.eclipse.jetty.websocket.jsr356.server.samples.echo.EchoReturnEndpoint;
+import org.junit.Assert;
+import org.junit.Rule;
+import org.junit.Test;
+
+public class OnMessageReturnTest
+{
+ @Rule
+ public TestingDir testdir = new TestingDir();
+
+ @Test
+ public void testEchoReturn() throws Exception
+ {
+ WSServer wsb = new WSServer(testdir,"app");
+ wsb.copyWebInf("empty-web.xml");
+ wsb.copyClass(EchoReturnEndpoint.class);
+
+ try
+ {
+ wsb.start();
+ URI uri = wsb.getServerBaseURI();
+
+ WebAppContext webapp = wsb.createWebAppContext();
+ wsb.deployWebapp(webapp);
+ wsb.dump();
+
+ WebSocketClient client = new WebSocketClient();
+ try
+ {
+ client.start();
+ JettyEchoSocket clientEcho = new JettyEchoSocket();
+ Future<Session> future = client.connect(clientEcho,uri.resolve("echoreturn"));
+ // wait for connect
+ future.get(1,TimeUnit.SECONDS);
+ clientEcho.sendMessage("Hello World");
+ Queue<String> msgs = clientEcho.awaitMessages(1);
+ Assert.assertEquals("Expected message","Hello World",msgs.poll());
+ }
+ finally
+ {
+ client.stop();
+ }
+ }
+ finally
+ {
+ wsb.stop();
+ }
+ }
+
+}
diff --git a/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/ServerAnnotatedEndpointScanner_GoodSignaturesTest.java b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/ServerAnnotatedEndpointScanner_GoodSignaturesTest.java
new file mode 100644
index 0000000..94f892f
--- /dev/null
+++ b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/ServerAnnotatedEndpointScanner_GoodSignaturesTest.java
@@ -0,0 +1,205 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.server;
+
+import static org.hamcrest.Matchers.*;
+
+import java.io.Reader;
+import java.lang.reflect.Field;
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Date;
+import java.util.List;
+
+import javax.websocket.CloseReason;
+import javax.websocket.PongMessage;
+import javax.websocket.Session;
+import javax.websocket.server.ServerEndpoint;
+import javax.websocket.server.ServerEndpointConfig;
+
+import org.eclipse.jetty.websocket.jsr356.annotations.AnnotatedEndpointScanner;
+import org.eclipse.jetty.websocket.jsr356.annotations.JsrCallable;
+import org.eclipse.jetty.websocket.jsr356.server.samples.BasicBinaryMessageByteBufferSocket;
+import org.eclipse.jetty.websocket.jsr356.server.samples.BasicCloseReasonSessionSocket;
+import org.eclipse.jetty.websocket.jsr356.server.samples.BasicCloseReasonSocket;
+import org.eclipse.jetty.websocket.jsr356.server.samples.BasicCloseSessionReasonSocket;
+import org.eclipse.jetty.websocket.jsr356.server.samples.BasicCloseSocket;
+import org.eclipse.jetty.websocket.jsr356.server.samples.BasicErrorSessionSocket;
+import org.eclipse.jetty.websocket.jsr356.server.samples.BasicErrorSessionThrowableSocket;
+import org.eclipse.jetty.websocket.jsr356.server.samples.BasicErrorSocket;
+import org.eclipse.jetty.websocket.jsr356.server.samples.BasicErrorThrowableSessionSocket;
+import org.eclipse.jetty.websocket.jsr356.server.samples.BasicErrorThrowableSocket;
+import org.eclipse.jetty.websocket.jsr356.server.samples.BasicOpenSessionSocket;
+import org.eclipse.jetty.websocket.jsr356.server.samples.BasicOpenSocket;
+import org.eclipse.jetty.websocket.jsr356.server.samples.BasicPongMessageSocket;
+import org.eclipse.jetty.websocket.jsr356.server.samples.BasicTextMessageStringSocket;
+import org.eclipse.jetty.websocket.jsr356.server.samples.StatelessTextMessageStringSocket;
+import org.eclipse.jetty.websocket.jsr356.server.samples.beans.DateTextSocket;
+import org.eclipse.jetty.websocket.jsr356.server.samples.primitives.BooleanObjectTextSocket;
+import org.eclipse.jetty.websocket.jsr356.server.samples.primitives.BooleanTextSocket;
+import org.eclipse.jetty.websocket.jsr356.server.samples.primitives.ByteObjectTextSocket;
+import org.eclipse.jetty.websocket.jsr356.server.samples.primitives.ByteTextSocket;
+import org.eclipse.jetty.websocket.jsr356.server.samples.primitives.CharTextSocket;
+import org.eclipse.jetty.websocket.jsr356.server.samples.primitives.CharacterObjectTextSocket;
+import org.eclipse.jetty.websocket.jsr356.server.samples.primitives.DoubleObjectTextSocket;
+import org.eclipse.jetty.websocket.jsr356.server.samples.primitives.DoubleTextSocket;
+import org.eclipse.jetty.websocket.jsr356.server.samples.primitives.FloatObjectTextSocket;
+import org.eclipse.jetty.websocket.jsr356.server.samples.primitives.FloatTextSocket;
+import org.eclipse.jetty.websocket.jsr356.server.samples.primitives.IntTextSocket;
+import org.eclipse.jetty.websocket.jsr356.server.samples.primitives.IntegerObjectTextSocket;
+import org.eclipse.jetty.websocket.jsr356.server.samples.primitives.ShortObjectTextSocket;
+import org.eclipse.jetty.websocket.jsr356.server.samples.primitives.ShortTextSocket;
+import org.eclipse.jetty.websocket.jsr356.server.samples.streaming.ReaderParamSocket;
+import org.eclipse.jetty.websocket.jsr356.server.samples.streaming.StringReturnReaderParamSocket;
+import org.eclipse.jetty.websocket.server.WebSocketServerFactory;
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+/**
+ * Test {@link JsrAnnotatedServerScanner} against various valid, simple, 1 method annotated classes with valid signatures.
+ */
+@RunWith(Parameterized.class)
+public class ServerAnnotatedEndpointScanner_GoodSignaturesTest
+{
+ public static class Case
+ {
+ public static void add(List<Case[]> data, Class<?> pojo, Field metadataField, Class<?>... expectedParams)
+ {
+ data.add(new Case[]
+ { new Case(pojo,metadataField,expectedParams) });
+ }
+
+ // The websocket pojo to test against
+ Class<?> pojo;
+ // The JsrAnnotatedMetadata field that should be populated
+ Field metadataField;
+ // The expected parameters for the Callable found by the scanner
+ Class<?> expectedParameters[];
+
+ public Case(Class<?> pojo, Field metadataField, Class<?>... expectedParams)
+ {
+ this.pojo = pojo;
+ this.metadataField = metadataField;
+ this.expectedParameters = expectedParams;
+ }
+ }
+
+ private static ServerContainer container = new ServerContainer(new DummyCreator(), new WebSocketServerFactory());
+
+ @Parameters
+ public static Collection<Case[]> data() throws Exception
+ {
+ List<Case[]> data = new ArrayList<>();
+ Field fOpen = findFieldRef(AnnotatedServerEndpointMetadata.class,"onOpen");
+ Field fClose = findFieldRef(AnnotatedServerEndpointMetadata.class,"onClose");
+ Field fError = findFieldRef(AnnotatedServerEndpointMetadata.class,"onError");
+ Field fText = findFieldRef(AnnotatedServerEndpointMetadata.class,"onText");
+ Field fTextStream = findFieldRef(AnnotatedServerEndpointMetadata.class,"onTextStream");
+ Field fBinary = findFieldRef(AnnotatedServerEndpointMetadata.class,"onBinary");
+ @SuppressWarnings("unused")
+ Field fBinaryStream = findFieldRef(AnnotatedServerEndpointMetadata.class,"onBinaryStream");
+ Field fPong = findFieldRef(AnnotatedServerEndpointMetadata.class,"onPong");
+
+ // @formatter:off
+ // -- Open Events
+ Case.add(data, BasicOpenSocket.class, fOpen);
+ Case.add(data, BasicOpenSessionSocket.class, fOpen, Session.class);
+ // -- Close Events
+ Case.add(data, BasicCloseSocket.class, fClose);
+ Case.add(data, BasicCloseReasonSocket.class, fClose, CloseReason.class);
+ Case.add(data, BasicCloseReasonSessionSocket.class, fClose, CloseReason.class, Session.class);
+ Case.add(data, BasicCloseSessionReasonSocket.class, fClose, Session.class, CloseReason.class);
+ // -- Error Events
+ Case.add(data, BasicErrorSocket.class, fError);
+ Case.add(data, BasicErrorSessionSocket.class, fError, Session.class);
+ Case.add(data, BasicErrorSessionThrowableSocket.class, fError, Session.class, Throwable.class);
+ Case.add(data, BasicErrorThrowableSocket.class, fError, Throwable.class);
+ Case.add(data, BasicErrorThrowableSessionSocket.class, fError, Throwable.class, Session.class);
+ // -- Text Events
+ Case.add(data, BasicTextMessageStringSocket.class, fText, String.class);
+ Case.add(data, StatelessTextMessageStringSocket.class, fText, Session.class, String.class);
+ // -- Primitives
+ Case.add(data, BooleanTextSocket.class, fText, Boolean.TYPE);
+ Case.add(data, BooleanObjectTextSocket.class, fText, Boolean.class);
+ Case.add(data, ByteTextSocket.class, fText, Byte.TYPE);
+ Case.add(data, ByteObjectTextSocket.class, fText, Byte.class);
+ Case.add(data, CharTextSocket.class, fText, Character.TYPE);
+ Case.add(data, CharacterObjectTextSocket.class, fText, Character.class);
+ Case.add(data, DoubleTextSocket.class, fText, Double.TYPE);
+ Case.add(data, DoubleObjectTextSocket.class, fText, Double.class);
+ Case.add(data, FloatTextSocket.class, fText, Float.TYPE);
+ Case.add(data, FloatObjectTextSocket.class, fText, Float.class);
+ Case.add(data, IntTextSocket.class, fText, Integer.TYPE);
+ Case.add(data, IntegerObjectTextSocket.class, fText, Integer.class);
+ Case.add(data, ShortTextSocket.class, fText, Short.TYPE);
+ Case.add(data, ShortObjectTextSocket.class, fText, Short.class);
+ // -- Beans
+ Case.add(data, DateTextSocket.class, fText, Date.class);
+ // -- Reader Events
+ Case.add(data, ReaderParamSocket.class, fTextStream, Reader.class, String.class);
+ Case.add(data, StringReturnReaderParamSocket.class, fTextStream, Reader.class, String.class);
+ // -- Binary Events
+ Case.add(data, BasicBinaryMessageByteBufferSocket.class, fBinary, ByteBuffer.class);
+ // -- Pong Events
+ Case.add(data, BasicPongMessageSocket.class, fPong, PongMessage.class);
+ // @formatter:on
+
+ // TODO: validate return types
+
+ return data;
+ }
+
+ private static Field findFieldRef(Class<?> clazz, String fldName) throws Exception
+ {
+ return clazz.getField(fldName);
+ }
+
+ private Case testcase;
+
+ public ServerAnnotatedEndpointScanner_GoodSignaturesTest(Case testcase)
+ {
+ this.testcase = testcase;
+ System.err.printf("Testing signature of %s%n",testcase.pojo.getName());
+ }
+
+ @Test
+ public void testScan_Basic() throws Exception
+ {
+ AnnotatedServerEndpointMetadata metadata = new AnnotatedServerEndpointMetadata(container,testcase.pojo,null);
+ AnnotatedEndpointScanner<ServerEndpoint, ServerEndpointConfig> scanner = new AnnotatedEndpointScanner<>(metadata);
+ scanner.scan();
+
+ Assert.assertThat("Metadata",metadata,notNullValue());
+
+ JsrCallable method = (JsrCallable)testcase.metadataField.get(metadata);
+ Assert.assertThat(testcase.metadataField.toString(),method,notNullValue());
+ int len = testcase.expectedParameters.length;
+ for (int i = 0; i < len; i++)
+ {
+ Class<?> expectedParam = testcase.expectedParameters[i];
+ Class<?> actualParam = method.getParamTypes()[i];
+
+ Assert.assertTrue("Parameter[" + i + "] - expected:[" + expectedParam + "], actual:[" + actualParam + "]",actualParam.equals(expectedParam));
+ }
+ }
+}
diff --git a/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/ServerAnnotatedEndpointScanner_InvalidSignaturesTest.java b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/ServerAnnotatedEndpointScanner_InvalidSignaturesTest.java
new file mode 100644
index 0000000..e459b1e
--- /dev/null
+++ b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/ServerAnnotatedEndpointScanner_InvalidSignaturesTest.java
@@ -0,0 +1,114 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.server;
+
+import static org.hamcrest.Matchers.*;
+
+import java.lang.annotation.Annotation;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+import javax.websocket.DeploymentException;
+import javax.websocket.OnClose;
+import javax.websocket.OnError;
+import javax.websocket.OnOpen;
+import javax.websocket.server.ServerEndpoint;
+import javax.websocket.server.ServerEndpointConfig;
+
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+import org.eclipse.jetty.websocket.common.events.annotated.InvalidSignatureException;
+import org.eclipse.jetty.websocket.jsr356.annotations.AnnotatedEndpointScanner;
+import org.eclipse.jetty.websocket.jsr356.server.samples.InvalidCloseIntSocket;
+import org.eclipse.jetty.websocket.jsr356.server.samples.InvalidErrorErrorSocket;
+import org.eclipse.jetty.websocket.jsr356.server.samples.InvalidErrorExceptionSocket;
+import org.eclipse.jetty.websocket.jsr356.server.samples.InvalidErrorIntSocket;
+import org.eclipse.jetty.websocket.jsr356.server.samples.InvalidOpenCloseReasonSocket;
+import org.eclipse.jetty.websocket.jsr356.server.samples.InvalidOpenIntSocket;
+import org.eclipse.jetty.websocket.jsr356.server.samples.InvalidOpenSessionIntSocket;
+import org.eclipse.jetty.websocket.server.WebSocketServerFactory;
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+/**
+ * Test {@link JsrAnnotatedServerScanner} against various simple, single method annotated classes with invalid signatures.
+ */
+@RunWith(Parameterized.class)
+public class ServerAnnotatedEndpointScanner_InvalidSignaturesTest
+{
+ private static final Logger LOG = Log.getLogger(ServerAnnotatedEndpointScanner_InvalidSignaturesTest.class);
+
+ private static ServerContainer container = new ServerContainer(new DummyCreator(), new WebSocketServerFactory());
+
+ @Parameters
+ public static Collection<Class<?>[]> data()
+ {
+ List<Class<?>[]> data = new ArrayList<>();
+
+ // @formatter:off
+ data.add(new Class<?>[]{ InvalidCloseIntSocket.class, OnClose.class });
+ data.add(new Class<?>[]{ InvalidErrorErrorSocket.class, OnError.class });
+ data.add(new Class<?>[]{ InvalidErrorExceptionSocket.class, OnError.class });
+ data.add(new Class<?>[]{ InvalidErrorIntSocket.class, OnError.class });
+ data.add(new Class<?>[]{ InvalidOpenCloseReasonSocket.class, OnOpen.class });
+ data.add(new Class<?>[]{ InvalidOpenIntSocket.class, OnOpen.class });
+ data.add(new Class<?>[]{ InvalidOpenSessionIntSocket.class, OnOpen.class });
+ // @formatter:on
+
+ // TODO: invalid return types
+ // TODO: static methods
+ // TODO: private or protected methods
+ // TODO: abstract methods
+
+ return data;
+ }
+
+ // The pojo to test
+ private Class<?> pojo;
+ // The annotation class expected to be mentioned in the error message
+ private Class<? extends Annotation> expectedAnnoClass;
+
+ public ServerAnnotatedEndpointScanner_InvalidSignaturesTest(Class<?> pojo, Class<? extends Annotation> expectedAnnotation)
+ {
+ this.pojo = pojo;
+ this.expectedAnnoClass = expectedAnnotation;
+ }
+
+ @Test
+ public void testScan_InvalidSignature() throws DeploymentException
+ {
+ AnnotatedServerEndpointMetadata metadata = new AnnotatedServerEndpointMetadata(container,pojo,null);
+ AnnotatedEndpointScanner<ServerEndpoint,ServerEndpointConfig> scanner = new AnnotatedEndpointScanner<>(metadata);
+
+ try
+ {
+ scanner.scan();
+ Assert.fail("Expected " + InvalidSignatureException.class + " with message that references " + expectedAnnoClass + " annotation");
+ }
+ catch (InvalidSignatureException e)
+ {
+ LOG.debug("{}:{}",e.getClass(),e.getMessage());
+ Assert.assertThat("Message",e.getMessage(),containsString(expectedAnnoClass.getSimpleName()));
+ }
+ }
+}
diff --git a/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/StackUtil.java b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/StackUtil.java
new file mode 100644
index 0000000..296c947
--- /dev/null
+++ b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/StackUtil.java
@@ -0,0 +1,42 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.server;
+
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+
+public final class StackUtil
+{
+ public static String toString(Throwable t)
+ {
+ try (StringWriter w = new StringWriter())
+ {
+ try (PrintWriter out = new PrintWriter(w))
+ {
+ t.printStackTrace(out);
+ return w.toString();
+ }
+ }
+ catch (IOException e)
+ {
+ return "Unable to get stacktrace for: " + t;
+ }
+ }
+}
diff --git a/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/TrackingSocket.java b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/TrackingSocket.java
new file mode 100644
index 0000000..788f23d
--- /dev/null
+++ b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/TrackingSocket.java
@@ -0,0 +1,125 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.server;
+
+import static org.hamcrest.Matchers.*;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+import javax.websocket.CloseReason;
+import javax.websocket.CloseReason.CloseCode;
+
+import org.eclipse.jetty.toolchain.test.EventQueue;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+import org.junit.Assert;
+
+/**
+ * Abstract base socket used for tracking state and events within the socket for testing reasons.
+ */
+public abstract class TrackingSocket
+{
+ private static final Logger LOG = Log.getLogger(TrackingSocket.class);
+
+ public CloseReason closeReason;
+ public EventQueue<String> eventQueue = new EventQueue<String>();
+ public EventQueue<Throwable> errorQueue = new EventQueue<>();
+ public CountDownLatch openLatch = new CountDownLatch(1);
+ public CountDownLatch closeLatch = new CountDownLatch(1);
+ public CountDownLatch dataLatch = new CountDownLatch(1);
+
+ protected void addError(Throwable t)
+ {
+ LOG.warn(t);
+ errorQueue.add(t);
+ }
+
+ protected void addEvent(String format, Object... args)
+ {
+ eventQueue.add(String.format(format,args));
+ }
+
+ public void assertClose(CloseCode expectedCode, String expectedReason) throws InterruptedException
+ {
+ assertCloseCode(expectedCode);
+ assertCloseReason(expectedReason);
+ }
+
+ public void assertCloseCode(CloseCode expectedCode) throws InterruptedException
+ {
+ Assert.assertThat("Was Closed",closeLatch.await(50,TimeUnit.MILLISECONDS),is(true));
+ Assert.assertThat("CloseReason",closeReason,notNullValue());
+ Assert.assertThat("Close Code",closeReason.getCloseCode(),is(expectedCode));
+ }
+
+ private void assertCloseReason(String expectedReason)
+ {
+ Assert.assertThat("Close Reason",closeReason.getReasonPhrase(),is(expectedReason));
+ }
+
+ public void assertEvent(String expected)
+ {
+ String actual = eventQueue.poll();
+ Assert.assertEquals("Event",expected,actual);
+ }
+
+ public void assertIsOpen() throws InterruptedException
+ {
+ assertWasOpened();
+ assertNotClosed();
+ }
+
+ public void assertNotClosed()
+ {
+ Assert.assertThat("Closed Latch",closeLatch.getCount(),greaterThanOrEqualTo(1L));
+ }
+
+ public void assertNotOpened()
+ {
+ Assert.assertThat("Open Latch",openLatch.getCount(),greaterThanOrEqualTo(1L));
+ }
+
+ public void assertWasOpened() throws InterruptedException
+ {
+ Assert.assertThat("Was Opened",openLatch.await(500,TimeUnit.MILLISECONDS),is(true));
+ }
+
+ public void clear()
+ {
+ eventQueue.clear();
+ errorQueue.clear();
+ }
+
+ public void waitForClose(int timeoutDuration, TimeUnit timeoutUnit) throws InterruptedException
+ {
+ Assert.assertThat("Client Socket Closed",closeLatch.await(timeoutDuration,timeoutUnit),is(true));
+ }
+
+ public void waitForConnected(int timeoutDuration, TimeUnit timeoutUnit) throws InterruptedException
+ {
+ Assert.assertThat("Client Socket Connected",openLatch.await(timeoutDuration,timeoutUnit),is(true));
+ }
+
+ public void waitForData(int timeoutDuration, TimeUnit timeoutUnit) throws InterruptedException
+ {
+ LOG.debug("Waiting for message");
+ Assert.assertThat("Data Received",dataLatch.await(timeoutDuration,timeoutUnit),is(true));
+ }
+}
diff --git a/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/WSServer.java b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/WSServer.java
new file mode 100644
index 0000000..d35fe99
--- /dev/null
+++ b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/WSServer.java
@@ -0,0 +1,197 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.server;
+
+import static org.hamcrest.Matchers.*;
+
+import java.io.File;
+import java.io.IOException;
+import java.net.MalformedURLException;
+import java.net.URI;
+import java.net.URL;
+
+import org.eclipse.jetty.annotations.AnnotationConfiguration;
+import org.eclipse.jetty.plus.webapp.EnvConfiguration;
+import org.eclipse.jetty.plus.webapp.PlusConfiguration;
+import org.eclipse.jetty.server.Server;
+import org.eclipse.jetty.server.ServerConnector;
+import org.eclipse.jetty.server.handler.ContextHandlerCollection;
+import org.eclipse.jetty.server.handler.HandlerCollection;
+import org.eclipse.jetty.toolchain.test.FS;
+import org.eclipse.jetty.toolchain.test.IO;
+import org.eclipse.jetty.toolchain.test.MavenTestingUtils;
+import org.eclipse.jetty.toolchain.test.OS;
+import org.eclipse.jetty.toolchain.test.TestingDir;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+import org.eclipse.jetty.util.resource.Resource;
+import org.eclipse.jetty.webapp.Configuration;
+import org.eclipse.jetty.webapp.FragmentConfiguration;
+import org.eclipse.jetty.webapp.MetaInfConfiguration;
+import org.eclipse.jetty.webapp.WebAppContext;
+import org.eclipse.jetty.webapp.WebInfConfiguration;
+import org.eclipse.jetty.webapp.WebXmlConfiguration;
+import org.junit.Assert;
+
+/**
+ * Utility to build out exploded directory WebApps, in the /target/tests/ directory, for testing out servers that use javax.websocket endpoints.
+ * <p>
+ * This is particularly useful when the WebSocket endpoints are discovered via the javax.websocket annotation scanning.
+ */
+public class WSServer
+{
+ private static final Logger LOG = Log.getLogger(WSServer.class);
+ private final File contextDir;
+ private final String contextPath;
+ private Server server;
+ private URI serverUri;
+ private ContextHandlerCollection contexts;
+ private File webinf;
+ private File classesDir;
+
+ public WSServer(TestingDir testdir, String contextName)
+ {
+ this(testdir.getDir(),contextName);
+ }
+
+ public WSServer(File testdir, String contextName)
+ {
+ this.contextDir = new File(testdir,contextName);
+ this.contextPath = "/" + contextName;
+ FS.ensureEmpty(contextDir);
+ }
+
+ public void copyClass(Class<?> clazz) throws Exception
+ {
+ ClassLoader cl = Thread.currentThread().getContextClassLoader();
+ String endpointPath = clazz.getName().replace('.','/') + ".class";
+ URL classUrl = cl.getResource(endpointPath);
+ Assert.assertThat("Class URL for: " + clazz,classUrl,notNullValue());
+ File destFile = new File(classesDir,OS.separators(endpointPath));
+ FS.ensureDirExists(destFile.getParentFile());
+ File srcFile = new File(classUrl.toURI());
+ IO.copy(srcFile,destFile);
+ }
+
+ public void copyEndpoint(Class<?> endpointClass) throws Exception
+ {
+ copyClass(endpointClass);
+ }
+
+ public void copyWebInf(String testResourceName) throws IOException
+ {
+ webinf = new File(contextDir,"WEB-INF");
+ FS.ensureDirExists(webinf);
+ classesDir = new File(webinf,"classes");
+ FS.ensureDirExists(classesDir);
+ File webxml = new File(webinf,"web.xml");
+ File testWebXml = MavenTestingUtils.getTestResourceFile(testResourceName);
+ IO.copy(testWebXml,webxml);
+ }
+
+ public WebAppContext createWebAppContext() throws MalformedURLException, IOException
+ {
+ WebAppContext context = new WebAppContext();
+ context.setContextPath(this.contextPath);
+ context.setBaseResource(Resource.newResource(this.contextDir));
+
+ // @formatter:off
+ context.setConfigurations(new Configuration[] {
+ new WebSocketConfiguration(),
+ new AnnotationConfiguration(),
+ new WebXmlConfiguration(),
+ new WebInfConfiguration(),
+ new PlusConfiguration(),
+ new MetaInfConfiguration(),
+ new FragmentConfiguration(),
+ new EnvConfiguration()});
+ // @formatter:on
+
+ return context;
+ }
+
+ public void createWebInf() throws IOException
+ {
+ copyWebInf("empty-web.xml");
+ }
+
+ public void deployWebapp(WebAppContext webapp) throws Exception
+ {
+ contexts.addHandler(webapp);
+ webapp.start();
+ if (LOG.isDebugEnabled())
+ {
+ webapp.dump(System.err);
+ }
+ }
+
+ public void dump()
+ {
+ server.dumpStdErr();
+ }
+
+ public URI getServerBaseURI()
+ {
+ return serverUri;
+ }
+
+ public File getWebAppDir()
+ {
+ return this.contextDir;
+ }
+
+ public void start() throws Exception
+ {
+ server = new Server();
+ ServerConnector connector = new ServerConnector(server);
+ server.addConnector(connector);
+
+ HandlerCollection handlers = new HandlerCollection();
+ contexts = new ContextHandlerCollection();
+ handlers.addHandler(contexts);
+ server.setHandler(handlers);
+
+ server.start();
+
+ String host = connector.getHost();
+ if (host == null)
+ {
+ host = "localhost";
+ }
+ int port = connector.getLocalPort();
+ serverUri = new URI(String.format("ws://%s:%d%s/",host,port,contextPath));
+ LOG.debug("Server started on {}",serverUri);
+
+ }
+
+ public void stop()
+ {
+ if (server != null)
+ {
+ try
+ {
+ server.stop();
+ }
+ catch (Exception e)
+ {
+ e.printStackTrace(System.err);
+ }
+ }
+ }
+}
diff --git a/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/deploy/DiscoveredEndpointsTest.java b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/deploy/DiscoveredEndpointsTest.java
new file mode 100644
index 0000000..3bcaf07
--- /dev/null
+++ b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/deploy/DiscoveredEndpointsTest.java
@@ -0,0 +1,57 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.server.deploy;
+
+import static org.hamcrest.Matchers.*;
+
+import java.net.URI;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+public class DiscoveredEndpointsTest
+{
+ /**
+ * Attempt to get an Archive URI, to a class known to be in an archive.
+ */
+ @Test
+ public void testGetArchiveURI_InJar()
+ {
+ Class<?> clazz = javax.websocket.server.ServerContainer.class;
+ URI archiveURI = DiscoveredEndpoints.getArchiveURI(clazz);
+ // should point to a JAR file
+ Assert.assertThat("Archive URI for: " + clazz.getName(),
+ archiveURI.toASCIIString(),
+ endsWith("javax.websocket-api-1.0.jar"));
+ }
+
+ /**
+ * Get an Archive URI for a class reference that is known to not be in an archive.
+ */
+ @Test
+ public void testGetArchiveURI_InClassDirectory()
+ {
+ Class<?> clazz = DiscoveredEndpointsTest.class;
+ URI archivePath = DiscoveredEndpoints.getArchiveURI(clazz);
+ // Should be null, as it does not point to an archive
+ Assert.assertThat("Archive URI for: " + clazz,
+ archivePath,
+ nullValue());
+ }
+}
diff --git a/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/pathmap/PathMappingsTest.java b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/pathmap/PathMappingsTest.java
new file mode 100644
index 0000000..247b738
--- /dev/null
+++ b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/pathmap/PathMappingsTest.java
@@ -0,0 +1,110 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.server.pathmap;
+
+import static org.hamcrest.Matchers.*;
+
+import org.eclipse.jetty.websocket.server.pathmap.PathMappings;
+import org.eclipse.jetty.websocket.server.pathmap.PathMappings.MappedResource;
+import org.eclipse.jetty.websocket.server.pathmap.ServletPathSpec;
+import org.junit.Assert;
+import org.junit.Test;
+
+public class PathMappingsTest
+{
+ private void assertMatch(PathMappings<String> pathmap, String path, String expectedValue)
+ {
+ String msg = String.format(".getMatch(\"%s\")",path);
+ MappedResource<String> match = pathmap.getMatch(path);
+ Assert.assertThat(msg,match,notNullValue());
+ String actualMatch = match.getResource();
+ Assert.assertEquals(msg,expectedValue,actualMatch);
+ }
+
+ public void dumpMappings(PathMappings<String> p)
+ {
+ for (MappedResource<String> res : p)
+ {
+ System.out.printf(" %s%n",res);
+ }
+ }
+
+ /**
+ * Test the match order rules with a mixed Servlet and WebSocket path specs
+ * <p>
+ * <ul>
+ * <li>Exact match</li>
+ * <li>Longest prefix match</li>
+ * <li>Longest suffix match</li>
+ * </ul>
+ */
+ @Test
+ public void testMixedMatchOrder()
+ {
+ PathMappings<String> p = new PathMappings<>();
+
+ p.put(new ServletPathSpec("/"),"default");
+ p.put(new ServletPathSpec("/animal/bird/*"),"birds");
+ p.put(new ServletPathSpec("/animal/fish/*"),"fishes");
+ p.put(new ServletPathSpec("/animal/*"),"animals");
+ p.put(new WebSocketPathSpec("/animal/{type}/{name}/chat"),"animalChat");
+ p.put(new WebSocketPathSpec("/animal/{type}/{name}/cam"),"animalCam");
+ p.put(new WebSocketPathSpec("/entrance/cam"),"entranceCam");
+
+ // dumpMappings(p);
+
+ assertMatch(p,"/animal/bird/eagle","birds");
+ assertMatch(p,"/animal/fish/bass/sea","fishes");
+ assertMatch(p,"/animal/peccary/javalina/evolution","animals");
+ assertMatch(p,"/","default");
+ assertMatch(p,"/animal/bird/eagle/chat","animalChat");
+ assertMatch(p,"/animal/bird/penguin/chat","animalChat");
+ assertMatch(p,"/animal/fish/trout/cam","animalCam");
+ assertMatch(p,"/entrance/cam","entranceCam");
+ }
+
+ /**
+ * Test the match order rules imposed by the WebSocket API (JSR-356)
+ * <p>
+ * <ul>
+ * <li>Exact match</li>
+ * <li>Longest prefix match</li>
+ * <li>Longest suffix match</li>
+ * </ul>
+ */
+ @Test
+ public void testWebsocketMatchOrder()
+ {
+ PathMappings<String> p = new PathMappings<>();
+
+ p.put(new WebSocketPathSpec("/a/{var}/c"),"endpointA");
+ p.put(new WebSocketPathSpec("/a/b/c"),"endpointB");
+ p.put(new WebSocketPathSpec("/a/{var1}/{var2}"),"endpointC");
+ p.put(new WebSocketPathSpec("/{var1}/d"),"endpointD");
+ p.put(new WebSocketPathSpec("/b/{var2}"),"endpointE");
+
+ // dumpMappings(p);
+
+ assertMatch(p,"/a/b/c","endpointB");
+ assertMatch(p,"/a/d/c","endpointA");
+ assertMatch(p,"/a/x/y","endpointC");
+
+ assertMatch(p,"/b/d","endpointE");
+ }
+}
diff --git a/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/pathmap/WebSocketPathSpecBadSpecsTest.java b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/pathmap/WebSocketPathSpecBadSpecsTest.java
new file mode 100644
index 0000000..24e443a
--- /dev/null
+++ b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/pathmap/WebSocketPathSpecBadSpecsTest.java
@@ -0,0 +1,87 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.server.pathmap;
+
+import static org.junit.Assert.*;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+/**
+ * Tests for bad path specs on ServerEndpoint Path Param / URI Template
+ */
+@RunWith(Parameterized.class)
+public class WebSocketPathSpecBadSpecsTest
+{
+ private static void bad(List<String[]> data, String str)
+ {
+ data.add(new String[]
+ { str });
+ }
+
+ @Parameters
+ public static Collection<String[]> data()
+ {
+ List<String[]> data = new ArrayList<>();
+ bad(data,"/a/b{var}"); // bad syntax - variable does not encompass whole path segment
+ bad(data,"a/{var}"); // bad syntax - no start slash
+ bad(data,"/a/{var/b}"); // path segment separator in variable name
+ bad(data,"/{var}/*"); // bad syntax - no globs allowed
+ bad(data,"/{var}.do"); // bad syntax - variable does not encompass whole path segment
+ bad(data,"/a/{var*}"); // use of glob character not allowed in variable name
+ bad(data,"/a/{}"); // bad syntax - no variable name
+ // MIGHT BE ALLOWED bad(data,"/a/{---}"); // no alpha in variable name
+ bad(data,"{var}"); // bad syntax - no start slash
+ bad(data,"/a/{my special variable}"); // bad syntax - space in variable name
+ bad(data,"/a/{var}/{var}"); // variable name duplicate
+ // MIGHT BE ALLOWED bad(data,"/a/{var}/{Var}/{vAR}"); // variable name duplicated (diff case)
+ bad(data,"/a/../../../{var}"); // path navigation not allowed
+ bad(data,"/a/./{var}"); // path navigation not allowed
+ bad(data,"/a//{var}"); // bad syntax - double path slash (no path segment)
+ return data;
+ }
+
+ private String pathSpec;
+
+ public WebSocketPathSpecBadSpecsTest(String pathSpec)
+ {
+ this.pathSpec = pathSpec;
+ }
+
+ @Test
+ public void testBadPathSpec()
+ {
+ try
+ {
+ new WebSocketPathSpec(this.pathSpec);
+ fail("Expected IllegalArgumentException for a bad PathParam pathspec on: " + pathSpec);
+ }
+ catch (IllegalArgumentException e)
+ {
+ // expected path
+ System.out.println(e.getMessage());
+ }
+ }
+}
diff --git a/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/pathmap/WebSocketPathSpecTest.java b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/pathmap/WebSocketPathSpecTest.java
new file mode 100644
index 0000000..864f685
--- /dev/null
+++ b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/pathmap/WebSocketPathSpecTest.java
@@ -0,0 +1,268 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.server.pathmap;
+
+import static org.hamcrest.Matchers.*;
+import static org.junit.Assert.*;
+
+import java.util.Map;
+
+import org.eclipse.jetty.websocket.server.pathmap.PathSpec;
+import org.eclipse.jetty.websocket.server.pathmap.PathSpecGroup;
+import org.eclipse.jetty.websocket.server.pathmap.ServletPathSpec;
+import org.junit.Test;
+
+/**
+ * Tests for ServerEndpoint Path Param / URI Template Path Specs
+ */
+public class WebSocketPathSpecTest
+{
+ private void assertDetectedVars(WebSocketPathSpec spec, String... expectedVars)
+ {
+ String prefix = String.format("Spec(\"%s\")",spec.getPathSpec());
+ assertEquals(prefix + ".variableCount",expectedVars.length,spec.getVariableCount());
+ assertEquals(prefix + ".variable.length",expectedVars.length,spec.getVariables().length);
+ for (int i = 0; i < expectedVars.length; i++)
+ {
+ assertEquals(String.format("%s.variable[%d]",prefix,i),expectedVars[i],spec.getVariables()[i]);
+ }
+ }
+
+ private void assertMatches(PathSpec spec, String path)
+ {
+ String msg = String.format("Spec(\"%s\").matches(\"%s\")",spec.getPathSpec(),path);
+ assertThat(msg,spec.matches(path),is(true));
+ }
+
+ private void assertNotMatches(PathSpec spec, String path)
+ {
+ String msg = String.format("!Spec(\"%s\").matches(\"%s\")",spec.getPathSpec(),path);
+ assertThat(msg,spec.matches(path),is(false));
+ }
+
+ @Test
+ public void testDefaultPathSpec()
+ {
+ WebSocketPathSpec spec = new WebSocketPathSpec("/");
+ assertEquals("Spec.pathSpec","/",spec.getPathSpec());
+ assertEquals("Spec.pattern","^/$",spec.getPattern().pattern());
+ assertEquals("Spec.pathDepth",1,spec.getPathDepth());
+ assertEquals("Spec.group",PathSpecGroup.EXACT,spec.getGroup());
+
+ assertEquals("Spec.variableCount",0,spec.getVariableCount());
+ assertEquals("Spec.variable.length",0,spec.getVariables().length);
+ }
+
+ @Test
+ public void testExactOnePathSpec()
+ {
+ WebSocketPathSpec spec = new WebSocketPathSpec("/a");
+ assertEquals("Spec.pathSpec","/a",spec.getPathSpec());
+ assertEquals("Spec.pattern","^/a$",spec.getPattern().pattern());
+ assertEquals("Spec.pathDepth",1,spec.getPathDepth());
+ assertEquals("Spec.group",PathSpecGroup.EXACT,spec.getGroup());
+
+ assertMatches(spec,"/a");
+ assertNotMatches(spec,"/a/b");
+ assertNotMatches(spec,"/a/");
+
+ assertEquals("Spec.variableCount",0,spec.getVariableCount());
+ assertEquals("Spec.variable.length",0,spec.getVariables().length);
+ }
+
+ @Test
+ public void testExactTwoPathSpec()
+ {
+ WebSocketPathSpec spec = new WebSocketPathSpec("/a/b");
+ assertEquals("Spec.pathSpec","/a/b",spec.getPathSpec());
+ assertEquals("Spec.pattern","^/a/b$",spec.getPattern().pattern());
+ assertEquals("Spec.pathDepth",2,spec.getPathDepth());
+ assertEquals("Spec.group",PathSpecGroup.EXACT,spec.getGroup());
+
+ assertEquals("Spec.variableCount",0,spec.getVariableCount());
+ assertEquals("Spec.variable.length",0,spec.getVariables().length);
+
+ assertMatches(spec,"/a/b");
+
+ assertNotMatches(spec,"/a/b/");
+ assertNotMatches(spec,"/a/");
+ assertNotMatches(spec,"/a/bb");
+ }
+
+ @Test
+ public void testMiddleVarPathSpec()
+ {
+ WebSocketPathSpec spec = new WebSocketPathSpec("/a/{var}/c");
+ assertEquals("Spec.pathSpec","/a/{var}/c",spec.getPathSpec());
+ assertEquals("Spec.pattern","^/a/([^/]+)/c$",spec.getPattern().pattern());
+ assertEquals("Spec.pathDepth",3,spec.getPathDepth());
+ assertEquals("Spec.group",PathSpecGroup.MIDDLE_GLOB,spec.getGroup());
+
+ assertDetectedVars(spec,"var");
+
+ assertMatches(spec,"/a/b/c");
+ assertMatches(spec,"/a/zz/c");
+ assertMatches(spec,"/a/hello+world/c");
+ assertNotMatches(spec,"/a/bc");
+ assertNotMatches(spec,"/a/b/");
+ assertNotMatches(spec,"/a/b");
+
+ Map<String, String> mapped = spec.getPathParams("/a/b/c");
+ assertThat("Spec.pathParams",mapped,notNullValue());
+ assertThat("Spec.pathParams.size",mapped.size(),is(1));
+ assertEquals("Spec.pathParams[var]","b",mapped.get("var"));
+ }
+
+ @Test
+ public void testOneVarPathSpec()
+ {
+ WebSocketPathSpec spec = new WebSocketPathSpec("/a/{foo}");
+ assertEquals("Spec.pathSpec","/a/{foo}",spec.getPathSpec());
+ assertEquals("Spec.pattern","^/a/([^/]+)$",spec.getPattern().pattern());
+ assertEquals("Spec.pathDepth",2,spec.getPathDepth());
+ assertEquals("Spec.group",PathSpecGroup.PREFIX_GLOB,spec.getGroup());
+
+ assertDetectedVars(spec,"foo");
+
+ assertMatches(spec,"/a/b");
+ assertNotMatches(spec,"/a/");
+ assertNotMatches(spec,"/a");
+
+ Map<String, String> mapped = spec.getPathParams("/a/b");
+ assertThat("Spec.pathParams",mapped,notNullValue());
+ assertThat("Spec.pathParams.size",mapped.size(),is(1));
+ assertEquals("Spec.pathParams[foo]","b",mapped.get("foo"));
+ }
+
+ @Test
+ public void testOneVarSuffixPathSpec()
+ {
+ WebSocketPathSpec spec = new WebSocketPathSpec("/{var}/b/c");
+ assertEquals("Spec.pathSpec","/{var}/b/c",spec.getPathSpec());
+ assertEquals("Spec.pattern","^/([^/]+)/b/c$",spec.getPattern().pattern());
+ assertEquals("Spec.pathDepth",3,spec.getPathDepth());
+ assertEquals("Spec.group",PathSpecGroup.SUFFIX_GLOB,spec.getGroup());
+
+ assertDetectedVars(spec,"var");
+
+ assertMatches(spec,"/a/b/c");
+ assertMatches(spec,"/az/b/c");
+ assertMatches(spec,"/hello+world/b/c");
+ assertNotMatches(spec,"/a/bc");
+ assertNotMatches(spec,"/a/b/");
+ assertNotMatches(spec,"/a/b");
+
+ Map<String, String> mapped = spec.getPathParams("/a/b/c");
+ assertThat("Spec.pathParams",mapped,notNullValue());
+ assertThat("Spec.pathParams.size",mapped.size(),is(1));
+ assertEquals("Spec.pathParams[var]","a",mapped.get("var"));
+ }
+
+ @Test
+ public void testTwoVarComplexInnerPathSpec()
+ {
+ WebSocketPathSpec spec = new WebSocketPathSpec("/a/{var1}/c/{var2}/e");
+ assertEquals("Spec.pathSpec","/a/{var1}/c/{var2}/e",spec.getPathSpec());
+ assertEquals("Spec.pattern","^/a/([^/]+)/c/([^/]+)/e$",spec.getPattern().pattern());
+ assertEquals("Spec.pathDepth",5,spec.getPathDepth());
+ assertEquals("Spec.group",PathSpecGroup.MIDDLE_GLOB,spec.getGroup());
+
+ assertDetectedVars(spec,"var1","var2");
+
+ assertMatches(spec,"/a/b/c/d/e");
+ assertNotMatches(spec,"/a/bc/d/e");
+ assertNotMatches(spec,"/a/b/d/e");
+ assertNotMatches(spec,"/a/b//d/e");
+
+ Map<String, String> mapped = spec.getPathParams("/a/b/c/d/e");
+ assertThat("Spec.pathParams",mapped,notNullValue());
+ assertThat("Spec.pathParams.size",mapped.size(),is(2));
+ assertEquals("Spec.pathParams[var1]","b",mapped.get("var1"));
+ assertEquals("Spec.pathParams[var2]","d",mapped.get("var2"));
+ }
+
+ @Test
+ public void testTwoVarComplexOuterPathSpec()
+ {
+ WebSocketPathSpec spec = new WebSocketPathSpec("/{var1}/b/{var2}/{var3}");
+ assertEquals("Spec.pathSpec","/{var1}/b/{var2}/{var3}",spec.getPathSpec());
+ assertEquals("Spec.pattern","^/([^/]+)/b/([^/]+)/([^/]+)$",spec.getPattern().pattern());
+ assertEquals("Spec.pathDepth",4,spec.getPathDepth());
+ assertEquals("Spec.group",PathSpecGroup.MIDDLE_GLOB,spec.getGroup());
+
+ assertDetectedVars(spec,"var1","var2","var3");
+
+ assertMatches(spec,"/a/b/c/d");
+ assertNotMatches(spec,"/a/bc/d/e");
+ assertNotMatches(spec,"/a/c/d/e");
+ assertNotMatches(spec,"/a//d/e");
+
+ Map<String, String> mapped = spec.getPathParams("/a/b/c/d");
+ assertThat("Spec.pathParams",mapped,notNullValue());
+ assertThat("Spec.pathParams.size",mapped.size(),is(3));
+ assertEquals("Spec.pathParams[var1]","a",mapped.get("var1"));
+ assertEquals("Spec.pathParams[var2]","c",mapped.get("var2"));
+ assertEquals("Spec.pathParams[var3]","d",mapped.get("var3"));
+ }
+
+ @Test
+ public void testTwoVarPrefixPathSpec()
+ {
+ WebSocketPathSpec spec = new WebSocketPathSpec("/a/{var1}/{var2}");
+ assertEquals("Spec.pathSpec","/a/{var1}/{var2}",spec.getPathSpec());
+ assertEquals("Spec.pattern","^/a/([^/]+)/([^/]+)$",spec.getPattern().pattern());
+ assertEquals("Spec.pathDepth",3,spec.getPathDepth());
+ assertEquals("Spec.group",PathSpecGroup.PREFIX_GLOB,spec.getGroup());
+
+ assertDetectedVars(spec,"var1","var2");
+
+ assertMatches(spec,"/a/b/c");
+ assertNotMatches(spec,"/a/bc");
+ assertNotMatches(spec,"/a/b/");
+ assertNotMatches(spec,"/a/b");
+
+ Map<String, String> mapped = spec.getPathParams("/a/b/c");
+ assertThat("Spec.pathParams",mapped,notNullValue());
+ assertThat("Spec.pathParams.size",mapped.size(),is(2));
+ assertEquals("Spec.pathParams[var1]","b",mapped.get("var1"));
+ assertEquals("Spec.pathParams[var2]","c",mapped.get("var2"));
+ }
+
+ @Test
+ public void testVarOnlyPathSpec()
+ {
+ WebSocketPathSpec spec = new WebSocketPathSpec("/{var1}");
+ assertEquals("Spec.pathSpec","/{var1}",spec.getPathSpec());
+ assertEquals("Spec.pattern","^/([^/]+)$",spec.getPattern().pattern());
+ assertEquals("Spec.pathDepth",1,spec.getPathDepth());
+ assertEquals("Spec.group",PathSpecGroup.PREFIX_GLOB,spec.getGroup());
+
+ assertDetectedVars(spec,"var1");
+
+ assertMatches(spec,"/a");
+ assertNotMatches(spec,"/");
+ assertNotMatches(spec,"/a/b");
+ assertNotMatches(spec,"/a/b/c");
+
+ Map<String, String> mapped = spec.getPathParams("/a");
+ assertThat("Spec.pathParams",mapped,notNullValue());
+ assertThat("Spec.pathParams.size",mapped.size(),is(1));
+ assertEquals("Spec.pathParams[var1]","a",mapped.get("var1"));
+ }
+}
diff --git a/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/samples/BasicBinaryMessageByteBufferSocket.java b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/samples/BasicBinaryMessageByteBufferSocket.java
new file mode 100644
index 0000000..c654e35
--- /dev/null
+++ b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/samples/BasicBinaryMessageByteBufferSocket.java
@@ -0,0 +1,37 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.server.samples;
+
+import java.nio.ByteBuffer;
+
+import javax.websocket.OnMessage;
+import javax.websocket.server.ServerEndpoint;
+
+import org.eclipse.jetty.websocket.jsr356.server.TrackingSocket;
+
+@ServerEndpoint(value="/basic")
+public class BasicBinaryMessageByteBufferSocket extends TrackingSocket
+{
+ @OnMessage
+ public void onBinary(ByteBuffer data)
+ {
+ addEvent("onBinary(%s)",data);
+ dataLatch.countDown();
+ }
+}
diff --git a/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/samples/BasicCloseReasonSessionSocket.java b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/samples/BasicCloseReasonSessionSocket.java
new file mode 100644
index 0000000..bf26e84
--- /dev/null
+++ b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/samples/BasicCloseReasonSessionSocket.java
@@ -0,0 +1,37 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.server.samples;
+
+import javax.websocket.CloseReason;
+import javax.websocket.OnClose;
+import javax.websocket.Session;
+import javax.websocket.server.ServerEndpoint;
+
+import org.eclipse.jetty.websocket.jsr356.server.TrackingSocket;
+
+@ServerEndpoint(value="/basic")
+public class BasicCloseReasonSessionSocket extends TrackingSocket
+{
+ @OnClose
+ public void onClose(CloseReason reason, Session session)
+ {
+ addEvent("onClose(%s,%s)",reason,session);
+ closeLatch.countDown();
+ }
+}
diff --git a/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/samples/BasicCloseReasonSocket.java b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/samples/BasicCloseReasonSocket.java
new file mode 100644
index 0000000..33b8125
--- /dev/null
+++ b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/samples/BasicCloseReasonSocket.java
@@ -0,0 +1,36 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.server.samples;
+
+import javax.websocket.CloseReason;
+import javax.websocket.OnClose;
+import javax.websocket.server.ServerEndpoint;
+
+import org.eclipse.jetty.websocket.jsr356.server.TrackingSocket;
+
+@ServerEndpoint(value="/basic")
+public class BasicCloseReasonSocket extends TrackingSocket
+{
+ @OnClose
+ public void onClose(CloseReason reason)
+ {
+ addEvent("onClose(%s)", reason);
+ closeLatch.countDown();
+ }
+}
diff --git a/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/samples/BasicCloseSessionReasonSocket.java b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/samples/BasicCloseSessionReasonSocket.java
new file mode 100644
index 0000000..ec7b630
--- /dev/null
+++ b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/samples/BasicCloseSessionReasonSocket.java
@@ -0,0 +1,37 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.server.samples;
+
+import javax.websocket.CloseReason;
+import javax.websocket.OnClose;
+import javax.websocket.Session;
+import javax.websocket.server.ServerEndpoint;
+
+import org.eclipse.jetty.websocket.jsr356.server.TrackingSocket;
+
+@ServerEndpoint(value="/basic")
+public class BasicCloseSessionReasonSocket extends TrackingSocket
+{
+ @OnClose
+ public void onClose(Session session, CloseReason reason)
+ {
+ addEvent("onClose(%s,%s)",session,reason);
+ closeLatch.countDown();
+ }
+}
diff --git a/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/samples/BasicCloseSocket.java b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/samples/BasicCloseSocket.java
new file mode 100644
index 0000000..c8408aa
--- /dev/null
+++ b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/samples/BasicCloseSocket.java
@@ -0,0 +1,35 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.server.samples;
+
+import javax.websocket.OnClose;
+import javax.websocket.server.ServerEndpoint;
+
+import org.eclipse.jetty.websocket.jsr356.server.TrackingSocket;
+
+@ServerEndpoint(value = "/basic")
+public class BasicCloseSocket extends TrackingSocket
+{
+ @OnClose
+ public void onClose()
+ {
+ addEvent("onClose()");
+ closeLatch.countDown();
+ }
+}
diff --git a/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/samples/BasicErrorSessionSocket.java b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/samples/BasicErrorSessionSocket.java
new file mode 100644
index 0000000..36c99b1
--- /dev/null
+++ b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/samples/BasicErrorSessionSocket.java
@@ -0,0 +1,35 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.server.samples;
+
+import javax.websocket.OnError;
+import javax.websocket.Session;
+import javax.websocket.server.ServerEndpoint;
+
+import org.eclipse.jetty.websocket.jsr356.server.TrackingSocket;
+
+@ServerEndpoint(value="/basic")
+public class BasicErrorSessionSocket extends TrackingSocket
+{
+ @OnError
+ public void onError(Session session)
+ {
+ addEvent("onError(%s)",session);
+ }
+}
diff --git a/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/samples/BasicErrorSessionThrowableSocket.java b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/samples/BasicErrorSessionThrowableSocket.java
new file mode 100644
index 0000000..5f7d6f9
--- /dev/null
+++ b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/samples/BasicErrorSessionThrowableSocket.java
@@ -0,0 +1,36 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.server.samples;
+
+import javax.websocket.OnError;
+import javax.websocket.Session;
+import javax.websocket.server.ServerEndpoint;
+
+import org.eclipse.jetty.websocket.jsr356.server.TrackingSocket;
+
+@ServerEndpoint(value="/basic")
+public class BasicErrorSessionThrowableSocket extends TrackingSocket
+{
+ @OnError
+ public void onError(Session session, Throwable t)
+ {
+ addEvent("onError(%s,%s)",session,t);
+ addError(t);
+ }
+}
diff --git a/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/samples/BasicErrorSocket.java b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/samples/BasicErrorSocket.java
new file mode 100644
index 0000000..86f42d8
--- /dev/null
+++ b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/samples/BasicErrorSocket.java
@@ -0,0 +1,34 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.server.samples;
+
+import javax.websocket.OnError;
+import javax.websocket.server.ServerEndpoint;
+
+import org.eclipse.jetty.websocket.jsr356.server.TrackingSocket;
+
+@ServerEndpoint(value="/basic")
+public class BasicErrorSocket extends TrackingSocket
+{
+ @OnError
+ public void onError()
+ {
+ addEvent("onError()");
+ }
+}
diff --git a/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/samples/BasicErrorThrowableSessionSocket.java b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/samples/BasicErrorThrowableSessionSocket.java
new file mode 100644
index 0000000..172cd18
--- /dev/null
+++ b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/samples/BasicErrorThrowableSessionSocket.java
@@ -0,0 +1,36 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.server.samples;
+
+import javax.websocket.OnError;
+import javax.websocket.Session;
+import javax.websocket.server.ServerEndpoint;
+
+import org.eclipse.jetty.websocket.jsr356.server.TrackingSocket;
+
+@ServerEndpoint(value="/basic")
+public class BasicErrorThrowableSessionSocket extends TrackingSocket
+{
+ @OnError
+ public void onError(Throwable t, Session session)
+ {
+ addEvent("onError(%s,%s)",t,session);
+ addError(t);
+ }
+}
diff --git a/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/samples/BasicErrorThrowableSocket.java b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/samples/BasicErrorThrowableSocket.java
new file mode 100644
index 0000000..6b3b8ac
--- /dev/null
+++ b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/samples/BasicErrorThrowableSocket.java
@@ -0,0 +1,35 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.server.samples;
+
+import javax.websocket.OnError;
+import javax.websocket.server.ServerEndpoint;
+
+import org.eclipse.jetty.websocket.jsr356.server.TrackingSocket;
+
+@ServerEndpoint(value="/basic")
+public class BasicErrorThrowableSocket extends TrackingSocket
+{
+ @OnError
+ public void onError(Throwable t)
+ {
+ addEvent("onError(%s)",t);
+ addError(t);
+ }
+}
diff --git a/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/samples/BasicOpenCloseSessionSocket.java b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/samples/BasicOpenCloseSessionSocket.java
new file mode 100644
index 0000000..62ad1b7
--- /dev/null
+++ b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/samples/BasicOpenCloseSessionSocket.java
@@ -0,0 +1,46 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.server.samples;
+
+import javax.websocket.CloseReason;
+import javax.websocket.OnClose;
+import javax.websocket.OnOpen;
+import javax.websocket.Session;
+import javax.websocket.server.ServerEndpoint;
+
+import org.eclipse.jetty.websocket.jsr356.server.TrackingSocket;
+
+@ServerEndpoint(value="/basic")
+public class BasicOpenCloseSessionSocket extends TrackingSocket
+{
+ @OnClose
+ public void onClose(CloseReason close, Session session)
+ {
+ addEvent("onClose(%s, %s)",close,session);
+ this.closeReason = close;
+ closeLatch.countDown();
+ }
+
+ @OnOpen
+ public void onOpen(Session session)
+ {
+ addEvent("onOpen(%s)",session);
+ openLatch.countDown();
+ }
+}
diff --git a/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/samples/BasicOpenCloseSocket.java b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/samples/BasicOpenCloseSocket.java
new file mode 100644
index 0000000..3a3c69c
--- /dev/null
+++ b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/samples/BasicOpenCloseSocket.java
@@ -0,0 +1,41 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.server.samples;
+
+import javax.websocket.CloseReason;
+import javax.websocket.OnClose;
+import javax.websocket.OnOpen;
+import javax.websocket.server.ServerEndpoint;
+
+import org.eclipse.jetty.websocket.jsr356.server.TrackingSocket;
+
+@ServerEndpoint(value="/basic")
+public class BasicOpenCloseSocket extends TrackingSocket
+{
+ @OnOpen
+ public void onOpen() {
+ openLatch.countDown();
+ }
+
+ @OnClose
+ public void onClose(CloseReason close) {
+ this.closeReason = close;
+ closeLatch.countDown();
+ }
+}
diff --git a/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/samples/BasicOpenSessionSocket.java b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/samples/BasicOpenSessionSocket.java
new file mode 100644
index 0000000..014a41d
--- /dev/null
+++ b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/samples/BasicOpenSessionSocket.java
@@ -0,0 +1,35 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.server.samples;
+
+import javax.websocket.OnOpen;
+import javax.websocket.Session;
+import javax.websocket.server.ServerEndpoint;
+
+import org.eclipse.jetty.websocket.jsr356.server.TrackingSocket;
+
+@ServerEndpoint(value="/basic")
+public class BasicOpenSessionSocket extends TrackingSocket
+{
+ @OnOpen
+ public void onOpen(Session session)
+ {
+ openLatch.countDown();
+ }
+}
diff --git a/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/samples/BasicOpenSocket.java b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/samples/BasicOpenSocket.java
new file mode 100644
index 0000000..a62f1d9
--- /dev/null
+++ b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/samples/BasicOpenSocket.java
@@ -0,0 +1,34 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.server.samples;
+
+import javax.websocket.OnOpen;
+import javax.websocket.server.ServerEndpoint;
+
+import org.eclipse.jetty.websocket.jsr356.server.TrackingSocket;
+
+@ServerEndpoint(value="/basic")
+public class BasicOpenSocket extends TrackingSocket
+{
+ @OnOpen
+ public void onOpen()
+ {
+ openLatch.countDown();
+ }
+}
diff --git a/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/samples/BasicPongMessageSocket.java b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/samples/BasicPongMessageSocket.java
new file mode 100644
index 0000000..979d212
--- /dev/null
+++ b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/samples/BasicPongMessageSocket.java
@@ -0,0 +1,36 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.server.samples;
+
+import javax.websocket.OnMessage;
+import javax.websocket.PongMessage;
+import javax.websocket.server.ServerEndpoint;
+
+import org.eclipse.jetty.websocket.jsr356.server.TrackingSocket;
+
+@ServerEndpoint(value="/basic")
+public class BasicPongMessageSocket extends TrackingSocket
+{
+ @OnMessage
+ public void onPong(PongMessage pong)
+ {
+ addEvent("onPong(%s)",pong);
+ dataLatch.countDown();
+ }
+}
diff --git a/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/samples/BasicTextMessageStringSocket.java b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/samples/BasicTextMessageStringSocket.java
new file mode 100644
index 0000000..a4a0a52
--- /dev/null
+++ b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/samples/BasicTextMessageStringSocket.java
@@ -0,0 +1,35 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.server.samples;
+
+import javax.websocket.OnMessage;
+import javax.websocket.server.ServerEndpoint;
+
+import org.eclipse.jetty.websocket.jsr356.server.TrackingSocket;
+
+@ServerEndpoint(value="/basic")
+public class BasicTextMessageStringSocket extends TrackingSocket
+{
+ @OnMessage
+ public void onText(String message)
+ {
+ addEvent("onText(%s)",message);
+ dataLatch.countDown();
+ }
+}
diff --git a/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/samples/InvalidCloseIntSocket.java b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/samples/InvalidCloseIntSocket.java
new file mode 100644
index 0000000..5c16ced
--- /dev/null
+++ b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/samples/InvalidCloseIntSocket.java
@@ -0,0 +1,37 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.server.samples;
+
+import javax.websocket.OnClose;
+import javax.websocket.server.ServerEndpoint;
+
+import org.eclipse.jetty.websocket.jsr356.server.TrackingSocket;
+
+@ServerEndpoint(value="/invalid")
+public class InvalidCloseIntSocket extends TrackingSocket
+{
+ /**
+ * Invalid Close Method Declaration (parameter type int)
+ */
+ @OnClose
+ public void onClose(int statusCode)
+ {
+ closeLatch.countDown();
+ }
+}
diff --git a/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/samples/InvalidErrorErrorSocket.java b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/samples/InvalidErrorErrorSocket.java
new file mode 100644
index 0000000..12b5b6f
--- /dev/null
+++ b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/samples/InvalidErrorErrorSocket.java
@@ -0,0 +1,37 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.server.samples;
+
+import javax.websocket.OnError;
+import javax.websocket.server.ServerEndpoint;
+
+import org.eclipse.jetty.websocket.jsr356.server.TrackingSocket;
+
+@ServerEndpoint(value="/invalid")
+public class InvalidErrorErrorSocket extends TrackingSocket
+{
+ /**
+ * Invalid Error Method Declaration (parameter type Error)
+ */
+ @OnError
+ public void onError(Error error)
+ {
+ /* no impl */
+ }
+}
diff --git a/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/samples/InvalidErrorExceptionSocket.java b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/samples/InvalidErrorExceptionSocket.java
new file mode 100644
index 0000000..206578b
--- /dev/null
+++ b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/samples/InvalidErrorExceptionSocket.java
@@ -0,0 +1,37 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.server.samples;
+
+import javax.websocket.OnError;
+import javax.websocket.server.ServerEndpoint;
+
+import org.eclipse.jetty.websocket.jsr356.server.TrackingSocket;
+
+@ServerEndpoint(value="/invalid")
+public class InvalidErrorExceptionSocket extends TrackingSocket
+{
+ /**
+ * Invalid Error Method Declaration (parameter type Exception)
+ */
+ @OnError
+ public void onError(Exception e)
+ {
+ /* no impl */
+ }
+}
diff --git a/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/samples/InvalidErrorIntSocket.java b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/samples/InvalidErrorIntSocket.java
new file mode 100644
index 0000000..2c8fb3d
--- /dev/null
+++ b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/samples/InvalidErrorIntSocket.java
@@ -0,0 +1,37 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.server.samples;
+
+import javax.websocket.OnError;
+import javax.websocket.server.ServerEndpoint;
+
+import org.eclipse.jetty.websocket.jsr356.server.TrackingSocket;
+
+@ServerEndpoint(value="/invalid")
+public class InvalidErrorIntSocket extends TrackingSocket
+{
+ /**
+ * Invalid Error Method Declaration (parameter type int)
+ */
+ @OnError
+ public void onError(int errorCount)
+ {
+ /* no impl */
+ }
+}
diff --git a/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/samples/InvalidOpenCloseReasonSocket.java b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/samples/InvalidOpenCloseReasonSocket.java
new file mode 100644
index 0000000..72593c7
--- /dev/null
+++ b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/samples/InvalidOpenCloseReasonSocket.java
@@ -0,0 +1,38 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.server.samples;
+
+import javax.websocket.CloseReason;
+import javax.websocket.OnOpen;
+import javax.websocket.server.ServerEndpoint;
+
+import org.eclipse.jetty.websocket.jsr356.server.TrackingSocket;
+
+@ServerEndpoint(value="/invalid")
+public class InvalidOpenCloseReasonSocket extends TrackingSocket
+{
+ /**
+ * Invalid Open Method Declaration (parameter type CloseReason)
+ */
+ @OnOpen
+ public void onOpen(CloseReason reason)
+ {
+ openLatch.countDown();
+ }
+}
diff --git a/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/samples/InvalidOpenIntSocket.java b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/samples/InvalidOpenIntSocket.java
new file mode 100644
index 0000000..b703d5d
--- /dev/null
+++ b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/samples/InvalidOpenIntSocket.java
@@ -0,0 +1,37 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.server.samples;
+
+import javax.websocket.OnOpen;
+import javax.websocket.server.ServerEndpoint;
+
+import org.eclipse.jetty.websocket.jsr356.server.TrackingSocket;
+
+@ServerEndpoint(value="/invalid")
+public class InvalidOpenIntSocket extends TrackingSocket
+{
+ /**
+ * Invalid Open Method Declaration (parameter type int)
+ */
+ @OnOpen
+ public void onOpen(int value)
+ {
+ openLatch.countDown();
+ }
+}
diff --git a/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/samples/InvalidOpenSessionIntSocket.java b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/samples/InvalidOpenSessionIntSocket.java
new file mode 100644
index 0000000..dc5290e
--- /dev/null
+++ b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/samples/InvalidOpenSessionIntSocket.java
@@ -0,0 +1,38 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.server.samples;
+
+import javax.websocket.OnOpen;
+import javax.websocket.Session;
+import javax.websocket.server.ServerEndpoint;
+
+import org.eclipse.jetty.websocket.jsr356.server.TrackingSocket;
+
+@ServerEndpoint(value="/invalid")
+public class InvalidOpenSessionIntSocket extends TrackingSocket
+{
+ /**
+ * Invalid Open Method Declaration (parameter of type int)
+ */
+ @OnOpen
+ public void onOpen(Session session, int count)
+ {
+ openLatch.countDown();
+ }
+}
diff --git a/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/samples/StatelessTextMessageStringSocket.java b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/samples/StatelessTextMessageStringSocket.java
new file mode 100644
index 0000000..6afdfd9
--- /dev/null
+++ b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/samples/StatelessTextMessageStringSocket.java
@@ -0,0 +1,36 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.server.samples;
+
+import javax.websocket.OnMessage;
+import javax.websocket.Session;
+import javax.websocket.server.ServerEndpoint;
+
+import org.eclipse.jetty.websocket.jsr356.server.TrackingSocket;
+
+@ServerEndpoint(value = "/stateless")
+public class StatelessTextMessageStringSocket extends TrackingSocket
+{
+ @OnMessage
+ public void onText(Session session, String message)
+ {
+ addEvent("onText(%s,%s)",session,message);
+ dataLatch.countDown();
+ }
+}
diff --git a/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/samples/beans/DateDecoder.java b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/samples/beans/DateDecoder.java
new file mode 100644
index 0000000..b2ab7e5
--- /dev/null
+++ b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/samples/beans/DateDecoder.java
@@ -0,0 +1,62 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.server.samples.beans;
+
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+
+import javax.websocket.DecodeException;
+import javax.websocket.Decoder;
+import javax.websocket.EndpointConfig;
+
+/**
+ * Decode Date
+ */
+public class DateDecoder implements Decoder.Text<Date>
+{
+ @Override
+ public Date decode(String s) throws DecodeException
+ {
+ try
+ {
+ return new SimpleDateFormat("yyyy.MM.dd").parse(s);
+ }
+ catch (ParseException e)
+ {
+ throw new DecodeException(s,e.getMessage(),e);
+ }
+ }
+
+ @Override
+ public void destroy()
+ {
+ }
+
+ @Override
+ public void init(EndpointConfig config)
+ {
+ }
+
+ @Override
+ public boolean willDecode(String s)
+ {
+ return true;
+ }
+}
diff --git a/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/samples/beans/DateEncoder.java b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/samples/beans/DateEncoder.java
new file mode 100644
index 0000000..4171d3e
--- /dev/null
+++ b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/samples/beans/DateEncoder.java
@@ -0,0 +1,48 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.server.samples.beans;
+
+import java.text.SimpleDateFormat;
+import java.util.Date;
+
+import javax.websocket.EncodeException;
+import javax.websocket.Encoder;
+import javax.websocket.EndpointConfig;
+
+/**
+ * Encode Date
+ */
+public class DateEncoder implements Encoder.Text<Date>
+{
+ @Override
+ public void destroy()
+ {
+ }
+
+ @Override
+ public String encode(Date object) throws EncodeException
+ {
+ return new SimpleDateFormat("yyyy.MM.dd").format(object);
+ }
+
+ @Override
+ public void init(EndpointConfig config)
+ {
+ }
+}
diff --git a/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/samples/beans/DateTextSocket.java b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/samples/beans/DateTextSocket.java
new file mode 100644
index 0000000..07644d0
--- /dev/null
+++ b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/samples/beans/DateTextSocket.java
@@ -0,0 +1,69 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.server.samples.beans;
+
+import java.io.IOException;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+
+import javax.websocket.OnError;
+import javax.websocket.OnMessage;
+import javax.websocket.OnOpen;
+import javax.websocket.Session;
+import javax.websocket.server.ServerEndpoint;
+
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+import org.eclipse.jetty.websocket.jsr356.server.StackUtil;
+
+@ServerEndpoint(value = "/echo/beans/date", decoders =
+{ DateDecoder.class })
+public class DateTextSocket
+{
+ private static final Logger LOG = Log.getLogger(DateTextSocket.class);
+
+ private Session session;
+
+ @OnOpen
+ public void onOpen(Session session)
+ {
+ this.session = session;
+ }
+
+ @OnMessage
+ public void onMessage(Date d) throws IOException
+ {
+ if (d == null)
+ {
+ session.getAsyncRemote().sendText("Error: Date is null");
+ }
+ else
+ {
+ String msg = SimpleDateFormat.getDateInstance(SimpleDateFormat.SHORT).format(d);
+ session.getAsyncRemote().sendText(msg);
+ }
+ }
+
+ @OnError
+ public void onError(Throwable cause) throws IOException
+ {
+ LOG.warn("Error",cause);
+ session.getBasicRemote().sendText("Exception: " + StackUtil.toString(cause));
+ }
+}
diff --git a/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/samples/beans/DateTimeDecoder.java b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/samples/beans/DateTimeDecoder.java
new file mode 100644
index 0000000..56892dc
--- /dev/null
+++ b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/samples/beans/DateTimeDecoder.java
@@ -0,0 +1,62 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.server.samples.beans;
+
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+
+import javax.websocket.DecodeException;
+import javax.websocket.Decoder;
+import javax.websocket.EndpointConfig;
+
+/**
+ * Decode Date and Time
+ */
+public class DateTimeDecoder implements Decoder.Text<Date>
+{
+ @Override
+ public Date decode(String s) throws DecodeException
+ {
+ try
+ {
+ return new SimpleDateFormat("yyyy.MM.dd G 'at' HH:mm:ss z").parse(s);
+ }
+ catch (ParseException e)
+ {
+ throw new DecodeException(s,e.getMessage(),e);
+ }
+ }
+
+ @Override
+ public void destroy()
+ {
+ }
+
+ @Override
+ public void init(EndpointConfig config)
+ {
+ }
+
+ @Override
+ public boolean willDecode(String s)
+ {
+ return true;
+ }
+}
diff --git a/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/samples/beans/DateTimeEncoder.java b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/samples/beans/DateTimeEncoder.java
new file mode 100644
index 0000000..67dcf22
--- /dev/null
+++ b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/samples/beans/DateTimeEncoder.java
@@ -0,0 +1,48 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.server.samples.beans;
+
+import java.text.SimpleDateFormat;
+import java.util.Date;
+
+import javax.websocket.EncodeException;
+import javax.websocket.Encoder;
+import javax.websocket.EndpointConfig;
+
+/**
+ * Encode Date
+ */
+public class DateTimeEncoder implements Encoder.Text<Date>
+{
+ @Override
+ public void destroy()
+ {
+ }
+
+ @Override
+ public String encode(Date object) throws EncodeException
+ {
+ return new SimpleDateFormat("yyyy.MM.dd G 'at' HH:mm:ss z").format(object);
+ }
+
+ @Override
+ public void init(EndpointConfig config)
+ {
+ }
+}
diff --git a/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/samples/beans/TimeDecoder.java b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/samples/beans/TimeDecoder.java
new file mode 100644
index 0000000..326804c
--- /dev/null
+++ b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/samples/beans/TimeDecoder.java
@@ -0,0 +1,62 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.server.samples.beans;
+
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+
+import javax.websocket.DecodeException;
+import javax.websocket.Decoder;
+import javax.websocket.EndpointConfig;
+
+/**
+ * Decode Time
+ */
+public class TimeDecoder implements Decoder.Text<Date>
+{
+ @Override
+ public Date decode(String s) throws DecodeException
+ {
+ try
+ {
+ return new SimpleDateFormat("HH:mm:ss z").parse(s);
+ }
+ catch (ParseException e)
+ {
+ throw new DecodeException(s,e.getMessage(),e);
+ }
+ }
+
+ @Override
+ public void destroy()
+ {
+ }
+
+ @Override
+ public void init(EndpointConfig config)
+ {
+ }
+
+ @Override
+ public boolean willDecode(String s)
+ {
+ return true;
+ }
+}
diff --git a/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/samples/beans/TimeEncoder.java b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/samples/beans/TimeEncoder.java
new file mode 100644
index 0000000..87e49ba
--- /dev/null
+++ b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/samples/beans/TimeEncoder.java
@@ -0,0 +1,48 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.server.samples.beans;
+
+import java.text.SimpleDateFormat;
+import java.util.Date;
+
+import javax.websocket.EncodeException;
+import javax.websocket.Encoder;
+import javax.websocket.EndpointConfig;
+
+/**
+ * Encode Time
+ */
+public class TimeEncoder implements Encoder.Text<Date>
+{
+ @Override
+ public void destroy()
+ {
+ }
+
+ @Override
+ public String encode(Date object) throws EncodeException
+ {
+ return new SimpleDateFormat("HH:mm:ss z").format(object);
+ }
+
+ @Override
+ public void init(EndpointConfig config)
+ {
+ }
+}
diff --git a/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/samples/echo/BasicEchoEndpoint.java b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/samples/echo/BasicEchoEndpoint.java
new file mode 100644
index 0000000..761fc0c
--- /dev/null
+++ b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/samples/echo/BasicEchoEndpoint.java
@@ -0,0 +1,46 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.server.samples.echo;
+
+import javax.websocket.Endpoint;
+import javax.websocket.EndpointConfig;
+import javax.websocket.MessageHandler;
+import javax.websocket.Session;
+
+/**
+ * Example of websocket endpoint based on extending {@link Endpoint}
+ */
+public class BasicEchoEndpoint extends Endpoint implements MessageHandler.Whole<String>
+{
+ private Session session;
+
+ @Override
+ public void onMessage(String msg)
+ {
+ // reply with echo
+ session.getAsyncRemote().sendText(msg);
+ }
+
+ @Override
+ public void onOpen(Session session, EndpointConfig config)
+ {
+ this.session = session;
+ this.session.addMessageHandler(this);
+ }
+}
diff --git a/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/samples/echo/BasicEchoEndpointConfigContextListener.java b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/samples/echo/BasicEchoEndpointConfigContextListener.java
new file mode 100644
index 0000000..fa14891
--- /dev/null
+++ b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/samples/echo/BasicEchoEndpointConfigContextListener.java
@@ -0,0 +1,55 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.server.samples.echo;
+
+import javax.servlet.ServletContextEvent;
+import javax.servlet.ServletContextListener;
+import javax.websocket.DeploymentException;
+import javax.websocket.Endpoint;
+import javax.websocket.server.ServerContainer;
+import javax.websocket.server.ServerEndpointConfig;
+
+/**
+ * Example of adding a server WebSocket (extending {@link Endpoint}) programmatically via config
+ */
+public class BasicEchoEndpointConfigContextListener implements ServletContextListener
+{
+ @Override
+ public void contextDestroyed(ServletContextEvent sce)
+ {
+ /* do nothing */
+ }
+
+ @Override
+ public void contextInitialized(ServletContextEvent sce)
+ {
+ ServerContainer container = (ServerContainer)sce.getServletContext().getAttribute(ServerContainer.class.getName());
+ // Build up a configuration with a specific path
+ String path = "/echo";
+ ServerEndpointConfig.Builder builder = ServerEndpointConfig.Builder.create(BasicEchoEndpoint.class,path);
+ try
+ {
+ container.addEndpoint(builder.build());
+ }
+ catch (DeploymentException e)
+ {
+ throw new RuntimeException("Unable to add endpoint via config file",e);
+ }
+ }
+}
diff --git a/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/samples/echo/BasicEchoEndpointContextListener.java b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/samples/echo/BasicEchoEndpointContextListener.java
new file mode 100644
index 0000000..3a90bc8
--- /dev/null
+++ b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/samples/echo/BasicEchoEndpointContextListener.java
@@ -0,0 +1,54 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.server.samples.echo;
+
+import javax.servlet.ServletContextEvent;
+import javax.servlet.ServletContextListener;
+import javax.websocket.DeploymentException;
+import javax.websocket.Endpoint;
+import javax.websocket.server.ServerContainer;
+
+/**
+ * Example of adding a server WebSocket (extending {@link Endpoint}) programmatically directly.
+ * <p>
+ * NOTE: this shouldn't work as the endpoint has no path associated with it.
+ */
+public class BasicEchoEndpointContextListener implements ServletContextListener
+{
+ @Override
+ public void contextDestroyed(ServletContextEvent sce)
+ {
+ /* do nothing */
+ }
+
+ @Override
+ public void contextInitialized(ServletContextEvent sce)
+ {
+ ServerContainer container = (ServerContainer)sce.getServletContext().getAttribute(ServerContainer.class.getName());
+ try
+ {
+ // Should fail as there is no path associated with this endpoint
+ container.addEndpoint(BasicEchoEndpoint.class);
+ }
+ catch (DeploymentException e)
+ {
+ throw new RuntimeException("Unable to add endpoint directly",e);
+ }
+ }
+}
diff --git a/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/samples/echo/BasicEchoSocket.java b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/samples/echo/BasicEchoSocket.java
new file mode 100644
index 0000000..49a7e2e
--- /dev/null
+++ b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/samples/echo/BasicEchoSocket.java
@@ -0,0 +1,37 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.server.samples.echo;
+
+import javax.websocket.OnMessage;
+import javax.websocket.Session;
+import javax.websocket.server.ServerEndpoint;
+
+/**
+ * Annotated echo socket
+ */
+@ServerEndpoint("/echo")
+public class BasicEchoSocket
+{
+ @OnMessage
+ public void echo(Session session, String msg)
+ {
+ // reply with echo
+ session.getAsyncRemote().sendText(msg);
+ }
+}
diff --git a/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/samples/echo/BasicEchoSocketConfigContextListener.java b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/samples/echo/BasicEchoSocketConfigContextListener.java
new file mode 100644
index 0000000..70fe55d
--- /dev/null
+++ b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/samples/echo/BasicEchoSocketConfigContextListener.java
@@ -0,0 +1,56 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.server.samples.echo;
+
+import javax.servlet.ServletContextEvent;
+import javax.servlet.ServletContextListener;
+import javax.websocket.DeploymentException;
+import javax.websocket.Endpoint;
+import javax.websocket.server.ServerContainer;
+import javax.websocket.server.ServerEndpointConfig;
+
+/**
+ * Example of adding a server socket (which extends {@link Endpoint}) programmatically via the {@link ServerContainer#addEndpoint(ServerEndpointConfig)}
+ */
+public class BasicEchoSocketConfigContextListener implements ServletContextListener
+{
+ @Override
+ public void contextDestroyed(ServletContextEvent sce)
+ {
+ /* do nothing */
+ }
+
+ @Override
+ public void contextInitialized(ServletContextEvent sce)
+ {
+ ServerContainer container = (ServerContainer)sce.getServletContext().getAttribute(ServerContainer.class.getName());
+ // Build up a configuration with a specific path
+ // Intentionally using alternate path in config (which differs from @ServerEndpoint declaration)
+ String path = "/echo-alt";
+ ServerEndpointConfig.Builder builder = ServerEndpointConfig.Builder.create(BasicEchoSocket.class,path);
+ try
+ {
+ container.addEndpoint(builder.build());
+ }
+ catch (DeploymentException e)
+ {
+ throw new RuntimeException("Unable to add endpoint via config file",e);
+ }
+ }
+}
diff --git a/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/samples/echo/BasicEchoSocketContextListener.java b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/samples/echo/BasicEchoSocketContextListener.java
new file mode 100644
index 0000000..e278c72
--- /dev/null
+++ b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/samples/echo/BasicEchoSocketContextListener.java
@@ -0,0 +1,50 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.server.samples.echo;
+
+import javax.servlet.ServletContextEvent;
+import javax.servlet.ServletContextListener;
+import javax.websocket.DeploymentException;
+import javax.websocket.server.ServerContainer;
+
+/**
+ * Example of adding a server socket (annotated) programmatically directly with no config
+ */
+public class BasicEchoSocketContextListener implements ServletContextListener
+{
+ @Override
+ public void contextDestroyed(ServletContextEvent sce)
+ {
+ /* do nothing */
+ }
+
+ @Override
+ public void contextInitialized(ServletContextEvent sce)
+ {
+ ServerContainer container = (ServerContainer)sce.getServletContext().getAttribute(ServerContainer.class.getName());
+ try
+ {
+ container.addEndpoint(BasicEchoSocket.class);
+ }
+ catch (DeploymentException e)
+ {
+ throw new RuntimeException("Unable to add endpoint directly",e);
+ }
+ }
+}
diff --git a/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/samples/echo/EchoReturnEndpoint.java b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/samples/echo/EchoReturnEndpoint.java
new file mode 100644
index 0000000..c9f0165
--- /dev/null
+++ b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/samples/echo/EchoReturnEndpoint.java
@@ -0,0 +1,64 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.server.samples.echo;
+
+import java.io.IOException;
+
+import javax.websocket.CloseReason;
+import javax.websocket.OnMessage;
+import javax.websocket.OnOpen;
+import javax.websocket.Session;
+import javax.websocket.server.ServerEndpoint;
+
+import org.eclipse.jetty.toolchain.test.EventQueue;
+
+@ServerEndpoint(value = "/echoreturn")
+public class EchoReturnEndpoint
+{
+ private Session session = null;
+ public CloseReason close = null;
+ public EventQueue<String> messageQueue = new EventQueue<>();
+
+ public void onClose(CloseReason close)
+ {
+ this.close = close;
+ }
+
+ @OnMessage
+ public String onMessage(String message)
+ {
+ this.messageQueue.offer(message);
+ // Return the message
+ return message;
+ }
+
+ @OnOpen
+ public void onOpen(Session session)
+ {
+ this.session = session;
+ }
+
+ public void sendText(String text) throws IOException
+ {
+ if (session != null)
+ {
+ session.getBasicRemote().sendText(text);
+ }
+ }
+}
diff --git a/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/samples/echo/LargeEchoConfiguredSocket.java b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/samples/echo/LargeEchoConfiguredSocket.java
new file mode 100644
index 0000000..f0acfb4
--- /dev/null
+++ b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/samples/echo/LargeEchoConfiguredSocket.java
@@ -0,0 +1,47 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.server.samples.echo;
+
+import javax.websocket.OnMessage;
+import javax.websocket.OnOpen;
+import javax.websocket.Session;
+import javax.websocket.server.ServerEndpoint;
+
+/**
+ * Annotated echo socket
+ */
+@ServerEndpoint(value = "/echo/large")
+public class LargeEchoConfiguredSocket
+{
+ private Session session;
+
+ @OnOpen
+ public void open(Session session)
+ {
+ this.session = session;
+ this.session.setMaxTextMessageBufferSize(128 * 1024);
+ }
+
+ @OnMessage
+ public void echo(String msg)
+ {
+ // reply with echo
+ session.getAsyncRemote().sendText(msg);
+ }
+}
diff --git a/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/samples/echo/LargeEchoContextListener.java b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/samples/echo/LargeEchoContextListener.java
new file mode 100644
index 0000000..9a73823
--- /dev/null
+++ b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/samples/echo/LargeEchoContextListener.java
@@ -0,0 +1,42 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.server.samples.echo;
+
+import javax.servlet.ServletContextEvent;
+import javax.servlet.ServletContextListener;
+import javax.websocket.server.ServerContainer;
+
+/**
+ * Configure the Large Text Message Size via the Container
+ */
+public class LargeEchoContextListener implements ServletContextListener
+{
+ @Override
+ public void contextDestroyed(ServletContextEvent sce)
+ {
+ /* do nothing */
+ }
+
+ @Override
+ public void contextInitialized(ServletContextEvent sce)
+ {
+ ServerContainer container = (ServerContainer)sce.getServletContext().getAttribute(ServerContainer.class.getName());
+ container.setDefaultMaxTextMessageBufferSize(128 * 1024);
+ }
+}
diff --git a/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/samples/echo/LargeEchoDefaultSocket.java b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/samples/echo/LargeEchoDefaultSocket.java
new file mode 100644
index 0000000..851a1ac
--- /dev/null
+++ b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/samples/echo/LargeEchoDefaultSocket.java
@@ -0,0 +1,38 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.server.samples.echo;
+
+import javax.websocket.OnMessage;
+import javax.websocket.Session;
+import javax.websocket.WebSocketContainer;
+import javax.websocket.server.ServerEndpoint;
+
+/**
+ * Annotated echo socket (default behavior as defined from {@link WebSocketContainer#setDefaultMaxTextMessageBufferSize(int)})
+ */
+@ServerEndpoint(value = "/echo/large")
+public class LargeEchoDefaultSocket
+{
+ @OnMessage
+ public void echo(Session session, String msg)
+ {
+ // reply with echo
+ session.getAsyncRemote().sendText(msg);
+ }
+}
diff --git a/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/samples/partial/PartialTextSessionSocket.java b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/samples/partial/PartialTextSessionSocket.java
new file mode 100644
index 0000000..fac9bab
--- /dev/null
+++ b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/samples/partial/PartialTextSessionSocket.java
@@ -0,0 +1,55 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.server.samples.partial;
+
+import java.io.IOException;
+
+import javax.websocket.OnError;
+import javax.websocket.OnMessage;
+import javax.websocket.Session;
+import javax.websocket.server.ServerEndpoint;
+
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+import org.eclipse.jetty.websocket.jsr356.server.StackUtil;
+
+@ServerEndpoint("/echo/partial/textsession")
+public class PartialTextSessionSocket
+{
+ private static final Logger LOG = Log.getLogger(PartialTextSessionSocket.class);
+ private StringBuilder buf = new StringBuilder();
+
+ @OnMessage
+ public void onPartial(String msg, boolean fin, Session session) throws IOException
+ {
+ buf.append("('").append(msg).append("',").append(fin).append(')');
+ if (fin)
+ {
+ session.getBasicRemote().sendText(buf.toString());
+ buf.setLength(0);
+ }
+ }
+
+ @OnError
+ public void onError(Throwable cause, Session session) throws IOException
+ {
+ LOG.warn("Error",cause);
+ session.getBasicRemote().sendText("Exception: " + StackUtil.toString(cause));
+ }
+}
diff --git a/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/samples/partial/PartialTextSocket.java b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/samples/partial/PartialTextSocket.java
new file mode 100644
index 0000000..009c3fa
--- /dev/null
+++ b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/samples/partial/PartialTextSocket.java
@@ -0,0 +1,63 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.server.samples.partial;
+
+import java.io.IOException;
+
+import javax.websocket.OnError;
+import javax.websocket.OnMessage;
+import javax.websocket.OnOpen;
+import javax.websocket.Session;
+import javax.websocket.server.ServerEndpoint;
+
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+import org.eclipse.jetty.websocket.jsr356.server.StackUtil;
+
+@ServerEndpoint("/echo/partial/text")
+public class PartialTextSocket
+{
+ private static final Logger LOG = Log.getLogger(PartialTextSocket.class);
+ private Session session;
+ private StringBuilder buf = new StringBuilder();
+
+ @OnOpen
+ public void onOpen(Session session)
+ {
+ this.session = session;
+ }
+
+ @OnMessage
+ public void onPartial(String msg, boolean fin) throws IOException
+ {
+ buf.append("('").append(msg).append("',").append(fin).append(')');
+ if (fin)
+ {
+ session.getBasicRemote().sendText(buf.toString());
+ buf.setLength(0);
+ }
+ }
+
+ @OnError
+ public void onError(Throwable cause) throws IOException
+ {
+ LOG.warn("Error",cause);
+ session.getBasicRemote().sendText("Exception: " + StackUtil.toString(cause));
+ }
+}
diff --git a/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/samples/primitives/BooleanObjectTextParamSocket.java b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/samples/primitives/BooleanObjectTextParamSocket.java
new file mode 100644
index 0000000..441a2f1
--- /dev/null
+++ b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/samples/primitives/BooleanObjectTextParamSocket.java
@@ -0,0 +1,67 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.server.samples.primitives;
+
+import java.io.IOException;
+
+import javax.websocket.OnError;
+import javax.websocket.OnMessage;
+import javax.websocket.OnOpen;
+import javax.websocket.Session;
+import javax.websocket.server.PathParam;
+import javax.websocket.server.ServerEndpoint;
+
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+import org.eclipse.jetty.websocket.jsr356.server.StackUtil;
+
+@ServerEndpoint("/echo/primitives/booleanobject/params/{a}")
+public class BooleanObjectTextParamSocket
+{
+ private static final Logger LOG = Log.getLogger(BooleanObjectTextParamSocket.class);
+
+ private Session session;
+
+ @OnOpen
+ public void onOpen(Session session)
+ {
+ this.session = session;
+ }
+
+ @OnMessage
+ public void onMessage(Boolean b, @PathParam("a") Boolean param) throws IOException
+ {
+ if (b == null)
+ {
+ session.getAsyncRemote().sendText("Error: Boolean is null");
+ }
+ else
+ {
+ String msg = String.format("%b|%b", b, param);
+ session.getAsyncRemote().sendText(msg);
+ }
+ }
+
+ @OnError
+ public void onError(Throwable cause) throws IOException
+ {
+ LOG.warn("Error",cause);
+ session.getBasicRemote().sendText("Exception: " + StackUtil.toString(cause));
+ }
+}
diff --git a/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/samples/primitives/BooleanObjectTextSocket.java b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/samples/primitives/BooleanObjectTextSocket.java
new file mode 100644
index 0000000..0a53134
--- /dev/null
+++ b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/samples/primitives/BooleanObjectTextSocket.java
@@ -0,0 +1,66 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.server.samples.primitives;
+
+import java.io.IOException;
+
+import javax.websocket.OnError;
+import javax.websocket.OnMessage;
+import javax.websocket.OnOpen;
+import javax.websocket.Session;
+import javax.websocket.server.ServerEndpoint;
+
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+import org.eclipse.jetty.websocket.jsr356.server.StackUtil;
+
+@ServerEndpoint("/echo/primitives/booleanobject")
+public class BooleanObjectTextSocket
+{
+ private static final Logger LOG = Log.getLogger(BooleanObjectTextSocket.class);
+
+ private Session session;
+
+ @OnOpen
+ public void onOpen(Session session)
+ {
+ this.session = session;
+ }
+
+ @OnMessage
+ public void onMessage(Boolean b) throws IOException
+ {
+ if (b == null)
+ {
+ session.getAsyncRemote().sendText("Error: Boolean is null");
+ }
+ else
+ {
+ String msg = b.toString();
+ session.getAsyncRemote().sendText(msg);
+ }
+ }
+
+ @OnError
+ public void onError(Throwable cause) throws IOException
+ {
+ LOG.warn("Error",cause);
+ session.getBasicRemote().sendText("Exception: " + StackUtil.toString(cause));
+ }
+}
diff --git a/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/samples/primitives/BooleanTextParamSocket.java b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/samples/primitives/BooleanTextParamSocket.java
new file mode 100644
index 0000000..d9d0824
--- /dev/null
+++ b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/samples/primitives/BooleanTextParamSocket.java
@@ -0,0 +1,60 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.server.samples.primitives;
+
+import java.io.IOException;
+
+import javax.websocket.OnError;
+import javax.websocket.OnMessage;
+import javax.websocket.OnOpen;
+import javax.websocket.Session;
+import javax.websocket.server.PathParam;
+import javax.websocket.server.ServerEndpoint;
+
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+import org.eclipse.jetty.websocket.jsr356.server.StackUtil;
+
+@ServerEndpoint("/echo/primitives/boolean/params/{a}")
+public class BooleanTextParamSocket
+{
+ private static final Logger LOG = Log.getLogger(BooleanTextParamSocket.class);
+
+ private Session session;
+
+ @OnOpen
+ public void onOpen(Session session)
+ {
+ this.session = session;
+ }
+
+ @OnMessage
+ public void onMessage(boolean b, @PathParam("a") boolean param) throws IOException
+ {
+ String msg = String.format("%b|%b", b, param);
+ session.getAsyncRemote().sendText(msg);
+ }
+
+ @OnError
+ public void onError(Throwable cause) throws IOException
+ {
+ LOG.warn("Error",cause);
+ session.getBasicRemote().sendText("Exception: " + StackUtil.toString(cause));
+ }
+}
diff --git a/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/samples/primitives/BooleanTextSocket.java b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/samples/primitives/BooleanTextSocket.java
new file mode 100644
index 0000000..d90e941
--- /dev/null
+++ b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/samples/primitives/BooleanTextSocket.java
@@ -0,0 +1,59 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.server.samples.primitives;
+
+import java.io.IOException;
+
+import javax.websocket.OnError;
+import javax.websocket.OnMessage;
+import javax.websocket.OnOpen;
+import javax.websocket.Session;
+import javax.websocket.server.ServerEndpoint;
+
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+import org.eclipse.jetty.websocket.jsr356.server.StackUtil;
+
+@ServerEndpoint("/echo/primitives/boolean")
+public class BooleanTextSocket
+{
+ private static final Logger LOG = Log.getLogger(BooleanTextSocket.class);
+
+ private Session session;
+
+ @OnOpen
+ public void onOpen(Session session)
+ {
+ this.session = session;
+ }
+
+ @OnMessage
+ public void onMessage(boolean b) throws IOException
+ {
+ String msg = Boolean.toString(b);
+ session.getAsyncRemote().sendText(msg);
+ }
+
+ @OnError
+ public void onError(Throwable cause) throws IOException
+ {
+ LOG.warn("Error",cause);
+ session.getBasicRemote().sendText("Exception: " + StackUtil.toString(cause));
+ }
+}
diff --git a/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/samples/primitives/ByteObjectTextSocket.java b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/samples/primitives/ByteObjectTextSocket.java
new file mode 100644
index 0000000..1d56470
--- /dev/null
+++ b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/samples/primitives/ByteObjectTextSocket.java
@@ -0,0 +1,66 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.server.samples.primitives;
+
+import java.io.IOException;
+
+import javax.websocket.OnError;
+import javax.websocket.OnMessage;
+import javax.websocket.OnOpen;
+import javax.websocket.Session;
+import javax.websocket.server.ServerEndpoint;
+
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+import org.eclipse.jetty.websocket.jsr356.server.StackUtil;
+
+@ServerEndpoint("/echo/primitives/byteobject")
+public class ByteObjectTextSocket
+{
+ private static final Logger LOG = Log.getLogger(ByteObjectTextSocket.class);
+
+ private Session session;
+
+ @OnOpen
+ public void onOpen(Session session)
+ {
+ this.session = session;
+ }
+
+ @OnMessage
+ public void onMessage(Byte b) throws IOException
+ {
+ if (b == null)
+ {
+ session.getAsyncRemote().sendText("Error: Byte is null");
+ }
+ else
+ {
+ String msg = String.format("0x%02X",b);
+ session.getAsyncRemote().sendText(msg);
+ }
+ }
+
+ @OnError
+ public void onError(Throwable cause) throws IOException
+ {
+ LOG.warn("Error",cause);
+ session.getBasicRemote().sendText("Exception: " + StackUtil.toString(cause));
+ }
+}
diff --git a/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/samples/primitives/ByteTextSocket.java b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/samples/primitives/ByteTextSocket.java
new file mode 100644
index 0000000..d2f9971
--- /dev/null
+++ b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/samples/primitives/ByteTextSocket.java
@@ -0,0 +1,59 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.server.samples.primitives;
+
+import java.io.IOException;
+
+import javax.websocket.OnError;
+import javax.websocket.OnMessage;
+import javax.websocket.OnOpen;
+import javax.websocket.Session;
+import javax.websocket.server.ServerEndpoint;
+
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+import org.eclipse.jetty.websocket.jsr356.server.StackUtil;
+
+@ServerEndpoint("/echo/primitives/byte")
+public class ByteTextSocket
+{
+ private static final Logger LOG = Log.getLogger(ByteTextSocket.class);
+
+ private Session session;
+
+ @OnOpen
+ public void onOpen(Session session)
+ {
+ this.session = session;
+ }
+
+ @OnMessage
+ public void onMessage(byte b) throws IOException
+ {
+ String msg = String.format("0x%02X",b);
+ session.getAsyncRemote().sendText(msg);
+ }
+
+ @OnError
+ public void onError(Throwable cause) throws IOException
+ {
+ LOG.warn("Error",cause);
+ session.getBasicRemote().sendText("Exception: " + StackUtil.toString(cause));
+ }
+}
diff --git a/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/samples/primitives/CharTextSocket.java b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/samples/primitives/CharTextSocket.java
new file mode 100644
index 0000000..d7fee3c
--- /dev/null
+++ b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/samples/primitives/CharTextSocket.java
@@ -0,0 +1,59 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.server.samples.primitives;
+
+import java.io.IOException;
+
+import javax.websocket.OnError;
+import javax.websocket.OnMessage;
+import javax.websocket.OnOpen;
+import javax.websocket.Session;
+import javax.websocket.server.ServerEndpoint;
+
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+import org.eclipse.jetty.websocket.jsr356.server.StackUtil;
+
+@ServerEndpoint("/echo/primitives/char")
+public class CharTextSocket
+{
+ private static final Logger LOG = Log.getLogger(CharTextSocket.class);
+
+ private Session session;
+
+ @OnOpen
+ public void onOpen(Session session)
+ {
+ this.session = session;
+ }
+
+ @OnMessage
+ public void onMessage(char c) throws IOException
+ {
+ String msg = Character.toString(c);
+ session.getAsyncRemote().sendText(msg);
+ }
+
+ @OnError
+ public void onError(Throwable cause) throws IOException
+ {
+ LOG.warn("Error",cause);
+ session.getBasicRemote().sendText("Exception: " + StackUtil.toString(cause));
+ }
+}
diff --git a/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/samples/primitives/CharacterObjectTextSocket.java b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/samples/primitives/CharacterObjectTextSocket.java
new file mode 100644
index 0000000..0b9f347
--- /dev/null
+++ b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/samples/primitives/CharacterObjectTextSocket.java
@@ -0,0 +1,66 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.server.samples.primitives;
+
+import java.io.IOException;
+
+import javax.websocket.OnError;
+import javax.websocket.OnMessage;
+import javax.websocket.OnOpen;
+import javax.websocket.Session;
+import javax.websocket.server.ServerEndpoint;
+
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+import org.eclipse.jetty.websocket.jsr356.server.StackUtil;
+
+@ServerEndpoint("/echo/primitives/characterobject")
+public class CharacterObjectTextSocket
+{
+ private static final Logger LOG = Log.getLogger(CharacterObjectTextSocket.class);
+
+ private Session session;
+
+ @OnOpen
+ public void onOpen(Session session)
+ {
+ this.session = session;
+ }
+
+ @OnMessage
+ public void onMessage(Character c) throws IOException
+ {
+ if (c == null)
+ {
+ session.getAsyncRemote().sendText("Error: Character is null");
+ }
+ else
+ {
+ String msg = c.toString();
+ session.getAsyncRemote().sendText(msg);
+ }
+ }
+
+ @OnError
+ public void onError(Throwable cause) throws IOException
+ {
+ LOG.warn("Error",cause);
+ session.getBasicRemote().sendText("Exception: " + StackUtil.toString(cause));
+ }
+}
diff --git a/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/samples/primitives/DoubleObjectTextSocket.java b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/samples/primitives/DoubleObjectTextSocket.java
new file mode 100644
index 0000000..9abc572
--- /dev/null
+++ b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/samples/primitives/DoubleObjectTextSocket.java
@@ -0,0 +1,66 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.server.samples.primitives;
+
+import java.io.IOException;
+
+import javax.websocket.OnError;
+import javax.websocket.OnMessage;
+import javax.websocket.OnOpen;
+import javax.websocket.Session;
+import javax.websocket.server.ServerEndpoint;
+
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+import org.eclipse.jetty.websocket.jsr356.server.StackUtil;
+
+@ServerEndpoint("/echo/primitives/doubleobject")
+public class DoubleObjectTextSocket
+{
+ private static final Logger LOG = Log.getLogger(DoubleObjectTextSocket.class);
+
+ private Session session;
+
+ @OnOpen
+ public void onOpen(Session session)
+ {
+ this.session = session;
+ }
+
+ @OnMessage
+ public void onMessage(Double d) throws IOException
+ {
+ if (d == null)
+ {
+ session.getAsyncRemote().sendText("Error: Double is null");
+ }
+ else
+ {
+ String msg = String.format("%.4f",d);
+ session.getAsyncRemote().sendText(msg);
+ }
+ }
+
+ @OnError
+ public void onError(Throwable cause) throws IOException
+ {
+ LOG.warn("Error",cause);
+ session.getBasicRemote().sendText("Exception: " + StackUtil.toString(cause));
+ }
+}
diff --git a/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/samples/primitives/DoubleTextSocket.java b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/samples/primitives/DoubleTextSocket.java
new file mode 100644
index 0000000..bd3669e
--- /dev/null
+++ b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/samples/primitives/DoubleTextSocket.java
@@ -0,0 +1,59 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.server.samples.primitives;
+
+import java.io.IOException;
+
+import javax.websocket.OnError;
+import javax.websocket.OnMessage;
+import javax.websocket.OnOpen;
+import javax.websocket.Session;
+import javax.websocket.server.ServerEndpoint;
+
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+import org.eclipse.jetty.websocket.jsr356.server.StackUtil;
+
+@ServerEndpoint("/echo/primitives/double")
+public class DoubleTextSocket
+{
+ private static final Logger LOG = Log.getLogger(DoubleTextSocket.class);
+
+ private Session session;
+
+ @OnOpen
+ public void onOpen(Session session)
+ {
+ this.session = session;
+ }
+
+ @OnMessage
+ public void onMessage(double d) throws IOException
+ {
+ String msg = String.format("%.4f",d);
+ session.getAsyncRemote().sendText(msg);
+ }
+
+ @OnError
+ public void onError(Throwable cause) throws IOException
+ {
+ LOG.warn("Error",cause);
+ session.getBasicRemote().sendText("Exception: " + StackUtil.toString(cause));
+ }
+}
diff --git a/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/samples/primitives/FloatObjectTextSocket.java b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/samples/primitives/FloatObjectTextSocket.java
new file mode 100644
index 0000000..c6e44f2
--- /dev/null
+++ b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/samples/primitives/FloatObjectTextSocket.java
@@ -0,0 +1,66 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.server.samples.primitives;
+
+import java.io.IOException;
+
+import javax.websocket.OnError;
+import javax.websocket.OnMessage;
+import javax.websocket.OnOpen;
+import javax.websocket.Session;
+import javax.websocket.server.ServerEndpoint;
+
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+import org.eclipse.jetty.websocket.jsr356.server.StackUtil;
+
+@ServerEndpoint("/echo/primitives/floatobject")
+public class FloatObjectTextSocket
+{
+ private static final Logger LOG = Log.getLogger(FloatObjectTextSocket.class);
+
+ private Session session;
+
+ @OnOpen
+ public void onOpen(Session session)
+ {
+ this.session = session;
+ }
+
+ @OnMessage
+ public void onMessage(Float f) throws IOException
+ {
+ if (f == null)
+ {
+ session.getAsyncRemote().sendText("Error: Float is null");
+ }
+ else
+ {
+ String msg = String.format("%.4f",f);
+ session.getAsyncRemote().sendText(msg);
+ }
+ }
+
+ @OnError
+ public void onError(Throwable cause) throws IOException
+ {
+ LOG.warn("Error",cause);
+ session.getBasicRemote().sendText("Exception: " + StackUtil.toString(cause));
+ }
+}
diff --git a/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/samples/primitives/FloatTextSocket.java b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/samples/primitives/FloatTextSocket.java
new file mode 100644
index 0000000..0c86cab
--- /dev/null
+++ b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/samples/primitives/FloatTextSocket.java
@@ -0,0 +1,59 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.server.samples.primitives;
+
+import java.io.IOException;
+
+import javax.websocket.OnError;
+import javax.websocket.OnMessage;
+import javax.websocket.OnOpen;
+import javax.websocket.Session;
+import javax.websocket.server.ServerEndpoint;
+
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+import org.eclipse.jetty.websocket.jsr356.server.StackUtil;
+
+@ServerEndpoint("/echo/primitives/float")
+public class FloatTextSocket
+{
+ private static final Logger LOG = Log.getLogger(FloatTextSocket.class);
+
+ private Session session;
+
+ @OnOpen
+ public void onOpen(Session session)
+ {
+ this.session = session;
+ }
+
+ @OnMessage
+ public void onMessage(float f) throws IOException
+ {
+ String msg = String.format("%.4f",f);
+ session.getAsyncRemote().sendText(msg);
+ }
+
+ @OnError
+ public void onError(Throwable cause) throws IOException
+ {
+ LOG.warn("Error",cause);
+ session.getBasicRemote().sendText("Exception: " + StackUtil.toString(cause));
+ }
+}
diff --git a/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/samples/primitives/IntParamTextSocket.java b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/samples/primitives/IntParamTextSocket.java
new file mode 100644
index 0000000..14f25be
--- /dev/null
+++ b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/samples/primitives/IntParamTextSocket.java
@@ -0,0 +1,60 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.server.samples.primitives;
+
+import java.io.IOException;
+
+import javax.websocket.OnError;
+import javax.websocket.OnMessage;
+import javax.websocket.OnOpen;
+import javax.websocket.Session;
+import javax.websocket.server.PathParam;
+import javax.websocket.server.ServerEndpoint;
+
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+import org.eclipse.jetty.websocket.jsr356.server.StackUtil;
+
+@ServerEndpoint("/echo/primitives/integer/params/{a}")
+public class IntParamTextSocket
+{
+ private static final Logger LOG = Log.getLogger(IntParamTextSocket.class);
+
+ private Session session;
+
+ @OnOpen
+ public void onOpen(Session session)
+ {
+ this.session = session;
+ }
+
+ @OnMessage
+ public void onMessage(int i, @PathParam("a") int param) throws IOException
+ {
+ String msg = String.format("%d|%d",i,param);
+ session.getAsyncRemote().sendText(msg);
+ }
+
+ @OnError
+ public void onError(Throwable cause) throws IOException
+ {
+ LOG.warn("Error",cause);
+ session.getBasicRemote().sendText("Exception: " + StackUtil.toString(cause));
+ }
+}
diff --git a/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/samples/primitives/IntTextSocket.java b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/samples/primitives/IntTextSocket.java
new file mode 100644
index 0000000..0e35130
--- /dev/null
+++ b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/samples/primitives/IntTextSocket.java
@@ -0,0 +1,59 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.server.samples.primitives;
+
+import java.io.IOException;
+
+import javax.websocket.OnError;
+import javax.websocket.OnMessage;
+import javax.websocket.OnOpen;
+import javax.websocket.Session;
+import javax.websocket.server.ServerEndpoint;
+
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+import org.eclipse.jetty.websocket.jsr356.server.StackUtil;
+
+@ServerEndpoint("/echo/primitives/integer")
+public class IntTextSocket
+{
+ private static final Logger LOG = Log.getLogger(IntTextSocket.class);
+
+ private Session session;
+
+ @OnOpen
+ public void onOpen(Session session)
+ {
+ this.session = session;
+ }
+
+ @OnMessage
+ public void onMessage(int i) throws IOException
+ {
+ String msg = Integer.toString(i);
+ session.getAsyncRemote().sendText(msg);
+ }
+
+ @OnError
+ public void onError(Throwable cause) throws IOException
+ {
+ LOG.warn("Error",cause);
+ session.getBasicRemote().sendText("Exception: " + StackUtil.toString(cause));
+ }
+}
diff --git a/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/samples/primitives/IntegerObjectParamTextSocket.java b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/samples/primitives/IntegerObjectParamTextSocket.java
new file mode 100644
index 0000000..ebee7b8
--- /dev/null
+++ b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/samples/primitives/IntegerObjectParamTextSocket.java
@@ -0,0 +1,67 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.server.samples.primitives;
+
+import java.io.IOException;
+
+import javax.websocket.OnError;
+import javax.websocket.OnMessage;
+import javax.websocket.OnOpen;
+import javax.websocket.Session;
+import javax.websocket.server.PathParam;
+import javax.websocket.server.ServerEndpoint;
+
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+import org.eclipse.jetty.websocket.jsr356.server.StackUtil;
+
+@ServerEndpoint("/echo/primitives/integerobject/params/{a}")
+public class IntegerObjectParamTextSocket
+{
+ private static final Logger LOG = Log.getLogger(IntegerObjectParamTextSocket.class);
+
+ private Session session;
+
+ @OnOpen
+ public void onOpen(Session session)
+ {
+ this.session = session;
+ }
+
+ @OnMessage
+ public void onMessage(Integer i, @PathParam("a") int param) throws IOException
+ {
+ if (i == null)
+ {
+ session.getAsyncRemote().sendText("Error: Integer is null");
+ }
+ else
+ {
+ String msg = String.format("%d|%d",i,param);
+ session.getAsyncRemote().sendText(msg);
+ }
+ }
+
+ @OnError
+ public void onError(Throwable cause) throws IOException
+ {
+ LOG.warn("Error",cause);
+ session.getBasicRemote().sendText("Exception: " + StackUtil.toString(cause));
+ }
+}
diff --git a/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/samples/primitives/IntegerObjectTextSocket.java b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/samples/primitives/IntegerObjectTextSocket.java
new file mode 100644
index 0000000..fa8fce5
--- /dev/null
+++ b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/samples/primitives/IntegerObjectTextSocket.java
@@ -0,0 +1,66 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.server.samples.primitives;
+
+import java.io.IOException;
+
+import javax.websocket.OnError;
+import javax.websocket.OnMessage;
+import javax.websocket.OnOpen;
+import javax.websocket.Session;
+import javax.websocket.server.ServerEndpoint;
+
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+import org.eclipse.jetty.websocket.jsr356.server.StackUtil;
+
+@ServerEndpoint("/echo/primitives/integerobject")
+public class IntegerObjectTextSocket
+{
+ private static final Logger LOG = Log.getLogger(IntegerObjectTextSocket.class);
+
+ private Session session;
+
+ @OnOpen
+ public void onOpen(Session session)
+ {
+ this.session = session;
+ }
+
+ @OnMessage
+ public void onMessage(Integer i) throws IOException
+ {
+ if (i == null)
+ {
+ session.getAsyncRemote().sendText("Error: Integer is null");
+ }
+ else
+ {
+ String msg = i.toString();
+ session.getAsyncRemote().sendText(msg);
+ }
+ }
+
+ @OnError
+ public void onError(Throwable cause) throws IOException
+ {
+ LOG.warn("Error",cause);
+ session.getBasicRemote().sendText("Exception: " + StackUtil.toString(cause));
+ }
+}
diff --git a/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/samples/primitives/LongObjectTextSocket.java b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/samples/primitives/LongObjectTextSocket.java
new file mode 100644
index 0000000..96e74fa
--- /dev/null
+++ b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/samples/primitives/LongObjectTextSocket.java
@@ -0,0 +1,66 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.server.samples.primitives;
+
+import java.io.IOException;
+
+import javax.websocket.OnError;
+import javax.websocket.OnMessage;
+import javax.websocket.OnOpen;
+import javax.websocket.Session;
+import javax.websocket.server.ServerEndpoint;
+
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+import org.eclipse.jetty.websocket.jsr356.server.StackUtil;
+
+@ServerEndpoint("/echo/primitives/longobject")
+public class LongObjectTextSocket
+{
+ private static final Logger LOG = Log.getLogger(LongObjectTextSocket.class);
+
+ private Session session;
+
+ @OnOpen
+ public void onOpen(Session session)
+ {
+ this.session = session;
+ }
+
+ @OnMessage
+ public void onMessage(Long l) throws IOException
+ {
+ if (l == null)
+ {
+ session.getAsyncRemote().sendText("Error: Long is null");
+ }
+ else
+ {
+ String msg = l.toString();
+ session.getAsyncRemote().sendText(msg);
+ }
+ }
+
+ @OnError
+ public void onError(Throwable cause) throws IOException
+ {
+ LOG.warn("Error",cause);
+ session.getBasicRemote().sendText("Exception: " + StackUtil.toString(cause));
+ }
+}
diff --git a/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/samples/primitives/LongTextSocket.java b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/samples/primitives/LongTextSocket.java
new file mode 100644
index 0000000..cac5856
--- /dev/null
+++ b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/samples/primitives/LongTextSocket.java
@@ -0,0 +1,59 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.server.samples.primitives;
+
+import java.io.IOException;
+
+import javax.websocket.OnError;
+import javax.websocket.OnMessage;
+import javax.websocket.OnOpen;
+import javax.websocket.Session;
+import javax.websocket.server.ServerEndpoint;
+
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+import org.eclipse.jetty.websocket.jsr356.server.StackUtil;
+
+@ServerEndpoint("/echo/primitives/long")
+public class LongTextSocket
+{
+ private static final Logger LOG = Log.getLogger(LongTextSocket.class);
+
+ private Session session;
+
+ @OnOpen
+ public void onOpen(Session session)
+ {
+ this.session = session;
+ }
+
+ @OnMessage
+ public void onMessage(long l) throws IOException
+ {
+ String msg = Long.toString(l);
+ session.getAsyncRemote().sendText(msg);
+ }
+
+ @OnError
+ public void onError(Throwable cause) throws IOException
+ {
+ LOG.warn("Error",cause);
+ session.getBasicRemote().sendText("Exception: " + StackUtil.toString(cause));
+ }
+}
diff --git a/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/samples/primitives/ShortObjectTextSocket.java b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/samples/primitives/ShortObjectTextSocket.java
new file mode 100644
index 0000000..8f83284
--- /dev/null
+++ b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/samples/primitives/ShortObjectTextSocket.java
@@ -0,0 +1,66 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.server.samples.primitives;
+
+import java.io.IOException;
+
+import javax.websocket.OnError;
+import javax.websocket.OnMessage;
+import javax.websocket.OnOpen;
+import javax.websocket.Session;
+import javax.websocket.server.ServerEndpoint;
+
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+import org.eclipse.jetty.websocket.jsr356.server.StackUtil;
+
+@ServerEndpoint("/echo/primitives/shortobject")
+public class ShortObjectTextSocket
+{
+ private static final Logger LOG = Log.getLogger(ShortObjectTextSocket.class);
+
+ private Session session;
+
+ @OnOpen
+ public void onOpen(Session session)
+ {
+ this.session = session;
+ }
+
+ @OnMessage
+ public void onMessage(Short s) throws IOException
+ {
+ if (s == null)
+ {
+ session.getAsyncRemote().sendText("Error: Short is null");
+ }
+ else
+ {
+ String msg = s.toString();
+ session.getAsyncRemote().sendText(msg);
+ }
+ }
+
+ @OnError
+ public void onError(Throwable cause) throws IOException
+ {
+ LOG.warn("Error",cause);
+ session.getBasicRemote().sendText("Exception: " + StackUtil.toString(cause));
+ }
+}
diff --git a/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/samples/primitives/ShortTextSocket.java b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/samples/primitives/ShortTextSocket.java
new file mode 100644
index 0000000..fc4d528
--- /dev/null
+++ b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/samples/primitives/ShortTextSocket.java
@@ -0,0 +1,59 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.server.samples.primitives;
+
+import java.io.IOException;
+
+import javax.websocket.OnError;
+import javax.websocket.OnMessage;
+import javax.websocket.OnOpen;
+import javax.websocket.Session;
+import javax.websocket.server.ServerEndpoint;
+
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+import org.eclipse.jetty.websocket.jsr356.server.StackUtil;
+
+@ServerEndpoint("/echo/primitives/short")
+public class ShortTextSocket
+{
+ private static final Logger LOG = Log.getLogger(ShortTextSocket.class);
+
+ private Session session;
+
+ @OnOpen
+ public void onOpen(Session session)
+ {
+ this.session = session;
+ }
+
+ @OnMessage
+ public void onMessage(short s) throws IOException
+ {
+ String msg = Short.toString(s);
+ session.getAsyncRemote().sendText(msg);
+ }
+
+ @OnError
+ public void onError(Throwable cause) throws IOException
+ {
+ LOG.warn("Error",cause);
+ session.getBasicRemote().sendText("Exception: " + StackUtil.toString(cause));
+ }
+}
diff --git a/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/samples/streaming/ReaderParamSocket.java b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/samples/streaming/ReaderParamSocket.java
new file mode 100644
index 0000000..a15ca84
--- /dev/null
+++ b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/samples/streaming/ReaderParamSocket.java
@@ -0,0 +1,65 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.server.samples.streaming;
+
+import java.io.IOException;
+import java.io.Reader;
+
+import javax.websocket.OnError;
+import javax.websocket.OnMessage;
+import javax.websocket.OnOpen;
+import javax.websocket.Session;
+import javax.websocket.server.PathParam;
+import javax.websocket.server.ServerEndpoint;
+
+import org.eclipse.jetty.util.IO;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+import org.eclipse.jetty.websocket.jsr356.server.StackUtil;
+
+@ServerEndpoint("/echo/streaming/readerparam/{param}")
+public class ReaderParamSocket
+{
+ private static final Logger LOG = Log.getLogger(ReaderParamSocket.class);
+
+ private Session session;
+
+ @OnOpen
+ public void onOpen(Session session)
+ {
+ this.session = session;
+ }
+
+ @OnMessage
+ public void onReader(Reader reader, @PathParam("param") String param) throws IOException
+ {
+ StringBuilder msg = new StringBuilder();
+ msg.append(IO.toString(reader));
+ msg.append('|');
+ msg.append(param);
+ session.getAsyncRemote().sendText(msg.toString());
+ }
+
+ @OnError
+ public void onError(Throwable cause) throws IOException
+ {
+ LOG.warn("Error",cause);
+ session.getBasicRemote().sendText("Exception: " + StackUtil.toString(cause));
+ }
+}
diff --git a/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/samples/streaming/ReaderSocket.java b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/samples/streaming/ReaderSocket.java
new file mode 100644
index 0000000..0bf4372
--- /dev/null
+++ b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/samples/streaming/ReaderSocket.java
@@ -0,0 +1,51 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.server.samples.streaming;
+
+import java.io.IOException;
+import java.io.Reader;
+
+import javax.websocket.OnError;
+import javax.websocket.OnMessage;
+import javax.websocket.Session;
+import javax.websocket.server.ServerEndpoint;
+
+import org.eclipse.jetty.util.IO;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+import org.eclipse.jetty.websocket.jsr356.server.StackUtil;
+
+@ServerEndpoint("/echo/streaming/reader")
+public class ReaderSocket
+{
+ private static final Logger LOG = Log.getLogger(ReaderSocket.class);
+
+ @OnMessage
+ public String onReader(Reader reader) throws IOException
+ {
+ return IO.toString(reader);
+ }
+
+ @OnError
+ public void onError(Session session, Throwable cause) throws IOException
+ {
+ LOG.warn("Error",cause);
+ session.getBasicRemote().sendText("Exception: " + StackUtil.toString(cause));
+ }
+}
diff --git a/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/samples/streaming/StringReturnReaderParamSocket.java b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/samples/streaming/StringReturnReaderParamSocket.java
new file mode 100644
index 0000000..81d7a50
--- /dev/null
+++ b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/samples/streaming/StringReturnReaderParamSocket.java
@@ -0,0 +1,56 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.jsr356.server.samples.streaming;
+
+import java.io.IOException;
+import java.io.Reader;
+
+import javax.websocket.OnError;
+import javax.websocket.OnMessage;
+import javax.websocket.Session;
+import javax.websocket.server.PathParam;
+import javax.websocket.server.ServerEndpoint;
+
+import org.eclipse.jetty.util.IO;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+import org.eclipse.jetty.websocket.jsr356.server.StackUtil;
+
+@ServerEndpoint("/echo/streaming/readerparam2/{param}")
+public class StringReturnReaderParamSocket
+{
+ private static final Logger LOG = Log.getLogger(StringReturnReaderParamSocket.class);
+
+ @OnMessage
+ public String onReader(Reader reader, @PathParam("param") String param) throws IOException
+ {
+ StringBuilder msg = new StringBuilder();
+ msg.append(IO.toString(reader));
+ msg.append('|');
+ msg.append(param);
+ return msg.toString();
+ }
+
+ @OnError
+ public void onError(Session session, Throwable cause) throws IOException
+ {
+ LOG.warn("Error",cause);
+ session.getBasicRemote().sendText("Exception: " + StackUtil.toString(cause));
+ }
+}
diff --git a/jetty-websocket/javax-websocket-server-impl/src/test/resources/basic-echo-endpoint-config-web.xml b/jetty-websocket/javax-websocket-server-impl/src/test/resources/basic-echo-endpoint-config-web.xml
new file mode 100644
index 0000000..afd9cef
--- /dev/null
+++ b/jetty-websocket/javax-websocket-server-impl/src/test/resources/basic-echo-endpoint-config-web.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<web-app
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xmlns="http://java.sun.com/xml/ns/javaee"
+ xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
+ metadata-complete="false"
+ version="3.0">
+
+ <listener>
+ <listener-class>org.eclipse.jetty.websocket.jsr356.server.samples.echo.BasicEchoEndpointConfigContextListener</listener-class>
+ </listener>
+</web-app>
\ No newline at end of file
diff --git a/jetty-websocket/javax-websocket-server-impl/src/test/resources/empty-web.xml b/jetty-websocket/javax-websocket-server-impl/src/test/resources/empty-web.xml
new file mode 100644
index 0000000..18aafa5
--- /dev/null
+++ b/jetty-websocket/javax-websocket-server-impl/src/test/resources/empty-web.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<web-app
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xmlns="http://java.sun.com/xml/ns/javaee"
+ xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
+ metadata-complete="false"
+ version="3.0">
+</web-app>
\ No newline at end of file
diff --git a/jetty-websocket/javax-websocket-server-impl/src/test/resources/jetty-logging.properties b/jetty-websocket/javax-websocket-server-impl/src/test/resources/jetty-logging.properties
new file mode 100644
index 0000000..d091aab
--- /dev/null
+++ b/jetty-websocket/javax-websocket-server-impl/src/test/resources/jetty-logging.properties
@@ -0,0 +1,8 @@
+org.eclipse.jetty.util.log.class=org.eclipse.jetty.util.log.StdErrLog
+org.eclipse.jetty.LEVEL=WARN
+
+# org.eclipse.jetty.websocket.LEVEL=DEBUG
+org.eclipse.jetty.websocket.LEVEL=INFO
+# org.eclipse.jetty.websocket.LEVEL=WARN
+# org.eclipse.jetty.websocket.common.io.LEVEL=DEBUG
+
diff --git a/jetty-websocket/javax-websocket-server-impl/src/test/resources/large-echo-config-web.xml b/jetty-websocket/javax-websocket-server-impl/src/test/resources/large-echo-config-web.xml
new file mode 100644
index 0000000..08e696f
--- /dev/null
+++ b/jetty-websocket/javax-websocket-server-impl/src/test/resources/large-echo-config-web.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<web-app
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xmlns="http://java.sun.com/xml/ns/javaee"
+ xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
+ metadata-complete="false"
+ version="3.0">
+
+ <listener>
+ <listener-class>org.eclipse.jetty.websocket.jsr356.server.samples.echo.LargeEchoContextListener</listener-class>
+ </listener>
+</web-app>
\ No newline at end of file
diff --git a/jetty-websocket/pom.xml b/jetty-websocket/pom.xml
index 07e52e6..f30f2b4 100644
--- a/jetty-websocket/pom.xml
+++ b/jetty-websocket/pom.xml
@@ -3,7 +3,7 @@
<parent>
<artifactId>jetty-project</artifactId>
<groupId>org.eclipse.jetty</groupId>
- <version>9.0.5-SNAPSHOT</version>
+ <version>9.1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
@@ -19,6 +19,9 @@
<module>websocket-client</module>
<module>websocket-server</module>
<module>websocket-servlet</module>
+ <module>websocket-mux-extension</module>
+ <module>javax-websocket-client-impl</module>
+ <module>javax-websocket-server-impl</module>
</modules>
<build>
@@ -41,8 +44,8 @@
</goals>
<configuration>
<instructions>
- <Export-Package>${bundle-symbolic-name}.*;version="9.0"</Export-Package>
- <Import-Package>javax.servlet.*;version="[2.6.0,3.0)",org.eclipse.jetty.*;version="[9.0,10.0)",*</Import-Package>
+ <Export-Package>${bundle-symbolic-name}.*;version="9.1"</Export-Package>
+ <Import-Package>javax.servlet.*;version="[3.0,4.0)",org.eclipse.jetty.*;version="[9.0,10.0)",*</Import-Package>
<_nouses>true</_nouses>
</instructions>
</configuration>
@@ -58,6 +61,24 @@
</archive>
</configuration>
</plugin>
+ <plugin>
+ <groupId>org.codehaus.mojo</groupId>
+ <artifactId>clirr-maven-plugin</artifactId>
+ <version>2.5</version>
+ <executions>
+ <execution>
+ <id>compare-api</id>
+ <phase>package</phase>
+ <goals>
+ <goal>clirr</goal>
+ </goals>
+ </execution>
+ </executions>
+ <configuration>
+ <minSeverity>info</minSeverity>
+ <comparisonVersion>9.0.3.v20130506</comparisonVersion>
+ </configuration>
+ </plugin>
</plugins>
</build>
</project>
diff --git a/jetty-websocket/websocket-api/pom.xml b/jetty-websocket/websocket-api/pom.xml
index e24e073..e23ef1d 100644
--- a/jetty-websocket/websocket-api/pom.xml
+++ b/jetty-websocket/websocket-api/pom.xml
@@ -3,7 +3,7 @@
<parent>
<groupId>org.eclipse.jetty.websocket</groupId>
<artifactId>websocket-parent</artifactId>
- <version>9.0.5-SNAPSHOT</version>
+ <version>9.1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
diff --git a/jetty-websocket/websocket-api/src/main/java/org/eclipse/jetty/websocket/api/RemoteEndpoint.java b/jetty-websocket/websocket-api/src/main/java/org/eclipse/jetty/websocket/api/RemoteEndpoint.java
index dfdefec..c1d000c 100644
--- a/jetty-websocket/websocket-api/src/main/java/org/eclipse/jetty/websocket/api/RemoteEndpoint.java
+++ b/jetty-websocket/websocket-api/src/main/java/org/eclipse/jetty/websocket/api/RemoteEndpoint.java
@@ -35,19 +35,27 @@
void sendBytes(ByteBuffer data) throws IOException;
/**
- * Initiates the asynchronous transmission of a binary message. This method returns before the message is transmitted. Developers may provide a callback to
- * be notified when the message has been transmitted, or may use the returned Future object to track progress of the transmission. Errors in transmission
- * are given to the developer in the WriteResult object in either case.
+ * Initiates the asynchronous transmission of a binary message. This method returns before the message is transmitted. Developers may use the returned
+ * Future object to track progress of the transmission.
*
* @param data
* the data being sent
- * @param completion
- * handler that will be notified of progress
* @return the Future object representing the send operation.
*/
Future<Void> sendBytesByFuture(ByteBuffer data);
/**
+ * Initiates the asynchronous transmission of a binary message. This method returns before the message is transmitted. Developers may provide a callback to
+ * be notified when the message has been transmitted or resulted in an error.
+ *
+ * @param data
+ * the data being sent
+ * @param callback
+ * callback to notify of success or failure of the write operation
+ */
+ void sendBytes(ByteBuffer data, WriteCallback callback);
+
+ /**
* Send a binary message in pieces, blocking until all of the message has been transmitted. The runtime reads the message in order. Non-final pieces are
* sent with isLast set to false. The final piece must be sent with isLast set to true.
*
@@ -94,15 +102,23 @@
void sendString(String text) throws IOException;
/**
- * Initiates the asynchronous transmission of a text message. This method returns before the message is transmitted. Developers may provide a callback to be
- * notified when the message has been transmitted, or may use the returned Future object to track progress of the transmission. Errors in transmission are
- * given to the developer in the WriteResult object in either case.
+ * Initiates the asynchronous transmission of a text message. This method may return before the message is transmitted. Developers may use the returned
+ * Future object to track progress of the transmission.
*
* @param text
* the text being sent
- * @param completion
- * the handler which will be notified of progress
* @return the Future object representing the send operation.
*/
Future<Void> sendStringByFuture(String text);
+
+ /**
+ * Initiates the asynchronous transmission of a text message. This method may return before the message is transmitted. Developers may provide a callback to
+ * be notified when the message has been transmitted or resulted in an error.
+ *
+ * @param text
+ * the text being sent
+ * @param callback
+ * callback to notify of success or failure of the write operation
+ */
+ void sendString(String text, WriteCallback callback);
}
diff --git a/jetty-websocket/websocket-api/src/main/java/org/eclipse/jetty/websocket/api/Session.java b/jetty-websocket/websocket-api/src/main/java/org/eclipse/jetty/websocket/api/Session.java
index 7748dd0..28b3b77 100644
--- a/jetty-websocket/websocket-api/src/main/java/org/eclipse/jetty/websocket/api/Session.java
+++ b/jetty-websocket/websocket-api/src/main/java/org/eclipse/jetty/websocket/api/Session.java
@@ -40,7 +40,7 @@
* @see #disconnect()
*/
@Override
- void close() throws IOException;
+ void close();
/**
* Request Close the current conversation, giving a reason for the closure. Note the websocket spec defines the acceptable uses of status codes and reason
@@ -55,7 +55,7 @@
* @see #close(int, String)
* @see #disconnect()
*/
- void close(CloseStatus closeStatus) throws IOException;
+ void close(CloseStatus closeStatus);
/**
* Send a websocket Close frame, with status code.
@@ -72,7 +72,7 @@
* @see #close(CloseStatus)
* @see #disconnect()
*/
- void close(int statusCode, String reason) throws IOException;
+ void close(int statusCode, String reason);
/**
* Issue a harsh disconnect of the underlying connection.
@@ -107,13 +107,6 @@
public InetSocketAddress getLocalAddress();
/**
- * The maximum total length of messages, text or binary, that this Session can handle.
- *
- * @return the message size
- */
- long getMaximumMessageSize();
-
- /**
* Access the (now read-only) {@link WebSocketPolicy} in use for this connection.
*
* @return the policy in use
@@ -179,11 +172,6 @@
void setIdleTimeout(long ms);
/**
- * Sets the maximum total length of messages, text or binary, that this Session can handle.
- */
- void setMaximumMessageSize(long length);
-
- /**
* Suspend a the incoming read events on the connection.
*
* @return the suspend token suitable for resuming the reading of data on the connection.
diff --git a/jetty-websocket/websocket-api/src/main/java/org/eclipse/jetty/websocket/api/UpgradeRequest.java b/jetty-websocket/websocket-api/src/main/java/org/eclipse/jetty/websocket/api/UpgradeRequest.java
index e805f86..826a808 100644
--- a/jetty-websocket/websocket-api/src/main/java/org/eclipse/jetty/websocket/api/UpgradeRequest.java
+++ b/jetty-websocket/websocket-api/src/main/java/org/eclipse/jetty/websocket/api/UpgradeRequest.java
@@ -20,12 +20,13 @@
import java.net.HttpCookie;
import java.net.URI;
+import java.security.Principal;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
-import java.util.Locale;
import java.util.Map;
+import java.util.TreeMap;
import org.eclipse.jetty.websocket.api.extensions.ExtensionConfig;
import org.eclipse.jetty.websocket.api.util.QuoteUtil;
@@ -36,8 +37,8 @@
private List<String> subProtocols = new ArrayList<>();
private List<ExtensionConfig> extensions = new ArrayList<>();
private List<HttpCookie> cookies = new ArrayList<>();
- private Map<String, List<String>> headers = new HashMap<>();
- private Map<String, String[]> parameters = new HashMap<>();
+ private Map<String, List<String>> headers = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
+ private Map<String, List<String>> parameters = new HashMap<>();
private Object session;
private String httpVersion;
private String method;
@@ -76,6 +77,11 @@
}
}
+ public void clearHeaders()
+ {
+ headers.clear();
+ }
+
public List<HttpCookie> getCookies()
{
return cookies;
@@ -88,7 +94,7 @@
public String getHeader(String name)
{
- List<String> values = headers.get(name.toLowerCase(Locale.ENGLISH));
+ List<String> values = headers.get(name);
// no value list
if (values == null)
{
@@ -122,7 +128,7 @@
public int getHeaderInt(String name)
{
- List<String> values = headers.get(name.toLowerCase(Locale.ENGLISH));
+ List<String> values = headers.get(name);
// no value list
if (values == null)
{
@@ -177,11 +183,21 @@
*
* @return a unmodifiable map of query parameters of the request.
*/
- public Map<String, String[]> getParameterMap()
+ public Map<String, List<String>> getParameterMap()
{
return Collections.unmodifiableMap(parameters);
}
+ public String getProtocolVersion()
+ {
+ String version = getHeader("Sec-WebSocket-Version");
+ if (version == null)
+ {
+ return "13";
+ }
+ return version;
+ }
+
public String getQueryString()
{
return requestURI.getQuery();
@@ -209,6 +225,19 @@
return subProtocols;
}
+ /**
+ * Get the User Principal for this request.
+ * <p>
+ * Only applicable when using UpgradeRequest from server side.
+ *
+ * @return the user principal
+ */
+ public Principal getUserPrincipal()
+ {
+ // Server side should override to implement
+ return null;
+ }
+
public boolean hasSubProtocol(String test)
{
for (String protocol : subProtocols)
@@ -238,14 +267,26 @@
public void setHeader(String name, List<String> values)
{
- headers.put(name.toLowerCase(Locale.ENGLISH),values);
+ headers.put(name,values);
}
public void setHeader(String name, String value)
{
List<String> values = new ArrayList<>();
values.add(value);
- setHeader(name.toLowerCase(Locale.ENGLISH),values);
+ setHeader(name,values);
+ }
+
+ public void setHeaders(Map<String, List<String>> headers)
+ {
+ clearHeaders();
+
+ for (Map.Entry<String, List<String>> entry : headers.entrySet())
+ {
+ String name = entry.getKey();
+ List<String> values = entry.getValue();
+ setHeader(name,values);
+ }
}
public void setHttpVersion(String httpVersion)
@@ -258,7 +299,7 @@
this.method = method;
}
- protected void setParameterMap(Map<String, String[]> parameters)
+ protected void setParameterMap(Map<String, List<String>> parameters)
{
this.parameters.clear();
this.parameters.putAll(parameters);
diff --git a/jetty-websocket/websocket-api/src/main/java/org/eclipse/jetty/websocket/api/UpgradeResponse.java b/jetty-websocket/websocket-api/src/main/java/org/eclipse/jetty/websocket/api/UpgradeResponse.java
index 51a04c0..a6cea88 100644
--- a/jetty-websocket/websocket-api/src/main/java/org/eclipse/jetty/websocket/api/UpgradeResponse.java
+++ b/jetty-websocket/websocket-api/src/main/java/org/eclipse/jetty/websocket/api/UpgradeResponse.java
@@ -20,10 +20,10 @@
import java.io.IOException;
import java.util.ArrayList;
-import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
+import java.util.TreeMap;
import org.eclipse.jetty.websocket.api.extensions.ExtensionConfig;
import org.eclipse.jetty.websocket.api.util.QuoteUtil;
@@ -33,13 +33,13 @@
public static final String SEC_WEBSOCKET_PROTOCOL = "Sec-WebSocket-Protocol";
private int statusCode;
private String statusReason;
- private Map<String, List<String>> headers = new HashMap<>();
+ private Map<String, List<String>> headers = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
private List<ExtensionConfig> extensions = new ArrayList<>();
private boolean success = false;
public void addHeader(String name, String value)
{
- String key = name.toLowerCase();
+ String key = name;
List<String> values = headers.get(key);
if (values == null)
{
@@ -115,7 +115,7 @@
public List<String> getHeaders(String name)
{
- return headers.get(name.toLowerCase());
+ return headers.get(name);
}
public int getStatusCode()
@@ -188,7 +188,7 @@
{
List<String> values = new ArrayList<>();
values.add(value);
- headers.put(name.toLowerCase(),values);
+ headers.put(name,values);
}
public void setStatusCode(int statusCode)
diff --git a/jetty-websocket/websocket-api/src/main/java/org/eclipse/jetty/websocket/api/WebSocketPolicy.java b/jetty-websocket/websocket-api/src/main/java/org/eclipse/jetty/websocket/api/WebSocketPolicy.java
index 348a0e0..afb3c50 100644
--- a/jetty-websocket/websocket-api/src/main/java/org/eclipse/jetty/websocket/api/WebSocketPolicy.java
+++ b/jetty-websocket/websocket-api/src/main/java/org/eclipse/jetty/websocket/api/WebSocketPolicy.java
@@ -38,9 +38,45 @@
/**
* The maximum size of a text message during parsing/generating.
* <p>
+ * Text messages over this maximum will result in a close code 1009 {@link StatusCode#MESSAGE_TOO_LARGE}
+ * <p>
* Default: 65536 (64 K)
*/
- private long maxMessageSize = 64 * KB;
+ private int maxTextMessageSize = 64 * KB;
+
+ /**
+ * The maximum size of a text message buffer.
+ * <p>
+ * Used ONLY for stream based message writing.
+ * <p>
+ * Default: 32768 (32 K)
+ */
+ private int maxTextMessageBufferSize = 32 * KB;
+
+ /**
+ * The maximum size of a binary message during parsing/generating.
+ * <p>
+ * Binary messages over this maximum will result in a close code 1009 {@link StatusCode#MESSAGE_TOO_LARGE}
+ * <p>
+ * Default: 65536 (64 K)
+ */
+ private int maxBinaryMessageSize = 64 * KB;
+
+ /**
+ * The maximum size of a binary message buffer
+ * <p>
+ * Used ONLY for for stream based message writing
+ * <p>
+ * Default: 32768 (32 K)
+ */
+ private int maxBinaryMessageBufferSize = 32 * KB;
+
+ /**
+ * The timeout in ms (milliseconds) for async write operations.
+ * <p>
+ * Negative values indicate a disabled timeout.
+ */
+ private long asyncWriteTimeout = 60000;
/**
* The time in ms (milliseconds) that a websocket may be idle before closing.
@@ -66,14 +102,42 @@
this.behavior = behavior;
}
- public void assertValidMessageSize(int requestedSize)
+ private void assertLessThan(String name, long size, String otherName, long otherSize)
{
- if (maxMessageSize > 0)
+ if (size > otherSize)
+ {
+ throw new IllegalArgumentException(String.format("%s [%d] must be less than %s [%d]",name,size,otherName,otherSize));
+ }
+ }
+
+ private void assertPositive(String name, long size)
+ {
+ if (size < 1)
+ {
+ throw new IllegalArgumentException(String.format("%s [%d] must be a postive value larger than 0",name,size));
+ }
+ }
+
+ public void assertValidBinaryMessageSize(int requestedSize)
+ {
+ if (maxBinaryMessageSize > 0)
{
// validate it
- if (requestedSize > maxMessageSize)
+ if (requestedSize > maxBinaryMessageSize)
{
- throw new MessageTooLargeException("Requested message size [" + requestedSize + "] exceeds maximum size [" + maxMessageSize + "]");
+ throw new MessageTooLargeException("Binary message size [" + requestedSize + "] exceeds maximum size [" + maxBinaryMessageSize + "]");
+ }
+ }
+ }
+
+ public void assertValidTextMessageSize(int requestedSize)
+ {
+ if (maxTextMessageSize > 0)
+ {
+ // validate it
+ if (requestedSize > maxTextMessageSize)
+ {
+ throw new MessageTooLargeException("Text message size [" + requestedSize + "] exceeds maximum size [" + maxTextMessageSize + "]");
}
}
}
@@ -82,43 +146,217 @@
{
WebSocketPolicy clone = new WebSocketPolicy(this.behavior);
clone.idleTimeout = this.idleTimeout;
- clone.maxMessageSize = this.maxMessageSize;
+ clone.maxTextMessageSize = this.maxTextMessageSize;
+ clone.maxTextMessageBufferSize = this.maxTextMessageBufferSize;
+ clone.maxBinaryMessageSize = this.maxBinaryMessageSize;
+ clone.maxBinaryMessageBufferSize = this.maxBinaryMessageBufferSize;
clone.inputBufferSize = this.inputBufferSize;
+ clone.asyncWriteTimeout = this.asyncWriteTimeout;
return clone;
}
+ /**
+ * The timeout in ms (milliseconds) for async write operations.
+ * <p>
+ * Negative values indicate a disabled timeout.
+ *
+ * @return the timeout for async write operations. negative values indicate disabled timeout.
+ */
+ public long getAsyncWriteTimeout()
+ {
+ return asyncWriteTimeout;
+ }
+
public WebSocketBehavior getBehavior()
{
return behavior;
}
+ /**
+ * The time in ms (milliseconds) that a websocket connection mad by idle before being closed automatically.
+ *
+ * @return the timeout in milliseconds for idle timeout.
+ */
public long getIdleTimeout()
{
return idleTimeout;
}
+ /**
+ * The size of the input (read from network layer) buffer size.
+ * <p>
+ * This is the raw read operation buffer size, before the parsing of the websocket frames.
+ *
+ * @return the raw network bytes read operation buffer size.
+ */
public int getInputBufferSize()
{
return inputBufferSize;
}
- public long getMaxMessageSize()
+ /**
+ * Get the maximum size of a binary message buffer (for streaming writing)
+ * <p>
+ * Note: Cannot be greater than {@link #getMaxBinaryMessageSize()}
+ *
+ * @return the maximum size of a binary message buffer
+ */
+ public int getMaxBinaryMessageBufferSize()
{
- return maxMessageSize;
+ return maxBinaryMessageBufferSize;
}
- public void setIdleTimeout(long idleTimeout)
+ /**
+ * Get the maximum size of a binary message during parsing/generating.
+ * <p>
+ * Binary messages over this maximum will result in a close code 1009 {@link StatusCode#MESSAGE_TOO_LARGE}
+ *
+ * @return the maximum size of a binary message
+ */
+ public int getMaxBinaryMessageSize()
{
- this.idleTimeout = idleTimeout;
+ return maxBinaryMessageSize;
}
- public void setInputBufferSize(int inputBufferSize)
+ /**
+ * Get the maximum size of a text message buffer (for streaming writing)
+ * <p>
+ * Note: Cannot be greater than {@link #maxTextMessageSize}
+ *
+ * @return the maximum size of a text message buffer
+ */
+ public int getMaxTextMessageBufferSize()
{
- this.inputBufferSize = inputBufferSize;
+ return maxTextMessageBufferSize;
}
- public void setMaxMessageSize(long maxMessageSize)
+ /**
+ * Get the maximum size of a text message during parsing/generating.
+ * <p>
+ * Text messages over this maximum will result in a close code 1009 {@link StatusCode#MESSAGE_TOO_LARGE}
+ *
+ * @return the maximum size of a text message.
+ */
+ public int getMaxTextMessageSize()
{
- this.maxMessageSize = maxMessageSize;
+ return maxTextMessageSize;
+ }
+
+ /**
+ * The timeout in ms (milliseconds) for async write operations.
+ * <p>
+ * Negative values indicate a disabled timeout.
+ *
+ * @param ms
+ * the timeout in milliseconds
+ */
+ public void setAsyncWriteTimeout(long ms)
+ {
+ assertLessThan("AsyncWriteTimeout",ms,"IdleTimeout",idleTimeout);
+ this.asyncWriteTimeout = ms;
+ }
+
+ /**
+ * The time in ms (milliseconds) that a websocket may be idle before closing.
+ *
+ * @param ms
+ * the timeout in milliseconds
+ */
+ public void setIdleTimeout(long ms)
+ {
+ assertPositive("IdleTimeout",ms);
+ this.idleTimeout = ms;
+ }
+
+ /**
+ * The size of the input (read from network layer) buffer size.
+ *
+ * @param size
+ * the size in bytes
+ */
+ public void setInputBufferSize(int size)
+ {
+ assertPositive("InputBufferSize",size);
+ assertLessThan("InputBufferSize",size,"MaxTextMessageBufferSize",maxTextMessageBufferSize);
+ assertLessThan("InputBufferSize",size,"MaxBinaryMessageBufferSize",maxBinaryMessageBufferSize);
+
+ this.inputBufferSize = size;
+ }
+
+ /**
+ * The maximum size of a binary message buffer.
+ * <p>
+ * Used ONLY for stream based message writing.
+ *
+ * @param size
+ * the maximum size of the binary message buffer
+ */
+ public void setMaxBinaryMessageBufferSize(int size)
+ {
+ assertPositive("MaxBinaryMessageBufferSize",size);
+
+ this.maxBinaryMessageBufferSize = size;
+ }
+
+ /**
+ * The maximum size of a binary message during parsing/generating.
+ * <p>
+ * Binary messages over this maximum will result in a close code 1009 {@link StatusCode#MESSAGE_TOO_LARGE}
+ *
+ * @param size
+ * the maximum allowed size of a binary message.
+ */
+ public void setMaxBinaryMessageSize(int size)
+ {
+ assertPositive("MaxBinaryMessageSize",size);
+
+ this.maxBinaryMessageSize = size;
+ }
+
+ /**
+ * The maximum size of a text message buffer.
+ * <p>
+ * Used ONLY for stream based message writing.
+ *
+ * @param size
+ * the maximum size of the text message buffer
+ */
+ public void setMaxTextMessageBufferSize(int size)
+ {
+ assertPositive("MaxTextMessageBufferSize",size);
+
+ this.maxTextMessageBufferSize = size;
+ }
+
+ /**
+ * The maximum size of a text message during parsing/generating.
+ * <p>
+ * Text messages over this maximum will result in a close code 1009 {@link StatusCode#MESSAGE_TOO_LARGE}
+ *
+ * @param size
+ * the maximum allowed size of a text message.
+ */
+ public void setMaxTextMessageSize(int size)
+ {
+ assertPositive("MaxTextMessageSize",size);
+
+ this.maxTextMessageSize = size;
+ }
+
+ @Override
+ public String toString()
+ {
+ StringBuilder builder = new StringBuilder();
+ builder.append("WebSocketPolicy@").append(Integer.toHexString(hashCode()));
+ builder.append("[behavior=").append(behavior);
+ builder.append(",maxTextMessageSize=").append(maxTextMessageSize);
+ builder.append(",maxTextMessageBufferSize=").append(maxTextMessageBufferSize);
+ builder.append(",maxBinaryMessageSize=").append(maxBinaryMessageSize);
+ builder.append(",maxBinaryMessageBufferSize=").append(maxBinaryMessageBufferSize);
+ builder.append(",asyncWriteTimeout=").append(asyncWriteTimeout);
+ builder.append(",idleTimeout=").append(idleTimeout);
+ builder.append(",inputBufferSize=").append(inputBufferSize);
+ builder.append("]");
+ return builder.toString();
}
}
diff --git a/jetty-websocket/websocket-api/src/main/java/org/eclipse/jetty/websocket/api/annotations/WebSocket.java b/jetty-websocket/websocket-api/src/main/java/org/eclipse/jetty/websocket/api/annotations/WebSocket.java
index a95378b..a62c36b 100644
--- a/jetty-websocket/websocket-api/src/main/java/org/eclipse/jetty/websocket/api/annotations/WebSocket.java
+++ b/jetty-websocket/websocket-api/src/main/java/org/eclipse/jetty/websocket/api/annotations/WebSocket.java
@@ -35,7 +35,9 @@
{
int inputBufferSize() default -2;
+ int maxBinaryMessageSize() default -2;
+
int maxIdleTime() default -2;
- int maxMessageSize() default -2;
+ int maxTextMessageSize() default -2;
}
diff --git a/jetty-websocket/websocket-api/src/main/java/org/eclipse/jetty/websocket/api/extensions/ExtensionFactory.java b/jetty-websocket/websocket-api/src/main/java/org/eclipse/jetty/websocket/api/extensions/ExtensionFactory.java
index b95d10d..7edb2db 100644
--- a/jetty-websocket/websocket-api/src/main/java/org/eclipse/jetty/websocket/api/extensions/ExtensionFactory.java
+++ b/jetty-websocket/websocket-api/src/main/java/org/eclipse/jetty/websocket/api/extensions/ExtensionFactory.java
@@ -34,7 +34,10 @@
availableExtensions = new HashMap<>();
for (Extension ext : extensionLoader)
{
- availableExtensions.put(ext.getName(),ext.getClass());
+ if (ext != null)
+ {
+ availableExtensions.put(ext.getName(),ext.getClass());
+ }
}
}
diff --git a/jetty-websocket/websocket-api/src/main/java/org/eclipse/jetty/websocket/api/extensions/IncomingFrames.java b/jetty-websocket/websocket-api/src/main/java/org/eclipse/jetty/websocket/api/extensions/IncomingFrames.java
index 2ea7e09..a4b5780 100644
--- a/jetty-websocket/websocket-api/src/main/java/org/eclipse/jetty/websocket/api/extensions/IncomingFrames.java
+++ b/jetty-websocket/websocket-api/src/main/java/org/eclipse/jetty/websocket/api/extensions/IncomingFrames.java
@@ -18,15 +18,12 @@
package org.eclipse.jetty.websocket.api.extensions;
-import org.eclipse.jetty.websocket.api.WebSocketException;
-
/**
* Interface for dealing with Incoming Frames.
*/
public interface IncomingFrames
{
- // TODO: JSR-356 change to Throwable
- public void incomingError(WebSocketException e);
+ public void incomingError(Throwable t);
public void incomingFrame(Frame frame);
}
diff --git a/jetty-websocket/websocket-api/src/main/java/org/eclipse/jetty/websocket/api/util/QuoteUtil.java b/jetty-websocket/websocket-api/src/main/java/org/eclipse/jetty/websocket/api/util/QuoteUtil.java
index 42a63fc..1d45fa0 100644
--- a/jetty-websocket/websocket-api/src/main/java/org/eclipse/jetty/websocket/api/util/QuoteUtil.java
+++ b/jetty-websocket/websocket-api/src/main/java/org/eclipse/jetty/websocket/api/util/QuoteUtil.java
@@ -448,4 +448,30 @@
}
return ret.toString();
}
+
+ public static String join(Object[] objs, String delim)
+ {
+ if (objs == null)
+ {
+ return "";
+ }
+ StringBuilder ret = new StringBuilder();
+ int len = objs.length;
+ for (int i = 0; i < len; i++)
+ {
+ if (i > 0)
+ {
+ ret.append(delim);
+ }
+ if (objs[i] instanceof String)
+ {
+ ret.append('"').append(objs[i]).append('"');
+ }
+ else
+ {
+ ret.append(objs[i]);
+ }
+ }
+ return ret.toString();
+ }
}
diff --git a/jetty-websocket/websocket-client/pom.xml b/jetty-websocket/websocket-client/pom.xml
index 8036f8e..297e9ed 100644
--- a/jetty-websocket/websocket-client/pom.xml
+++ b/jetty-websocket/websocket-client/pom.xml
@@ -3,7 +3,7 @@
<parent>
<groupId>org.eclipse.jetty.websocket</groupId>
<artifactId>websocket-parent</artifactId>
- <version>9.0.5-SNAPSHOT</version>
+ <version>9.1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
diff --git a/jetty-websocket/websocket-client/src/main/java/org/eclipse/jetty/websocket/client/ClientUpgradeRequest.java b/jetty-websocket/websocket-client/src/main/java/org/eclipse/jetty/websocket/client/ClientUpgradeRequest.java
index 5c662d6..f3e2d47 100644
--- a/jetty-websocket/websocket-client/src/main/java/org/eclipse/jetty/websocket/client/ClientUpgradeRequest.java
+++ b/jetty-websocket/websocket-client/src/main/java/org/eclipse/jetty/websocket/client/ClientUpgradeRequest.java
@@ -21,6 +21,7 @@
import java.net.CookieStore;
import java.net.HttpCookie;
import java.net.URI;
+import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
@@ -172,7 +173,7 @@
// Other headers
for (String key : getHeaders().keySet())
{
- if (FORBIDDEN_HEADERS.contains(key.toLowerCase()))
+ if (FORBIDDEN_HEADERS.contains(key))
{
LOG.warn("Skipping forbidden header - {}",key);
continue; // skip
@@ -215,7 +216,7 @@
super.setRequestURI(uri);
// parse parameter map
- Map<String, String[]> pmap = new HashMap<>();
+ Map<String, List<String>> pmap = new HashMap<>();
String query = uri.getQuery();
@@ -229,12 +230,14 @@
List<String> values = params.getValues(key);
if (values == null)
{
- pmap.put(key,new String[0]);
+ pmap.put(key,new ArrayList<String>());
}
else
{
- int len = values.size();
- pmap.put(key,values.toArray(new String[len]));
+ // break link to original
+ List<String> copy = new ArrayList<>();
+ copy.addAll(values);
+ pmap.put(key,copy);
}
}
diff --git a/jetty-websocket/websocket-client/src/main/java/org/eclipse/jetty/websocket/client/WebSocketClient.java b/jetty-websocket/websocket-client/src/main/java/org/eclipse/jetty/websocket/client/WebSocketClient.java
index e875612..ab2b0bb 100644
--- a/jetty-websocket/websocket-client/src/main/java/org/eclipse/jetty/websocket/client/WebSocketClient.java
+++ b/jetty-websocket/websocket-client/src/main/java/org/eclipse/jetty/websocket/client/WebSocketClient.java
@@ -47,8 +47,12 @@
import org.eclipse.jetty.websocket.api.extensions.ExtensionFactory;
import org.eclipse.jetty.websocket.client.io.ConnectPromise;
import org.eclipse.jetty.websocket.client.io.ConnectionManager;
+import org.eclipse.jetty.websocket.client.io.UpgradeListener;
import org.eclipse.jetty.websocket.client.masks.Masker;
import org.eclipse.jetty.websocket.client.masks.RandomMasker;
+import org.eclipse.jetty.websocket.client.masks.ZeroMasker;
+import org.eclipse.jetty.websocket.common.SessionFactory;
+import org.eclipse.jetty.websocket.common.WebSocketSessionFactory;
import org.eclipse.jetty.websocket.common.events.EventDriver;
import org.eclipse.jetty.websocket.common.events.EventDriverFactory;
import org.eclipse.jetty.websocket.common.extensions.WebSocketExtensionFactory;
@@ -63,7 +67,8 @@
private final WebSocketPolicy policy;
private final SslContextFactory sslContextFactory;
private final WebSocketExtensionFactory extensionRegistry;
- private final EventDriverFactory eventDriverFactory;
+ private EventDriverFactory eventDriverFactory;
+ private SessionFactory sessionFactory;
private ByteBufferPool bufferPool;
private Executor executor;
private Scheduler scheduler;
@@ -82,9 +87,16 @@
{
this.sslContextFactory = sslContextFactory;
this.policy = WebSocketPolicy.newClientPolicy();
+ this.bufferPool = new MappedByteBufferPool();
this.extensionRegistry = new WebSocketExtensionFactory(policy,bufferPool);
- this.masker = new RandomMasker();
+ if(LOG.isDebugEnabled()) {
+ LOG.debug("Using ZeroMasker (DEBUG)");
+ this.masker = new ZeroMasker();
+ } else {
+ this.masker = new RandomMasker();
+ }
this.eventDriverFactory = new EventDriverFactory(policy);
+ this.sessionFactory = new WebSocketSessionFactory();
}
public Future<Session> connect(Object websocket, URI toUri) throws IOException
@@ -98,6 +110,11 @@
public Future<Session> connect(Object websocket, URI toUri, ClientUpgradeRequest request) throws IOException
{
+ return connect(websocket,toUri,request,null);
+ }
+
+ public Future<Session> connect(Object websocket, URI toUri, ClientUpgradeRequest request, UpgradeListener upgradeListener) throws IOException
+ {
if (!isStarted())
{
throw new IllegalStateException(WebSocketClient.class.getSimpleName() + "@" + this.hashCode() + " is not started");
@@ -133,17 +150,39 @@
}
// Validate websocket URI
- LOG.debug("connect websocket:{} to:{}",websocket,toUri);
+ LOG.debug("connect websocket {} to {}",websocket,toUri);
// Grab Connection Manager
ConnectionManager manager = getConnectionManager();
// Setup Driver for user provided websocket
- EventDriver driver = eventDriverFactory.wrap(websocket);
+ EventDriver driver = null;
+ if (websocket instanceof EventDriver)
+ {
+ // Use the EventDriver as-is
+ driver = (EventDriver)websocket;
+ }
+ else
+ {
+ // Wrap websocket with appropriate EventDriver
+ driver = eventDriverFactory.wrap(websocket);
+ }
+
+ if (driver == null)
+ {
+ throw new IllegalStateException("Unable to identify as websocket object: " + websocket.getClass().getName());
+ }
// Create the appropriate (physical vs virtual) connection task
ConnectPromise promise = manager.connect(this,driver,request);
+ if (upgradeListener != null)
+ {
+ promise.setUpgradeListener(upgradeListener);
+ }
+
+ LOG.debug("Connect Promise: {}",promise);
+
// Execute the connection on the executor thread
executor.execute(promise);
@@ -211,6 +250,16 @@
LOG.info("Stopped {}",this);
}
+ /**
+ * Return the number of milliseconds for a timeout of an attempted write operation.
+ *
+ * @return number of milliseconds for timeout of an attempted write operation
+ */
+ public long getAsyncWriteTimeout()
+ {
+ return this.policy.getAsyncWriteTimeout();
+ }
+
public SocketAddress getBindAddress()
{
return bindAddress;
@@ -236,6 +285,11 @@
return cookieStore;
}
+ public EventDriverFactory getEventDriverFactory()
+ {
+ return eventDriverFactory;
+ }
+
public Executor getExecutor()
{
return executor;
@@ -252,6 +306,26 @@
}
/**
+ * Get the maximum size for buffering of a binary message.
+ *
+ * @return the maximum size of a binary message buffer.
+ */
+ public int getMaxBinaryMessageBufferSize()
+ {
+ return this.policy.getMaxBinaryMessageBufferSize();
+ }
+
+ /**
+ * Get the maximum size for a binary message.
+ *
+ * @return the maximum size of a binary message.
+ */
+ public long getMaxBinaryMessageSize()
+ {
+ return this.policy.getMaxBinaryMessageSize();
+ }
+
+ /**
* Get the max idle timeout for new connections.
*
* @return the max idle timeout in milliseconds for new connections.
@@ -261,6 +335,26 @@
return this.policy.getIdleTimeout();
}
+ /**
+ * Get the maximum size for buffering of a text message.
+ *
+ * @return the maximum size of a text message buffer.
+ */
+ public int getMaxTextMessageBufferSize()
+ {
+ return this.policy.getMaxTextMessageBufferSize();
+ }
+
+ /**
+ * Get the maximum size for a text message.
+ *
+ * @return the maximum size of a text message.
+ */
+ public long getMaxTextMessageSize()
+ {
+ return this.policy.getMaxTextMessageSize();
+ }
+
public WebSocketPolicy getPolicy()
{
return this.policy;
@@ -271,6 +365,11 @@
return scheduler;
}
+ public SessionFactory getSessionFactory()
+ {
+ return sessionFactory;
+ }
+
/**
* @return the {@link SslContextFactory} that manages TLS encryption
* @see WebSocketClient(SslContextFactory)
@@ -310,6 +409,11 @@
return new ConnectionManager(this);
}
+ public void setAsyncWriteTimeout(long ms)
+ {
+ this.policy.setAsyncWriteTimeout(ms);
+ }
+
public void setBindAdddress(SocketAddress bindAddress)
{
this.bindAddress = bindAddress;
@@ -323,16 +427,16 @@
/**
* Set the timeout for connecting to the remote server.
*
- * @param timeoutMilliseconds
+ * @param ms
* the timeout in milliseconds
*/
- public void setConnectTimeout(long timeoutMilliseconds)
+ public void setConnectTimeout(long ms)
{
- if (timeoutMilliseconds < 0)
+ if (ms < 0)
{
throw new IllegalStateException("Connect Timeout cannot be negative");
}
- this.connectTimeout = timeoutMilliseconds;
+ this.connectTimeout = ms;
}
public void setCookieStore(CookieStore cookieStore)
@@ -340,6 +444,11 @@
this.cookieStore = cookieStore;
}
+ public void setEventDriverFactory(EventDriverFactory factory)
+ {
+ this.eventDriverFactory = factory;
+ }
+
public void setExecutor(Executor executor)
{
this.executor = executor;
@@ -350,16 +459,31 @@
this.masker = masker;
}
+ public void setMaxBinaryMessageBufferSize(int max)
+ {
+ this.policy.setMaxBinaryMessageBufferSize(max);
+ }
+
/**
* Set the max idle timeout for new connections.
* <p>
* Existing connections will not have their max idle timeout adjusted.
*
- * @param milliseconds
+ * @param ms
* the timeout in milliseconds
*/
- public void setMaxIdleTimeout(long milliseconds)
+ public void setMaxIdleTimeout(long ms)
{
- this.policy.setIdleTimeout(milliseconds);
+ this.policy.setIdleTimeout(ms);
+ }
+
+ public void setMaxTextMessageBufferSize(int max)
+ {
+ this.policy.setMaxTextMessageBufferSize(max);
+ }
+
+ public void setSessionFactory(SessionFactory sessionFactory)
+ {
+ this.sessionFactory = sessionFactory;
}
}
diff --git a/jetty-websocket/websocket-client/src/main/java/org/eclipse/jetty/websocket/client/io/ConnectPromise.java b/jetty-websocket/websocket-client/src/main/java/org/eclipse/jetty/websocket/client/io/ConnectPromise.java
index f918abf..f52e1d3 100644
--- a/jetty-websocket/websocket-client/src/main/java/org/eclipse/jetty/websocket/client/io/ConnectPromise.java
+++ b/jetty-websocket/websocket-client/src/main/java/org/eclipse/jetty/websocket/client/io/ConnectPromise.java
@@ -36,6 +36,7 @@
private final EventDriver driver;
private final ClientUpgradeRequest request;
private final Masker masker;
+ private UpgradeListener upgradeListener;
private ClientUpgradeResponse response;
public ConnectPromise(WebSocketClient client, EventDriver driver, ClientUpgradeRequest request)
@@ -81,11 +82,21 @@
return response;
}
+ public UpgradeListener getUpgradeListener()
+ {
+ return upgradeListener;
+ }
+
public void setResponse(ClientUpgradeResponse response)
{
this.response = response;
}
+ public void setUpgradeListener(UpgradeListener upgradeListener)
+ {
+ this.upgradeListener = upgradeListener;
+ }
+
public void succeeded(WebSocketSession session)
{
session.setUpgradeRequest(request);
diff --git a/jetty-websocket/websocket-client/src/main/java/org/eclipse/jetty/websocket/client/io/ConnectionManager.java b/jetty-websocket/websocket-client/src/main/java/org/eclipse/jetty/websocket/client/io/ConnectionManager.java
index d228144..0eefe65 100644
--- a/jetty-websocket/websocket-client/src/main/java/org/eclipse/jetty/websocket/client/io/ConnectionManager.java
+++ b/jetty-websocket/websocket-client/src/main/java/org/eclipse/jetty/websocket/client/io/ConnectionManager.java
@@ -203,7 +203,7 @@
return Collections.unmodifiableCollection(sessions);
}
- private boolean isVirtualConnectionPossibleTo(String hostname)
+ public boolean isVirtualConnectionPossibleTo(String hostname)
{
// TODO Auto-generated method stub
return false;
diff --git a/jetty-websocket/websocket-client/src/main/java/org/eclipse/jetty/websocket/client/io/UpgradeConnection.java b/jetty-websocket/websocket-client/src/main/java/org/eclipse/jetty/websocket/client/io/UpgradeConnection.java
index ca5a9fe..2ab4e4b 100644
--- a/jetty-websocket/websocket-client/src/main/java/org/eclipse/jetty/websocket/client/io/UpgradeConnection.java
+++ b/jetty-websocket/websocket-client/src/main/java/org/eclipse/jetty/websocket/client/io/UpgradeConnection.java
@@ -40,6 +40,7 @@
import org.eclipse.jetty.websocket.client.ClientUpgradeRequest;
import org.eclipse.jetty.websocket.client.ClientUpgradeResponse;
import org.eclipse.jetty.websocket.common.AcceptHash;
+import org.eclipse.jetty.websocket.common.SessionFactory;
import org.eclipse.jetty.websocket.common.WebSocketSession;
import org.eclipse.jetty.websocket.common.events.EventDriver;
import org.eclipse.jetty.websocket.common.extensions.ExtensionStack;
@@ -60,6 +61,13 @@
{
URI uri = connectPromise.getRequest().getRequestURI();
request.setRequestURI(uri);
+
+ UpgradeListener handshakeListener = connectPromise.getUpgradeListener();
+ if (handshakeListener != null)
+ {
+ handshakeListener.onHandshakeRequest(request);
+ }
+
String rawRequest = request.generate();
ByteBuffer buf = BufferUtil.toBuffer(rawRequest,StringUtil.__UTF8_CHARSET);
@@ -113,6 +121,12 @@
private void notifyConnect(ClientUpgradeResponse response)
{
connectPromise.setResponse(response);
+
+ UpgradeListener handshakeListener = connectPromise.getUpgradeListener();
+ if (handshakeListener != null)
+ {
+ handshakeListener.onHandshakeResponse(response);
+ }
}
@Override
@@ -213,9 +227,10 @@
// Initialize / Negotiate Extensions
EventDriver websocket = connectPromise.getDriver();
- WebSocketPolicy policy = connectPromise.getClient().getPolicy();
+ WebSocketPolicy policy = websocket.getPolicy();
- WebSocketSession session = new WebSocketSession(request.getRequestURI(),websocket,connection);
+ SessionFactory sessionFactory = connectPromise.getClient().getSessionFactory();
+ WebSocketSession session = sessionFactory.createSession(request.getRequestURI(),websocket,connection);
session.setPolicy(policy);
session.setUpgradeResponse(response);
diff --git a/jetty-websocket/websocket-client/src/main/java/org/eclipse/jetty/websocket/client/io/UpgradeListener.java b/jetty-websocket/websocket-client/src/main/java/org/eclipse/jetty/websocket/client/io/UpgradeListener.java
new file mode 100644
index 0000000..3edfe51
--- /dev/null
+++ b/jetty-websocket/websocket-client/src/main/java/org/eclipse/jetty/websocket/client/io/UpgradeListener.java
@@ -0,0 +1,32 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.client.io;
+
+import org.eclipse.jetty.websocket.api.UpgradeRequest;
+import org.eclipse.jetty.websocket.api.UpgradeResponse;
+
+/**
+ * Listener for Handshake/Upgrade events.
+ */
+public interface UpgradeListener
+{
+ public void onHandshakeRequest(UpgradeRequest request);
+
+ public void onHandshakeResponse(UpgradeResponse response);
+}
diff --git a/jetty-websocket/websocket-client/src/main/java/org/eclipse/jetty/websocket/client/mux/MuxClientAddHandler.java b/jetty-websocket/websocket-client/src/main/java/org/eclipse/jetty/websocket/client/mux/MuxClientAddHandler.java
deleted file mode 100644
index bd9b562..0000000
--- a/jetty-websocket/websocket-client/src/main/java/org/eclipse/jetty/websocket/client/mux/MuxClientAddHandler.java
+++ /dev/null
@@ -1,33 +0,0 @@
-//
-// ========================================================================
-// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
-// ------------------------------------------------------------------------
-// All rights reserved. This program and the accompanying materials
-// are made available under the terms of the Eclipse Public License v1.0
-// and Apache License v2.0 which accompanies this distribution.
-//
-// The Eclipse Public License is available at
-// http://www.eclipse.org/legal/epl-v10.html
-//
-// The Apache License v2.0 is available at
-// http://www.opensource.org/licenses/apache2.0.php
-//
-// You may elect to redistribute this code under either of these licenses.
-// ========================================================================
-//
-
-package org.eclipse.jetty.websocket.client.mux;
-
-import org.eclipse.jetty.websocket.common.WebSocketSession;
-import org.eclipse.jetty.websocket.common.extensions.mux.add.MuxAddClient;
-import org.eclipse.jetty.websocket.common.extensions.mux.op.MuxAddChannelResponse;
-
-public class MuxClientAddHandler implements MuxAddClient
-{
- @Override
- public WebSocketSession createSession(MuxAddChannelResponse response)
- {
- // TODO Auto-generated method stub
- return null;
- }
-}
diff --git a/jetty-websocket/websocket-client/src/main/java/org/eclipse/jetty/websocket/client/mux/MuxClientExtension.java b/jetty-websocket/websocket-client/src/main/java/org/eclipse/jetty/websocket/client/mux/MuxClientExtension.java
deleted file mode 100644
index 58cec30..0000000
--- a/jetty-websocket/websocket-client/src/main/java/org/eclipse/jetty/websocket/client/mux/MuxClientExtension.java
+++ /dev/null
@@ -1,31 +0,0 @@
-//
-// ========================================================================
-// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
-// ------------------------------------------------------------------------
-// All rights reserved. This program and the accompanying materials
-// are made available under the terms of the Eclipse Public License v1.0
-// and Apache License v2.0 which accompanies this distribution.
-//
-// The Eclipse Public License is available at
-// http://www.eclipse.org/legal/epl-v10.html
-//
-// The Apache License v2.0 is available at
-// http://www.opensource.org/licenses/apache2.0.php
-//
-// You may elect to redistribute this code under either of these licenses.
-// ========================================================================
-//
-
-package org.eclipse.jetty.websocket.client.mux;
-
-import org.eclipse.jetty.websocket.common.extensions.mux.AbstractMuxExtension;
-import org.eclipse.jetty.websocket.common.extensions.mux.Muxer;
-
-public class MuxClientExtension extends AbstractMuxExtension
-{
- @Override
- public void configureMuxer(Muxer muxer)
- {
- muxer.setAddClient(new MuxClientAddHandler());
- }
-}
diff --git a/jetty-websocket/websocket-client/src/main/java/org/eclipse/jetty/websocket/client/mux/package-info.java b/jetty-websocket/websocket-client/src/main/java/org/eclipse/jetty/websocket/client/mux/package-info.java
deleted file mode 100644
index 6df80bb..0000000
--- a/jetty-websocket/websocket-client/src/main/java/org/eclipse/jetty/websocket/client/mux/package-info.java
+++ /dev/null
@@ -1,23 +0,0 @@
-//
-// ========================================================================
-// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
-// ------------------------------------------------------------------------
-// All rights reserved. This program and the accompanying materials
-// are made available under the terms of the Eclipse Public License v1.0
-// and Apache License v2.0 which accompanies this distribution.
-//
-// The Eclipse Public License is available at
-// http://www.eclipse.org/legal/epl-v10.html
-//
-// The Apache License v2.0 is available at
-// http://www.opensource.org/licenses/apache2.0.php
-//
-// You may elect to redistribute this code under either of these licenses.
-// ========================================================================
-//
-
-/**
- * Jetty WebSocket Client : MUX Extension [<em>Unstable Early Draft</em>]
- */
-package org.eclipse.jetty.websocket.client.mux;
-
diff --git a/jetty-websocket/websocket-client/src/test/java/examples/SimpleEchoSocket.java b/jetty-websocket/websocket-client/src/test/java/examples/SimpleEchoSocket.java
index dca965e..801eddd 100644
--- a/jetty-websocket/websocket-client/src/test/java/examples/SimpleEchoSocket.java
+++ b/jetty-websocket/websocket-client/src/test/java/examples/SimpleEchoSocket.java
@@ -32,7 +32,7 @@
/**
* Basic Echo Client Socket
*/
-@WebSocket(maxMessageSize = 64 * 1024)
+@WebSocket(maxTextMessageSize = 64 * 1024)
public class SimpleEchoSocket
{
private final CountDownLatch closeLatch;
diff --git a/jetty-websocket/websocket-client/src/test/java/org/eclipse/jetty/websocket/client/BadNetworkTest.java b/jetty-websocket/websocket-client/src/test/java/org/eclipse/jetty/websocket/client/BadNetworkTest.java
index bab9416..c7df302 100644
--- a/jetty-websocket/websocket-client/src/test/java/org/eclipse/jetty/websocket/client/BadNetworkTest.java
+++ b/jetty-websocket/websocket-client/src/test/java/org/eclipse/jetty/websocket/client/BadNetworkTest.java
@@ -74,7 +74,7 @@
@Test
public void testAbruptClientClose() throws Exception
{
- TrackingSocket wsocket = new TrackingSocket();
+ JettyTrackingSocket wsocket = new JettyTrackingSocket();
URI wsUri = server.getWsUri();
Future<Session> future = client.connect(wsocket,wsUri);
@@ -103,7 +103,7 @@
@Test
public void testAbruptServerClose() throws Exception
{
- TrackingSocket wsocket = new TrackingSocket();
+ JettyTrackingSocket wsocket = new JettyTrackingSocket();
URI wsUri = server.getWsUri();
Future<Session> future = client.connect(wsocket,wsUri);
diff --git a/jetty-websocket/websocket-client/src/test/java/org/eclipse/jetty/websocket/client/ClientConnectTest.java b/jetty-websocket/websocket-client/src/test/java/org/eclipse/jetty/websocket/client/ClientConnectTest.java
index 92a5888..713073e 100644
--- a/jetty-websocket/websocket-client/src/test/java/org/eclipse/jetty/websocket/client/ClientConnectTest.java
+++ b/jetty-websocket/websocket-client/src/test/java/org/eclipse/jetty/websocket/client/ClientConnectTest.java
@@ -55,7 +55,7 @@
private WebSocketClient client;
@SuppressWarnings("unchecked")
- private <E extends Throwable> E assertExpectedError(ExecutionException e, TrackingSocket wsocket, Class<E> errorClass) throws IOException
+ private <E extends Throwable> E assertExpectedError(ExecutionException e, JettyTrackingSocket wsocket, Class<E> errorClass) throws IOException
{
// Validate thrown cause
Throwable cause = e.getCause();
@@ -104,7 +104,7 @@
@Test
public void testBadHandshake() throws Exception
{
- TrackingSocket wsocket = new TrackingSocket();
+ JettyTrackingSocket wsocket = new JettyTrackingSocket();
URI wsUri = server.getWsUri();
Future<Session> future = client.connect(wsocket,wsUri);
@@ -133,7 +133,7 @@
@Test
public void testBadHandshake_GetOK() throws Exception
{
- TrackingSocket wsocket = new TrackingSocket();
+ JettyTrackingSocket wsocket = new JettyTrackingSocket();
URI wsUri = server.getWsUri();
Future<Session> future = client.connect(wsocket,wsUri);
@@ -162,7 +162,7 @@
@Test
public void testBadHandshake_GetOK_WithSecWebSocketAccept() throws Exception
{
- TrackingSocket wsocket = new TrackingSocket();
+ JettyTrackingSocket wsocket = new JettyTrackingSocket();
URI wsUri = server.getWsUri();
Future<Session> future = client.connect(wsocket,wsUri);
@@ -198,7 +198,7 @@
@Test
public void testBadHandshake_SwitchingProtocols_InvalidConnectionHeader() throws Exception
{
- TrackingSocket wsocket = new TrackingSocket();
+ JettyTrackingSocket wsocket = new JettyTrackingSocket();
URI wsUri = server.getWsUri();
Future<Session> future = client.connect(wsocket,wsUri);
@@ -234,7 +234,7 @@
@Test
public void testBadHandshake_SwitchingProtocols_NoConnectionHeader() throws Exception
{
- TrackingSocket wsocket = new TrackingSocket();
+ JettyTrackingSocket wsocket = new JettyTrackingSocket();
URI wsUri = server.getWsUri();
Future<Session> future = client.connect(wsocket,wsUri);
@@ -270,7 +270,7 @@
@Test
public void testBadUpgrade() throws Exception
{
- TrackingSocket wsocket = new TrackingSocket();
+ JettyTrackingSocket wsocket = new JettyTrackingSocket();
URI wsUri = server.getWsUri();
Future<Session> future = client.connect(wsocket,wsUri);
@@ -300,7 +300,7 @@
@Ignore("Opened bug 399525")
public void testConnectionNotAccepted() throws Exception
{
- TrackingSocket wsocket = new TrackingSocket();
+ JettyTrackingSocket wsocket = new JettyTrackingSocket();
URI wsUri = server.getWsUri();
Future<Session> future = client.connect(wsocket,wsUri);
@@ -330,7 +330,7 @@
@Test
public void testConnectionRefused() throws Exception
{
- TrackingSocket wsocket = new TrackingSocket();
+ JettyTrackingSocket wsocket = new JettyTrackingSocket();
// Intentionally bad port with nothing listening on it
URI wsUri = new URI("ws://127.0.0.1:1");
@@ -359,7 +359,7 @@
@Test(expected = TimeoutException.class)
public void testConnectionTimeout_Concurrent() throws Exception
{
- TrackingSocket wsocket = new TrackingSocket();
+ JettyTrackingSocket wsocket = new JettyTrackingSocket();
URI wsUri = server.getWsUri();
Future<Session> future = client.connect(wsocket,wsUri);
diff --git a/jetty-websocket/websocket-client/src/test/java/org/eclipse/jetty/websocket/client/JettyTrackingSocket.java b/jetty-websocket/websocket-client/src/test/java/org/eclipse/jetty/websocket/client/JettyTrackingSocket.java
new file mode 100644
index 0000000..a7fd296
--- /dev/null
+++ b/jetty-websocket/websocket-client/src/test/java/org/eclipse/jetty/websocket/client/JettyTrackingSocket.java
@@ -0,0 +1,171 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.client;
+
+import static org.hamcrest.Matchers.*;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.Exchanger;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+import org.eclipse.jetty.toolchain.test.EventQueue;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+import org.eclipse.jetty.websocket.api.Session;
+import org.eclipse.jetty.websocket.api.WebSocketAdapter;
+import org.junit.Assert;
+
+/**
+ * Testing Socket used on client side WebSocket testing.
+ */
+public class JettyTrackingSocket extends WebSocketAdapter
+{
+ private static final Logger LOG = Log.getLogger(JettyTrackingSocket.class);
+
+ public int closeCode = -1;
+ public Exchanger<String> messageExchanger;
+ public StringBuilder closeMessage = new StringBuilder();
+ public CountDownLatch openLatch = new CountDownLatch(1);
+ public CountDownLatch closeLatch = new CountDownLatch(1);
+ public CountDownLatch dataLatch = new CountDownLatch(1);
+ public EventQueue<String> messageQueue = new EventQueue<>();
+ public EventQueue<Throwable> errorQueue = new EventQueue<>();
+
+ public void assertClose(int expectedStatusCode, String expectedReason) throws InterruptedException
+ {
+ assertCloseCode(expectedStatusCode);
+ assertCloseReason(expectedReason);
+ }
+
+ public void assertCloseCode(int expectedCode) throws InterruptedException
+ {
+ Assert.assertThat("Was Closed",closeLatch.await(50,TimeUnit.MILLISECONDS),is(true));
+ Assert.assertThat("Close Code",closeCode,is(expectedCode));
+ }
+
+ private void assertCloseReason(String expectedReason)
+ {
+ Assert.assertThat("Close Reason",closeMessage.toString(),is(expectedReason));
+ }
+
+ public void assertIsOpen() throws InterruptedException
+ {
+ assertWasOpened();
+ assertNotClosed();
+ }
+
+ public void assertMessage(String expected)
+ {
+ String actual = messageQueue.poll();
+ Assert.assertEquals("Message",expected,actual);
+ }
+
+ public void assertNotClosed()
+ {
+ Assert.assertThat("Closed Latch",closeLatch.getCount(),greaterThanOrEqualTo(1L));
+ }
+
+ public void assertNotOpened()
+ {
+ Assert.assertThat("Open Latch",openLatch.getCount(),greaterThanOrEqualTo(1L));
+ }
+
+ public void assertWasOpened() throws InterruptedException
+ {
+ Assert.assertThat("Was Opened",openLatch.await(500,TimeUnit.MILLISECONDS),is(true));
+ }
+
+ public void awaitMessage(int expectedMessageCount, TimeUnit timeoutUnit, int timeoutDuration) throws TimeoutException, InterruptedException
+ {
+ messageQueue.awaitEventCount(expectedMessageCount,timeoutDuration,timeoutUnit);
+ }
+
+ public void clear()
+ {
+ messageQueue.clear();
+ }
+
+ @Override
+ public void onWebSocketBinary(byte[] payload, int offset, int len)
+ {
+ LOG.debug("onWebSocketBinary()");
+ dataLatch.countDown();
+ }
+
+ @Override
+ public void onWebSocketClose(int statusCode, String reason)
+ {
+ LOG.debug("onWebSocketClose({},{})",statusCode,reason);
+ super.onWebSocketClose(statusCode,reason);
+ closeCode = statusCode;
+ closeMessage.append(reason);
+ closeLatch.countDown();
+ }
+
+ @Override
+ public void onWebSocketConnect(Session session)
+ {
+ super.onWebSocketConnect(session);
+ openLatch.countDown();
+ }
+
+ @Override
+ public void onWebSocketError(Throwable cause)
+ {
+ LOG.debug("onWebSocketError",cause);
+ Assert.assertThat("Error capture",errorQueue.offer(cause),is(true));
+ }
+
+ @Override
+ public void onWebSocketText(String message)
+ {
+ LOG.debug("onWebSocketText({})",message);
+ messageQueue.offer(message);
+ dataLatch.countDown();
+
+ if (messageExchanger != null)
+ {
+ try
+ {
+ messageExchanger.exchange(message);
+ }
+ catch (InterruptedException e)
+ {
+ LOG.debug(e);
+ }
+ }
+ }
+
+ public void waitForClose(int timeoutDuration, TimeUnit timeoutUnit) throws InterruptedException
+ {
+ Assert.assertThat("Client Socket Closed",closeLatch.await(timeoutDuration,timeoutUnit),is(true));
+ }
+
+ public void waitForConnected(int timeoutDuration, TimeUnit timeoutUnit) throws InterruptedException
+ {
+ Assert.assertThat("Client Socket Connected",openLatch.await(timeoutDuration,timeoutUnit),is(true));
+ }
+
+ public void waitForMessage(int timeoutDuration, TimeUnit timeoutUnit) throws InterruptedException
+ {
+ LOG.debug("Waiting for message");
+ Assert.assertThat("Message Received",dataLatch.await(timeoutDuration,timeoutUnit),is(true));
+ }
+}
diff --git a/jetty-websocket/websocket-client/src/test/java/org/eclipse/jetty/websocket/client/SlowClientTest.java b/jetty-websocket/websocket-client/src/test/java/org/eclipse/jetty/websocket/client/SlowClientTest.java
index 49ca8c1..6b706e6 100644
--- a/jetty-websocket/websocket-client/src/test/java/org/eclipse/jetty/websocket/client/SlowClientTest.java
+++ b/jetty-websocket/websocket-client/src/test/java/org/eclipse/jetty/websocket/client/SlowClientTest.java
@@ -75,7 +75,7 @@
@Slow
public void testClientSlowToSend() throws Exception
{
- TrackingSocket tsocket = new TrackingSocket();
+ JettyTrackingSocket tsocket = new JettyTrackingSocket();
client.getPolicy().setIdleTimeout(60000);
URI wsUri = server.getWsUri();
diff --git a/jetty-websocket/websocket-client/src/test/java/org/eclipse/jetty/websocket/client/SlowServerTest.java b/jetty-websocket/websocket-client/src/test/java/org/eclipse/jetty/websocket/client/SlowServerTest.java
index 741d8fa..f059f51 100644
--- a/jetty-websocket/websocket-client/src/test/java/org/eclipse/jetty/websocket/client/SlowServerTest.java
+++ b/jetty-websocket/websocket-client/src/test/java/org/eclipse/jetty/websocket/client/SlowServerTest.java
@@ -76,7 +76,7 @@
@Slow
public void testServerSlowToRead() throws Exception
{
- TrackingSocket tsocket = new TrackingSocket();
+ JettyTrackingSocket tsocket = new JettyTrackingSocket();
client.setMasker(new ZeroMasker());
client.getPolicy().setIdleTimeout(60000);
@@ -125,7 +125,7 @@
public void testServerSlowToSend() throws Exception
{
// final Exchanger<String> exchanger = new Exchanger<String>();
- TrackingSocket tsocket = new TrackingSocket();
+ JettyTrackingSocket tsocket = new JettyTrackingSocket();
// tsocket.messageExchanger = exchanger;
client.setMasker(new ZeroMasker());
client.getPolicy().setIdleTimeout(60000);
diff --git a/jetty-websocket/websocket-client/src/test/java/org/eclipse/jetty/websocket/client/TimeoutTest.java b/jetty-websocket/websocket-client/src/test/java/org/eclipse/jetty/websocket/client/TimeoutTest.java
index ecfd50c..48e671d 100644
--- a/jetty-websocket/websocket-client/src/test/java/org/eclipse/jetty/websocket/client/TimeoutTest.java
+++ b/jetty-websocket/websocket-client/src/test/java/org/eclipse/jetty/websocket/client/TimeoutTest.java
@@ -80,7 +80,7 @@
@Test
public void testIdleDetectedByClient() throws Exception
{
- TrackingSocket wsocket = new TrackingSocket();
+ JettyTrackingSocket wsocket = new JettyTrackingSocket();
URI wsUri = server.getWsUri();
client.setMaxIdleTimeout(1000);
diff --git a/jetty-websocket/websocket-client/src/test/java/org/eclipse/jetty/websocket/client/TrackingSocket.java b/jetty-websocket/websocket-client/src/test/java/org/eclipse/jetty/websocket/client/TrackingSocket.java
deleted file mode 100644
index eb1c4ae..0000000
--- a/jetty-websocket/websocket-client/src/test/java/org/eclipse/jetty/websocket/client/TrackingSocket.java
+++ /dev/null
@@ -1,171 +0,0 @@
-//
-// ========================================================================
-// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
-// ------------------------------------------------------------------------
-// All rights reserved. This program and the accompanying materials
-// are made available under the terms of the Eclipse Public License v1.0
-// and Apache License v2.0 which accompanies this distribution.
-//
-// The Eclipse Public License is available at
-// http://www.eclipse.org/legal/epl-v10.html
-//
-// The Apache License v2.0 is available at
-// http://www.opensource.org/licenses/apache2.0.php
-//
-// You may elect to redistribute this code under either of these licenses.
-// ========================================================================
-//
-
-package org.eclipse.jetty.websocket.client;
-
-import static org.hamcrest.Matchers.*;
-
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.Exchanger;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.TimeoutException;
-
-import org.eclipse.jetty.toolchain.test.EventQueue;
-import org.eclipse.jetty.util.log.Log;
-import org.eclipse.jetty.util.log.Logger;
-import org.eclipse.jetty.websocket.api.Session;
-import org.eclipse.jetty.websocket.api.WebSocketAdapter;
-import org.junit.Assert;
-
-/**
- * Testing Socket used on client side WebSocket testing.
- */
-public class TrackingSocket extends WebSocketAdapter
-{
- private static final Logger LOG = Log.getLogger(TrackingSocket.class);
-
- public int closeCode = -1;
- public Exchanger<String> messageExchanger;
- public StringBuilder closeMessage = new StringBuilder();
- public CountDownLatch openLatch = new CountDownLatch(1);
- public CountDownLatch closeLatch = new CountDownLatch(1);
- public CountDownLatch dataLatch = new CountDownLatch(1);
- public EventQueue<String> messageQueue = new EventQueue<>();
- public EventQueue<Throwable> errorQueue = new EventQueue<>();
-
- public void assertClose(int expectedStatusCode, String expectedReason) throws InterruptedException
- {
- assertCloseCode(expectedStatusCode);
- assertCloseReason(expectedReason);
- }
-
- public void assertCloseCode(int expectedCode) throws InterruptedException
- {
- Assert.assertThat("Was Closed",closeLatch.await(50,TimeUnit.MILLISECONDS),is(true));
- Assert.assertThat("Close Code",closeCode,is(expectedCode));
- }
-
- private void assertCloseReason(String expectedReason)
- {
- Assert.assertThat("Close Reason",closeMessage.toString(),is(expectedReason));
- }
-
- public void assertIsOpen() throws InterruptedException
- {
- assertWasOpened();
- assertNotClosed();
- }
-
- public void assertMessage(String expected)
- {
- String actual = messageQueue.poll();
- Assert.assertEquals("Message",expected,actual);
- }
-
- public void assertNotClosed()
- {
- Assert.assertThat("Closed Latch",closeLatch.getCount(),greaterThanOrEqualTo(1L));
- }
-
- public void assertNotOpened()
- {
- Assert.assertThat("Open Latch",openLatch.getCount(),greaterThanOrEqualTo(1L));
- }
-
- public void assertWasOpened() throws InterruptedException
- {
- Assert.assertThat("Was Opened",openLatch.await(500,TimeUnit.MILLISECONDS),is(true));
- }
-
- public void awaitMessage(int expectedMessageCount, TimeUnit timeoutUnit, int timeoutDuration) throws TimeoutException, InterruptedException
- {
- messageQueue.awaitEventCount(expectedMessageCount,timeoutDuration,timeoutUnit);
- }
-
- public void clear()
- {
- messageQueue.clear();
- }
-
- @Override
- public void onWebSocketBinary(byte[] payload, int offset, int len)
- {
- LOG.debug("onWebSocketBinary()");
- dataLatch.countDown();
- }
-
- @Override
- public void onWebSocketClose(int statusCode, String reason)
- {
- LOG.debug("onWebSocketClose({},{})",statusCode,reason);
- super.onWebSocketClose(statusCode,reason);
- closeCode = statusCode;
- closeMessage.append(reason);
- closeLatch.countDown();
- }
-
- @Override
- public void onWebSocketConnect(Session session)
- {
- super.onWebSocketConnect(session);
- openLatch.countDown();
- }
-
- @Override
- public void onWebSocketError(Throwable cause)
- {
- LOG.debug("onWebSocketError",cause);
- Assert.assertThat("Error capture",errorQueue.offer(cause),is(true));
- }
-
- @Override
- public void onWebSocketText(String message)
- {
- LOG.debug("onWebSocketText({})",message);
- messageQueue.offer(message);
- dataLatch.countDown();
-
- if (messageExchanger != null)
- {
- try
- {
- messageExchanger.exchange(message);
- }
- catch (InterruptedException e)
- {
- LOG.debug(e);
- }
- }
- }
-
- public void waitForClose(int timeoutDuration, TimeUnit timeoutUnit) throws InterruptedException
- {
- Assert.assertThat("Client Socket Closed",closeLatch.await(timeoutDuration,timeoutUnit),is(true));
- }
-
- public void waitForConnected(int timeoutDuration, TimeUnit timeoutUnit) throws InterruptedException
- {
- Assert.assertThat("Client Socket Connected",openLatch.await(timeoutDuration,timeoutUnit),is(true));
- }
-
- public void waitForMessage(int timeoutDuration, TimeUnit timeoutUnit) throws InterruptedException
- {
- LOG.debug("Waiting for message");
- Assert.assertThat("Message Received",dataLatch.await(timeoutDuration,timeoutUnit),is(true));
- }
-}
diff --git a/jetty-websocket/websocket-client/src/test/java/org/eclipse/jetty/websocket/client/WebSocketClientBadUriTest.java b/jetty-websocket/websocket-client/src/test/java/org/eclipse/jetty/websocket/client/WebSocketClientBadUriTest.java
index a25b6dd..7258c2a 100644
--- a/jetty-websocket/websocket-client/src/test/java/org/eclipse/jetty/websocket/client/WebSocketClientBadUriTest.java
+++ b/jetty-websocket/websocket-client/src/test/java/org/eclipse/jetty/websocket/client/WebSocketClientBadUriTest.java
@@ -85,7 +85,7 @@
@Test
public void testBadURI() throws Exception
{
- TrackingSocket wsocket = new TrackingSocket();
+ JettyTrackingSocket wsocket = new JettyTrackingSocket();
try
{
diff --git a/jetty-websocket/websocket-client/src/test/java/org/eclipse/jetty/websocket/client/WebSocketClientTest.java b/jetty-websocket/websocket-client/src/test/java/org/eclipse/jetty/websocket/client/WebSocketClientTest.java
index a9152fc..69ef01e 100644
--- a/jetty-websocket/websocket-client/src/test/java/org/eclipse/jetty/websocket/client/WebSocketClientTest.java
+++ b/jetty-websocket/websocket-client/src/test/java/org/eclipse/jetty/websocket/client/WebSocketClientTest.java
@@ -22,6 +22,8 @@
import java.net.InetSocketAddress;
import java.net.URI;
+import java.util.Arrays;
+import java.util.List;
import java.util.Map;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
@@ -32,6 +34,7 @@
import org.eclipse.jetty.websocket.client.blockhead.BlockheadServer;
import org.eclipse.jetty.websocket.client.blockhead.BlockheadServer.ServerConnection;
import org.eclipse.jetty.websocket.common.WebSocketFrame;
+import org.eclipse.jetty.websocket.common.io.FutureWriteCallback;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
@@ -73,7 +76,7 @@
@Test(expected = IllegalArgumentException.class)
public void testAddExtension_NotInstalled() throws Exception
{
- TrackingSocket cliSock = new TrackingSocket();
+ JettyTrackingSocket cliSock = new JettyTrackingSocket();
client.getPolicy().setIdleTimeout(10000);
@@ -89,7 +92,7 @@
@Test
public void testBasicEcho_FromClient() throws Exception
{
- TrackingSocket cliSock = new TrackingSocket();
+ JettyTrackingSocket cliSock = new JettyTrackingSocket();
client.getPolicy().setIdleTimeout(10000);
@@ -119,11 +122,49 @@
cliSock.assertMessage("Hello World!");
}
+
+ @Test
+ public void testBasicEcho_UsingCallback() throws Exception
+ {
+ JettyTrackingSocket cliSock = new JettyTrackingSocket();
+
+ client.getPolicy().setIdleTimeout(10000);
+
+ URI wsUri = server.getWsUri();
+ ClientUpgradeRequest request = new ClientUpgradeRequest();
+ request.setSubProtocols("echo");
+ Future<Session> future = client.connect(cliSock,wsUri,request);
+
+ final ServerConnection srvSock = server.accept();
+ srvSock.upgrade();
+
+ Session sess = future.get(500,TimeUnit.MILLISECONDS);
+ Assert.assertThat("Session",sess,notNullValue());
+ Assert.assertThat("Session.open",sess.isOpen(),is(true));
+ Assert.assertThat("Session.upgradeRequest",sess.getUpgradeRequest(),notNullValue());
+ Assert.assertThat("Session.upgradeResponse",sess.getUpgradeResponse(),notNullValue());
+
+ cliSock.assertWasOpened();
+ cliSock.assertNotClosed();
+
+ Assert.assertThat("client.connectionManager.sessions.size",client.getConnectionManager().getSessions().size(),is(1));
+
+ FutureWriteCallback callback = new FutureWriteCallback();
+
+ cliSock.getSession().getRemote().sendString("Hello World!",callback);
+ callback.get(1,TimeUnit.SECONDS);
+
+ srvSock.echoMessage(1,TimeUnit.MILLISECONDS,500);
+ // wait for response from server
+ cliSock.waitForMessage(500,TimeUnit.MILLISECONDS);
+
+ cliSock.assertMessage("Hello World!");
+ }
@Test
public void testBasicEcho_FromServer() throws Exception
{
- TrackingSocket wsocket = new TrackingSocket();
+ JettyTrackingSocket wsocket = new JettyTrackingSocket();
Future<Session> future = client.connect(wsocket,server.getWsUri());
// Server
@@ -155,7 +196,7 @@
fact.start();
try
{
- TrackingSocket wsocket = new TrackingSocket();
+ JettyTrackingSocket wsocket = new JettyTrackingSocket();
URI wsUri = server.getWsUri();
Future<Session> future = client.connect(wsocket,wsUri);
@@ -195,7 +236,7 @@
{
int bufferSize = 512;
- TrackingSocket wsocket = new TrackingSocket();
+ JettyTrackingSocket wsocket = new JettyTrackingSocket();
URI wsUri = server.getWsUri();
Future<Session> future = client.connect(wsocket,wsUri);
@@ -233,7 +274,7 @@
fact.start();
try
{
- TrackingSocket wsocket = new TrackingSocket();
+ JettyTrackingSocket wsocket = new JettyTrackingSocket();
URI wsUri = server.getWsUri().resolve("/test?snack=cashews&amount=handful&brand=off");
Future<Session> future = client.connect(wsocket,wsUri);
@@ -249,15 +290,15 @@
UpgradeRequest req = session.getUpgradeRequest();
Assert.assertThat("Upgrade Request",req,notNullValue());
- Map<String, String[]> parameterMap = req.getParameterMap();
+ Map<String, List<String>> parameterMap = req.getParameterMap();
Assert.assertThat("Parameter Map",parameterMap,notNullValue());
- Assert.assertThat("Parameter[snack]",parameterMap.get("snack"),is(new String[]
- { "cashews" }));
- Assert.assertThat("Parameter[amount]",parameterMap.get("amount"),is(new String[]
- { "handful" }));
- Assert.assertThat("Parameter[brand]",parameterMap.get("brand"),is(new String[]
- { "off" }));
+ Assert.assertThat("Parameter[snack]",parameterMap.get("snack"),is(Arrays.asList(new String[]
+ { "cashews" })));
+ Assert.assertThat("Parameter[amount]",parameterMap.get("amount"),is(Arrays.asList(new String[]
+ { "handful" })));
+ Assert.assertThat("Parameter[brand]",parameterMap.get("brand"),is(Arrays.asList(new String[]
+ { "off" })));
Assert.assertThat("Parameter[cost]",parameterMap.get("cost"),nullValue());
}
diff --git a/jetty-websocket/websocket-client/src/test/java/org/eclipse/jetty/websocket/client/blockhead/BlockheadServer.java b/jetty-websocket/websocket-client/src/test/java/org/eclipse/jetty/websocket/client/blockhead/BlockheadServer.java
index cb19368..4e2bc37 100644
--- a/jetty-websocket/websocket-client/src/test/java/org/eclipse/jetty/websocket/client/blockhead/BlockheadServer.java
+++ b/jetty-websocket/websocket-client/src/test/java/org/eclipse/jetty/websocket/client/blockhead/BlockheadServer.java
@@ -49,7 +49,6 @@
import org.eclipse.jetty.util.IO;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;
-import org.eclipse.jetty.websocket.api.WebSocketException;
import org.eclipse.jetty.websocket.api.WebSocketPolicy;
import org.eclipse.jetty.websocket.api.WriteCallback;
import org.eclipse.jetty.websocket.api.extensions.ExtensionConfig;
@@ -204,7 +203,7 @@
}
@Override
- public void incomingError(WebSocketException e)
+ public void incomingError(Throwable e)
{
incomingFrames.incomingError(e);
}
@@ -511,7 +510,7 @@
resp.append("Connection: upgrade\r\n");
resp.append("Sec-WebSocket-Accept: ");
resp.append(AcceptHash.hashKey(key)).append("\r\n");
- if (!extensionStack.hasNegotiatedExtensions())
+ if (extensionStack.hasNegotiatedExtensions())
{
// Respond to used extensions
resp.append("Sec-WebSocket-Extensions: ");
diff --git a/jetty-websocket/websocket-client/src/test/java/org/eclipse/jetty/websocket/client/blockhead/IncomingFramesCapture.java b/jetty-websocket/websocket-client/src/test/java/org/eclipse/jetty/websocket/client/blockhead/IncomingFramesCapture.java
index 5c0312c..92ee5a9 100644
--- a/jetty-websocket/websocket-client/src/test/java/org/eclipse/jetty/websocket/client/blockhead/IncomingFramesCapture.java
+++ b/jetty-websocket/websocket-client/src/test/java/org/eclipse/jetty/websocket/client/blockhead/IncomingFramesCapture.java
@@ -36,7 +36,7 @@
{
private static final Logger LOG = Log.getLogger(IncomingFramesCapture.class);
private LinkedList<WebSocketFrame> frames = new LinkedList<>();
- private LinkedList<WebSocketException> errors = new LinkedList<>();
+ private LinkedList<Throwable> errors = new LinkedList<>();
public void assertErrorCount(int expectedCount)
{
@@ -87,7 +87,7 @@
public int getErrorCount(Class<? extends WebSocketException> errorType)
{
int count = 0;
- for (WebSocketException error : errors)
+ for (Throwable error : errors)
{
if (errorType.isInstance(error))
{
@@ -97,7 +97,7 @@
return count;
}
- public LinkedList<WebSocketException> getErrors()
+ public LinkedList<Throwable> getErrors()
{
return errors;
}
@@ -121,7 +121,7 @@
}
@Override
- public void incomingError(WebSocketException e)
+ public void incomingError(Throwable e)
{
LOG.debug(e);
errors.add(e);
diff --git a/jetty-websocket/websocket-common/pom.xml b/jetty-websocket/websocket-common/pom.xml
index cf0ca81..8b72c85 100644
--- a/jetty-websocket/websocket-common/pom.xml
+++ b/jetty-websocket/websocket-common/pom.xml
@@ -3,7 +3,7 @@
<parent>
<groupId>org.eclipse.jetty.websocket</groupId>
<artifactId>websocket-parent</artifactId>
- <version>9.0.5-SNAPSHOT</version>
+ <version>9.1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
diff --git a/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/LogicalConnection.java b/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/LogicalConnection.java
index dd6431d..0429af4 100644
--- a/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/LogicalConnection.java
+++ b/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/LogicalConnection.java
@@ -19,7 +19,9 @@
package org.eclipse.jetty.websocket.common;
import java.net.InetSocketAddress;
+import java.util.concurrent.Executor;
+import org.eclipse.jetty.io.ByteBufferPool;
import org.eclipse.jetty.websocket.api.StatusCode;
import org.eclipse.jetty.websocket.api.SuspendToken;
import org.eclipse.jetty.websocket.api.WebSocketPolicy;
@@ -58,6 +60,25 @@
void disconnect();
/**
+ * Get the ByteBufferPool in use by the connection
+ * @return
+ */
+ ByteBufferPool getBufferPool();
+
+ /**
+ * Get the Executor used by this connection.
+ * @return
+ */
+ Executor getExecutor();
+
+ /**
+ * Get the read/write idle timeout.
+ *
+ * @return the idle timeout in milliseconds
+ */
+ public long getIdleTimeout();
+
+ /**
* Get the IOState of the connection.
*
* @return the IOState of the connection.
@@ -117,6 +138,9 @@
/**
* Set the maximum number of milliseconds of idleness before the connection is closed/disconnected, (ie no frames are either sent or received)
+ * <p>
+ * This idle timeout cannot be garunteed to take immediate effect for any active read/write actions.
+ * New read/write actions will have this new idle timeout.
*
* @param ms
* the number of milliseconds of idle timeout
diff --git a/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/Parser.java b/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/Parser.java
index bca7e30..8d1a8ba 100644
--- a/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/Parser.java
+++ b/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/Parser.java
@@ -91,14 +91,17 @@
private void assertSanePayloadLength(long len)
{
- LOG.debug("Payload Length: " + len);
+ if (LOG.isDebugEnabled())
+ {
+ LOG.debug("Payload Length: {} - {}",len,this);
+ }
+
// Since we use ByteBuffer so often, having lengths over Integer.MAX_VALUE is really impossible.
if (len > Integer.MAX_VALUE)
{
// OMG! Sanity Check! DO NOT WANT! Won't anyone think of the memory!
throw new MessageTooLargeException("[int-sane!] cannot handle payload lengths larger than " + Integer.MAX_VALUE);
}
- policy.assertValidMessageSize((int)len);
switch (frame.getOpCode())
{
@@ -116,6 +119,12 @@
+ WebSocketFrame.MAX_CONTROL_PAYLOAD + "]");
}
break;
+ case OpCode.TEXT:
+ policy.assertValidTextMessageSize((int)len);
+ break;
+ case OpCode.BINARY:
+ policy.assertValidBinaryMessageSize((int)len);
+ break;
}
}
@@ -597,7 +606,8 @@
public String toString()
{
StringBuilder builder = new StringBuilder();
- builder.append("Parser[");
+ builder.append("Parser@").append(Integer.toHexString(hashCode()));
+ builder.append("[");
if (incomingFramesHandler == null)
{
builder.append("NO_HANDLER");
@@ -606,14 +616,11 @@
{
builder.append(incomingFramesHandler.getClass().getSimpleName());
}
- builder.append(",s=");
- builder.append(state);
- builder.append(",c=");
- builder.append(cursor);
- builder.append(",len=");
- builder.append(payloadLength);
- builder.append(",f=");
- builder.append(frame);
+ builder.append(",s=").append(state);
+ builder.append(",c=").append(cursor);
+ builder.append(",len=").append(payloadLength);
+ builder.append(",f=").append(frame);
+ builder.append(",p=").append(policy);
builder.append("]");
return builder.toString();
}
diff --git a/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/SessionFactory.java b/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/SessionFactory.java
new file mode 100644
index 0000000..a0af9bf
--- /dev/null
+++ b/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/SessionFactory.java
@@ -0,0 +1,33 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.common;
+
+import java.net.URI;
+
+import org.eclipse.jetty.websocket.common.events.EventDriver;
+
+/**
+ * Interface for creating jetty {@link MutableSession} objects.
+ */
+public interface SessionFactory
+{
+ public boolean supports(EventDriver websocket);
+
+ public WebSocketSession createSession(URI requestURI, EventDriver websocket, LogicalConnection connection);
+}
diff --git a/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/WebSocketRemoteEndpoint.java b/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/WebSocketRemoteEndpoint.java
index 4f4d88d..1179c1f 100644
--- a/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/WebSocketRemoteEndpoint.java
+++ b/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/WebSocketRemoteEndpoint.java
@@ -21,13 +21,17 @@
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
+import java.util.Objects;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.locks.ReentrantLock;
import org.eclipse.jetty.util.BufferUtil;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;
import org.eclipse.jetty.websocket.api.RemoteEndpoint;
+import org.eclipse.jetty.websocket.api.WriteCallback;
import org.eclipse.jetty.websocket.api.extensions.OutgoingFrames;
import org.eclipse.jetty.websocket.common.io.FutureWriteCallback;
@@ -36,9 +40,19 @@
*/
public class WebSocketRemoteEndpoint implements RemoteEndpoint
{
+ private static final String PRIORMSG_ERROR = "Prior message pending, cannot start new message yet.";
+ /** Type of Message */
+ private static final int NONE = 0;
+ private static final int TEXT = 1;
+ private static final int BINARY = 2;
+ private static final int CONTROL = 3;
+
private static final Logger LOG = Log.getLogger(WebSocketRemoteEndpoint.class);
public final LogicalConnection connection;
public final OutgoingFrames outgoing;
+ private final ReentrantLock msgLock = new ReentrantLock();
+ private final AtomicInteger msgType = new AtomicInteger(NONE);
+ private boolean partialStarted = false;
public WebSocketRemoteEndpoint(LogicalConnection connection, OutgoingFrames outgoing)
{
@@ -82,15 +96,7 @@
private Future<Void> sendAsyncFrame(WebSocketFrame frame)
{
FutureWriteCallback future = new FutureWriteCallback();
- try
- {
- connection.getIOState().assertOutputOpen();
- outgoing.outgoingFrame(frame,future);
- }
- catch (IOException e)
- {
- future.writeFailed(e);
- }
+ sendFrame(frame,future);
return future;
}
@@ -100,18 +106,35 @@
@Override
public void sendBytes(ByteBuffer data) throws IOException
{
- connection.getIOState().assertOutputOpen();
- if (LOG.isDebugEnabled())
+ if (msgLock.tryLock())
{
- LOG.debug("sendBytes with {}",BufferUtil.toDetailString(data));
+ try
+ {
+ msgType.set(BINARY);
+ connection.getIOState().assertOutputOpen();
+ if (LOG.isDebugEnabled())
+ {
+ LOG.debug("sendBytes with {}",BufferUtil.toDetailString(data));
+ }
+ WebSocketFrame frame = WebSocketFrame.binary().setPayload(data);
+ blockingWrite(frame);
+ }
+ finally
+ {
+ msgType.set(NONE);
+ msgLock.unlock();
+ }
}
- WebSocketFrame frame = WebSocketFrame.binary().setPayload(data);
- blockingWrite(frame);
+ else
+ {
+ throw new IllegalStateException(PRIORMSG_ERROR);
+ }
}
@Override
public Future<Void> sendBytesByFuture(ByteBuffer data)
{
+ msgType.set(BINARY);
if (LOG.isDebugEnabled())
{
LOG.debug("sendBytesByFuture with {}",BufferUtil.toDetailString(data));
@@ -119,65 +142,196 @@
WebSocketFrame frame = WebSocketFrame.binary().setPayload(data);
return sendAsyncFrame(frame);
}
+
+ @Override
+ public void sendBytes(ByteBuffer data, WriteCallback callback)
+ {
+ Objects.requireNonNull(callback,"WriteCallback cannot be null");
+ msgType.set(BINARY);
+ if (LOG.isDebugEnabled())
+ {
+ LOG.debug("sendBytes({}, {})",BufferUtil.toDetailString(data),callback);
+ }
+ WebSocketFrame frame = WebSocketFrame.binary().setPayload(data);
+ sendFrame(frame,callback);
+ }
+
+ public void sendFrame(WebSocketFrame frame, WriteCallback callback)
+ {
+ try
+ {
+ connection.getIOState().assertOutputOpen();
+ outgoing.outgoingFrame(frame,callback);
+ }
+ catch (IOException e)
+ {
+ callback.writeFailed(e);
+ }
+ }
@Override
public void sendPartialBytes(ByteBuffer fragment, boolean isLast) throws IOException
{
- if (LOG.isDebugEnabled())
+ if (msgLock.tryLock())
{
- LOG.debug("sendPartialBytes({}, {})",BufferUtil.toDetailString(fragment),isLast);
+ try
+ {
+ if (msgType.get() == TEXT)
+ {
+ throw new IllegalStateException("Prior TEXT message pending, cannot start new BINARY message yet.");
+ }
+ msgType.set(BINARY);
+
+ if (LOG.isDebugEnabled())
+ {
+ LOG.debug("sendPartialBytes({}, {})",BufferUtil.toDetailString(fragment),isLast);
+ }
+ WebSocketFrame frame = WebSocketFrame.binary().setPayload(fragment).setFin(isLast);
+ if(partialStarted) {
+ frame.setContinuation(true);
+ }
+ blockingWrite(frame);
+ partialStarted = !isLast;
+ }
+ finally
+ {
+ if (isLast)
+ {
+ msgType.set(NONE);
+ }
+ msgLock.unlock();
+ }
}
- WebSocketFrame frame = WebSocketFrame.binary().setPayload(fragment).setFin(isLast);
- blockingWrite(frame);
+ else
+ {
+ throw new IllegalStateException(PRIORMSG_ERROR);
+ }
}
@Override
public void sendPartialString(String fragment, boolean isLast) throws IOException
{
- if (LOG.isDebugEnabled())
+ if (msgLock.tryLock())
{
- LOG.debug("sendPartialString({}, {})",fragment,isLast);
+ try
+ {
+ if (msgType.get() == BINARY)
+ {
+ throw new IllegalStateException("Prior BINARY message pending, cannot start new TEXT message yet.");
+ }
+ msgType.set(TEXT);
+
+ if (LOG.isDebugEnabled())
+ {
+ LOG.debug("sendPartialString({}, {})",fragment,isLast);
+ }
+ WebSocketFrame frame = WebSocketFrame.text(fragment).setFin(isLast);
+ if(partialStarted) {
+ frame.setContinuation(true);
+ }
+ blockingWrite(frame);
+ partialStarted = !isLast;
+ }
+ finally
+ {
+ if (isLast)
+ {
+ msgType.set(NONE);
+ }
+ msgLock.unlock();
+ }
}
- WebSocketFrame frame = WebSocketFrame.text(fragment).setFin(isLast);
- blockingWrite(frame);
+ else
+ {
+ throw new IllegalStateException(PRIORMSG_ERROR);
+ }
}
@Override
public void sendPing(ByteBuffer applicationData) throws IOException
{
- if (LOG.isDebugEnabled())
+ if (msgLock.tryLock())
{
- LOG.debug("sendPing with {}",BufferUtil.toDetailString(applicationData));
+ try
+ {
+ msgType.set(CONTROL);
+ if (LOG.isDebugEnabled())
+ {
+ LOG.debug("sendPing with {}",BufferUtil.toDetailString(applicationData));
+ }
+ WebSocketFrame frame = WebSocketFrame.ping().setPayload(applicationData);
+ blockingWrite(frame);
+ }
+ finally
+ {
+ msgType.set(NONE);
+ msgLock.unlock();
+ }
}
- WebSocketFrame frame = WebSocketFrame.ping().setPayload(applicationData);
- blockingWrite(frame);
+ else
+ {
+ throw new IllegalStateException(PRIORMSG_ERROR);
+ }
}
@Override
public void sendPong(ByteBuffer applicationData) throws IOException
{
- if (LOG.isDebugEnabled())
+ if (msgLock.tryLock())
{
- LOG.debug("sendPong with {}",BufferUtil.toDetailString(applicationData));
+ try
+ {
+ msgType.set(CONTROL);
+ if (LOG.isDebugEnabled())
+ {
+ LOG.debug("sendPong with {}",BufferUtil.toDetailString(applicationData));
+ }
+ WebSocketFrame frame = WebSocketFrame.pong().setPayload(applicationData);
+ blockingWrite(frame);
+ }
+ finally
+ {
+ msgType.set(NONE);
+ msgLock.unlock();
+ }
}
- WebSocketFrame frame = WebSocketFrame.pong().setPayload(applicationData);
- blockingWrite(frame);
+ else
+ {
+ throw new IllegalStateException(PRIORMSG_ERROR);
+ }
}
@Override
public void sendString(String text) throws IOException
{
- WebSocketFrame frame = WebSocketFrame.text(text);
- if (LOG.isDebugEnabled())
+ if (msgLock.tryLock())
{
- LOG.debug("sendString with {}",BufferUtil.toDetailString(frame.getPayload()));
+ try
+ {
+ msgType.set(TEXT);
+ WebSocketFrame frame = WebSocketFrame.text(text);
+ if (LOG.isDebugEnabled())
+ {
+ LOG.debug("sendString with {}",BufferUtil.toDetailString(frame.getPayload()));
+ }
+ blockingWrite(WebSocketFrame.text(text));
+ }
+ finally
+ {
+ msgType.set(NONE);
+ msgLock.unlock();
+ }
}
- blockingWrite(WebSocketFrame.text(text));
+ else
+ {
+ throw new IllegalStateException(PRIORMSG_ERROR);
+ }
}
@Override
public Future<Void> sendStringByFuture(String text)
{
+ msgType.set(TEXT);
WebSocketFrame frame = WebSocketFrame.text(text);
if (LOG.isDebugEnabled())
{
@@ -185,4 +339,17 @@
}
return sendAsyncFrame(frame);
}
+
+ @Override
+ public void sendString(String text, WriteCallback callback)
+ {
+ Objects.requireNonNull(callback,"WriteCallback cannot be null");
+ msgType.set(TEXT);
+ WebSocketFrame frame = WebSocketFrame.text(text);
+ if (LOG.isDebugEnabled())
+ {
+ LOG.debug("sendString({},{})",BufferUtil.toDetailString(frame.getPayload()),callback);
+ }
+ sendFrame(frame,callback);
+ }
}
diff --git a/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/WebSocketSession.java b/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/WebSocketSession.java
index 282c05f..c863535 100644
--- a/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/WebSocketSession.java
+++ b/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/WebSocketSession.java
@@ -24,7 +24,9 @@
import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import java.util.concurrent.Executor;
+import org.eclipse.jetty.io.ByteBufferPool;
import org.eclipse.jetty.util.MultiMap;
import org.eclipse.jetty.util.StringUtil;
import org.eclipse.jetty.util.UrlEncoded;
@@ -58,8 +60,8 @@
private final URI requestURI;
private final EventDriver websocket;
private final LogicalConnection connection;
+ private final Executor executor;
private ExtensionFactory extensionFactory;
- private long maximumMessageSize;
private String protocolVersion;
private Map<String, String[]> parameterMap = new HashMap<>();
private WebSocketRemoteEndpoint remote;
@@ -79,6 +81,7 @@
this.requestURI = requestURI;
this.websocket = websocket;
this.connection = connection;
+ this.executor = connection.getExecutor();
this.outgoingHandler = connection;
this.incomingHandler = websocket;
@@ -102,7 +105,7 @@
}
@Override
- public void close() throws IOException
+ public void close()
{
this.close(StatusCode.NORMAL,null);
}
@@ -132,6 +135,11 @@
notifyClose(StatusCode.NO_CLOSE,"Harsh disconnect");
}
+ public void dispatch(Runnable runnable)
+ {
+ executor.execute(runnable);
+ }
+
@Override
public void dump(Appendable out, String indent) throws IOException
{
@@ -187,6 +195,11 @@
return true;
}
+ public ByteBufferPool getBufferPool()
+ {
+ return this.connection.getBufferPool();
+ }
+
public LogicalConnection getConnection()
{
return connection;
@@ -218,12 +231,6 @@
return connection.getLocalAddress();
}
- @Override
- public long getMaximumMessageSize()
- {
- return maximumMessageSize;
- }
-
@ManagedAttribute(readonly = true)
public OutgoingFrames getOutgoingHandler()
{
@@ -291,12 +298,12 @@
* Incoming Errors from Parser
*/
@Override
- public void incomingError(WebSocketException e)
+ public void incomingError(Throwable t)
{
if (connection.getIOState().isInputAvailable())
{
// Forward Errors to User WebSocket Object
- websocket.incomingError(e);
+ websocket.incomingError(t);
}
}
@@ -341,6 +348,11 @@
websocket.onClose(new CloseInfo(statusCode,reason));
}
+ public void notifyError(Throwable cause)
+ {
+ incomingError(cause);
+ }
+
@Override
public void onConnectionStateChange(ConnectionState state)
{
@@ -404,12 +416,6 @@
connection.setMaxIdleTimeout(ms);
}
- @Override
- public void setMaximumMessageSize(long length)
- {
- this.maximumMessageSize = length;
- }
-
public void setOutgoingHandler(OutgoingFrames outgoing)
{
this.outgoingHandler = outgoing;
@@ -423,6 +429,7 @@
public void setUpgradeRequest(UpgradeRequest request)
{
this.upgradeRequest = request;
+ this.protocolVersion = request.getProtocolVersion();
}
public void setUpgradeResponse(UpgradeResponse response)
diff --git a/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/WebSocketSessionFactory.java b/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/WebSocketSessionFactory.java
new file mode 100644
index 0000000..c649369
--- /dev/null
+++ b/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/WebSocketSessionFactory.java
@@ -0,0 +1,43 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.common;
+
+import java.net.URI;
+
+import org.eclipse.jetty.websocket.common.events.EventDriver;
+import org.eclipse.jetty.websocket.common.events.JettyAnnotatedEventDriver;
+import org.eclipse.jetty.websocket.common.events.JettyListenerEventDriver;
+
+/**
+ * Default Session factory, creating WebSocketSession objects.
+ */
+public class WebSocketSessionFactory implements SessionFactory
+{
+ @Override
+ public boolean supports(EventDriver websocket)
+ {
+ return (websocket instanceof JettyAnnotatedEventDriver) || (websocket instanceof JettyListenerEventDriver);
+ }
+
+ @Override
+ public WebSocketSession createSession(URI requestURI, EventDriver websocket, LogicalConnection connection)
+ {
+ return new WebSocketSession(requestURI,websocket,connection);
+ }
+}
diff --git a/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/events/AbstractEventDriver.java b/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/events/AbstractEventDriver.java
new file mode 100644
index 0000000..881e400
--- /dev/null
+++ b/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/events/AbstractEventDriver.java
@@ -0,0 +1,249 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.common.events;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+
+import org.eclipse.jetty.util.BufferUtil;
+import org.eclipse.jetty.util.StringUtil;
+import org.eclipse.jetty.util.Utf8Appendable.NotUtf8Exception;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+import org.eclipse.jetty.websocket.api.CloseException;
+import org.eclipse.jetty.websocket.api.StatusCode;
+import org.eclipse.jetty.websocket.api.WebSocketPolicy;
+import org.eclipse.jetty.websocket.api.extensions.Frame;
+import org.eclipse.jetty.websocket.api.extensions.IncomingFrames;
+import org.eclipse.jetty.websocket.common.CloseInfo;
+import org.eclipse.jetty.websocket.common.OpCode;
+import org.eclipse.jetty.websocket.common.WebSocketFrame;
+import org.eclipse.jetty.websocket.common.WebSocketSession;
+import org.eclipse.jetty.websocket.common.message.MessageAppender;
+
+/**
+ * EventDriver is the main interface between the User's WebSocket POJO and the internal jetty implementation of WebSocket.
+ */
+public abstract class AbstractEventDriver implements IncomingFrames, EventDriver
+{
+ private static final Logger LOG = Log.getLogger(AbstractEventDriver.class);
+ protected final WebSocketPolicy policy;
+ protected final Object websocket;
+ protected WebSocketSession session;
+ protected MessageAppender activeMessage;
+
+ public AbstractEventDriver(WebSocketPolicy policy, Object websocket)
+ {
+ this.policy = policy;
+ this.websocket = websocket;
+ }
+
+ protected void appendMessage(ByteBuffer buffer, boolean fin) throws IOException
+ {
+ activeMessage.appendMessage(buffer,fin);
+
+ if (fin)
+ {
+ activeMessage.messageComplete();
+ activeMessage = null;
+ }
+ }
+
+ protected void dispatch(Runnable runnable)
+ {
+ session.dispatch(runnable);
+ }
+
+ @Override
+ public WebSocketPolicy getPolicy()
+ {
+ return policy;
+ }
+
+ @Override
+ public WebSocketSession getSession()
+ {
+ return session;
+ }
+
+ @Override
+ public final void incomingError(Throwable e)
+ {
+ if (LOG.isDebugEnabled())
+ {
+ LOG.debug("incoming(WebSocketException)",e);
+ }
+
+ if (e instanceof CloseException)
+ {
+ CloseException close = (CloseException)e;
+ terminateConnection(close.getStatusCode(),close.getMessage());
+ }
+
+ onError(e);
+ }
+
+ @Override
+ public void incomingFrame(Frame frame)
+ {
+ if (LOG.isDebugEnabled())
+ {
+ LOG.debug("{}.onFrame({})",websocket.getClass().getSimpleName(),frame);
+ }
+
+ try
+ {
+ onFrame(frame);
+
+ byte opcode = frame.getType().getOpCode();
+ switch (opcode)
+ {
+ case OpCode.CLOSE:
+ {
+ boolean validate = true;
+ CloseInfo close = new CloseInfo(frame,validate);
+
+ // notify user websocket pojo
+ onClose(close);
+
+ // process handshake
+ session.getConnection().getIOState().onCloseRemote(close);
+
+ return;
+ }
+ case OpCode.PING:
+ {
+ ByteBuffer pongBuf;
+ if (frame.hasPayload())
+ {
+ pongBuf = ByteBuffer.allocate(frame.getPayload().remaining());
+ BufferUtil.put(frame.getPayload(),pongBuf);
+ BufferUtil.flipToFlush(pongBuf,0);
+ }
+ else
+ {
+ pongBuf = ByteBuffer.allocate(0);
+ }
+ onPing(frame.getPayload());
+ session.getRemote().sendPong(pongBuf);
+ break;
+ }
+ case OpCode.PONG:
+ {
+ onPong(frame.getPayload());
+ break;
+ }
+ case OpCode.BINARY:
+ {
+ onBinaryFrame(frame.getPayload(),frame.isFin());
+ return;
+ }
+ case OpCode.TEXT:
+ {
+ onTextFrame(frame.getPayload(),frame.isFin());
+ return;
+ }
+ case OpCode.CONTINUATION:
+ {
+ onContinuationFrame(frame.getPayload(),frame.isFin());
+ return;
+ }
+ default:
+ {
+ LOG.debug("Unhandled OpCode: {}",opcode);
+ }
+ }
+ }
+ catch (NotUtf8Exception e)
+ {
+ terminateConnection(StatusCode.BAD_PAYLOAD,e.getMessage());
+ }
+ catch (CloseException e)
+ {
+ terminateConnection(e.getStatusCode(),e.getMessage());
+ }
+ catch (Throwable t)
+ {
+ unhandled(t);
+ }
+ }
+
+ @Override
+ public void onContinuationFrame(ByteBuffer buffer, boolean fin) throws IOException
+ {
+ if (activeMessage == null)
+ {
+ throw new IOException("Out of order Continuation frame encountered");
+ }
+
+ appendMessage(buffer,fin);
+ }
+
+ @Override
+ public void onPong(ByteBuffer buffer)
+ {
+ /* TODO: provide annotation in future */
+ }
+
+ @Override
+ public void onPing(ByteBuffer buffer)
+ {
+ /* TODO: provide annotation in future */
+ }
+
+ @Override
+ public void openSession(WebSocketSession session)
+ {
+ LOG.debug("openSession({})",session);
+ this.session = session;
+ try
+ {
+ this.onConnect();
+ }
+ catch (Throwable t)
+ {
+ unhandled(t);
+ }
+ }
+
+ protected void terminateConnection(int statusCode, String rawreason)
+ {
+ String reason = rawreason;
+ reason = StringUtil.truncate(reason,(WebSocketFrame.MAX_CONTROL_PAYLOAD - 2));
+ LOG.debug("terminateConnection({},{})",statusCode,rawreason);
+ session.close(statusCode,reason);
+ }
+
+ private void unhandled(Throwable t)
+ {
+ LOG.warn("Unhandled Error (closing connection)",t);
+ onError(t);
+
+ // Unhandled Error, close the connection.
+ switch (policy.getBehavior())
+ {
+ case SERVER:
+ terminateConnection(StatusCode.SERVER_ERROR,t.getClass().getSimpleName());
+ break;
+ case CLIENT:
+ terminateConnection(StatusCode.POLICY_VIOLATION,t.getClass().getSimpleName());
+ break;
+ }
+ }
+}
diff --git a/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/events/AnnotatedEventDriver.java b/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/events/AnnotatedEventDriver.java
deleted file mode 100644
index 083904f..0000000
--- a/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/events/AnnotatedEventDriver.java
+++ /dev/null
@@ -1,207 +0,0 @@
-//
-// ========================================================================
-// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
-// ------------------------------------------------------------------------
-// All rights reserved. This program and the accompanying materials
-// are made available under the terms of the Eclipse Public License v1.0
-// and Apache License v2.0 which accompanies this distribution.
-//
-// The Eclipse Public License is available at
-// http://www.eclipse.org/legal/epl-v10.html
-//
-// The Apache License v2.0 is available at
-// http://www.opensource.org/licenses/apache2.0.php
-//
-// You may elect to redistribute this code under either of these licenses.
-// ========================================================================
-//
-
-package org.eclipse.jetty.websocket.common.events;
-
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.Reader;
-import java.nio.ByteBuffer;
-
-import org.eclipse.jetty.websocket.api.WebSocketPolicy;
-import org.eclipse.jetty.websocket.api.annotations.WebSocket;
-import org.eclipse.jetty.websocket.api.extensions.Frame;
-import org.eclipse.jetty.websocket.common.CloseInfo;
-import org.eclipse.jetty.websocket.common.message.MessageAppender;
-import org.eclipse.jetty.websocket.common.message.MessageInputStream;
-import org.eclipse.jetty.websocket.common.message.MessageReader;
-import org.eclipse.jetty.websocket.common.message.SimpleBinaryMessage;
-import org.eclipse.jetty.websocket.common.message.SimpleTextMessage;
-
-/**
- * Handler for Annotated User WebSocket objects.
- */
-public class AnnotatedEventDriver extends EventDriver
-{
- private final EventMethods events;
- private MessageAppender activeMessage;
- private boolean hasCloseBeenCalled = false;
-
- public AnnotatedEventDriver(WebSocketPolicy policy, Object websocket, EventMethods events)
- {
- super(policy,websocket);
- this.events = events;
-
- WebSocket anno = websocket.getClass().getAnnotation(WebSocket.class);
- // Setup the policy
- if (anno.maxMessageSize() > 0)
- {
- this.policy.setMaxMessageSize(anno.maxMessageSize());
- }
- if (anno.inputBufferSize() > 0)
- {
- this.policy.setInputBufferSize(anno.inputBufferSize());
- }
- if (anno.maxIdleTime() > 0)
- {
- this.policy.setIdleTimeout(anno.maxIdleTime());
- }
- }
-
- @Override
- public void onBinaryFrame(ByteBuffer buffer, boolean fin) throws IOException
- {
- if (events.onBinary == null)
- {
- // not interested in binary events
- return;
- }
-
- if (activeMessage == null)
- {
- if (events.onBinary.isStreaming())
- {
- activeMessage = new MessageInputStream(this);
- }
- else
- {
- activeMessage = new SimpleBinaryMessage(this);
- }
- }
-
- activeMessage.appendMessage(buffer);
-
- if (fin)
- {
- activeMessage.messageComplete();
- activeMessage = null;
- }
- }
-
- @Override
- public void onBinaryMessage(byte[] data)
- {
- if (events.onBinary != null)
- {
- events.onBinary.call(websocket,session,data,0,data.length);
- }
- }
-
- @Override
- public void onClose(CloseInfo close)
- {
- if (hasCloseBeenCalled)
- {
- // avoid duplicate close events (possible when using harsh Session.disconnect())
- return;
- }
- hasCloseBeenCalled = true;
- if (events.onClose != null)
- {
- events.onClose.call(websocket,session,close.getStatusCode(),close.getReason());
- }
- }
-
- @Override
- public void onConnect()
- {
- if (events.onConnect != null)
- {
- events.onConnect.call(websocket,session);
- }
- }
-
- @Override
- public void onError(Throwable cause)
- {
- if (events.onError != null)
- {
- events.onError.call(websocket,session,cause);
- }
- }
-
- @Override
- public void onFrame(Frame frame)
- {
- if (events.onFrame != null)
- {
- events.onFrame.call(websocket,session,frame);
- }
- }
-
- public void onInputStream(InputStream stream)
- {
- if (events.onBinary != null)
- {
- events.onBinary.call(websocket,session,stream);
- }
- }
-
- public void onReader(Reader reader)
- {
- if (events.onText != null)
- {
- events.onText.call(websocket,session,reader);
- }
- }
-
- @Override
- public void onTextFrame(ByteBuffer buffer, boolean fin) throws IOException
- {
- if (events.onText == null)
- {
- // not interested in text events
- return;
- }
-
- if (activeMessage == null)
- {
- if (events.onText.isStreaming())
- {
- activeMessage = new MessageReader(this);
- }
- else
- {
- activeMessage = new SimpleTextMessage(this);
- }
- }
-
- activeMessage.appendMessage(buffer);
-
- if (fin)
- {
- activeMessage.messageComplete();
- activeMessage = null;
- }
- }
-
- @Override
- public void onTextMessage(String message)
- {
- if (events.onText != null)
- {
- events.onText.call(websocket,session,message);
- }
- }
-
- @Override
- public String toString()
- {
- return String.format("%s[%s]", this.getClass().getSimpleName(), websocket);
- }
-}
diff --git a/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/events/EventDriver.java b/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/events/EventDriver.java
index 5461df4..ff0d584 100644
--- a/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/events/EventDriver.java
+++ b/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/events/EventDriver.java
@@ -19,182 +19,47 @@
package org.eclipse.jetty.websocket.common.events;
import java.io.IOException;
+import java.io.InputStream;
+import java.io.Reader;
import java.nio.ByteBuffer;
-import org.eclipse.jetty.util.BufferUtil;
-import org.eclipse.jetty.util.StringUtil;
-import org.eclipse.jetty.util.Utf8Appendable.NotUtf8Exception;
-import org.eclipse.jetty.util.log.Log;
-import org.eclipse.jetty.util.log.Logger;
-import org.eclipse.jetty.websocket.api.CloseException;
-import org.eclipse.jetty.websocket.api.StatusCode;
-import org.eclipse.jetty.websocket.api.WebSocketException;
import org.eclipse.jetty.websocket.api.WebSocketPolicy;
import org.eclipse.jetty.websocket.api.extensions.Frame;
import org.eclipse.jetty.websocket.api.extensions.IncomingFrames;
import org.eclipse.jetty.websocket.common.CloseInfo;
-import org.eclipse.jetty.websocket.common.OpCode;
-import org.eclipse.jetty.websocket.common.WebSocketFrame;
import org.eclipse.jetty.websocket.common.WebSocketSession;
-/**
- * EventDriver is the main interface between the User's WebSocket POJO and the internal jetty implementation of WebSocket.
- */
-public abstract class EventDriver implements IncomingFrames
+public interface EventDriver extends IncomingFrames
{
- private static final Logger LOG = Log.getLogger(EventDriver.class);
- protected final WebSocketPolicy policy;
- protected final Object websocket;
- protected WebSocketSession session;
+ public WebSocketPolicy getPolicy();
- public EventDriver(WebSocketPolicy policy, Object websocket)
- {
- this.policy = policy;
- this.websocket = websocket;
- }
+ public WebSocketSession getSession();
- public WebSocketPolicy getPolicy()
- {
- return policy;
- }
+ public void onBinaryFrame(ByteBuffer buffer, boolean fin) throws IOException;
- public WebSocketSession getSession()
- {
- return session;
- }
+ public void onBinaryMessage(byte[] data);
- @Override
- public final void incomingError(WebSocketException e)
- {
- if (LOG.isDebugEnabled())
- {
- LOG.debug("incoming(WebSocketException)",e);
- }
+ public void onClose(CloseInfo close);
- if (e instanceof CloseException)
- {
- CloseException close = (CloseException)e;
- terminateConnection(close.getStatusCode(),close.getMessage());
- }
+ public void onConnect();
- onError(e);
- }
+ public void onContinuationFrame(ByteBuffer buffer, boolean fin) throws IOException;
- @Override
- public void incomingFrame(Frame frame)
- {
- if (LOG.isDebugEnabled())
- {
- LOG.debug("{}.onFrame({})",websocket.getClass().getSimpleName(),frame);
- }
+ public void onError(Throwable t);
- onFrame(frame);
+ public void onFrame(Frame frame);
- try
- {
- switch (frame.getType().getOpCode())
- {
- case OpCode.CLOSE:
- {
- boolean validate = true;
- CloseInfo close = new CloseInfo(frame,validate);
+ public void onInputStream(InputStream stream);
- // notify user websocket pojo
- onClose(close);
+ public void onPing(ByteBuffer buffer);
+
+ public void onPong(ByteBuffer buffer);
- // process handshake
- session.getConnection().getIOState().onCloseRemote(close);
+ public void onReader(Reader reader);
- return;
- }
- case OpCode.PING:
- {
- byte pongBuf[] = new byte[0];
- if (frame.hasPayload())
- {
- pongBuf = BufferUtil.toArray(frame.getPayload());
- }
- session.getRemote().sendPong(ByteBuffer.wrap(pongBuf));
- break;
- }
- case OpCode.BINARY:
- {
- onBinaryFrame(frame.getPayload(),frame.isFin());
- return;
- }
- case OpCode.TEXT:
- {
- onTextFrame(frame.getPayload(),frame.isFin());
- return;
- }
- }
- }
- catch (NotUtf8Exception e)
- {
- terminateConnection(StatusCode.BAD_PAYLOAD,e.getMessage());
- }
- catch (CloseException e)
- {
- terminateConnection(e.getStatusCode(),e.getMessage());
- }
- catch (Throwable t)
- {
- unhandled(t);
- }
- }
+ public void onTextFrame(ByteBuffer buffer, boolean fin) throws IOException;
- public abstract void onBinaryFrame(ByteBuffer buffer, boolean fin) throws IOException;
+ public void onTextMessage(String message);
- public abstract void onBinaryMessage(byte[] data);
-
- public abstract void onClose(CloseInfo close);
-
- public abstract void onConnect();
-
- public abstract void onError(Throwable t);
-
- public abstract void onFrame(Frame frame);
-
- public abstract void onTextFrame(ByteBuffer buffer, boolean fin) throws IOException;
-
- public abstract void onTextMessage(String message);
-
- public void openSession(WebSocketSession session)
- {
- LOG.debug("openSession({})",session);
- this.session = session;
- try
- {
- this.onConnect();
- }
- catch (Throwable t)
- {
- unhandled(t);
- }
- }
-
- protected void terminateConnection(int statusCode, String rawreason)
- {
- String reason = rawreason;
- reason = StringUtil.truncate(reason,(WebSocketFrame.MAX_CONTROL_PAYLOAD - 2));
- LOG.debug("terminateConnection({},{})",statusCode,rawreason);
- session.close(statusCode,reason);
- }
-
- private void unhandled(Throwable t)
- {
- LOG.warn("Unhandled Error (closing connection)",t);
- onError(t);
-
- // Unhandled Error, close the connection.
- switch (policy.getBehavior())
- {
- case SERVER:
- terminateConnection(StatusCode.SERVER_ERROR,t.getClass().getSimpleName());
- break;
- case CLIENT:
- terminateConnection(StatusCode.POLICY_VIOLATION,t.getClass().getSimpleName());
- break;
- }
- }
-}
+ public void openSession(WebSocketSession session);
+}
\ No newline at end of file
diff --git a/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/events/EventDriverFactory.java b/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/events/EventDriverFactory.java
index e8e9ea6..05cfc0f 100644
--- a/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/events/EventDriverFactory.java
+++ b/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/events/EventDriverFactory.java
@@ -18,340 +18,83 @@
package org.eclipse.jetty.websocket.common.events;
-import java.io.InputStream;
-import java.io.Reader;
-import java.lang.annotation.Annotation;
-import java.lang.reflect.Method;
-import java.lang.reflect.Modifier;
-import java.util.concurrent.ConcurrentHashMap;
+import java.util.ArrayList;
+import java.util.List;
-import org.eclipse.jetty.util.StringUtil;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
import org.eclipse.jetty.websocket.api.InvalidWebSocketException;
-import org.eclipse.jetty.websocket.api.Session;
import org.eclipse.jetty.websocket.api.WebSocketListener;
import org.eclipse.jetty.websocket.api.WebSocketPolicy;
-import org.eclipse.jetty.websocket.api.annotations.OnWebSocketClose;
-import org.eclipse.jetty.websocket.api.annotations.OnWebSocketConnect;
-import org.eclipse.jetty.websocket.api.annotations.OnWebSocketError;
-import org.eclipse.jetty.websocket.api.annotations.OnWebSocketFrame;
-import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage;
import org.eclipse.jetty.websocket.api.annotations.WebSocket;
-import org.eclipse.jetty.websocket.api.extensions.Frame;
/**
* Create EventDriver implementations.
*/
public class EventDriverFactory
{
- /**
- * Parameter list for @OnWebSocketMessage (Binary mode)
- */
- private static final ParamList validBinaryParams;
-
- /**
- * Parameter list for @OnWebSocketConnect
- */
- private static final ParamList validConnectParams;
-
- /**
- * Parameter list for @OnWebSocketClose
- */
- private static final ParamList validCloseParams;
-
- /**
- * Parameter list for @OnWebSocketError
- */
- private static final ParamList validErrorParams;
-
- /**
- * Parameter list for @OnWebSocketFrame
- */
- private static final ParamList validFrameParams;
-
- /**
- * Parameter list for @OnWebSocketMessage (Text mode)
- */
- private static final ParamList validTextParams;
-
- static
- {
- validConnectParams = new ParamList();
- validConnectParams.addParams(Session.class);
-
- validCloseParams = new ParamList();
- validCloseParams.addParams(int.class,String.class);
- validCloseParams.addParams(Session.class,int.class,String.class);
-
- validErrorParams = new ParamList();
- validErrorParams.addParams(Throwable.class);
- validErrorParams.addParams(Session.class,Throwable.class);
-
- validTextParams = new ParamList();
- validTextParams.addParams(String.class);
- validTextParams.addParams(Session.class,String.class);
- validTextParams.addParams(Reader.class);
- validTextParams.addParams(Session.class,Reader.class);
-
- validBinaryParams = new ParamList();
- validBinaryParams.addParams(byte[].class,int.class,int.class);
- validBinaryParams.addParams(Session.class,byte[].class,int.class,int.class);
- validBinaryParams.addParams(InputStream.class);
- validBinaryParams.addParams(Session.class,InputStream.class);
-
- validFrameParams = new ParamList();
- validFrameParams.addParams(Frame.class);
- validFrameParams.addParams(Session.class,Frame.class);
- }
-
- private ConcurrentHashMap<Class<?>, EventMethods> cache;
+ private static final Logger LOG = Log.getLogger(EventDriverFactory.class);
private final WebSocketPolicy policy;
+ private final List<EventDriverImpl> implementations;
public EventDriverFactory(WebSocketPolicy policy)
{
this.policy = policy;
- this.cache = new ConcurrentHashMap<>();
+ this.implementations = new ArrayList<>();
+
+ addImplementation(new JettyListenerImpl());
+ addImplementation(new JettyAnnotatedImpl());
}
- private void assertIsPublicNonStatic(Method method)
+ public void addImplementation(EventDriverImpl impl)
{
- int mods = method.getModifiers();
- if (!Modifier.isPublic(mods))
+ if (implementations.contains(impl))
{
- StringBuilder err = new StringBuilder();
- err.append("Invalid declaration of ");
- err.append(method);
- err.append(StringUtil.__LINE_SEPARATOR);
-
- err.append("Method modifier must be public");
-
- throw new InvalidWebSocketException(err.toString());
+ LOG.warn("Ignoring attempt to add duplicate EventDriverImpl: " + impl);
+ return;
}
- if (Modifier.isStatic(mods))
- {
- StringBuilder err = new StringBuilder();
- err.append("Invalid declaration of ");
- err.append(method);
- err.append(StringUtil.__LINE_SEPARATOR);
-
- err.append("Method modifier may not be static");
-
- throw new InvalidWebSocketException(err.toString());
- }
+ implementations.add(impl);
}
- private void assertIsReturn(Method method, Class<?> type)
+ public void clearImplementations()
{
- if (!type.equals(method.getReturnType()))
- {
- StringBuilder err = new StringBuilder();
- err.append("Invalid declaration of ");
- err.append(method);
- err.append(StringUtil.__LINE_SEPARATOR);
-
- err.append("Return type must be ").append(type);
-
- throw new InvalidWebSocketException(err.toString());
- }
+ this.implementations.clear();
}
- private void assertUnset(EventMethod event, Class<? extends Annotation> annoClass, Method method)
+ protected String getClassName(Object websocket)
{
- if (event != null)
- {
- // Attempt to add duplicate frame type (a no-no)
- StringBuilder err = new StringBuilder();
- err.append("Duplicate @").append(annoClass.getSimpleName()).append(" declaration on ");
- err.append(method);
- err.append(StringUtil.__LINE_SEPARATOR);
-
- err.append("@").append(annoClass.getSimpleName()).append(" previously declared at ");
- err.append(event.getMethod());
-
- throw new InvalidWebSocketException(err.toString());
- }
+ return websocket.getClass().getName();
}
- private void assertValidSignature(Method method, Class<? extends Annotation> annoClass, ParamList validParams)
+ public List<EventDriverImpl> getImplementations()
{
- assertIsPublicNonStatic(method);
- assertIsReturn(method,Void.TYPE);
-
- boolean valid = false;
-
- // validate parameters
- Class<?> actual[] = method.getParameterTypes();
- for (Class<?>[] params : validParams)
- {
- if (isSameParameters(actual,params))
- {
- valid = true;
- break;
- }
- }
-
- if (!valid)
- {
- throw InvalidSignatureException.build(method,annoClass,validParams);
- }
+ return implementations;
}
- /**
- * Perform the basic discovery mechanism for WebSocket events from the provided pojo.
- *
- * @param pojo
- * the pojo to scan
- * @return the discovered event methods
- * @throws InvalidWebSocketException
- */
- private EventMethods discoverMethods(Class<?> pojo) throws InvalidWebSocketException
+ public boolean removeImplementation(EventDriverImpl impl)
{
- WebSocket anno = pojo.getAnnotation(WebSocket.class);
- if (anno == null)
- {
- return null;
- }
-
- return scanAnnotatedMethods(pojo);
- }
-
- public EventMethods getMethods(Class<?> pojo) throws InvalidWebSocketException
- {
- if (pojo == null)
- {
- throw new InvalidWebSocketException("Cannot get methods for null class");
- }
- if (cache.containsKey(pojo))
- {
- return cache.get(pojo);
- }
- EventMethods methods = discoverMethods(pojo);
- if (methods == null)
- {
- return null;
- }
- cache.put(pojo,methods);
- return methods;
- }
-
- private boolean isSameParameters(Class<?>[] actual, Class<?>[] params)
- {
- if (actual.length != params.length)
- {
- // skip
- return false;
- }
-
- int len = params.length;
- for (int i = 0; i < len; i++)
- {
- if (!actual[i].equals(params[i]))
- {
- return false; // not valid
- }
- }
-
- return true;
- }
-
- private boolean isSignatureMatch(Method method, ParamList validParams)
- {
- assertIsPublicNonStatic(method);
- assertIsReturn(method,Void.TYPE);
-
- // validate parameters
- Class<?> actual[] = method.getParameterTypes();
- for (Class<?>[] params : validParams)
- {
- if (isSameParameters(actual,params))
- {
- return true;
- }
- }
-
- return false;
- }
-
- private EventMethods scanAnnotatedMethods(Class<?> pojo)
- {
- Class<?> clazz = pojo;
- EventMethods events = new EventMethods(pojo);
-
- clazz = pojo;
- while (clazz.getAnnotation(WebSocket.class) != null)
- {
- for (Method method : clazz.getDeclaredMethods())
- {
- if (method.getAnnotation(OnWebSocketConnect.class) != null)
- {
- assertValidSignature(method,OnWebSocketConnect.class,validConnectParams);
- assertUnset(events.onConnect,OnWebSocketConnect.class,method);
- events.onConnect = new EventMethod(pojo,method);
- continue;
- }
-
- if (method.getAnnotation(OnWebSocketMessage.class) != null)
- {
- if (isSignatureMatch(method,validTextParams))
- {
- // Text mode
- // TODO
-
- assertUnset(events.onText,OnWebSocketMessage.class,method);
- events.onText = new EventMethod(pojo,method);
- continue;
- }
-
- if (isSignatureMatch(method,validBinaryParams))
- {
- // Binary Mode
- // TODO
- assertUnset(events.onBinary,OnWebSocketMessage.class,method);
- events.onBinary = new EventMethod(pojo,method);
- continue;
- }
-
- throw InvalidSignatureException.build(method,OnWebSocketMessage.class,validTextParams,validBinaryParams);
- }
-
- if (method.getAnnotation(OnWebSocketClose.class) != null)
- {
- assertValidSignature(method,OnWebSocketClose.class,validCloseParams);
- assertUnset(events.onClose,OnWebSocketClose.class,method);
- events.onClose = new EventMethod(pojo,method);
- continue;
- }
-
- if (method.getAnnotation(OnWebSocketError.class) != null)
- {
- assertValidSignature(method,OnWebSocketError.class,validErrorParams);
- assertUnset(events.onError,OnWebSocketError.class,method);
- events.onError = new EventMethod(pojo,method);
- continue;
- }
-
- if (method.getAnnotation(OnWebSocketFrame.class) != null)
- {
- assertValidSignature(method,OnWebSocketFrame.class,validFrameParams);
- assertUnset(events.onFrame,OnWebSocketFrame.class,method);
- events.onFrame = new EventMethod(pojo,method);
- continue;
- }
-
- // Not a tagged method we are interested in, ignore
- }
-
- // try superclass now
- clazz = clazz.getSuperclass();
- }
-
- return events;
+ return this.implementations.remove(impl);
}
@Override
public String toString()
{
- return String.format("EventMethodsCache [cache.count=%d]",cache.size());
+ StringBuilder msg = new StringBuilder();
+ msg.append(this.getClass().getSimpleName());
+ msg.append("[implementations=[");
+ boolean delim = false;
+ for (EventDriverImpl impl : implementations)
+ {
+ if (delim)
+ {
+ msg.append(',');
+ }
+ msg.append(impl.toString());
+ delim = true;
+ }
+ msg.append("]");
+ return msg.toString();
}
/**
@@ -368,21 +111,39 @@
throw new InvalidWebSocketException("null websocket object");
}
- if (websocket instanceof WebSocketListener)
+ for (EventDriverImpl impl : implementations)
{
- WebSocketPolicy pojoPolicy = policy.clonePolicy();
- WebSocketListener listener = (WebSocketListener)websocket;
- return new ListenerEventDriver(pojoPolicy,listener);
+ if (impl.supports(websocket))
+ {
+ try
+ {
+ return impl.create(websocket,policy.clonePolicy());
+ }
+ catch (Throwable e)
+ {
+ throw new InvalidWebSocketException("Unable to create websocket",e);
+ }
+ }
}
- EventMethods methods = getMethods(websocket.getClass());
- if (methods != null)
+ // Create a clear error message for the developer
+ StringBuilder err = new StringBuilder();
+ err.append(getClassName(websocket));
+ err.append(" is not a valid WebSocket object.");
+ err.append(" Object must obey one of the following rules: ");
+
+ int len = implementations.size();
+ for (int i = 0; i < len; i++)
{
- WebSocketPolicy pojoPolicy = policy.clonePolicy();
- return new AnnotatedEventDriver(pojoPolicy,websocket,methods);
+ EventDriverImpl impl = implementations.get(i);
+ if (i > 0)
+ {
+ err.append(" or ");
+ }
+ err.append("\n(").append(i + 1).append(") ");
+ err.append(impl.describeRule());
}
- throw new InvalidWebSocketException(websocket.getClass().getName() + " does not implement " + WebSocketListener.class.getName()
- + " or declare @WebSocket");
+ throw new InvalidWebSocketException(err.toString());
}
}
diff --git a/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/events/EventDriverImpl.java b/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/events/EventDriverImpl.java
new file mode 100644
index 0000000..1ea9b43
--- /dev/null
+++ b/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/events/EventDriverImpl.java
@@ -0,0 +1,58 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.common.events;
+
+import org.eclipse.jetty.websocket.api.WebSocketPolicy;
+
+/**
+ * A specific implementation of a EventDriver.
+ */
+public interface EventDriverImpl
+{
+ /**
+ * Create the EventDriver based on this implementation.
+ *
+ * @param websocket
+ * the websocket to wrap
+ * @param policy
+ * the policy to use
+ * @return the created EventDriver
+ * @throws Throwable
+ * if unable to create the EventDriver
+ */
+ EventDriver create(Object websocket, WebSocketPolicy policy) throws Throwable;
+
+ /**
+ * human readable string describing the rule that would support this EventDriver.
+ * <p>
+ * Used to help developer with possible object annotations, listeners, or base classes.
+ *
+ * @return the human readable description of this event driver rule(s).
+ */
+ String describeRule();
+
+ /**
+ * Test for if this implementation can support the provided websocket.
+ *
+ * @param websocket
+ * the possible websocket to test
+ * @return true if implementation can support it, false if otherwise.
+ */
+ boolean supports(Object websocket);
+}
diff --git a/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/events/EventMethod.java b/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/events/EventMethod.java
deleted file mode 100644
index e551d26..0000000
--- a/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/events/EventMethod.java
+++ /dev/null
@@ -1,153 +0,0 @@
-//
-// ========================================================================
-// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
-// ------------------------------------------------------------------------
-// All rights reserved. This program and the accompanying materials
-// are made available under the terms of the Eclipse Public License v1.0
-// and Apache License v2.0 which accompanies this distribution.
-//
-// The Eclipse Public License is available at
-// http://www.eclipse.org/legal/epl-v10.html
-//
-// The Apache License v2.0 is available at
-// http://www.opensource.org/licenses/apache2.0.php
-//
-// You may elect to redistribute this code under either of these licenses.
-// ========================================================================
-//
-
-package org.eclipse.jetty.websocket.common.events;
-
-import java.io.InputStream;
-import java.io.Reader;
-import java.lang.reflect.InvocationTargetException;
-import java.lang.reflect.Method;
-
-import org.eclipse.jetty.util.log.Log;
-import org.eclipse.jetty.util.log.Logger;
-import org.eclipse.jetty.websocket.api.Session;
-import org.eclipse.jetty.websocket.api.WebSocketException;
-
-public class EventMethod
-{
- private static final Logger LOG = Log.getLogger(EventMethod.class);
-
- private static Object[] dropFirstArg(Object[] args)
- {
- if (args.length == 1)
- {
- return new Object[0];
- }
- Object ret[] = new Object[args.length - 1];
- System.arraycopy(args,1,ret,0,ret.length);
- return ret;
- }
-
- protected Class<?> pojo;
- protected Method method;
- private boolean hasSession = false;
- private boolean isStreaming = false;
- private Class<?>[] paramTypes;
-
- public EventMethod(Class<?> pojo, Method method)
- {
- this.pojo = pojo;
- this.paramTypes = method.getParameterTypes();
- this.method = method;
- identifyPresentParamTypes();
- }
-
- public EventMethod(Class<?> pojo, String methodName, Class<?>... paramTypes)
- {
- try
- {
- this.pojo = pojo;
- this.paramTypes = paramTypes;
- this.method = pojo.getMethod(methodName,paramTypes);
- identifyPresentParamTypes();
- }
- catch (NoSuchMethodException | SecurityException e)
- {
- LOG.warn("Cannot use method {}({}): {}",methodName,paramTypes,e.getMessage());
- this.method = null;
- }
- }
-
- public void call(Object obj, Object... args)
- {
- if ((this.pojo == null) || (this.method == null))
- {
- LOG.warn("Cannot execute call: pojo={}, method={}",pojo,method);
- return; // no call event method determined
- }
- if (obj == null)
- {
- LOG.warn("Cannot call {} on null object",this.method);
- return;
- }
- if (args.length > paramTypes.length)
- {
- Object trimArgs[] = dropFirstArg(args);
- call(obj,trimArgs);
- return;
- }
- if (args.length < paramTypes.length)
- {
- throw new IllegalArgumentException("Call arguments length [" + args.length + "] must always be greater than or equal to captured args length ["
- + paramTypes.length + "]");
- }
-
- try
- {
- this.method.invoke(obj,args);
- }
- catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e)
- {
- String err = String.format("Cannot call method %s on %s with args: %s",method,pojo,args);
- throw new WebSocketException(err,e);
- }
- }
-
- protected Method getMethod()
- {
- return method;
- }
-
- protected Class<?>[] getParamTypes()
- {
- return this.paramTypes;
- }
-
- private void identifyPresentParamTypes()
- {
- this.hasSession = false;
- this.isStreaming = false;
-
- if (paramTypes == null)
- {
- return;
- }
-
- for (Class<?> paramType : paramTypes)
- {
- if (Session.class.isAssignableFrom(paramType))
- {
- this.hasSession = true;
- }
- if (Reader.class.isAssignableFrom(paramType) || InputStream.class.isAssignableFrom(paramType))
- {
- this.isStreaming = true;
- }
- }
- }
-
- public boolean isHasSession()
- {
- return hasSession;
- }
-
- public boolean isStreaming()
- {
- return isStreaming;
- }
-}
\ No newline at end of file
diff --git a/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/events/EventMethods.java b/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/events/EventMethods.java
deleted file mode 100644
index 80e2bfe..0000000
--- a/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/events/EventMethods.java
+++ /dev/null
@@ -1,107 +0,0 @@
-//
-// ========================================================================
-// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
-// ------------------------------------------------------------------------
-// All rights reserved. This program and the accompanying materials
-// are made available under the terms of the Eclipse Public License v1.0
-// and Apache License v2.0 which accompanies this distribution.
-//
-// The Eclipse Public License is available at
-// http://www.eclipse.org/legal/epl-v10.html
-//
-// The Apache License v2.0 is available at
-// http://www.opensource.org/licenses/apache2.0.php
-//
-// You may elect to redistribute this code under either of these licenses.
-// ========================================================================
-//
-
-package org.eclipse.jetty.websocket.common.events;
-
-/**
- * A representation of the methods available to call for a particular class.
- * <p>
- * This class used to cache the method lookups via the {@link EventMethodsCache}
- */
-public class EventMethods
-{
- private Class<?> pojoClass;
- public EventMethod onConnect = null;
- public EventMethod onClose = null;
- public EventMethod onBinary = null;
- public EventMethod onText = null;
- public EventMethod onError = null;
- public EventMethod onFrame = null;
-
- public EventMethods(Class<?> pojoClass)
- {
- this.pojoClass = pojoClass;
- }
-
- @Override
- public boolean equals(Object obj)
- {
- if (this == obj)
- {
- return true;
- }
- if (obj == null)
- {
- return false;
- }
- if (getClass() != obj.getClass())
- {
- return false;
- }
- EventMethods other = (EventMethods)obj;
- if (pojoClass == null)
- {
- if (other.pojoClass != null)
- {
- return false;
- }
- }
- else if (!pojoClass.getName().equals(other.pojoClass.getName()))
- {
- return false;
- }
- return true;
- }
-
- public Class<?> getPojoClass()
- {
- return pojoClass;
- }
-
- @Override
- public int hashCode()
- {
- final int prime = 31;
- int result = 1;
- result = (prime * result) + ((pojoClass == null)?0:pojoClass.getName().hashCode());
- return result;
- }
-
- @Override
- public String toString()
- {
- StringBuilder builder = new StringBuilder();
- builder.append("EventMethods [pojoClass=");
- builder.append(pojoClass);
- builder.append(", onConnect=");
- builder.append(onConnect);
- builder.append(", onClose=");
- builder.append(onClose);
- builder.append(", onBinary=");
- builder.append(onBinary);
- builder.append(", onText=");
- builder.append(onText);
- builder.append(", onException=");
- builder.append(onError);
- builder.append(", onFrame=");
- builder.append(onFrame);
- builder.append("]");
- return builder.toString();
- }
-
-}
diff --git a/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/events/InvalidSignatureException.java b/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/events/InvalidSignatureException.java
deleted file mode 100644
index bae7e42..0000000
--- a/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/events/InvalidSignatureException.java
+++ /dev/null
@@ -1,73 +0,0 @@
-//
-// ========================================================================
-// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
-// ------------------------------------------------------------------------
-// All rights reserved. This program and the accompanying materials
-// are made available under the terms of the Eclipse Public License v1.0
-// and Apache License v2.0 which accompanies this distribution.
-//
-// The Eclipse Public License is available at
-// http://www.eclipse.org/legal/epl-v10.html
-//
-// The Apache License v2.0 is available at
-// http://www.opensource.org/licenses/apache2.0.php
-//
-// You may elect to redistribute this code under either of these licenses.
-// ========================================================================
-//
-
-package org.eclipse.jetty.websocket.common.events;
-
-import java.lang.annotation.Annotation;
-import java.lang.reflect.Method;
-
-import org.eclipse.jetty.util.StringUtil;
-import org.eclipse.jetty.websocket.api.InvalidWebSocketException;
-
-@SuppressWarnings("serial")
-public class InvalidSignatureException extends InvalidWebSocketException
-{
- public static InvalidSignatureException build(Method method, Class<? extends Annotation> annoClass, ParamList... paramlists)
- {
- // Build big detailed exception to help the developer
- StringBuilder err = new StringBuilder();
- err.append("Invalid declaration of ");
- err.append(method);
- err.append(StringUtil.__LINE_SEPARATOR);
-
- err.append("Acceptable method declarations for @");
- err.append(annoClass.getSimpleName());
- err.append(" are:");
- for (ParamList validParams : paramlists)
- {
- for (Class<?>[] params : validParams)
- {
- err.append(StringUtil.__LINE_SEPARATOR);
- err.append("public void ").append(method.getName());
- err.append('(');
- boolean delim = false;
- for (Class<?> type : params)
- {
- if (delim)
- {
- err.append(',');
- }
- err.append(' ');
- err.append(type.getName());
- if (type.isArray())
- {
- err.append("[]");
- }
- delim = true;
- }
- err.append(')');
- }
- }
- return new InvalidSignatureException(err.toString());
- }
-
- public InvalidSignatureException(String message)
- {
- super(message);
- }
-}
diff --git a/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/events/JettyAnnotatedEventDriver.java b/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/events/JettyAnnotatedEventDriver.java
new file mode 100644
index 0000000..a2bec04
--- /dev/null
+++ b/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/events/JettyAnnotatedEventDriver.java
@@ -0,0 +1,218 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.common.events;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.Reader;
+import java.nio.ByteBuffer;
+
+import org.eclipse.jetty.websocket.api.WebSocketPolicy;
+import org.eclipse.jetty.websocket.api.annotations.WebSocket;
+import org.eclipse.jetty.websocket.api.extensions.Frame;
+import org.eclipse.jetty.websocket.common.CloseInfo;
+import org.eclipse.jetty.websocket.common.message.MessageAppender;
+import org.eclipse.jetty.websocket.common.message.MessageInputStream;
+import org.eclipse.jetty.websocket.common.message.MessageReader;
+import org.eclipse.jetty.websocket.common.message.SimpleBinaryMessage;
+import org.eclipse.jetty.websocket.common.message.SimpleTextMessage;
+
+/**
+ * Handler for Annotated User WebSocket objects.
+ */
+public class JettyAnnotatedEventDriver extends AbstractEventDriver
+{
+ private final JettyAnnotatedMetadata events;
+ private boolean hasCloseBeenCalled = false;
+
+ public JettyAnnotatedEventDriver(WebSocketPolicy policy, Object websocket, JettyAnnotatedMetadata events)
+ {
+ super(policy,websocket);
+ this.events = events;
+
+ WebSocket anno = websocket.getClass().getAnnotation(WebSocket.class);
+ // Setup the policy
+ if (anno.maxTextMessageSize() > 0)
+ {
+ this.policy.setMaxTextMessageSize(anno.maxTextMessageSize());
+ }
+ if (anno.maxBinaryMessageSize() > 0)
+ {
+ this.policy.setMaxTextMessageSize(anno.maxBinaryMessageSize());
+ }
+ if (anno.inputBufferSize() > 0)
+ {
+ this.policy.setInputBufferSize(anno.inputBufferSize());
+ }
+ if (anno.maxIdleTime() > 0)
+ {
+ this.policy.setIdleTimeout(anno.maxIdleTime());
+ }
+ }
+
+ @Override
+ public void onBinaryFrame(ByteBuffer buffer, boolean fin) throws IOException
+ {
+ if (events.onBinary == null)
+ {
+ // not interested in binary events
+ return;
+ }
+
+ if (activeMessage == null)
+ {
+ if (events.onBinary.isStreaming())
+ {
+ activeMessage = new MessageInputStream(session.getConnection());
+ final MessageAppender msg = activeMessage;
+ dispatch(new Runnable()
+ {
+ @Override
+ public void run()
+ {
+ events.onBinary.call(websocket,session,msg);
+ }
+ });
+ }
+ else
+ {
+ activeMessage = new SimpleBinaryMessage(this);
+ }
+ }
+
+ appendMessage(buffer,fin);
+ }
+
+ @Override
+ public void onBinaryMessage(byte[] data)
+ {
+ if (events.onBinary != null)
+ {
+ events.onBinary.call(websocket,session,data,0,data.length);
+ }
+ }
+
+ @Override
+ public void onClose(CloseInfo close)
+ {
+ if (hasCloseBeenCalled)
+ {
+ // avoid duplicate close events (possible when using harsh Session.disconnect())
+ return;
+ }
+ hasCloseBeenCalled = true;
+ if (events.onClose != null)
+ {
+ events.onClose.call(websocket,session,close.getStatusCode(),close.getReason());
+ }
+ }
+
+ @Override
+ public void onConnect()
+ {
+ if (events.onConnect != null)
+ {
+ events.onConnect.call(websocket,session);
+ }
+ }
+
+ @Override
+ public void onError(Throwable cause)
+ {
+ if (events.onError != null)
+ {
+ events.onError.call(websocket,session,cause);
+ }
+ }
+
+ @Override
+ public void onFrame(Frame frame)
+ {
+ if (events.onFrame != null)
+ {
+ events.onFrame.call(websocket,session,frame);
+ }
+ }
+
+ @Override
+ public void onInputStream(InputStream stream)
+ {
+ if (events.onBinary != null)
+ {
+ events.onBinary.call(websocket,session,stream);
+ }
+ }
+
+ @Override
+ public void onReader(Reader reader)
+ {
+ if (events.onText != null)
+ {
+ events.onText.call(websocket,session,reader);
+ }
+ }
+
+ @Override
+ public void onTextFrame(ByteBuffer buffer, boolean fin) throws IOException
+ {
+ if (events.onText == null)
+ {
+ // not interested in text events
+ return;
+ }
+
+ if (activeMessage == null)
+ {
+ if (events.onText.isStreaming())
+ {
+ activeMessage = new MessageReader(new MessageInputStream(session.getConnection()));
+ final MessageAppender msg = activeMessage;
+ dispatch(new Runnable()
+ {
+ @Override
+ public void run()
+ {
+ events.onText.call(websocket,session,msg);
+ }
+ });
+ }
+ else
+ {
+ activeMessage = new SimpleTextMessage(this);
+ }
+ }
+
+ appendMessage(buffer,fin);
+ }
+
+ @Override
+ public void onTextMessage(String message)
+ {
+ if (events.onText != null)
+ {
+ events.onText.call(websocket,session,message);
+ }
+ }
+
+ @Override
+ public String toString()
+ {
+ return String.format("%s[%s]", this.getClass().getSimpleName(), websocket);
+ }
+}
diff --git a/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/events/JettyAnnotatedImpl.java b/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/events/JettyAnnotatedImpl.java
new file mode 100644
index 0000000..07cf5e5
--- /dev/null
+++ b/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/events/JettyAnnotatedImpl.java
@@ -0,0 +1,65 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.common.events;
+
+import java.util.concurrent.ConcurrentHashMap;
+
+import org.eclipse.jetty.websocket.api.WebSocketPolicy;
+import org.eclipse.jetty.websocket.api.annotations.WebSocket;
+
+public class JettyAnnotatedImpl implements EventDriverImpl
+{
+ private ConcurrentHashMap<Class<?>, JettyAnnotatedMetadata> cache = new ConcurrentHashMap<>();
+
+ @Override
+ public EventDriver create(Object websocket, WebSocketPolicy policy)
+ {
+ Class<?> websocketClass = websocket.getClass();
+ synchronized (this)
+ {
+ JettyAnnotatedMetadata metadata = cache.get(websocketClass);
+ if (metadata == null)
+ {
+ JettyAnnotatedScanner scanner = new JettyAnnotatedScanner();
+ metadata = scanner.scan(websocketClass);
+ cache.put(websocketClass,metadata);
+ }
+ return new JettyAnnotatedEventDriver(policy,websocket,metadata);
+ }
+ }
+
+ @Override
+ public String describeRule()
+ {
+ return "class is annotated with @" + WebSocket.class.getName();
+ }
+
+ @Override
+ public boolean supports(Object websocket)
+ {
+ WebSocket anno = websocket.getClass().getAnnotation(WebSocket.class);
+ return (anno != null);
+ }
+
+ @Override
+ public String toString()
+ {
+ return String.format("%s [cache.count=%d]",this.getClass().getSimpleName(),cache.size());
+ }
+}
diff --git a/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/events/JettyAnnotatedMetadata.java b/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/events/JettyAnnotatedMetadata.java
new file mode 100644
index 0000000..4a5265f
--- /dev/null
+++ b/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/events/JettyAnnotatedMetadata.java
@@ -0,0 +1,53 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.common.events;
+
+import org.eclipse.jetty.websocket.common.events.annotated.CallableMethod;
+import org.eclipse.jetty.websocket.common.events.annotated.OptionalSessionCallableMethod;
+
+public class JettyAnnotatedMetadata
+{
+ /** @OnWebSocketConnect () */
+ public CallableMethod onConnect;
+ /** @OnWebSocketMessage (byte[], or ByteBuffer, or InputStream) */
+ public OptionalSessionCallableMethod onBinary;
+ /** @OnWebSocketMessage (String, or Reader) */
+ public OptionalSessionCallableMethod onText;
+ /** @OnWebSocketFrame (Frame) */
+ public OptionalSessionCallableMethod onFrame;
+ /** @OnWebSocketError (Throwable) */
+ public OptionalSessionCallableMethod onError;
+ /** @OnWebSocketClose (Frame) */
+ public OptionalSessionCallableMethod onClose;
+
+ @Override
+ public String toString()
+ {
+ StringBuilder s = new StringBuilder();
+ s.append("JettyPojoMetadata[");
+ s.append("onConnect=").append(onConnect);
+ s.append(",onBinary=").append(onBinary);
+ s.append(",onText=").append(onText);
+ s.append(",onFrame=").append(onFrame);
+ s.append(",onError=").append(onError);
+ s.append(",onClose=").append(onClose);
+ s.append("]");
+ return s.toString();
+ }
+}
diff --git a/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/events/JettyAnnotatedScanner.java b/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/events/JettyAnnotatedScanner.java
new file mode 100644
index 0000000..eb8c2af
--- /dev/null
+++ b/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/events/JettyAnnotatedScanner.java
@@ -0,0 +1,170 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.common.events;
+
+import java.io.InputStream;
+import java.io.Reader;
+import java.lang.annotation.Annotation;
+import java.lang.reflect.Method;
+
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+import org.eclipse.jetty.websocket.api.Session;
+import org.eclipse.jetty.websocket.api.annotations.OnWebSocketClose;
+import org.eclipse.jetty.websocket.api.annotations.OnWebSocketConnect;
+import org.eclipse.jetty.websocket.api.annotations.OnWebSocketError;
+import org.eclipse.jetty.websocket.api.annotations.OnWebSocketFrame;
+import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage;
+import org.eclipse.jetty.websocket.api.extensions.Frame;
+import org.eclipse.jetty.websocket.common.events.annotated.AbstractMethodAnnotationScanner;
+import org.eclipse.jetty.websocket.common.events.annotated.CallableMethod;
+import org.eclipse.jetty.websocket.common.events.annotated.InvalidSignatureException;
+import org.eclipse.jetty.websocket.common.events.annotated.OptionalSessionCallableMethod;
+
+public class JettyAnnotatedScanner extends AbstractMethodAnnotationScanner<JettyAnnotatedMetadata>
+{
+ private static final Logger LOG = Log.getLogger(JettyAnnotatedScanner.class);
+
+ /**
+ * Parameter list for @OnWebSocketMessage (Binary mode)
+ */
+ private static final ParamList validBinaryParams;
+
+ /**
+ * Parameter list for @OnWebSocketConnect
+ */
+ private static final ParamList validConnectParams;
+
+ /**
+ * Parameter list for @OnWebSocketClose
+ */
+ private static final ParamList validCloseParams;
+
+ /**
+ * Parameter list for @OnWebSocketError
+ */
+ private static final ParamList validErrorParams;
+
+ /**
+ * Parameter list for @OnWebSocketFrame
+ */
+ private static final ParamList validFrameParams;
+
+ /**
+ * Parameter list for @OnWebSocketMessage (Text mode)
+ */
+ private static final ParamList validTextParams;
+
+ static
+ {
+ validConnectParams = new ParamList();
+ validConnectParams.addParams(Session.class);
+
+ validCloseParams = new ParamList();
+ validCloseParams.addParams(int.class,String.class);
+ validCloseParams.addParams(Session.class,int.class,String.class);
+
+ validErrorParams = new ParamList();
+ validErrorParams.addParams(Throwable.class);
+ validErrorParams.addParams(Session.class,Throwable.class);
+
+ validTextParams = new ParamList();
+ validTextParams.addParams(String.class);
+ validTextParams.addParams(Session.class,String.class);
+ validTextParams.addParams(Reader.class);
+ validTextParams.addParams(Session.class,Reader.class);
+
+ validBinaryParams = new ParamList();
+ validBinaryParams.addParams(byte[].class,int.class,int.class);
+ validBinaryParams.addParams(Session.class,byte[].class,int.class,int.class);
+ validBinaryParams.addParams(InputStream.class);
+ validBinaryParams.addParams(Session.class,InputStream.class);
+
+ validFrameParams = new ParamList();
+ validFrameParams.addParams(Frame.class);
+ validFrameParams.addParams(Session.class,Frame.class);
+ }
+
+ @Override
+ public void onMethodAnnotation(JettyAnnotatedMetadata metadata, Class<?> pojo, Method method, Annotation annotation)
+ {
+ LOG.debug("onMethodAnnotation({}, {}, {}, {})",metadata,pojo,method,annotation);
+
+ if (isAnnotation(annotation,OnWebSocketConnect.class))
+ {
+ assertValidSignature(method,OnWebSocketConnect.class,validConnectParams);
+ assertUnset(metadata.onConnect,OnWebSocketConnect.class,method);
+ metadata.onConnect = new CallableMethod(pojo,method);
+ return;
+ }
+
+ if (isAnnotation(annotation,OnWebSocketMessage.class))
+ {
+ if (isSignatureMatch(method,validTextParams))
+ {
+ // Text mode
+ assertUnset(metadata.onText,OnWebSocketMessage.class,method);
+ metadata.onText = new OptionalSessionCallableMethod(pojo,method);
+ return;
+ }
+
+ if (isSignatureMatch(method,validBinaryParams))
+ {
+ // Binary Mode
+ // TODO
+ assertUnset(metadata.onBinary,OnWebSocketMessage.class,method);
+ metadata.onBinary = new OptionalSessionCallableMethod(pojo,method);
+ return;
+ }
+
+ throw InvalidSignatureException.build(method,OnWebSocketMessage.class,validTextParams,validBinaryParams);
+ }
+
+ if (isAnnotation(annotation,OnWebSocketClose.class))
+ {
+ assertValidSignature(method,OnWebSocketClose.class,validCloseParams);
+ assertUnset(metadata.onClose,OnWebSocketClose.class,method);
+ metadata.onClose = new OptionalSessionCallableMethod(pojo,method);
+ return;
+ }
+
+ if (isAnnotation(annotation,OnWebSocketError.class))
+ {
+ assertValidSignature(method,OnWebSocketError.class,validErrorParams);
+ assertUnset(metadata.onError,OnWebSocketError.class,method);
+ metadata.onError = new OptionalSessionCallableMethod(pojo,method);
+ return;
+ }
+
+ if (isAnnotation(annotation,OnWebSocketFrame.class))
+ {
+ assertValidSignature(method,OnWebSocketFrame.class,validFrameParams);
+ assertUnset(metadata.onFrame,OnWebSocketFrame.class,method);
+ metadata.onFrame = new OptionalSessionCallableMethod(pojo,method);
+ return;
+ }
+ }
+
+ public JettyAnnotatedMetadata scan(Class<?> pojo)
+ {
+ JettyAnnotatedMetadata metadata = new JettyAnnotatedMetadata();
+ scanMethodAnnotations(metadata,pojo);
+ return metadata;
+ }
+}
diff --git a/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/events/JettyListenerEventDriver.java b/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/events/JettyListenerEventDriver.java
new file mode 100644
index 0000000..a122b25
--- /dev/null
+++ b/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/events/JettyListenerEventDriver.java
@@ -0,0 +1,135 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.common.events;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.Reader;
+import java.nio.ByteBuffer;
+
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+import org.eclipse.jetty.websocket.api.WebSocketListener;
+import org.eclipse.jetty.websocket.api.WebSocketPolicy;
+import org.eclipse.jetty.websocket.api.extensions.Frame;
+import org.eclipse.jetty.websocket.common.CloseInfo;
+import org.eclipse.jetty.websocket.common.message.SimpleBinaryMessage;
+import org.eclipse.jetty.websocket.common.message.SimpleTextMessage;
+
+/**
+ * Handler for {@link WebSocketListener} based User WebSocket implementations.
+ */
+public class JettyListenerEventDriver extends AbstractEventDriver
+{
+ private static final Logger LOG = Log.getLogger(JettyListenerEventDriver.class);
+ private final WebSocketListener listener;
+ private boolean hasCloseBeenCalled = false;
+
+ public JettyListenerEventDriver(WebSocketPolicy policy, WebSocketListener listener)
+ {
+ super(policy,listener);
+ this.listener = listener;
+ }
+
+ @Override
+ public void onBinaryFrame(ByteBuffer buffer, boolean fin) throws IOException
+ {
+ if (activeMessage == null)
+ {
+ activeMessage = new SimpleBinaryMessage(this);
+ }
+
+ appendMessage(buffer,fin);
+ }
+
+ @Override
+ public void onBinaryMessage(byte[] data)
+ {
+ listener.onWebSocketBinary(data,0,data.length);
+ }
+
+ @Override
+ public void onClose(CloseInfo close)
+ {
+ if (hasCloseBeenCalled)
+ {
+ // avoid duplicate close events (possible when using harsh Session.disconnect())
+ return;
+ }
+ hasCloseBeenCalled = true;
+
+ int statusCode = close.getStatusCode();
+ String reason = close.getReason();
+ listener.onWebSocketClose(statusCode,reason);
+ }
+
+ @Override
+ public void onConnect()
+ {
+ LOG.debug("onConnect()");
+ listener.onWebSocketConnect(session);
+ }
+
+ @Override
+ public void onError(Throwable cause)
+ {
+ listener.onWebSocketError(cause);
+ }
+
+ @Override
+ public void onFrame(Frame frame)
+ {
+ /* ignore, not supported by WebSocketListener */
+ }
+
+ @Override
+ public void onInputStream(InputStream stream)
+ {
+ /* not supported in Listener mode (yet) */
+ }
+
+ @Override
+ public void onReader(Reader reader)
+ {
+ /* not supported in Listener mode (yet) */
+ }
+
+ @Override
+ public void onTextFrame(ByteBuffer buffer, boolean fin) throws IOException
+ {
+ if (activeMessage == null)
+ {
+ activeMessage = new SimpleTextMessage(this);
+ }
+
+ appendMessage(buffer,fin);
+ }
+
+ @Override
+ public void onTextMessage(String message)
+ {
+ listener.onWebSocketText(message);
+ }
+
+ @Override
+ public String toString()
+ {
+ return String.format("%s[%s]",JettyListenerEventDriver.class.getSimpleName(),listener.getClass().getName());
+ }
+}
diff --git a/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/events/JettyListenerImpl.java b/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/events/JettyListenerImpl.java
new file mode 100644
index 0000000..d62e9e7
--- /dev/null
+++ b/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/events/JettyListenerImpl.java
@@ -0,0 +1,44 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.common.events;
+
+import org.eclipse.jetty.websocket.api.WebSocketListener;
+import org.eclipse.jetty.websocket.api.WebSocketPolicy;
+
+public class JettyListenerImpl implements EventDriverImpl
+{
+ @Override
+ public EventDriver create(Object websocket, WebSocketPolicy policy)
+ {
+ WebSocketListener listener = (WebSocketListener)websocket;
+ return new JettyListenerEventDriver(policy,listener);
+ }
+
+ @Override
+ public String describeRule()
+ {
+ return "class implements " + WebSocketListener.class.getName();
+ }
+
+ @Override
+ public boolean supports(Object websocket)
+ {
+ return (websocket instanceof WebSocketListener);
+ }
+}
diff --git a/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/events/ListenerEventDriver.java b/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/events/ListenerEventDriver.java
deleted file mode 100644
index 07bad1a..0000000
--- a/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/events/ListenerEventDriver.java
+++ /dev/null
@@ -1,129 +0,0 @@
-//
-// ========================================================================
-// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
-// ------------------------------------------------------------------------
-// All rights reserved. This program and the accompanying materials
-// are made available under the terms of the Eclipse Public License v1.0
-// and Apache License v2.0 which accompanies this distribution.
-//
-// The Eclipse Public License is available at
-// http://www.eclipse.org/legal/epl-v10.html
-//
-// The Apache License v2.0 is available at
-// http://www.opensource.org/licenses/apache2.0.php
-//
-// You may elect to redistribute this code under either of these licenses.
-// ========================================================================
-//
-
-package org.eclipse.jetty.websocket.common.events;
-
-import java.io.IOException;
-import java.nio.ByteBuffer;
-
-import org.eclipse.jetty.util.log.Log;
-import org.eclipse.jetty.util.log.Logger;
-import org.eclipse.jetty.websocket.api.WebSocketListener;
-import org.eclipse.jetty.websocket.api.WebSocketPolicy;
-import org.eclipse.jetty.websocket.api.extensions.Frame;
-import org.eclipse.jetty.websocket.common.CloseInfo;
-import org.eclipse.jetty.websocket.common.message.MessageAppender;
-import org.eclipse.jetty.websocket.common.message.SimpleBinaryMessage;
-import org.eclipse.jetty.websocket.common.message.SimpleTextMessage;
-
-/**
- * Handler for {@link WebSocketListener} based User WebSocket implementations.
- */
-public class ListenerEventDriver extends EventDriver
-{
- private static final Logger LOG = Log.getLogger(ListenerEventDriver.class);
- private final WebSocketListener listener;
- private MessageAppender activeMessage;
- private boolean hasCloseBeenCalled = false;
-
- public ListenerEventDriver(WebSocketPolicy policy, WebSocketListener listener)
- {
- super(policy,listener);
- this.listener = listener;
- }
-
- @Override
- public void onBinaryFrame(ByteBuffer buffer, boolean fin) throws IOException
- {
- if (activeMessage == null)
- {
- activeMessage = new SimpleBinaryMessage(this);
- }
-
- activeMessage.appendMessage(buffer);
-
- if (fin)
- {
- activeMessage.messageComplete();
- activeMessage = null;
- }
- }
-
- @Override
- public void onBinaryMessage(byte[] data)
- {
- listener.onWebSocketBinary(data,0,data.length);
- }
-
- @Override
- public void onClose(CloseInfo close)
- {
- if (hasCloseBeenCalled)
- {
- // avoid duplicate close events (possible when using harsh Session.disconnect())
- return;
- }
- hasCloseBeenCalled = true;
-
- int statusCode = close.getStatusCode();
- String reason = close.getReason();
- listener.onWebSocketClose(statusCode,reason);
- }
-
- @Override
- public void onConnect()
- {
- LOG.debug("onConnect()");
- listener.onWebSocketConnect(session);
- }
-
- @Override
- public void onError(Throwable cause)
- {
- listener.onWebSocketError(cause);
- }
-
- @Override
- public void onFrame(Frame frame)
- {
- /* ignore, not supported by WebSocketListener */
- }
-
- @Override
- public void onTextFrame(ByteBuffer buffer, boolean fin) throws IOException
- {
- if (activeMessage == null)
- {
- activeMessage = new SimpleTextMessage(this);
- }
-
- activeMessage.appendMessage(buffer);
-
- if (fin)
- {
- activeMessage.messageComplete();
- activeMessage = null;
- }
- }
-
- @Override
- public void onTextMessage(String message)
- {
- listener.onWebSocketText(message);
- }
-}
diff --git a/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/events/annotated/AbstractMethodAnnotationScanner.java b/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/events/annotated/AbstractMethodAnnotationScanner.java
new file mode 100644
index 0000000..5abd9d7
--- /dev/null
+++ b/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/events/annotated/AbstractMethodAnnotationScanner.java
@@ -0,0 +1,195 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.common.events.annotated;
+
+import java.lang.annotation.Annotation;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+
+import org.eclipse.jetty.util.StringUtil;
+import org.eclipse.jetty.websocket.api.InvalidWebSocketException;
+import org.eclipse.jetty.websocket.common.events.ParamList;
+
+/**
+ * Basic scanner for Annotated Methods
+ */
+public abstract class AbstractMethodAnnotationScanner<T>
+{
+ protected void assertIsPublicNonStatic(Method method)
+ {
+ int mods = method.getModifiers();
+ if (!Modifier.isPublic(mods))
+ {
+ StringBuilder err = new StringBuilder();
+ err.append("Invalid declaration of ");
+ err.append(method);
+ err.append(StringUtil.__LINE_SEPARATOR);
+
+ err.append("Method modifier must be public");
+
+ throw new InvalidWebSocketException(err.toString());
+ }
+
+ if (Modifier.isStatic(mods))
+ {
+ StringBuilder err = new StringBuilder();
+ err.append("Invalid declaration of ");
+ err.append(method);
+ err.append(StringUtil.__LINE_SEPARATOR);
+
+ err.append("Method modifier may not be static");
+
+ throw new InvalidWebSocketException(err.toString());
+ }
+ }
+
+ protected void assertIsReturn(Method method, Class<?> type)
+ {
+ if (!type.equals(method.getReturnType()))
+ {
+ StringBuilder err = new StringBuilder();
+ err.append("Invalid declaration of ");
+ err.append(method);
+ err.append(StringUtil.__LINE_SEPARATOR);
+
+ err.append("Return type must be ").append(type);
+
+ throw new InvalidWebSocketException(err.toString());
+ }
+ }
+
+ protected void assertIsVoidReturn(Method method)
+ {
+ assertIsReturn(method,Void.TYPE);
+ }
+
+ protected void assertUnset(CallableMethod callable, Class<? extends Annotation> annoClass, Method method)
+ {
+ if (callable != null)
+ {
+ // Attempt to add duplicate frame type (a no-no)
+ StringBuilder err = new StringBuilder();
+ err.append("Duplicate @").append(annoClass.getSimpleName()).append(" declaration on ");
+ err.append(method);
+ err.append(StringUtil.__LINE_SEPARATOR);
+
+ err.append("@").append(annoClass.getSimpleName()).append(" previously declared at ");
+ err.append(callable.getMethod());
+
+ throw new InvalidWebSocketException(err.toString());
+ }
+ }
+
+ protected void assertValidSignature(Method method, Class<? extends Annotation> annoClass, ParamList validParams)
+ {
+ assertIsPublicNonStatic(method);
+ assertIsReturn(method,Void.TYPE);
+
+ boolean valid = false;
+
+ // validate parameters
+ Class<?> actual[] = method.getParameterTypes();
+ for (Class<?>[] params : validParams)
+ {
+ if (isSameParameters(actual,params))
+ {
+ valid = true;
+ break;
+ }
+ }
+
+ if (!valid)
+ {
+ throw InvalidSignatureException.build(method,annoClass,validParams);
+ }
+ }
+
+ public boolean isAnnotation(Annotation annotation, Class<? extends Annotation> annotationClass)
+ {
+ return annotation.annotationType().equals(annotationClass);
+ }
+
+ public boolean isSameParameters(Class<?>[] actual, Class<?>[] params)
+ {
+ if (actual.length != params.length)
+ {
+ // skip
+ return false;
+ }
+
+ int len = params.length;
+ for (int i = 0; i < len; i++)
+ {
+ if (!actual[i].equals(params[i]))
+ {
+ return false; // not valid
+ }
+ }
+
+ return true;
+ }
+
+ protected boolean isSignatureMatch(Method method, ParamList validParams)
+ {
+ assertIsPublicNonStatic(method);
+ assertIsReturn(method,Void.TYPE);
+
+ // validate parameters
+ Class<?> actual[] = method.getParameterTypes();
+ for (Class<?>[] params : validParams)
+ {
+ if (isSameParameters(actual,params))
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ protected boolean isTypeAnnotated(Class<?> pojo, Class<? extends Annotation> expectedAnnotation)
+ {
+ return pojo.getAnnotation(expectedAnnotation) != null;
+ }
+
+ public abstract void onMethodAnnotation(T metadata, Class<?> pojo, Method method, Annotation annotation);
+
+ public void scanMethodAnnotations(T metadata, Class<?> pojo)
+ {
+ Class<?> clazz = pojo;
+
+ while ((clazz != null) && Object.class.isAssignableFrom(clazz))
+ {
+ for (Method method : clazz.getDeclaredMethods())
+ {
+ Annotation annotations[] = method.getAnnotations();
+ if ((annotations == null) || (annotations.length <= 0))
+ {
+ continue; // skip
+ }
+ for (Annotation annotation : annotations)
+ {
+ onMethodAnnotation(metadata,clazz,method,annotation);
+ }
+ }
+
+ clazz = clazz.getSuperclass();
+ }
+ }
+}
diff --git a/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/events/annotated/CallableMethod.java b/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/events/annotated/CallableMethod.java
new file mode 100644
index 0000000..1b40451
--- /dev/null
+++ b/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/events/annotated/CallableMethod.java
@@ -0,0 +1,123 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.common.events.annotated;
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.Objects;
+
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+import org.eclipse.jetty.websocket.api.WebSocketException;
+import org.eclipse.jetty.websocket.common.util.ReflectUtils;
+
+/**
+ * A Callable Method
+ */
+public class CallableMethod
+{
+ private static final Logger LOG = Log.getLogger(CallableMethod.class);
+ protected final Class<?> pojo;
+ protected final Method method;
+ protected Class<?>[] paramTypes;
+
+ public CallableMethod(Class<?> pojo, Method method)
+ {
+ Objects.requireNonNull(pojo, "Pojo cannot be null");
+ Objects.requireNonNull(method, "Method cannot be null");
+ this.pojo = pojo;
+ this.method = method;
+ this.paramTypes = method.getParameterTypes();
+ }
+
+ public Object call(Object obj, Object... args)
+ {
+ if ((this.pojo == null) || (this.method == null))
+ {
+ LOG.warn("Cannot execute call: pojo={}, method={}",pojo,method);
+ return null; // no call event method determined
+ }
+
+ if (obj == null)
+ {
+ LOG.warn("Cannot call {} on null object",this.method);
+ return null;
+ }
+
+ if (args.length < paramTypes.length)
+ {
+ throw new IllegalArgumentException("Call arguments length [" + args.length + "] must always be greater than or equal to captured args length ["
+ + paramTypes.length + "]");
+ }
+
+ try
+ {
+ return this.method.invoke(obj,args);
+ }
+ catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e)
+ {
+ StringBuilder err = new StringBuilder();
+ err.append("Cannot call method ");
+ err.append(ReflectUtils.toString(pojo,method));
+ err.append(" with args: [");
+
+ boolean delim = false;
+ for (Object arg : args)
+ {
+ if (delim)
+ {
+ err.append(", ");
+ }
+ if (arg == null)
+ {
+ err.append("<null>");
+ }
+ else
+ {
+ err.append(arg.getClass().getName());
+ }
+ delim = true;
+ }
+ err.append("]");
+
+ throw new WebSocketException(err.toString(),e);
+ }
+ }
+
+ public Method getMethod()
+ {
+ return method;
+ }
+
+ public Class<?>[] getParamTypes()
+ {
+ return paramTypes;
+ }
+
+ public Class<?> getPojo()
+ {
+ return pojo;
+ }
+
+ @Override
+ public String toString()
+ {
+ return String.format("%s[%s]",this.getClass().getSimpleName(),method.toGenericString());
+ }
+}
\ No newline at end of file
diff --git a/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/events/annotated/EventMethod.java b/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/events/annotated/EventMethod.java
new file mode 100644
index 0000000..dcc1e93
--- /dev/null
+++ b/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/events/annotated/EventMethod.java
@@ -0,0 +1,154 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.common.events.annotated;
+
+import java.io.InputStream;
+import java.io.Reader;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+import org.eclipse.jetty.websocket.api.Session;
+import org.eclipse.jetty.websocket.api.WebSocketException;
+import org.eclipse.jetty.websocket.api.util.QuoteUtil;
+
+public class EventMethod
+{
+ private static final Logger LOG = Log.getLogger(EventMethod.class);
+
+ private static Object[] dropFirstArg(Object[] args)
+ {
+ if (args.length == 1)
+ {
+ return new Object[0];
+ }
+ Object ret[] = new Object[args.length - 1];
+ System.arraycopy(args,1,ret,0,ret.length);
+ return ret;
+ }
+
+ protected Class<?> pojo;
+ protected Method method;
+ private boolean hasSession = false;
+ private boolean isStreaming = false;
+ private Class<?>[] paramTypes;
+
+ public EventMethod(Class<?> pojo, Method method)
+ {
+ this.pojo = pojo;
+ this.paramTypes = method.getParameterTypes();
+ this.method = method;
+ identifyPresentParamTypes();
+ }
+
+ public EventMethod(Class<?> pojo, String methodName, Class<?>... paramTypes)
+ {
+ try
+ {
+ this.pojo = pojo;
+ this.paramTypes = paramTypes;
+ this.method = pojo.getMethod(methodName,paramTypes);
+ identifyPresentParamTypes();
+ }
+ catch (NoSuchMethodException | SecurityException e)
+ {
+ LOG.warn("Cannot use method {}({}): {}",methodName,paramTypes,e.getMessage());
+ this.method = null;
+ }
+ }
+
+ public void call(Object obj, Object... args)
+ {
+ if ((this.pojo == null) || (this.method == null))
+ {
+ LOG.warn("Cannot execute call: pojo={}, method={}",pojo,method);
+ return; // no call event method determined
+ }
+ if (obj == null)
+ {
+ LOG.warn("Cannot call {} on null object",this.method);
+ return;
+ }
+ if (args.length > paramTypes.length)
+ {
+ Object trimArgs[] = dropFirstArg(args);
+ call(obj,trimArgs);
+ return;
+ }
+ if (args.length < paramTypes.length)
+ {
+ throw new IllegalArgumentException("Call arguments length [" + args.length + "] must always be greater than or equal to captured args length ["
+ + paramTypes.length + "]");
+ }
+
+ try
+ {
+ this.method.invoke(obj,args);
+ }
+ catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e)
+ {
+ String err = String.format("Cannot call method %s on %s with args: %s",method,pojo, QuoteUtil.join(args,","));
+ throw new WebSocketException(err,e);
+ }
+ }
+
+ public Method getMethod()
+ {
+ return method;
+ }
+
+ protected Class<?>[] getParamTypes()
+ {
+ return this.paramTypes;
+ }
+
+ private void identifyPresentParamTypes()
+ {
+ this.hasSession = false;
+ this.isStreaming = false;
+
+ if (paramTypes == null)
+ {
+ return;
+ }
+
+ for (Class<?> paramType : paramTypes)
+ {
+ if (Session.class.isAssignableFrom(paramType))
+ {
+ this.hasSession = true;
+ }
+ if (Reader.class.isAssignableFrom(paramType) || InputStream.class.isAssignableFrom(paramType))
+ {
+ this.isStreaming = true;
+ }
+ }
+ }
+
+ public boolean isHasSession()
+ {
+ return hasSession;
+ }
+
+ public boolean isStreaming()
+ {
+ return isStreaming;
+ }
+}
\ No newline at end of file
diff --git a/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/events/annotated/EventMethods.java b/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/events/annotated/EventMethods.java
new file mode 100644
index 0000000..cf6a592
--- /dev/null
+++ b/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/events/annotated/EventMethods.java
@@ -0,0 +1,107 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.common.events.annotated;
+
+/**
+ * A representation of the methods available to call for a particular class.
+ * <p>
+ * This class used to cache the method lookups via the {@link EventMethodsCache}
+ */
+public class EventMethods
+{
+ private Class<?> pojoClass;
+ public EventMethod onConnect = null;
+ public EventMethod onClose = null;
+ public EventMethod onBinary = null;
+ public EventMethod onText = null;
+ public EventMethod onError = null;
+ public EventMethod onFrame = null;
+
+ public EventMethods(Class<?> pojoClass)
+ {
+ this.pojoClass = pojoClass;
+ }
+
+ @Override
+ public boolean equals(Object obj)
+ {
+ if (this == obj)
+ {
+ return true;
+ }
+ if (obj == null)
+ {
+ return false;
+ }
+ if (getClass() != obj.getClass())
+ {
+ return false;
+ }
+ EventMethods other = (EventMethods)obj;
+ if (pojoClass == null)
+ {
+ if (other.pojoClass != null)
+ {
+ return false;
+ }
+ }
+ else if (!pojoClass.getName().equals(other.pojoClass.getName()))
+ {
+ return false;
+ }
+ return true;
+ }
+
+ public Class<?> getPojoClass()
+ {
+ return pojoClass;
+ }
+
+ @Override
+ public int hashCode()
+ {
+ final int prime = 31;
+ int result = 1;
+ result = (prime * result) + ((pojoClass == null)?0:pojoClass.getName().hashCode());
+ return result;
+ }
+
+ @Override
+ public String toString()
+ {
+ StringBuilder builder = new StringBuilder();
+ builder.append("EventMethods [pojoClass=");
+ builder.append(pojoClass);
+ builder.append(", onConnect=");
+ builder.append(onConnect);
+ builder.append(", onClose=");
+ builder.append(onClose);
+ builder.append(", onBinary=");
+ builder.append(onBinary);
+ builder.append(", onText=");
+ builder.append(onText);
+ builder.append(", onException=");
+ builder.append(onError);
+ builder.append(", onFrame=");
+ builder.append(onFrame);
+ builder.append("]");
+ return builder.toString();
+ }
+
+}
diff --git a/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/events/annotated/InvalidSignatureException.java b/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/events/annotated/InvalidSignatureException.java
new file mode 100644
index 0000000..a953164
--- /dev/null
+++ b/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/events/annotated/InvalidSignatureException.java
@@ -0,0 +1,79 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.common.events.annotated;
+
+import java.lang.annotation.Annotation;
+import java.lang.reflect.Method;
+
+import org.eclipse.jetty.util.StringUtil;
+import org.eclipse.jetty.websocket.api.InvalidWebSocketException;
+import org.eclipse.jetty.websocket.common.events.ParamList;
+
+@SuppressWarnings("serial")
+public class InvalidSignatureException extends InvalidWebSocketException
+{
+ public static InvalidSignatureException build(Method method, Class<? extends Annotation> annoClass, ParamList... paramlists)
+ {
+ // Build big detailed exception to help the developer
+ StringBuilder err = new StringBuilder();
+ err.append("Invalid declaration of ");
+ err.append(method);
+ err.append(StringUtil.__LINE_SEPARATOR);
+
+ err.append("Acceptable method declarations for @");
+ err.append(annoClass.getSimpleName());
+ err.append(" are:");
+ for (ParamList validParams : paramlists)
+ {
+ for (Class<?>[] params : validParams)
+ {
+ err.append(StringUtil.__LINE_SEPARATOR);
+ err.append("public void ").append(method.getName());
+ err.append('(');
+ boolean delim = false;
+ for (Class<?> type : params)
+ {
+ if (delim)
+ {
+ err.append(',');
+ }
+ err.append(' ');
+ err.append(type.getName());
+ if (type.isArray())
+ {
+ err.append("[]");
+ }
+ delim = true;
+ }
+ err.append(')');
+ }
+ }
+ return new InvalidSignatureException(err.toString());
+ }
+
+ public InvalidSignatureException(String message)
+ {
+ super(message);
+ }
+
+ public InvalidSignatureException(String message, Throwable cause)
+ {
+ super(message,cause);
+ }
+}
diff --git a/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/events/annotated/OptionalSessionCallableMethod.java b/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/events/annotated/OptionalSessionCallableMethod.java
new file mode 100644
index 0000000..b0a17ed
--- /dev/null
+++ b/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/events/annotated/OptionalSessionCallableMethod.java
@@ -0,0 +1,91 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.common.events.annotated;
+
+import java.io.InputStream;
+import java.io.Reader;
+import java.lang.reflect.Method;
+
+import org.eclipse.jetty.websocket.api.Session;
+
+/**
+ * Simple CallableMethod that manages the optional {@link Session} argument
+ */
+public class OptionalSessionCallableMethod extends CallableMethod
+{
+ private final boolean wantsSession;
+ private final boolean streaming;
+
+ public OptionalSessionCallableMethod(Class<?> pojo, Method method)
+ {
+ super(pojo,method);
+
+ boolean foundConnection = false;
+ boolean foundStreaming = false;
+
+ if (paramTypes != null)
+ {
+ for (Class<?> paramType : paramTypes)
+ {
+ if (Session.class.isAssignableFrom(paramType))
+ {
+ foundConnection = true;
+ }
+ if (Reader.class.isAssignableFrom(paramType) || InputStream.class.isAssignableFrom(paramType))
+ {
+ foundStreaming = true;
+ }
+ }
+ }
+
+ this.wantsSession = foundConnection;
+ this.streaming = foundStreaming;
+ }
+
+ public void call(Object obj, Session connection, Object... args)
+ {
+ if (wantsSession)
+ {
+ Object fullArgs[] = new Object[args.length + 1];
+ fullArgs[0] = connection;
+ System.arraycopy(args,0,fullArgs,1,args.length);
+ call(obj,fullArgs);
+ }
+ else
+ {
+ call(obj,args);
+ }
+ }
+
+ public boolean isSessionAware()
+ {
+ return wantsSession;
+ }
+
+ public boolean isStreaming()
+ {
+ return streaming;
+ }
+
+ @Override
+ public String toString()
+ {
+ return String.format("%s[%s]",this.getClass().getSimpleName(),method.toGenericString());
+ }
+}
diff --git a/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/extensions/AbstractExtension.java b/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/extensions/AbstractExtension.java
index 5e208ec..ba63bc4 100644
--- a/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/extensions/AbstractExtension.java
+++ b/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/extensions/AbstractExtension.java
@@ -26,7 +26,6 @@
import org.eclipse.jetty.util.component.ContainerLifeCycle;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;
-import org.eclipse.jetty.websocket.api.WebSocketException;
import org.eclipse.jetty.websocket.api.WebSocketPolicy;
import org.eclipse.jetty.websocket.api.WriteCallback;
import org.eclipse.jetty.websocket.api.extensions.Extension;
@@ -108,7 +107,7 @@
}
@Override
- public void incomingError(WebSocketException e)
+ public void incomingError(Throwable e)
{
nextIncomingError(e);
}
@@ -169,7 +168,7 @@
return false;
}
- protected void nextIncomingError(WebSocketException e)
+ protected void nextIncomingError(Throwable e)
{
this.nextIncoming.incomingError(e);
}
diff --git a/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/extensions/ExtensionStack.java b/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/extensions/ExtensionStack.java
index 29450cf..d0c1ae2 100644
--- a/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/extensions/ExtensionStack.java
+++ b/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/extensions/ExtensionStack.java
@@ -28,7 +28,6 @@
import org.eclipse.jetty.util.component.ContainerLifeCycle;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;
-import org.eclipse.jetty.websocket.api.WebSocketException;
import org.eclipse.jetty.websocket.api.WriteCallback;
import org.eclipse.jetty.websocket.api.extensions.Extension;
import org.eclipse.jetty.websocket.api.extensions.ExtensionConfig;
@@ -192,7 +191,7 @@
}
@Override
- public void incomingError(WebSocketException e)
+ public void incomingError(Throwable e)
{
nextIncoming.incomingError(e);
}
diff --git a/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/extensions/WebSocketExtensionFactory.java b/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/extensions/WebSocketExtensionFactory.java
index 6ed9224..f56fd1b 100644
--- a/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/extensions/WebSocketExtensionFactory.java
+++ b/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/extensions/WebSocketExtensionFactory.java
@@ -25,10 +25,6 @@
import org.eclipse.jetty.websocket.api.extensions.Extension;
import org.eclipse.jetty.websocket.api.extensions.ExtensionConfig;
import org.eclipse.jetty.websocket.api.extensions.ExtensionFactory;
-import org.eclipse.jetty.websocket.common.extensions.compress.FrameCompressionExtension;
-import org.eclipse.jetty.websocket.common.extensions.compress.MessageCompressionExtension;
-import org.eclipse.jetty.websocket.common.extensions.fragment.FragmentExtension;
-import org.eclipse.jetty.websocket.common.extensions.identity.IdentityExtension;
public class WebSocketExtensionFactory extends ExtensionFactory
{
@@ -40,13 +36,6 @@
super();
this.policy = policy;
this.bufferPool = bufferPool;
-
- register("identity",IdentityExtension.class);
- register("fragment",FragmentExtension.class);
- /* FIXME: Disabled due to bug report - http://bugs.eclipse.org/395444
- * register("x-webkit-deflate-frame",FrameCompressionExtension.class);
- * register("permessage-compress",MessageCompressionExtension.class);
- */
}
@Override
diff --git a/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/extensions/compress/FrameCompressionExtension.java b/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/extensions/compress/FrameCompressionExtension.java
index a548f20..bf19f35 100644
--- a/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/extensions/compress/FrameCompressionExtension.java
+++ b/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/extensions/compress/FrameCompressionExtension.java
@@ -34,6 +34,12 @@
public class FrameCompressionExtension extends AbstractExtension
{
private CompressionMethod method = new DeflateCompressionMethod();
+
+ @Override
+ public String getName()
+ {
+ return "x-webkit-deflate-frame";
+ }
@Override
public synchronized void incomingFrame(Frame frame)
diff --git a/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/extensions/compress/MessageCompressionExtension.java b/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/extensions/compress/MessageCompressionExtension.java
deleted file mode 100644
index 75d7817..0000000
--- a/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/extensions/compress/MessageCompressionExtension.java
+++ /dev/null
@@ -1,148 +0,0 @@
-//
-// ========================================================================
-// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
-// ------------------------------------------------------------------------
-// All rights reserved. This program and the accompanying materials
-// are made available under the terms of the Eclipse Public License v1.0
-// and Apache License v2.0 which accompanies this distribution.
-//
-// The Eclipse Public License is available at
-// http://www.eclipse.org/legal/epl-v10.html
-//
-// The Apache License v2.0 is available at
-// http://www.opensource.org/licenses/apache2.0.php
-//
-// You may elect to redistribute this code under either of these licenses.
-// ========================================================================
-//
-
-package org.eclipse.jetty.websocket.common.extensions.compress;
-
-
-import java.nio.ByteBuffer;
-
-import org.eclipse.jetty.util.log.Log;
-import org.eclipse.jetty.util.log.Logger;
-import org.eclipse.jetty.websocket.api.WriteCallback;
-import org.eclipse.jetty.websocket.api.extensions.ExtensionConfig;
-import org.eclipse.jetty.websocket.api.extensions.Frame;
-import org.eclipse.jetty.websocket.common.WebSocketFrame;
-import org.eclipse.jetty.websocket.common.extensions.AbstractExtension;
-
-/**
- * Per Message Compression extension for WebSocket.
- * <p>
- * Attempts to follow <a href="https://tools.ietf.org/html/draft-ietf-hybi-permessage-compression-01">draft-ietf-hybi-permessage-compression-01</a>
- */
-public class MessageCompressionExtension extends AbstractExtension
-{
- private static final Logger LOG = Log.getLogger(MessageCompressionExtension.class);
-
- private CompressionMethod method;
-
- @Override
- public void incomingFrame(Frame frame)
- {
- if (frame.getType().isControl() || !frame.isRsv1())
- {
- // Cannot modify incoming control frames or ones with RSV1 set.
- nextIncomingFrame(frame);
- return;
- }
-
- ByteBuffer data = frame.getPayload();
- method.decompress().input(data);
- while (!method.decompress().isDone())
- {
- ByteBuffer uncompressed = method.decompress().process();
- if (uncompressed == null)
- {
- continue;
- }
- WebSocketFrame out = new WebSocketFrame(frame).setPayload(uncompressed);
- if (!method.decompress().isDone())
- {
- out.setFin(false);
- }
- out.setRsv1(false); // Unset RSV1 on decompressed frame
- nextIncomingFrame(out);
- }
-
- // reset only at the end of a message.
- if (frame.isFin())
- {
- method.decompress().end();
- }
- }
-
- /**
- * Indicates use of RSV1 flag for indicating deflation is in use.
- */
- @Override
- public boolean isRsv1User()
- {
- return true;
- }
-
- @Override
- public boolean isTextDataDecoder()
- {
- // this extension is responsible for text data frames
- return true;
- }
-
- @Override
- public void outgoingFrame(Frame frame, WriteCallback callback)
- {
- if (frame.getType().isControl())
- {
- // skip, cannot compress control frames.
- nextOutgoingFrame(frame,callback);
- return;
- }
-
- ByteBuffer data = frame.getPayload();
- // deflate data
- method.compress().input(data);
- while (!method.compress().isDone())
- {
- ByteBuffer buf = method.compress().process();
- WebSocketFrame out = new WebSocketFrame(frame).setPayload(buf);
- out.setRsv1(true);
- if (!method.compress().isDone())
- {
- out.setFin(false);
- // no callback for start/middle frames
- nextOutgoingFrame(out,null);
- }
- else
- {
- // pass through callback to last frame
- nextOutgoingFrame(out,callback);
- }
- }
-
- // reset only at end of message
- if (frame.isFin())
- {
- method.compress().end();
- }
- }
-
- @Override
- public void setConfig(ExtensionConfig config)
- {
- super.setConfig(config);
-
- String methodOptions = config.getParameter("method","deflate");
- LOG.debug("Method requested: {}",methodOptions);
-
- method = new DeflateCompressionMethod();
- }
-
- @Override
- public String toString()
- {
- return String.format("%s[method=%s]",this.getClass().getSimpleName(),method);
- }
-}
diff --git a/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/extensions/compress/MessageDeflateCompressionExtension.java b/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/extensions/compress/MessageDeflateCompressionExtension.java
new file mode 100644
index 0000000..51200c2
--- /dev/null
+++ b/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/extensions/compress/MessageDeflateCompressionExtension.java
@@ -0,0 +1,145 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.common.extensions.compress;
+
+import java.nio.ByteBuffer;
+
+import org.eclipse.jetty.websocket.api.WriteCallback;
+import org.eclipse.jetty.websocket.api.extensions.ExtensionConfig;
+import org.eclipse.jetty.websocket.api.extensions.Frame;
+import org.eclipse.jetty.websocket.common.WebSocketFrame;
+import org.eclipse.jetty.websocket.common.extensions.AbstractExtension;
+
+/**
+ * Per Message Deflate Compression extension for WebSocket.
+ * <p>
+ * Attempts to follow <a href="https://tools.ietf.org/html/draft-ietf-hybi-permessage-compression-01">draft-ietf-hybi-permessage-compression-01</a>
+ */
+public class MessageDeflateCompressionExtension extends AbstractExtension
+{
+ private CompressionMethod method;
+
+ @Override
+ public String getName()
+ {
+ return "permessage-deflate";
+ }
+
+ @Override
+ public void incomingFrame(Frame frame)
+ {
+ if (frame.getType().isControl() || !frame.isRsv1())
+ {
+ // Cannot modify incoming control frames or ones with RSV1 set.
+ nextIncomingFrame(frame);
+ return;
+ }
+
+ ByteBuffer data = frame.getPayload();
+ method.decompress().input(data);
+ while (!method.decompress().isDone())
+ {
+ ByteBuffer uncompressed = method.decompress().process();
+ if (uncompressed == null)
+ {
+ continue;
+ }
+ WebSocketFrame out = new WebSocketFrame(frame).setPayload(uncompressed);
+ if (!method.decompress().isDone())
+ {
+ out.setFin(false);
+ }
+ out.setRsv1(false); // Unset RSV1 on decompressed frame
+ nextIncomingFrame(out);
+ }
+
+ // reset only at the end of a message.
+ if (frame.isFin())
+ {
+ method.decompress().end();
+ }
+ }
+
+ /**
+ * Indicates use of RSV1 flag for indicating deflation is in use.
+ */
+ @Override
+ public boolean isRsv1User()
+ {
+ return true;
+ }
+
+ @Override
+ public boolean isTextDataDecoder()
+ {
+ // this extension is responsible for text data frames
+ return true;
+ }
+
+ @Override
+ public void outgoingFrame(Frame frame, WriteCallback callback)
+ {
+ if (frame.getType().isControl())
+ {
+ // skip, cannot compress control frames.
+ nextOutgoingFrame(frame,callback);
+ return;
+ }
+
+ ByteBuffer data = frame.getPayload();
+ // deflate data
+ method.compress().input(data);
+ while (!method.compress().isDone())
+ {
+ ByteBuffer buf = method.compress().process();
+ WebSocketFrame out = new WebSocketFrame(frame).setPayload(buf);
+ out.setRsv1(true);
+ if (!method.compress().isDone())
+ {
+ out.setFin(false);
+ // no callback for start/middle frames
+ nextOutgoingFrame(out,null);
+ }
+ else
+ {
+ // pass through callback to last frame
+ nextOutgoingFrame(out,callback);
+ }
+ }
+
+ // reset only at end of message
+ if (frame.isFin())
+ {
+ method.compress().end();
+ }
+ }
+
+ @Override
+ public void setConfig(ExtensionConfig config)
+ {
+ super.setConfig(config);
+ method = new DeflateCompressionMethod();
+ }
+
+ @Override
+ public String toString()
+ {
+ return String.format("%s[method=%s]",this.getClass().getSimpleName(),method);
+ }
+}
diff --git a/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/extensions/fragment/FragmentExtension.java b/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/extensions/fragment/FragmentExtension.java
index 13f13a5..f728c65 100644
--- a/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/extensions/fragment/FragmentExtension.java
+++ b/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/extensions/fragment/FragmentExtension.java
@@ -21,7 +21,6 @@
import java.nio.ByteBuffer;
-import org.eclipse.jetty.websocket.api.WebSocketException;
import org.eclipse.jetty.websocket.api.WriteCallback;
import org.eclipse.jetty.websocket.api.extensions.ExtensionConfig;
import org.eclipse.jetty.websocket.api.extensions.Frame;
@@ -35,9 +34,15 @@
public class FragmentExtension extends AbstractExtension
{
private int maxLength = -1;
+
+ @Override
+ public String getName()
+ {
+ return "fragment";
+ }
@Override
- public void incomingError(WebSocketException e)
+ public void incomingError(Throwable e)
{
// Pass thru
nextIncomingError(e);
diff --git a/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/extensions/identity/IdentityExtension.java b/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/extensions/identity/IdentityExtension.java
index fa6b535..e57f166 100644
--- a/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/extensions/identity/IdentityExtension.java
+++ b/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/extensions/identity/IdentityExtension.java
@@ -20,7 +20,6 @@
import org.eclipse.jetty.util.QuotedStringTokenizer;
import org.eclipse.jetty.util.annotation.ManagedObject;
-import org.eclipse.jetty.websocket.api.WebSocketException;
import org.eclipse.jetty.websocket.api.WriteCallback;
import org.eclipse.jetty.websocket.api.extensions.ExtensionConfig;
import org.eclipse.jetty.websocket.api.extensions.Frame;
@@ -35,9 +34,15 @@
{
return getConfig().getParameter(key,"?");
}
+
+ @Override
+ public String getName()
+ {
+ return "identity";
+ }
@Override
- public void incomingError(WebSocketException e)
+ public void incomingError(Throwable e)
{
// pass through
nextIncomingError(e);
diff --git a/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/extensions/mux/AbstractMuxExtension.java b/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/extensions/mux/AbstractMuxExtension.java
deleted file mode 100644
index f6db8af..0000000
--- a/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/extensions/mux/AbstractMuxExtension.java
+++ /dev/null
@@ -1,65 +0,0 @@
-//
-// ========================================================================
-// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
-// ------------------------------------------------------------------------
-// All rights reserved. This program and the accompanying materials
-// are made available under the terms of the Eclipse Public License v1.0
-// and Apache License v2.0 which accompanies this distribution.
-//
-// The Eclipse Public License is available at
-// http://www.eclipse.org/legal/epl-v10.html
-//
-// The Apache License v2.0 is available at
-// http://www.opensource.org/licenses/apache2.0.php
-//
-// You may elect to redistribute this code under either of these licenses.
-// ========================================================================
-//
-
-package org.eclipse.jetty.websocket.common.extensions.mux;
-
-import org.eclipse.jetty.websocket.api.WriteCallback;
-import org.eclipse.jetty.websocket.api.extensions.Frame;
-import org.eclipse.jetty.websocket.common.LogicalConnection;
-import org.eclipse.jetty.websocket.common.extensions.AbstractExtension;
-
-/**
- * Multiplexing Extension for WebSockets.
- * <p>
- * Supporting <a href="https://tools.ietf.org/html/draft-ietf-hybi-websocket-multiplexing-08">draft-ietf-hybi-websocket-multiplexing-08</a> Specification.
- */
-public abstract class AbstractMuxExtension extends AbstractExtension
-{
- private Muxer muxer;
-
- public AbstractMuxExtension()
- {
- super();
- }
-
- public abstract void configureMuxer(Muxer muxer);
-
- @Override
- public void incomingFrame(Frame frame)
- {
- this.muxer.incomingFrame(frame);
- }
-
- @Override
- public void outgoingFrame(Frame frame, WriteCallback callback)
- {
- /* do nothing here, allow Muxer to handle this aspect */
- }
-
- @Override
- public void setConnection(LogicalConnection connection)
- {
- super.setConnection(connection);
- if (muxer != null)
- {
- throw new RuntimeException("Cannot reset muxer physical connection once established");
- }
- this.muxer = new Muxer(connection);
- configureMuxer(this.muxer);
- }
-}
diff --git a/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/extensions/mux/MuxChannel.java b/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/extensions/mux/MuxChannel.java
deleted file mode 100644
index c3c42ce..0000000
--- a/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/extensions/mux/MuxChannel.java
+++ /dev/null
@@ -1,253 +0,0 @@
-//
-// ========================================================================
-// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
-// ------------------------------------------------------------------------
-// All rights reserved. This program and the accompanying materials
-// are made available under the terms of the Eclipse Public License v1.0
-// and Apache License v2.0 which accompanies this distribution.
-//
-// The Eclipse Public License is available at
-// http://www.eclipse.org/legal/epl-v10.html
-//
-// The Apache License v2.0 is available at
-// http://www.opensource.org/licenses/apache2.0.php
-//
-// You may elect to redistribute this code under either of these licenses.
-// ========================================================================
-//
-
-package org.eclipse.jetty.websocket.common.extensions.mux;
-
-import java.net.InetSocketAddress;
-import java.util.concurrent.Future;
-import java.util.concurrent.atomic.AtomicBoolean;
-
-import org.eclipse.jetty.util.log.Log;
-import org.eclipse.jetty.util.log.Logger;
-import org.eclipse.jetty.websocket.api.StatusCode;
-import org.eclipse.jetty.websocket.api.SuspendToken;
-import org.eclipse.jetty.websocket.api.WebSocketException;
-import org.eclipse.jetty.websocket.api.WebSocketPolicy;
-import org.eclipse.jetty.websocket.api.WriteCallback;
-import org.eclipse.jetty.websocket.api.extensions.Frame;
-import org.eclipse.jetty.websocket.api.extensions.IncomingFrames;
-import org.eclipse.jetty.websocket.common.CloseInfo;
-import org.eclipse.jetty.websocket.common.ConnectionState;
-import org.eclipse.jetty.websocket.common.LogicalConnection;
-import org.eclipse.jetty.websocket.common.WebSocketFrame;
-import org.eclipse.jetty.websocket.common.WebSocketSession;
-import org.eclipse.jetty.websocket.common.io.FutureWriteCallback;
-import org.eclipse.jetty.websocket.common.io.IOState;
-import org.eclipse.jetty.websocket.common.io.IOState.ConnectionStateListener;
-
-/**
- * MuxChannel, acts as WebSocketConnection for specific sub-channel.
- */
-public class MuxChannel implements LogicalConnection, IncomingFrames, SuspendToken, ConnectionStateListener
-{
- private static final Logger LOG = Log.getLogger(MuxChannel.class);
-
- private final long channelId;
- private final Muxer muxer;
- private final AtomicBoolean inputClosed;
- private final AtomicBoolean outputClosed;
- private final AtomicBoolean suspendToken;
- private IOState ioState;
- private WebSocketPolicy policy;
- private WebSocketSession session;
- private IncomingFrames incoming;
- private String subProtocol;
-
- public MuxChannel(long channelId, Muxer muxer)
- {
- this.channelId = channelId;
- this.muxer = muxer;
- this.policy = muxer.getPolicy().clonePolicy();
-
- this.suspendToken = new AtomicBoolean(false);
- this.ioState = new IOState();
- this.ioState.addListener(this);
-
- this.inputClosed = new AtomicBoolean(false);
- this.outputClosed = new AtomicBoolean(false);
- }
-
- @Override
- public void close()
- {
- close(StatusCode.NORMAL,null);
- }
-
- @Override
- public void close(int statusCode, String reason)
- {
- CloseInfo close = new CloseInfo(statusCode,reason);
- // TODO: disconnect callback?
- outgoingFrame(close.asFrame(),null);
- }
-
- @Override
- public void disconnect()
- {
- // TODO: disconnect the virtual end-point?
- }
-
- public long getChannelId()
- {
- return channelId;
- }
-
- @Override
- public IOState getIOState()
- {
- // TODO Auto-generated method stub
- return null;
- }
-
- @Override
- public InetSocketAddress getLocalAddress()
- {
- // TODO Auto-generated method stub
- return null;
- }
-
- @Override
- public long getMaxIdleTimeout()
- {
- // TODO Auto-generated method stub
- return 0;
- }
-
- @Override
- public WebSocketPolicy getPolicy()
- {
- return policy;
- }
-
- @Override
- public InetSocketAddress getRemoteAddress()
- {
- return muxer.getRemoteAddress();
- }
-
- @Override
- public WebSocketSession getSession()
- {
- return session;
- }
-
- /**
- * Incoming exceptions from Muxer.
- */
- @Override
- public void incomingError(WebSocketException e)
- {
- incoming.incomingError(e);
- }
-
- /**
- * Incoming frames from Muxer
- */
- @Override
- public void incomingFrame(Frame frame)
- {
- incoming.incomingFrame(frame);
- }
-
- public boolean isActive()
- {
- return (ioState.isOpen());
- }
-
- @Override
- public boolean isOpen()
- {
- return isActive() && muxer.isOpen();
- }
-
- @Override
- public boolean isReading()
- {
- return true;
- }
-
- public void onClose()
- {
- }
-
- @Override
- public void onConnectionStateChange(ConnectionState state)
- {
- // TODO Auto-generated method stub
-
- }
-
- public void onOpen()
- {
- this.ioState.onOpened();
- }
-
- /**
- * Internal
- *
- * @param frame the frame to write
- * @return the future for the network write of the frame
- */
- private Future<Void> outgoingAsyncFrame(WebSocketFrame frame)
- {
- FutureWriteCallback future = new FutureWriteCallback();
- outgoingFrame(frame,future);
- return future;
- }
-
- /**
- * Frames destined for the Muxer
- */
- @Override
- public void outgoingFrame(Frame frame, WriteCallback callback)
- {
- muxer.output(channelId,frame,callback);
- }
-
- @Override
- public void resume()
- {
- if (suspendToken.getAndSet(false))
- {
- // TODO: Start reading again. (how?)
- }
- }
-
- @Override
- public void setMaxIdleTimeout(long ms)
- {
- // TODO Auto-generated method stub
-
- }
-
- @Override
- public void setNextIncomingFrames(IncomingFrames incoming)
- {
- this.incoming = incoming;
- }
-
- @Override
- public void setSession(WebSocketSession session)
- {
- this.session = session;
- // session.setOutgoing(this);
- }
-
- public void setSubProtocol(String subProtocol)
- {
- this.subProtocol = subProtocol;
- }
-
- @Override
- public SuspendToken suspend()
- {
- suspendToken.set(true);
- // TODO: how to suspend reading?
- return this;
- }
-}
diff --git a/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/extensions/mux/MuxControlBlock.java b/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/extensions/mux/MuxControlBlock.java
deleted file mode 100644
index 7e364a4..0000000
--- a/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/extensions/mux/MuxControlBlock.java
+++ /dev/null
@@ -1,24 +0,0 @@
-//
-// ========================================================================
-// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
-// ------------------------------------------------------------------------
-// All rights reserved. This program and the accompanying materials
-// are made available under the terms of the Eclipse Public License v1.0
-// and Apache License v2.0 which accompanies this distribution.
-//
-// The Eclipse Public License is available at
-// http://www.eclipse.org/legal/epl-v10.html
-//
-// The Apache License v2.0 is available at
-// http://www.opensource.org/licenses/apache2.0.php
-//
-// You may elect to redistribute this code under either of these licenses.
-// ========================================================================
-//
-
-package org.eclipse.jetty.websocket.common.extensions.mux;
-
-public interface MuxControlBlock
-{
- public int getOpCode();
-}
diff --git a/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/extensions/mux/MuxException.java b/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/extensions/mux/MuxException.java
deleted file mode 100644
index ff8469f..0000000
--- a/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/extensions/mux/MuxException.java
+++ /dev/null
@@ -1,40 +0,0 @@
-//
-// ========================================================================
-// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
-// ------------------------------------------------------------------------
-// All rights reserved. This program and the accompanying materials
-// are made available under the terms of the Eclipse Public License v1.0
-// and Apache License v2.0 which accompanies this distribution.
-//
-// The Eclipse Public License is available at
-// http://www.eclipse.org/legal/epl-v10.html
-//
-// The Apache License v2.0 is available at
-// http://www.opensource.org/licenses/apache2.0.php
-//
-// You may elect to redistribute this code under either of these licenses.
-// ========================================================================
-//
-
-package org.eclipse.jetty.websocket.common.extensions.mux;
-
-import org.eclipse.jetty.websocket.api.WebSocketException;
-
-@SuppressWarnings("serial")
-public class MuxException extends WebSocketException
-{
- public MuxException(String message)
- {
- super(message);
- }
-
- public MuxException(String message, Throwable cause)
- {
- super(message,cause);
- }
-
- public MuxException(Throwable cause)
- {
- super(cause);
- }
-}
diff --git a/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/extensions/mux/MuxGenerator.java b/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/extensions/mux/MuxGenerator.java
deleted file mode 100644
index cf12f37..0000000
--- a/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/extensions/mux/MuxGenerator.java
+++ /dev/null
@@ -1,271 +0,0 @@
-//
-// ========================================================================
-// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
-// ------------------------------------------------------------------------
-// All rights reserved. This program and the accompanying materials
-// are made available under the terms of the Eclipse Public License v1.0
-// and Apache License v2.0 which accompanies this distribution.
-//
-// The Eclipse Public License is available at
-// http://www.eclipse.org/legal/epl-v10.html
-//
-// The Apache License v2.0 is available at
-// http://www.opensource.org/licenses/apache2.0.php
-//
-// You may elect to redistribute this code under either of these licenses.
-// ========================================================================
-//
-
-package org.eclipse.jetty.websocket.common.extensions.mux;
-
-import java.io.IOException;
-import java.nio.ByteBuffer;
-
-import org.eclipse.jetty.io.ArrayByteBufferPool;
-import org.eclipse.jetty.io.ByteBufferPool;
-import org.eclipse.jetty.util.BufferUtil;
-import org.eclipse.jetty.websocket.api.WriteCallback;
-import org.eclipse.jetty.websocket.api.extensions.Frame;
-import org.eclipse.jetty.websocket.api.extensions.OutgoingFrames;
-import org.eclipse.jetty.websocket.common.WebSocketFrame;
-import org.eclipse.jetty.websocket.common.extensions.mux.op.MuxAddChannelRequest;
-import org.eclipse.jetty.websocket.common.extensions.mux.op.MuxAddChannelResponse;
-import org.eclipse.jetty.websocket.common.extensions.mux.op.MuxDropChannel;
-import org.eclipse.jetty.websocket.common.extensions.mux.op.MuxFlowControl;
-import org.eclipse.jetty.websocket.common.extensions.mux.op.MuxNewChannelSlot;
-
-/**
- * Generate Mux frames destined for the physical connection.
- */
-public class MuxGenerator
-{
- private static final int CONTROL_BUFFER_SIZE = 2 * 1024;
- /** 4 bytes for channel ID + 1 for fin/rsv/opcode */
- private static final int DATA_FRAME_OVERHEAD = 5;
- private ByteBufferPool bufferPool;
- private OutgoingFrames outgoing;
-
- public MuxGenerator()
- {
- this(new ArrayByteBufferPool());
- }
-
- public MuxGenerator(ByteBufferPool bufferPool)
- {
- this.bufferPool = bufferPool;
- }
-
- public void generate(long channelId, Frame frame, WriteCallback callback)
- {
- ByteBuffer muxPayload = bufferPool.acquire(frame.getPayloadLength() + DATA_FRAME_OVERHEAD,false);
- BufferUtil.flipToFill(muxPayload);
-
- // start building mux payload
- writeChannelId(muxPayload,channelId);
- byte b = (byte)(frame.isFin()?0x80:0x00); // fin
- b |= (byte)(frame.isRsv1()?0x40:0x00); // rsv1
- b |= (byte)(frame.isRsv2()?0x20:0x00); // rsv2
- b |= (byte)(frame.isRsv3()?0x10:0x00); // rsv3
- b |= (byte)(frame.getType().getOpCode() & 0x0F); // opcode
- muxPayload.put(b);
- BufferUtil.put(frame.getPayload(),muxPayload);
-
- // build muxed frame
- WebSocketFrame muxFrame = WebSocketFrame.binary();
- BufferUtil.flipToFlush(muxPayload,0);
- muxFrame.setPayload(muxPayload);
- // NOTE: the physical connection will handle masking rules for this frame.
-
- // release original buffer (no longer needed)
- bufferPool.release(frame.getPayload());
-
- // send muxed frame down to the physical connection.
- outgoing.outgoingFrame(muxFrame,callback);
- }
-
- public void generate(WriteCallback callback,MuxControlBlock... blocks) throws IOException
- {
- if ((blocks == null) || (blocks.length <= 0))
- {
- return; // nothing to do
- }
-
- ByteBuffer payload = bufferPool.acquire(CONTROL_BUFFER_SIZE,false);
- BufferUtil.flipToFill(payload);
-
- writeChannelId(payload,0); // control channel
-
- for (MuxControlBlock block : blocks)
- {
- switch (block.getOpCode())
- {
- case MuxOp.ADD_CHANNEL_REQUEST:
- {
- MuxAddChannelRequest op = (MuxAddChannelRequest)block;
- byte b = (byte)((op.getOpCode() & 0x07) << 5); // opcode
- b |= (byte)((op.getRsv() & 0x07) << 2); // rsv
- b |= (op.getEncoding() & 0x03); // enc
- payload.put(b); // opcode + rsv + enc
- writeChannelId(payload,op.getChannelId());
- write139Buffer(payload,op.getHandshake());
- break;
- }
- case MuxOp.ADD_CHANNEL_RESPONSE:
- {
- MuxAddChannelResponse op = (MuxAddChannelResponse)block;
- byte b = (byte)((op.getOpCode() & 0x07) << 5); // opcode
- b |= (op.isFailed()?0x10:0x00); // failure bit
- b |= (byte)((op.getRsv() & 0x03) << 2); // rsv
- b |= (op.getEncoding() & 0x03); // enc
- payload.put(b); // opcode + f + rsv + enc
- writeChannelId(payload,op.getChannelId());
- if (op.getHandshake() != null)
- {
- write139Buffer(payload,op.getHandshake());
- }
- else
- {
- // no handshake details
- write139Size(payload,0);
- }
- break;
- }
- case MuxOp.DROP_CHANNEL:
- {
- MuxDropChannel op = (MuxDropChannel)block;
- byte b = (byte)((op.getOpCode() & 0x07) << 5); // opcode
- b |= (byte)(op.getRsv() & 0x1F); // rsv
- payload.put(b); // opcode + rsv
- writeChannelId(payload,op.getChannelId());
- write139Buffer(payload,op.asReasonBuffer());
- break;
- }
- case MuxOp.FLOW_CONTROL:
- {
- MuxFlowControl op = (MuxFlowControl)block;
- byte b = (byte)((op.getOpCode() & 0x07) << 5); // opcode
- b |= (byte)(op.getRsv() & 0x1F); // rsv
- payload.put(b); // opcode + rsv
- writeChannelId(payload,op.getChannelId());
- write139Size(payload,op.getSendQuotaSize());
- break;
- }
- case MuxOp.NEW_CHANNEL_SLOT:
- {
- MuxNewChannelSlot op = (MuxNewChannelSlot)block;
- byte b = (byte)((op.getOpCode() & 0x07) << 5); // opcode
- b |= (byte)(op.getRsv() & 0x0F) << 1; // rsv
- b |= (byte)(op.isFallback()?0x01:0x00); // fallback bit
- payload.put(b); // opcode + rsv + fallback bit
- write139Size(payload,op.getNumberOfSlots());
- write139Size(payload,op.getInitialSendQuota());
- break;
- }
- }
- }
- BufferUtil.flipToFlush(payload,0);
- WebSocketFrame frame = WebSocketFrame.binary();
- frame.setPayload(payload);
- outgoing.outgoingFrame(frame,callback);
- }
-
- public OutgoingFrames getOutgoing()
- {
- return outgoing;
- }
-
- public void setOutgoing(OutgoingFrames outgoing)
- {
- this.outgoing = outgoing;
- }
-
- /**
- * Write a 1/3/9 encoded size, then a byte buffer of that size.
- *
- * @param payload
- * @param buffer
- */
- public void write139Buffer(ByteBuffer payload, ByteBuffer buffer)
- {
- write139Size(payload,buffer.remaining());
- writeBuffer(payload,buffer);
- }
-
- /**
- * Write a 1/3/9 encoded size.
- *
- * @param payload
- * @param size
- */
- public void write139Size(ByteBuffer payload, long size)
- {
- if (size > 0xFF_FF)
- {
- // 9 byte encoded
- payload.put((byte)0x7F);
- payload.putLong(size);
- return;
- }
-
- if (size >= 0x7E)
- {
- // 3 byte encoded
- payload.put((byte)0x7E);
- payload.put((byte)(size >> 8));
- payload.put((byte)(size & 0xFF));
- return;
- }
-
- // 1 byte (7 bit) encoded
- payload.put((byte)(size & 0x7F));
- }
-
- public void writeBuffer(ByteBuffer payload, ByteBuffer buffer)
- {
- BufferUtil.put(buffer,payload);
- }
-
- /**
- * Write multiplexing channel id, using logical channel id encoding (of 1,2,3, or 4 octets)
- *
- * @param payload
- * @param channelId
- */
- public void writeChannelId(ByteBuffer payload, long channelId)
- {
- if (channelId > 0x1F_FF_FF_FF)
- {
- throw new MuxException("Illegal Channel ID: too big");
- }
-
- if (channelId > 0x1F_FF_FF)
- {
- // 29 bit channel id (4 bytes)
- payload.put((byte)(0xE0 | ((channelId >> 24) & 0x1F)));
- payload.put((byte)((channelId >> 16) & 0xFF));
- payload.put((byte)((channelId >> 8) & 0xFF));
- payload.put((byte)(channelId & 0xFF));
- return;
- }
-
- if (channelId > 0x3F_FF)
- {
- // 21 bit channel id (3 bytes)
- payload.put((byte)(0xC0 | ((channelId >> 16) & 0x1F)));
- payload.put((byte)((channelId >> 8) & 0xFF));
- payload.put((byte)(channelId & 0xFF));
- return;
- }
-
- if (channelId > 0x7F)
- {
- // 14 bit channel id (2 bytes)
- payload.put((byte)(0x80 | ((channelId >> 8) & 0x3F)));
- payload.put((byte)(channelId & 0xFF));
- return;
- }
-
- // 7 bit channel id
- payload.put((byte)(channelId & 0x7F));
- }
-}
diff --git a/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/extensions/mux/MuxOp.java b/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/extensions/mux/MuxOp.java
deleted file mode 100644
index f41383a..0000000
--- a/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/extensions/mux/MuxOp.java
+++ /dev/null
@@ -1,28 +0,0 @@
-//
-// ========================================================================
-// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
-// ------------------------------------------------------------------------
-// All rights reserved. This program and the accompanying materials
-// are made available under the terms of the Eclipse Public License v1.0
-// and Apache License v2.0 which accompanies this distribution.
-//
-// The Eclipse Public License is available at
-// http://www.eclipse.org/legal/epl-v10.html
-//
-// The Apache License v2.0 is available at
-// http://www.opensource.org/licenses/apache2.0.php
-//
-// You may elect to redistribute this code under either of these licenses.
-// ========================================================================
-//
-
-package org.eclipse.jetty.websocket.common.extensions.mux;
-
-public final class MuxOp
-{
- public static final byte ADD_CHANNEL_REQUEST = 0;
- public static final byte ADD_CHANNEL_RESPONSE = 1;
- public static final byte FLOW_CONTROL = 2;
- public static final byte DROP_CHANNEL = 3;
- public static final byte NEW_CHANNEL_SLOT = 4;
-}
diff --git a/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/extensions/mux/MuxParser.java b/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/extensions/mux/MuxParser.java
deleted file mode 100644
index 0fb2a49..0000000
--- a/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/extensions/mux/MuxParser.java
+++ /dev/null
@@ -1,410 +0,0 @@
-//
-// ========================================================================
-// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
-// ------------------------------------------------------------------------
-// All rights reserved. This program and the accompanying materials
-// are made available under the terms of the Eclipse Public License v1.0
-// and Apache License v2.0 which accompanies this distribution.
-//
-// The Eclipse Public License is available at
-// http://www.eclipse.org/legal/epl-v10.html
-//
-// The Apache License v2.0 is available at
-// http://www.opensource.org/licenses/apache2.0.php
-//
-// You may elect to redistribute this code under either of these licenses.
-// ========================================================================
-//
-
-package org.eclipse.jetty.websocket.common.extensions.mux;
-
-import java.nio.ByteBuffer;
-
-import org.eclipse.jetty.util.BufferUtil;
-import org.eclipse.jetty.util.log.Log;
-import org.eclipse.jetty.util.log.Logger;
-import org.eclipse.jetty.websocket.api.extensions.Frame;
-import org.eclipse.jetty.websocket.common.OpCode;
-import org.eclipse.jetty.websocket.common.WebSocketFrame;
-import org.eclipse.jetty.websocket.common.extensions.mux.op.MuxAddChannelRequest;
-import org.eclipse.jetty.websocket.common.extensions.mux.op.MuxAddChannelResponse;
-import org.eclipse.jetty.websocket.common.extensions.mux.op.MuxDropChannel;
-import org.eclipse.jetty.websocket.common.extensions.mux.op.MuxFlowControl;
-import org.eclipse.jetty.websocket.common.extensions.mux.op.MuxNewChannelSlot;
-
-public class MuxParser
-{
- public static interface Listener
- {
- public void onMuxAddChannelRequest(MuxAddChannelRequest request);
-
- public void onMuxAddChannelResponse(MuxAddChannelResponse response);
-
- public void onMuxDropChannel(MuxDropChannel drop);
-
- public void onMuxedFrame(MuxedFrame frame);
-
- public void onMuxException(MuxException e);
-
- public void onMuxFlowControl(MuxFlowControl flow);
-
- public void onMuxNewChannelSlot(MuxNewChannelSlot slot);
- }
-
- private final static Logger LOG = Log.getLogger(MuxParser.class);
-
- private MuxedFrame muxframe = new MuxedFrame();
- private MuxParser.Listener events;
- private long channelId;
-
- public MuxParser.Listener getEvents()
- {
- return events;
- }
-
- /**
- * Parse the raw {@link WebSocketFrame} payload data for various Mux frames.
- *
- * @param frame
- * the WebSocketFrame to parse for mux payload
- */
- public synchronized void parse(Frame frame)
- {
- if (events == null)
- {
- throw new RuntimeException("No " + MuxParser.Listener.class + " specified");
- }
-
- if (!frame.hasPayload())
- {
- LOG.debug("No payload data, skipping");
- return; // nothing to parse
- }
-
- if (frame.getType().getOpCode() != OpCode.BINARY)
- {
- LOG.debug("Not a binary opcode (base frame), skipping");
- return; // not a binary opcode
- }
-
- LOG.debug("Parsing Mux Payload of {}",frame);
-
- try
- {
- ByteBuffer buffer = frame.getPayload().slice();
-
- if (buffer.remaining() <= 0)
- {
- return;
- }
-
- if (frame.isContinuation())
- {
- muxframe.reset();
- muxframe.setFin(frame.isFin());
- muxframe.setFin(frame.isRsv1());
- muxframe.setFin(frame.isRsv2());
- muxframe.setFin(frame.isRsv3());
- muxframe.setContinuation(true);
- parseDataFramePayload(buffer);
- }
- else
- {
- // new frame
- channelId = readChannelId(buffer);
- if (channelId == 0)
- {
- parseControlBlocks(buffer);
- }
- else
- {
- parseDataFrame(buffer);
- }
- }
- }
- catch (MuxException e)
- {
- events.onMuxException(e);
- }
- catch (Throwable t)
- {
- events.onMuxException(new MuxException(t));
- }
- }
-
- private void parseControlBlocks(ByteBuffer buffer)
- {
- // process the remaining buffer here.
- while (buffer.remaining() > 0)
- {
- byte b = buffer.get();
- byte opc = (byte)((byte)(b >> 5) & 0xFF);
- b = (byte)(b & 0x1F);
-
- try {
- switch (opc)
- {
- case MuxOp.ADD_CHANNEL_REQUEST:
- {
- MuxAddChannelRequest op = new MuxAddChannelRequest();
- op.setRsv((byte)((b & 0x1C) >> 2));
- op.setEncoding((byte)(b & 0x03));
- op.setChannelId(readChannelId(buffer));
- long handshakeSize = read139EncodedSize(buffer);
- op.setHandshake(readBlock(buffer,handshakeSize));
- events.onMuxAddChannelRequest(op);
- break;
- }
- case MuxOp.ADD_CHANNEL_RESPONSE:
- {
- MuxAddChannelResponse op = new MuxAddChannelResponse();
- op.setFailed((b & 0x10) != 0);
- op.setRsv((byte)((byte)(b & 0x0C) >> 2));
- op.setEncoding((byte)(b & 0x03));
- op.setChannelId(readChannelId(buffer));
- long handshakeSize = read139EncodedSize(buffer);
- op.setHandshake(readBlock(buffer,handshakeSize));
- events.onMuxAddChannelResponse(op);
- break;
- }
- case MuxOp.DROP_CHANNEL:
- {
- int rsv = (b & 0x1F);
- long channelId = readChannelId(buffer);
- long reasonSize = read139EncodedSize(buffer);
- ByteBuffer reasonBuf = readBlock(buffer,reasonSize);
- MuxDropChannel op = MuxDropChannel.parse(channelId,reasonBuf);
- op.setRsv(rsv);
- events.onMuxDropChannel(op);
- break;
- }
- case MuxOp.FLOW_CONTROL:
- {
- MuxFlowControl op = new MuxFlowControl();
- op.setRsv((byte)(b & 0x1F));
- op.setChannelId(readChannelId(buffer));
- op.setSendQuotaSize(read139EncodedSize(buffer));
- events.onMuxFlowControl(op);
- break;
- }
- case MuxOp.NEW_CHANNEL_SLOT:
- {
- MuxNewChannelSlot op = new MuxNewChannelSlot();
- op.setRsv((byte)((b & 0x1E) >> 1));
- op.setFallback((b & 0x01) != 0);
- op.setNumberOfSlots(read139EncodedSize(buffer));
- op.setInitialSendQuota(read139EncodedSize(buffer));
- events.onMuxNewChannelSlot(op);
- break;
- }
- default:
- {
- String err = String.format("Unknown Mux Control Code OPC [0x%X]",opc);
- throw new MuxException(err);
- }
- }
- }
- catch (Throwable t)
- {
- LOG.warn(t);
- throw new MuxException(t);
- }
- }
- }
-
- private void parseDataFrame(ByteBuffer buffer)
- {
- byte b = buffer.get();
- boolean fin = ((b & 0x80) != 0);
- boolean rsv1 = ((b & 0x40) != 0);
- boolean rsv2 = ((b & 0x20) != 0);
- boolean rsv3 = ((b & 0x10) != 0);
- byte opcode = (byte)(b & 0x0F);
-
- if (opcode == OpCode.CONTINUATION)
- {
- muxframe.setContinuation(true);
- }
- else
- {
- muxframe.reset();
- muxframe.setOpCode(opcode);
- }
-
- muxframe.setChannelId(channelId);
- muxframe.setFin(fin);
- muxframe.setRsv1(rsv1);
- muxframe.setRsv2(rsv2);
- muxframe.setRsv3(rsv3);
-
- parseDataFramePayload(buffer);
- }
-
- private void parseDataFramePayload(ByteBuffer buffer)
- {
- int capacity = buffer.remaining();
- ByteBuffer payload = ByteBuffer.allocate(capacity);
- payload.put(buffer);
- BufferUtil.flipToFlush(payload,0);
- muxframe.setPayload(payload);
- try
- {
- LOG.debug("notifyFrame() - {}",muxframe);
- events.onMuxedFrame(muxframe);
- }
- catch (Throwable t)
- {
- LOG.warn(t);
- }
- }
-
- /**
- * Per section <a href="https://tools.ietf.org/html/draft-ietf-hybi-websocket-multiplexing#section-9.1">9.1. Number Encoding in Multiplex Control
- * Blocks</a>, read the 1/3/9 byte length using <a href="https://tools.ietf.org/html/rfc6455#section-5.2">Section 5.2 of RFC 6455</a>.
- *
- * @param buffer
- * the buffer to read from
- * @return the decoded size
- * @throws MuxException
- * when the encoding does not make sense per the spec, or it is a value above {@link Long#MAX_VALUE}
- */
- public long read139EncodedSize(ByteBuffer buffer)
- {
- long ret = -1;
- long minValue = 0x00; // used to validate minimum # of bytes (per spec)
- int cursor = 0;
-
- byte b = buffer.get();
- ret = (b & 0x7F);
-
- if (ret == 0x7F)
- {
- // 9 byte length
- ret = 0;
- minValue = 0xFF_FF;
- cursor = 8;
- }
- else if (ret == 0x7E)
- {
- // 3 byte length
- ret = 0;
- minValue = 0x7F;
- cursor = 2;
- }
- else
- {
- // 1 byte length
- // no validation of minimum bytes needed here
- return ret;
- }
-
- // parse multi-byte length
- while (cursor > 0)
- {
- ret = ret << 8;
- b = buffer.get();
- ret |= (b & 0xFF);
- --cursor;
- }
-
- // validate minimum value per spec.
- if (ret <= minValue)
- {
- String err = String.format("Invalid 1/3/9 length 0x%X (minimum value for chosen encoding is 0x%X)",ret,minValue);
- throw new MuxException(err);
- }
-
- return ret;
- }
-
- private ByteBuffer readBlock(ByteBuffer buffer, long size)
- {
- if (size == 0)
- {
- return null;
- }
-
- if (size > buffer.remaining())
- {
- String err = String.format("Truncated data, expected %,d byte(s), but only %,d byte(s) remain",size,buffer.remaining());
- throw new MuxException(err);
- }
-
- if (size > Integer.MAX_VALUE)
- {
- String err = String.format("[Int-Sane!] Buffer size %,d is too large to be supported (max allowed is %,d)",size,Integer.MAX_VALUE);
- throw new MuxException(err);
- }
-
- ByteBuffer ret = ByteBuffer.allocate((int)size);
- BufferUtil.put(buffer,ret);
- BufferUtil.flipToFlush(ret,0);
- return ret;
- }
-
- /**
- * Read Channel ID using <a href="https://tools.ietf.org/html/draft-ietf-hybi-websocket-multiplexing#section-7">Section 7. Framing</a> techniques
- *
- * @param buffer
- * the buffer to parse from.
- * @return the channel Id
- * @throws MuxException
- * when the encoding does not make sense per the spec.
- */
- public long readChannelId(ByteBuffer buffer)
- {
- long id = -1;
- long minValue = 0x00; // used to validate minimum # of bytes (per spec)
- byte b = buffer.get();
- int cursor = -1;
- if ((b & 0x80) == 0)
- {
- // 7 bit channel id
- // no validation of minimum bytes needed here
- return (b & 0x7F);
- }
- else if ((b & 0x40) == 0)
- {
- // 14 bit channel id
- id = (b & 0x3F);
- minValue = 0x7F;
- cursor = 1;
- }
- else if ((b & 0x20) == 0)
- {
- // 21 bit channel id
- id = (b & 0x1F);
- minValue = 0x3F_FF;
- cursor = 2;
- }
- else
- {
- // 29 bit channel id
- id = (b & 0x1F);
- minValue = 0x1F_FF_FF;
- cursor = 3;
- }
-
- while (cursor > 0)
- {
- id = id << 8;
- b = buffer.get();
- id |= (b & 0xFF);
- --cursor;
- }
-
- // validate minimum value per spec.
- if (id <= minValue)
- {
- String err = String.format("Invalid Channel ID 0x%X (minimum value for chosen encoding is 0x%X)",id,minValue);
- throw new MuxException(err);
- }
-
- return id;
- }
-
- public void setEvents(MuxParser.Listener events)
- {
- this.events = events;
- }
-}
diff --git a/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/extensions/mux/MuxPhysicalConnectionException.java b/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/extensions/mux/MuxPhysicalConnectionException.java
deleted file mode 100644
index a472cb2..0000000
--- a/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/extensions/mux/MuxPhysicalConnectionException.java
+++ /dev/null
@@ -1,44 +0,0 @@
-//
-// ========================================================================
-// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
-// ------------------------------------------------------------------------
-// All rights reserved. This program and the accompanying materials
-// are made available under the terms of the Eclipse Public License v1.0
-// and Apache License v2.0 which accompanies this distribution.
-//
-// The Eclipse Public License is available at
-// http://www.eclipse.org/legal/epl-v10.html
-//
-// The Apache License v2.0 is available at
-// http://www.opensource.org/licenses/apache2.0.php
-//
-// You may elect to redistribute this code under either of these licenses.
-// ========================================================================
-//
-
-package org.eclipse.jetty.websocket.common.extensions.mux;
-
-import org.eclipse.jetty.websocket.common.extensions.mux.op.MuxDropChannel;
-
-public class MuxPhysicalConnectionException extends MuxException
-{
- private static final long serialVersionUID = 1L;
- private MuxDropChannel drop;
-
- public MuxPhysicalConnectionException(MuxDropChannel.Reason code, String phrase)
- {
- super(phrase);
- drop = new MuxDropChannel(0,code,phrase);
- }
-
- public MuxPhysicalConnectionException(MuxDropChannel.Reason code, String phrase, Throwable t)
- {
- super(phrase,t);
- drop = new MuxDropChannel(0,code,phrase);
- }
-
- public MuxDropChannel getMuxDropChannel()
- {
- return drop;
- }
-}
diff --git a/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/extensions/mux/MuxRequest.java b/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/extensions/mux/MuxRequest.java
deleted file mode 100644
index 1b430e5..0000000
--- a/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/extensions/mux/MuxRequest.java
+++ /dev/null
@@ -1,63 +0,0 @@
-//
-// ========================================================================
-// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
-// ------------------------------------------------------------------------
-// All rights reserved. This program and the accompanying materials
-// are made available under the terms of the Eclipse Public License v1.0
-// and Apache License v2.0 which accompanies this distribution.
-//
-// The Eclipse Public License is available at
-// http://www.eclipse.org/legal/epl-v10.html
-//
-// The Apache License v2.0 is available at
-// http://www.opensource.org/licenses/apache2.0.php
-//
-// You may elect to redistribute this code under either of these licenses.
-// ========================================================================
-//
-
-package org.eclipse.jetty.websocket.common.extensions.mux;
-
-import java.nio.ByteBuffer;
-
-import org.eclipse.jetty.websocket.api.UpgradeRequest;
-
-public class MuxRequest extends UpgradeRequest
-{
- public static final String HEADER_VALUE_DELIM="\"\\\n\r\t\f\b%+ ;=";
-
- public static UpgradeRequest merge(UpgradeRequest baseReq, UpgradeRequest deltaReq)
- {
- MuxRequest req = new MuxRequest(baseReq);
-
- // TODO: finish
-
- return req;
- }
-
- private static String overlay(String val, String defVal)
- {
- if (val == null)
- {
- return defVal;
- }
- return val;
- }
-
- public static UpgradeRequest parse(ByteBuffer handshake)
- {
- MuxRequest req = new MuxRequest();
- // TODO Auto-generated method stub
- return req;
- }
-
- public MuxRequest()
- {
- super();
- }
-
- public MuxRequest(UpgradeRequest copy)
- {
- super();
- }
-}
diff --git a/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/extensions/mux/MuxResponse.java b/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/extensions/mux/MuxResponse.java
deleted file mode 100644
index 0a34448..0000000
--- a/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/extensions/mux/MuxResponse.java
+++ /dev/null
@@ -1,26 +0,0 @@
-//
-// ========================================================================
-// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
-// ------------------------------------------------------------------------
-// All rights reserved. This program and the accompanying materials
-// are made available under the terms of the Eclipse Public License v1.0
-// and Apache License v2.0 which accompanies this distribution.
-//
-// The Eclipse Public License is available at
-// http://www.eclipse.org/legal/epl-v10.html
-//
-// The Apache License v2.0 is available at
-// http://www.opensource.org/licenses/apache2.0.php
-//
-// You may elect to redistribute this code under either of these licenses.
-// ========================================================================
-//
-
-package org.eclipse.jetty.websocket.common.extensions.mux;
-
-import org.eclipse.jetty.websocket.api.UpgradeResponse;
-
-public class MuxResponse extends UpgradeResponse
-{
-
-}
diff --git a/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/extensions/mux/MuxedFrame.java b/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/extensions/mux/MuxedFrame.java
deleted file mode 100644
index 219b6b7..0000000
--- a/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/extensions/mux/MuxedFrame.java
+++ /dev/null
@@ -1,73 +0,0 @@
-//
-// ========================================================================
-// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
-// ------------------------------------------------------------------------
-// All rights reserved. This program and the accompanying materials
-// are made available under the terms of the Eclipse Public License v1.0
-// and Apache License v2.0 which accompanies this distribution.
-//
-// The Eclipse Public License is available at
-// http://www.eclipse.org/legal/epl-v10.html
-//
-// The Apache License v2.0 is available at
-// http://www.opensource.org/licenses/apache2.0.php
-//
-// You may elect to redistribute this code under either of these licenses.
-// ========================================================================
-//
-
-package org.eclipse.jetty.websocket.common.extensions.mux;
-
-import org.eclipse.jetty.websocket.common.OpCode;
-import org.eclipse.jetty.websocket.common.WebSocketFrame;
-
-public class MuxedFrame extends WebSocketFrame
-{
- private long channelId = -1;
-
- public MuxedFrame()
- {
- super();
- }
-
- public MuxedFrame(MuxedFrame frame)
- {
- super(frame);
- this.channelId = frame.channelId;
- }
-
- public long getChannelId()
- {
- return channelId;
- }
-
- @Override
- public void reset()
- {
- super.reset();
- this.channelId = -1;
- }
-
- public void setChannelId(long channelId)
- {
- this.channelId = channelId;
- }
-
- @Override
- public String toString()
- {
- StringBuilder b = new StringBuilder();
- b.append(OpCode.name(getOpCode()));
- b.append('[');
- b.append("channel=").append(channelId);
- b.append(",len=").append(getPayloadLength());
- b.append(",fin=").append(isFin());
- b.append(",rsv=");
- b.append(isRsv1()?'1':'.');
- b.append(isRsv2()?'1':'.');
- b.append(isRsv3()?'1':'.');
- b.append(",continuation=").append(isContinuation());
- b.append(']');
- return b.toString();
- }
-}
diff --git a/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/extensions/mux/Muxer.java b/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/extensions/mux/Muxer.java
deleted file mode 100644
index ab93663..0000000
--- a/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/extensions/mux/Muxer.java
+++ /dev/null
@@ -1,440 +0,0 @@
-//
-// ========================================================================
-// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
-// ------------------------------------------------------------------------
-// All rights reserved. This program and the accompanying materials
-// are made available under the terms of the Eclipse Public License v1.0
-// and Apache License v2.0 which accompanies this distribution.
-//
-// The Eclipse Public License is available at
-// http://www.eclipse.org/legal/epl-v10.html
-//
-// The Apache License v2.0 is available at
-// http://www.opensource.org/licenses/apache2.0.php
-//
-// You may elect to redistribute this code under either of these licenses.
-// ========================================================================
-//
-
-package org.eclipse.jetty.websocket.common.extensions.mux;
-
-import java.io.IOException;
-import java.net.InetSocketAddress;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-
-import org.eclipse.jetty.util.StringUtil;
-import org.eclipse.jetty.util.log.Log;
-import org.eclipse.jetty.util.log.Logger;
-import org.eclipse.jetty.websocket.api.StatusCode;
-import org.eclipse.jetty.websocket.api.UpgradeRequest;
-import org.eclipse.jetty.websocket.api.UpgradeResponse;
-import org.eclipse.jetty.websocket.api.WebSocketBehavior;
-import org.eclipse.jetty.websocket.api.WebSocketException;
-import org.eclipse.jetty.websocket.api.WebSocketPolicy;
-import org.eclipse.jetty.websocket.api.WriteCallback;
-import org.eclipse.jetty.websocket.api.extensions.Frame;
-import org.eclipse.jetty.websocket.api.extensions.IncomingFrames;
-import org.eclipse.jetty.websocket.api.extensions.OutgoingFrames;
-import org.eclipse.jetty.websocket.common.LogicalConnection;
-import org.eclipse.jetty.websocket.common.WebSocketFrame;
-import org.eclipse.jetty.websocket.common.extensions.mux.add.MuxAddClient;
-import org.eclipse.jetty.websocket.common.extensions.mux.add.MuxAddServer;
-import org.eclipse.jetty.websocket.common.extensions.mux.op.MuxAddChannelRequest;
-import org.eclipse.jetty.websocket.common.extensions.mux.op.MuxAddChannelResponse;
-import org.eclipse.jetty.websocket.common.extensions.mux.op.MuxDropChannel;
-import org.eclipse.jetty.websocket.common.extensions.mux.op.MuxFlowControl;
-import org.eclipse.jetty.websocket.common.extensions.mux.op.MuxNewChannelSlot;
-
-/**
- * Muxer responsible for managing sub-channels.
- * <p>
- * Maintains a 1 (incoming and outgoing mux encapsulated frames) to many (per-channel incoming/outgoing standard websocket frames) relationship, along with
- * routing of {@link MuxControlBlock} events.
- * <p>
- * Control Channel events (channel ID == 0) are handled by the Muxer.
- */
-public class Muxer implements IncomingFrames, MuxParser.Listener
-{
- private static final int CONTROL_CHANNEL_ID = 0;
-
- private static final Logger LOG = Log.getLogger(Muxer.class);
-
- /**
- * Map of sub-channels, key is the channel Id.
- */
- private Map<Long, MuxChannel> channels = new HashMap<Long, MuxChannel>();
-
- private final WebSocketPolicy policy;
- private final LogicalConnection physicalConnection;
- private InetSocketAddress remoteAddress;
- /** Parsing frames destined for sub-channels */
- private MuxParser parser;
- /** Generating frames destined for physical connection */
- private MuxGenerator generator;
- private MuxAddServer addServer;
- private MuxAddClient addClient;
- /** The original request headers, used for delta encoded AddChannelRequest blocks */
- private UpgradeRequest physicalRequestHeaders;
- /** The original response headers, used for delta encoded AddChannelResponse blocks */
- private UpgradeResponse physicalResponseHeaders;
-
- public Muxer(final LogicalConnection connection)
- {
- this.physicalConnection = connection;
- this.policy = connection.getPolicy().clonePolicy();
- this.parser = new MuxParser();
- this.parser.setEvents(this);
- this.generator = new MuxGenerator();
- }
-
- public MuxAddClient getAddClient()
- {
- return addClient;
- }
-
- public MuxAddServer getAddServer()
- {
- return addServer;
- }
-
- public MuxChannel getChannel(long channelId, boolean create)
- {
- if (channelId == CONTROL_CHANNEL_ID)
- {
- throw new MuxPhysicalConnectionException(MuxDropChannel.Reason.UNKNOWN_MUX_CONTROL_BLOCK,"Invalid Channel ID");
- }
-
- MuxChannel channel = channels.get(channelId);
- if (channel == null)
- {
- if (create)
- {
- channel = new MuxChannel(channelId,this);
- channels.put(channelId,channel);
- }
- else
- {
- throw new MuxPhysicalConnectionException(MuxDropChannel.Reason.UNKNOWN_MUX_CONTROL_BLOCK,"Unknown Channel ID");
- }
- }
- return channel;
- }
-
- public WebSocketPolicy getPolicy()
- {
- return policy;
- }
-
- /**
- * Get the remote address of the physical connection.
- *
- * @return the remote address of the physical connection
- */
- public InetSocketAddress getRemoteAddress()
- {
- return this.remoteAddress;
- }
-
- /**
- * Incoming parser errors
- */
- @Override
- public void incomingError(WebSocketException e)
- {
- MuxDropChannel.Reason reason = MuxDropChannel.Reason.PHYSICAL_CONNECTION_FAILED;
- String phrase = String.format("%s: %s", e.getClass().getName(), e.getMessage());
- mustFailPhysicalConnection(new MuxPhysicalConnectionException(reason,phrase));
- }
-
- /**
- * Incoming mux encapsulated frames.
- */
- @Override
- public void incomingFrame(Frame frame)
- {
- parser.parse(frame);
- }
-
- /**
- * Is the muxer and the physical connection still open?
- *
- * @return true if open
- */
- public boolean isOpen()
- {
- return physicalConnection.isOpen();
- }
-
- public String mergeHeaders(List<String> physicalHeaders, String deltaHeaders)
- {
- // TODO Auto-generated method stub
- return null;
- }
-
- /**
- * Per spec, the physical connection must be failed.
- * <p>
- * <a href="https://tools.ietf.org/html/draft-ietf-hybi-websocket-multiplexing-08#section-18">Section 18. Fail the Physical Connection.</a>
- *
- * <blockquote> To _Fail the Physical Connection_, an endpoint MUST send a DropChannel multiplex control block with objective channel ID of 0 and drop
- * reason code in the range of 2000-2999, and then _Fail the WebSocket Connection_ on the physical connection with status code of 1011. </blockquote>
- */
- private void mustFailPhysicalConnection(MuxPhysicalConnectionException muxe)
- {
- // TODO: stop muxer from receiving incoming sub-channel traffic.
-
- MuxDropChannel drop = muxe.getMuxDropChannel();
- LOG.warn(muxe);
- try
- {
- generator.generate(null,drop);
- }
- catch (IOException ioe)
- {
- LOG.warn("Unable to send mux DropChannel",ioe);
- }
-
- String reason = "Mux[MUST FAIL]" + drop.getPhrase();
- reason = StringUtil.truncate(reason,WebSocketFrame.MAX_CONTROL_PAYLOAD);
- this.physicalConnection.close(StatusCode.SERVER_ERROR,reason);
-
- // TODO: trigger abnormal close for all sub-channels.
- }
-
- /**
- * Incoming mux control block, destined for the control channel (id 0)
- */
- @Override
- public void onMuxAddChannelRequest(MuxAddChannelRequest request)
- {
- if (policy.getBehavior() == WebSocketBehavior.CLIENT)
- {
- throw new MuxPhysicalConnectionException(MuxDropChannel.Reason.UNKNOWN_MUX_CONTROL_BLOCK,"AddChannelRequest not allowed per spec");
- }
-
- if (request.getRsv() != 0)
- {
- throw new MuxPhysicalConnectionException(MuxDropChannel.Reason.UNKNOWN_REQUEST_ENCODING,"RSV Not allowed to be set");
- }
-
- // Pre-allocate channel.
- long channelId = request.getChannelId();
- MuxChannel channel = getChannel(channelId, true);
-
- // submit to upgrade handshake process
- try
- {
- switch (request.getEncoding())
- {
- case MuxAddChannelRequest.IDENTITY_ENCODING:
- {
- UpgradeRequest idenReq = MuxRequest.parse(request.getHandshake());
- addServer.handshake(this,channel,idenReq);
- break;
- }
- case MuxAddChannelRequest.DELTA_ENCODING:
- {
- UpgradeRequest baseReq = addServer.getPhysicalHandshakeRequest();
- UpgradeRequest deltaReq = MuxRequest.parse(request.getHandshake());
- UpgradeRequest mergedReq = MuxRequest.merge(baseReq,deltaReq);
-
- addServer.handshake(this,channel,mergedReq);
- break;
- }
- default:
- {
- throw new MuxPhysicalConnectionException(MuxDropChannel.Reason.BAD_REQUEST,"Unrecognized request encoding");
- }
- }
- }
- catch (MuxPhysicalConnectionException e)
- {
- throw e;
- }
- catch (Throwable t)
- {
- throw new MuxPhysicalConnectionException(MuxDropChannel.Reason.BAD_REQUEST,"Unable to parse request",t);
- }
- }
-
- /**
- * Incoming mux control block, destined for the control channel (id 0)
- */
- @Override
- public void onMuxAddChannelResponse(MuxAddChannelResponse response)
- {
- if (policy.getBehavior() == WebSocketBehavior.SERVER)
- {
- throw new MuxPhysicalConnectionException(MuxDropChannel.Reason.UNKNOWN_MUX_CONTROL_BLOCK,"AddChannelResponse not allowed per spec");
- }
-
- if (response.getRsv() != 0)
- {
- throw new MuxPhysicalConnectionException(MuxDropChannel.Reason.UNKNOWN_RESPONSE_ENCODING,"RSV Not allowed to be set");
- }
-
- // Process channel
- long channelId = response.getChannelId();
- MuxChannel channel = getChannel(channelId,false);
-
- // Process Response headers
- try
- {
- // Parse Response
-
- // TODO: Sec-WebSocket-Accept header
- // TODO: Sec-WebSocket-Extensions header
- // TODO: Setup extensions
- // TODO: Setup sessions
-
- // Trigger channel open
- channel.onOpen();
- }
- catch (Throwable t)
- {
- throw new MuxPhysicalConnectionException(MuxDropChannel.Reason.BAD_RESPONSE,"Unable to parse response",t);
- }
- }
-
- /**
- * Incoming mux control block, destined for the control channel (id 0)
- */
- @Override
- public void onMuxDropChannel(MuxDropChannel drop)
- {
- // Process channel
- long channelId = drop.getChannelId();
- MuxChannel channel = getChannel(channelId,false);
-
- String reason = "Mux " + drop.toString();
- reason = StringUtil.truncate(reason,(WebSocketFrame.MAX_CONTROL_PAYLOAD - 2));
- channel.close(StatusCode.PROTOCOL,reason);
- // TODO: set channel to inactive?
- }
-
- /**
- * Incoming mux-unwrapped frames, destined for a sub-channel
- */
- @Override
- public void onMuxedFrame(MuxedFrame frame)
- {
- MuxChannel subchannel = channels.get(frame.getChannelId());
- subchannel.incomingFrame(frame);
- }
-
- @Override
- public void onMuxException(MuxException e)
- {
- if (e instanceof MuxPhysicalConnectionException)
- {
- mustFailPhysicalConnection((MuxPhysicalConnectionException)e);
- }
-
- LOG.warn(e);
- // TODO: handle other (non physical) mux exceptions how?
- }
-
- /**
- * Incoming mux control block, destined for the control channel (id 0)
- */
- @Override
- public void onMuxFlowControl(MuxFlowControl flow)
- {
- if (flow.getSendQuotaSize() > 0x7F_FF_FF_FF_FF_FF_FF_FFL)
- {
- throw new MuxPhysicalConnectionException(MuxDropChannel.Reason.SEND_QUOTA_OVERFLOW,"Send Quota Overflow");
- }
-
- // Process channel
- long channelId = flow.getChannelId();
- MuxChannel channel = getChannel(channelId,false);
-
- // TODO: set channel quota
- }
-
- /**
- * Incoming mux control block, destined for the control channel (id 0)
- */
- @Override
- public void onMuxNewChannelSlot(MuxNewChannelSlot slot)
- {
- if (policy.getBehavior() == WebSocketBehavior.SERVER)
- {
- throw new MuxPhysicalConnectionException(MuxDropChannel.Reason.UNKNOWN_MUX_CONTROL_BLOCK,"NewChannelSlot not allowed per spec");
- }
-
- if (slot.isFallback())
- {
- if (slot.getNumberOfSlots() == 0)
- {
- throw new MuxPhysicalConnectionException(MuxDropChannel.Reason.UNKNOWN_MUX_CONTROL_BLOCK,"Cannot have 0 number of slots during fallback");
- }
- if (slot.getInitialSendQuota() == 0)
- {
- throw new MuxPhysicalConnectionException(MuxDropChannel.Reason.UNKNOWN_MUX_CONTROL_BLOCK,"Cannot have 0 initial send quota during fallback");
- }
- }
-
- // TODO: handle channel slot
- }
-
- /**
- * Outgoing frame, without mux encapsulated payload.
- */
- public void output(long channelId, Frame frame, WriteCallback callback)
- {
- if (LOG.isDebugEnabled())
- {
- LOG.debug("output({}, {})",channelId,frame,callback);
- }
- generator.generate(channelId,frame,callback);
- }
-
- /**
- * Write an OP out the physical connection.
- *
- * @param op
- * the mux operation to write
- * @throws IOException
- */
- public void output(MuxControlBlock op) throws IOException
- {
- generator.generate(null,op);
- }
-
- public void setAddClient(MuxAddClient addClient)
- {
- this.addClient = addClient;
- }
-
- public void setAddServer(MuxAddServer addServer)
- {
- this.addServer = addServer;
- }
-
- public void setOutgoingFramesHandler(OutgoingFrames outgoing)
- {
- this.generator.setOutgoing(outgoing);
- }
-
- /**
- * Set the remote address of the physical connection.
- * <p>
- * This address made available to sub-channels.
- *
- * @param remoteAddress
- * the remote address
- */
- public void setRemoteAddress(InetSocketAddress remoteAddress)
- {
- this.remoteAddress = remoteAddress;
- }
-
- @Override
- public String toString()
- {
- return String.format("Muxer[subChannels.size=%d]",channels.size());
- }
-}
diff --git a/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/extensions/mux/add/MuxAddClient.java b/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/extensions/mux/add/MuxAddClient.java
deleted file mode 100644
index 155c615..0000000
--- a/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/extensions/mux/add/MuxAddClient.java
+++ /dev/null
@@ -1,30 +0,0 @@
-//
-// ========================================================================
-// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
-// ------------------------------------------------------------------------
-// All rights reserved. This program and the accompanying materials
-// are made available under the terms of the Eclipse Public License v1.0
-// and Apache License v2.0 which accompanies this distribution.
-//
-// The Eclipse Public License is available at
-// http://www.eclipse.org/legal/epl-v10.html
-//
-// The Apache License v2.0 is available at
-// http://www.opensource.org/licenses/apache2.0.php
-//
-// You may elect to redistribute this code under either of these licenses.
-// ========================================================================
-//
-
-package org.eclipse.jetty.websocket.common.extensions.mux.add;
-
-import org.eclipse.jetty.websocket.common.WebSocketSession;
-import org.eclipse.jetty.websocket.common.extensions.mux.op.MuxAddChannelResponse;
-
-/**
- * Interface for Mux Client to handle receiving a AddChannelResponse
- */
-public interface MuxAddClient
-{
- WebSocketSession createSession(MuxAddChannelResponse response);
-}
diff --git a/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/extensions/mux/add/MuxAddServer.java b/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/extensions/mux/add/MuxAddServer.java
deleted file mode 100644
index fa5b4f8..0000000
--- a/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/extensions/mux/add/MuxAddServer.java
+++ /dev/null
@@ -1,52 +0,0 @@
-//
-// ========================================================================
-// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
-// ------------------------------------------------------------------------
-// All rights reserved. This program and the accompanying materials
-// are made available under the terms of the Eclipse Public License v1.0
-// and Apache License v2.0 which accompanies this distribution.
-//
-// The Eclipse Public License is available at
-// http://www.eclipse.org/legal/epl-v10.html
-//
-// The Apache License v2.0 is available at
-// http://www.opensource.org/licenses/apache2.0.php
-//
-// You may elect to redistribute this code under either of these licenses.
-// ========================================================================
-//
-
-package org.eclipse.jetty.websocket.common.extensions.mux.add;
-
-import java.io.IOException;
-
-import org.eclipse.jetty.websocket.api.UpgradeRequest;
-import org.eclipse.jetty.websocket.api.UpgradeResponse;
-import org.eclipse.jetty.websocket.common.WebSocketSession;
-import org.eclipse.jetty.websocket.common.extensions.mux.MuxChannel;
-import org.eclipse.jetty.websocket.common.extensions.mux.MuxException;
-import org.eclipse.jetty.websocket.common.extensions.mux.Muxer;
-
-/**
- * Server interface, for dealing with incoming AddChannelRequest / AddChannelResponse flows.
- */
-public interface MuxAddServer
-{
- public UpgradeRequest getPhysicalHandshakeRequest();
-
- public UpgradeResponse getPhysicalHandshakeResponse();
-
- /**
- * Perform the handshake.
- *
- * @param channel
- * the channel to attach the {@link WebSocketSession} to.
- * @param requestHandshake
- * the request handshake (request headers)
- * @throws AbstractMuxException
- * if unable to handshake
- * @throws IOException
- * if unable to parse request headers
- */
- void handshake(Muxer muxer, MuxChannel channel, UpgradeRequest request) throws MuxException, IOException;
-}
diff --git a/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/extensions/mux/add/package-info.java b/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/extensions/mux/add/package-info.java
deleted file mode 100644
index 87985ef..0000000
--- a/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/extensions/mux/add/package-info.java
+++ /dev/null
@@ -1,23 +0,0 @@
-//
-// ========================================================================
-// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
-// ------------------------------------------------------------------------
-// All rights reserved. This program and the accompanying materials
-// are made available under the terms of the Eclipse Public License v1.0
-// and Apache License v2.0 which accompanies this distribution.
-//
-// The Eclipse Public License is available at
-// http://www.eclipse.org/legal/epl-v10.html
-//
-// The Apache License v2.0 is available at
-// http://www.opensource.org/licenses/apache2.0.php
-//
-// You may elect to redistribute this code under either of these licenses.
-// ========================================================================
-//
-
-/**
- * Jetty WebSocket Common : MUX Extension Add Channel Handling [<em>Unstable Early Draft</em>]
- */
-package org.eclipse.jetty.websocket.common.extensions.mux.add;
-
diff --git a/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/extensions/mux/op/MuxAddChannelRequest.java b/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/extensions/mux/op/MuxAddChannelRequest.java
deleted file mode 100644
index 64fb928..0000000
--- a/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/extensions/mux/op/MuxAddChannelRequest.java
+++ /dev/null
@@ -1,114 +0,0 @@
-//
-// ========================================================================
-// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
-// ------------------------------------------------------------------------
-// All rights reserved. This program and the accompanying materials
-// are made available under the terms of the Eclipse Public License v1.0
-// and Apache License v2.0 which accompanies this distribution.
-//
-// The Eclipse Public License is available at
-// http://www.eclipse.org/legal/epl-v10.html
-//
-// The Apache License v2.0 is available at
-// http://www.opensource.org/licenses/apache2.0.php
-//
-// You may elect to redistribute this code under either of these licenses.
-// ========================================================================
-//
-
-package org.eclipse.jetty.websocket.common.extensions.mux.op;
-
-import java.nio.ByteBuffer;
-
-import org.eclipse.jetty.util.BufferUtil;
-import org.eclipse.jetty.util.StringUtil;
-import org.eclipse.jetty.websocket.common.extensions.mux.MuxControlBlock;
-import org.eclipse.jetty.websocket.common.extensions.mux.MuxOp;
-
-public class MuxAddChannelRequest implements MuxControlBlock
-{
- public static final byte IDENTITY_ENCODING = (byte)0x00;
- public static final byte DELTA_ENCODING = (byte)0x01;
-
- private long channelId = -1;
- private byte encoding;
- private ByteBuffer handshake;
- private byte rsv;
-
- public long getChannelId()
- {
- return channelId;
- }
-
- public byte getEncoding()
- {
- return encoding;
- }
-
- public ByteBuffer getHandshake()
- {
- return handshake;
- }
-
- public long getHandshakeSize()
- {
- if (handshake == null)
- {
- return 0;
- }
- return handshake.remaining();
- }
-
- @Override
- public int getOpCode()
- {
- return MuxOp.ADD_CHANNEL_REQUEST;
- }
-
- public byte getRsv()
- {
- return rsv;
- }
-
- public boolean isDeltaEncoded()
- {
- return (encoding == DELTA_ENCODING);
- }
-
- public boolean isIdentityEncoded()
- {
- return (encoding == IDENTITY_ENCODING);
- }
-
- public void setChannelId(long channelId)
- {
- this.channelId = channelId;
- }
-
- public void setEncoding(byte enc)
- {
- this.encoding = enc;
- }
-
- public void setHandshake(ByteBuffer handshake)
- {
- if (handshake == null)
- {
- this.handshake = null;
- }
- else
- {
- this.handshake = handshake.slice();
- }
- }
-
- public void setHandshake(String rawstring)
- {
- setHandshake(BufferUtil.toBuffer(rawstring,StringUtil.__UTF8_CHARSET));
- }
-
- public void setRsv(byte rsv)
- {
- this.rsv = rsv;
- }
-}
diff --git a/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/extensions/mux/op/MuxAddChannelResponse.java b/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/extensions/mux/op/MuxAddChannelResponse.java
deleted file mode 100644
index a30b6ec..0000000
--- a/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/extensions/mux/op/MuxAddChannelResponse.java
+++ /dev/null
@@ -1,125 +0,0 @@
-//
-// ========================================================================
-// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
-// ------------------------------------------------------------------------
-// All rights reserved. This program and the accompanying materials
-// are made available under the terms of the Eclipse Public License v1.0
-// and Apache License v2.0 which accompanies this distribution.
-//
-// The Eclipse Public License is available at
-// http://www.eclipse.org/legal/epl-v10.html
-//
-// The Apache License v2.0 is available at
-// http://www.opensource.org/licenses/apache2.0.php
-//
-// You may elect to redistribute this code under either of these licenses.
-// ========================================================================
-//
-
-package org.eclipse.jetty.websocket.common.extensions.mux.op;
-
-import java.nio.ByteBuffer;
-
-import org.eclipse.jetty.util.BufferUtil;
-import org.eclipse.jetty.util.StringUtil;
-import org.eclipse.jetty.websocket.common.extensions.mux.MuxControlBlock;
-import org.eclipse.jetty.websocket.common.extensions.mux.MuxOp;
-
-public class MuxAddChannelResponse implements MuxControlBlock
-{
- public static final byte IDENTITY_ENCODING = (byte)0x00;
- public static final byte DELTA_ENCODING = (byte)0x01;
-
- private long channelId;
- private byte encoding;
- private byte rsv;
- private boolean failed = false;
- private ByteBuffer handshake;
-
- public long getChannelId()
- {
- return channelId;
- }
-
- public byte getEncoding()
- {
- return encoding;
- }
-
- public ByteBuffer getHandshake()
- {
- return handshake;
- }
-
- public long getHandshakeSize()
- {
- if (handshake == null)
- {
- return 0;
- }
- return handshake.remaining();
- }
-
- @Override
- public int getOpCode()
- {
- return MuxOp.ADD_CHANNEL_RESPONSE;
- }
-
- public byte getRsv()
- {
- return rsv;
- }
-
- public boolean isDeltaEncoded()
- {
- return (encoding == DELTA_ENCODING);
- }
-
- public boolean isFailed()
- {
- return failed;
- }
-
- public boolean isIdentityEncoded()
- {
- return (encoding == IDENTITY_ENCODING);
- }
-
- public void setChannelId(long channelId)
- {
- this.channelId = channelId;
- }
-
- public void setEncoding(byte enc)
- {
- this.encoding = enc;
- }
-
- public void setFailed(boolean failed)
- {
- this.failed = failed;
- }
-
- public void setHandshake(ByteBuffer handshake)
- {
- if (handshake == null)
- {
- this.handshake = null;
- }
- else
- {
- this.handshake = handshake.slice();
- }
- }
-
- public void setHandshake(String responseHandshake)
- {
- setHandshake(BufferUtil.toBuffer(responseHandshake,StringUtil.__UTF8_CHARSET));
- }
-
- public void setRsv(byte rsv)
- {
- this.rsv = rsv;
- }
-}
diff --git a/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/extensions/mux/op/MuxDropChannel.java b/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/extensions/mux/op/MuxDropChannel.java
deleted file mode 100644
index 1d062e1..0000000
--- a/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/extensions/mux/op/MuxDropChannel.java
+++ /dev/null
@@ -1,183 +0,0 @@
-//
-// ========================================================================
-// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
-// ------------------------------------------------------------------------
-// All rights reserved. This program and the accompanying materials
-// are made available under the terms of the Eclipse Public License v1.0
-// and Apache License v2.0 which accompanies this distribution.
-//
-// The Eclipse Public License is available at
-// http://www.eclipse.org/legal/epl-v10.html
-//
-// The Apache License v2.0 is available at
-// http://www.opensource.org/licenses/apache2.0.php
-//
-// You may elect to redistribute this code under either of these licenses.
-// ========================================================================
-//
-
-package org.eclipse.jetty.websocket.common.extensions.mux.op;
-
-import java.nio.ByteBuffer;
-import java.util.HashMap;
-import java.util.Map;
-
-import org.eclipse.jetty.websocket.common.extensions.mux.MuxControlBlock;
-import org.eclipse.jetty.websocket.common.extensions.mux.MuxOp;
-
-public class MuxDropChannel implements MuxControlBlock
-{
- /**
- * Outlined in <a href="https://tools.ietf.org/html/draft-ietf-hybi-websocket-multiplexing-05#section-9.4.1">Section 9.4.1. Drop Reason Codes</a>
- */
- public static enum Reason
- {
- // Normal Close : (1000-1999)
- NORMAL_CLOSURE(1000),
-
- // Failures in Physical Connection : (2000-2999)
- PHYSICAL_CONNECTION_FAILED(2000),
- INVALID_ENCAPSULATING_MESSAGE(2001),
- CHANNEL_ID_TRUNCATED(2002),
- ENCAPSULATED_FRAME_TRUNCATED(2003),
- UNKNOWN_MUX_CONTROL_OPC(2004),
- UNKNOWN_MUX_CONTROL_BLOCK(2005),
- CHANNEL_ALREADY_EXISTS(2006),
- NEW_CHANNEL_SLOT_VIOLATION(2007),
- NEW_CHANNEL_SLOT_OVERFLOW(2008),
- BAD_REQUEST(2009),
- UNKNOWN_REQUEST_ENCODING(2010),
- BAD_RESPONSE(2011),
- UNKNOWN_RESPONSE_ENCODING(2012),
-
- // Failures in Logical Connections : (3000-3999)
- LOGICAL_CHANNEL_FAILED(3000),
- SEND_QUOTA_VIOLATION(3005),
- SEND_QUOTA_OVERFLOW(3006),
- IDLE_TIMEOUT(3007),
- DROP_CHANNEL_ACK(3008),
-
- // Other Peer Actions : (4000-4999)
- USE_ANOTHER_PHYSICAL_CONNECTION(4001),
- BUSY(4002);
-
- private static final Map<Integer, Reason> codeMap;
-
- static
- {
- codeMap = new HashMap<>();
- for (Reason r : values())
- {
- codeMap.put(r.getValue(),r);
- }
- }
-
- public static Reason valueOf(int code)
- {
- return codeMap.get(code);
- }
-
- private final int code;
-
- private Reason(int code)
- {
- this.code = code;
- }
-
- public int getValue()
- {
- return code;
- }
- }
-
- public static MuxDropChannel parse(long channelId, ByteBuffer payload)
- {
- // TODO Auto-generated method stub
- return null;
- }
-
- private final long channelId;
- private final Reason code;
- private String phrase;
- private int rsv;
-
- /**
- * Normal Drop. no reason Phrase.
- *
- * @param channelId
- * the logical channel Id to perform drop against.
- */
- public MuxDropChannel(long channelId)
- {
- this(channelId,Reason.NORMAL_CLOSURE,null);
- }
-
- /**
- * Drop with reason code and optional phrase
- *
- * @param channelId
- * the logical channel Id to perform drop against.
- * @param code
- * reason code
- * @param phrase
- * optional human readable phrase
- */
- public MuxDropChannel(long channelId, int code, String phrase)
- {
- this(channelId, Reason.valueOf(code), phrase);
- }
-
- /**
- * Drop with reason code and optional phrase
- *
- * @param channelId
- * the logical channel Id to perform drop against.
- * @param code
- * reason code
- * @param phrase
- * optional human readable phrase
- */
- public MuxDropChannel(long channelId, Reason code, String phrase)
- {
- this.channelId = channelId;
- this.code = code;
- this.phrase = phrase;
- }
-
- public ByteBuffer asReasonBuffer()
- {
- // TODO: convert to reason buffer
- return null;
- }
-
- public long getChannelId()
- {
- return channelId;
- }
-
- public Reason getCode()
- {
- return code;
- }
-
- @Override
- public int getOpCode()
- {
- return MuxOp.DROP_CHANNEL;
- }
-
- public String getPhrase()
- {
- return phrase;
- }
-
- public int getRsv()
- {
- return rsv;
- }
-
- public void setRsv(int rsv)
- {
- this.rsv = rsv;
- }
-}
diff --git a/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/extensions/mux/op/MuxFlowControl.java b/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/extensions/mux/op/MuxFlowControl.java
deleted file mode 100644
index 32b6d96..0000000
--- a/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/extensions/mux/op/MuxFlowControl.java
+++ /dev/null
@@ -1,65 +0,0 @@
-//
-// ========================================================================
-// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
-// ------------------------------------------------------------------------
-// All rights reserved. This program and the accompanying materials
-// are made available under the terms of the Eclipse Public License v1.0
-// and Apache License v2.0 which accompanies this distribution.
-//
-// The Eclipse Public License is available at
-// http://www.eclipse.org/legal/epl-v10.html
-//
-// The Apache License v2.0 is available at
-// http://www.opensource.org/licenses/apache2.0.php
-//
-// You may elect to redistribute this code under either of these licenses.
-// ========================================================================
-//
-
-package org.eclipse.jetty.websocket.common.extensions.mux.op;
-
-import org.eclipse.jetty.websocket.common.extensions.mux.MuxControlBlock;
-import org.eclipse.jetty.websocket.common.extensions.mux.MuxOp;
-
-public class MuxFlowControl implements MuxControlBlock
-{
- private long channelId;
- private byte rsv;
- private long sendQuotaSize;
-
- public long getChannelId()
- {
- return channelId;
- }
-
- @Override
- public int getOpCode()
- {
- return MuxOp.FLOW_CONTROL;
- }
-
- public byte getRsv()
- {
- return rsv;
- }
-
- public long getSendQuotaSize()
- {
- return sendQuotaSize;
- }
-
- public void setChannelId(long channelId)
- {
- this.channelId = channelId;
- }
-
- public void setRsv(byte rsv)
- {
- this.rsv = rsv;
- }
-
- public void setSendQuotaSize(long sendQuotaSize)
- {
- this.sendQuotaSize = sendQuotaSize;
- }
-}
diff --git a/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/extensions/mux/op/MuxNewChannelSlot.java b/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/extensions/mux/op/MuxNewChannelSlot.java
deleted file mode 100644
index 09aadf1..0000000
--- a/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/extensions/mux/op/MuxNewChannelSlot.java
+++ /dev/null
@@ -1,76 +0,0 @@
-//
-// ========================================================================
-// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
-// ------------------------------------------------------------------------
-// All rights reserved. This program and the accompanying materials
-// are made available under the terms of the Eclipse Public License v1.0
-// and Apache License v2.0 which accompanies this distribution.
-//
-// The Eclipse Public License is available at
-// http://www.eclipse.org/legal/epl-v10.html
-//
-// The Apache License v2.0 is available at
-// http://www.opensource.org/licenses/apache2.0.php
-//
-// You may elect to redistribute this code under either of these licenses.
-// ========================================================================
-//
-
-package org.eclipse.jetty.websocket.common.extensions.mux.op;
-
-import org.eclipse.jetty.websocket.common.extensions.mux.MuxControlBlock;
-import org.eclipse.jetty.websocket.common.extensions.mux.MuxOp;
-
-public class MuxNewChannelSlot implements MuxControlBlock
-{
- private boolean fallback;
- private long initialSendQuota;
- private long numberOfSlots;
- private byte rsv;
-
- public long getInitialSendQuota()
- {
- return initialSendQuota;
- }
-
- public long getNumberOfSlots()
- {
- return numberOfSlots;
- }
-
- @Override
- public int getOpCode()
- {
- return MuxOp.NEW_CHANNEL_SLOT;
- }
-
- public byte getRsv()
- {
- return rsv;
- }
-
- public boolean isFallback()
- {
- return fallback;
- }
-
- public void setFallback(boolean fallback)
- {
- this.fallback = fallback;
- }
-
- public void setInitialSendQuota(long initialSendQuota)
- {
- this.initialSendQuota = initialSendQuota;
- }
-
- public void setNumberOfSlots(long numberOfSlots)
- {
- this.numberOfSlots = numberOfSlots;
- }
-
- public void setRsv(byte rsv)
- {
- this.rsv = rsv;
- }
-}
diff --git a/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/extensions/mux/op/package-info.java b/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/extensions/mux/op/package-info.java
deleted file mode 100644
index cd5c7ac..0000000
--- a/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/extensions/mux/op/package-info.java
+++ /dev/null
@@ -1,23 +0,0 @@
-//
-// ========================================================================
-// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
-// ------------------------------------------------------------------------
-// All rights reserved. This program and the accompanying materials
-// are made available under the terms of the Eclipse Public License v1.0
-// and Apache License v2.0 which accompanies this distribution.
-//
-// The Eclipse Public License is available at
-// http://www.eclipse.org/legal/epl-v10.html
-//
-// The Apache License v2.0 is available at
-// http://www.opensource.org/licenses/apache2.0.php
-//
-// You may elect to redistribute this code under either of these licenses.
-// ========================================================================
-//
-
-/**
- * Jetty WebSocket Common : MUX Extension OpCode Handling [<em>Unstable Early Draft</em>]
- */
-package org.eclipse.jetty.websocket.common.extensions.mux.op;
-
diff --git a/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/extensions/mux/package-info.java b/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/extensions/mux/package-info.java
deleted file mode 100644
index 7112f3b..0000000
--- a/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/extensions/mux/package-info.java
+++ /dev/null
@@ -1,23 +0,0 @@
-//
-// ========================================================================
-// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
-// ------------------------------------------------------------------------
-// All rights reserved. This program and the accompanying materials
-// are made available under the terms of the Eclipse Public License v1.0
-// and Apache License v2.0 which accompanies this distribution.
-//
-// The Eclipse Public License is available at
-// http://www.eclipse.org/legal/epl-v10.html
-//
-// The Apache License v2.0 is available at
-// http://www.opensource.org/licenses/apache2.0.php
-//
-// You may elect to redistribute this code under either of these licenses.
-// ========================================================================
-//
-
-/**
- * Jetty WebSocket Common : MUX Extension Core [<em>Unstable Early Draft</em>]
- */
-package org.eclipse.jetty.websocket.common.extensions.mux;
-
diff --git a/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/io/AbstractWebSocketConnection.java b/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/io/AbstractWebSocketConnection.java
index 032c45c..b293bba 100644
--- a/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/io/AbstractWebSocketConnection.java
+++ b/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/io/AbstractWebSocketConnection.java
@@ -96,7 +96,7 @@
// Abnormal Close
reason = CloseStatus.trimMaxReasonLength(reason);
- session.incomingError(new WebSocketException(x)); // TODO: JSR-356 change to Throwable
+ session.notifyError(x);
session.notifyClose(StatusCode.NO_CLOSE,reason);
disconnect(); // disconnect endpoint & connection
@@ -216,7 +216,13 @@
this.writeBytes = new WriteBytesProvider(generator,new FlushCallback());
this.setInputBufferSize(policy.getInputBufferSize());
}
-
+
+ @Override
+ public Executor getExecutor()
+ {
+ return super.getExecutor();
+ }
+
@Override
public void close()
{
@@ -348,6 +354,7 @@
write(buffer);
}
+ @Override
public ByteBufferPool getBufferPool()
{
return bufferPool;
@@ -371,6 +378,12 @@
}
@Override
+ public long getIdleTimeout()
+ {
+ return getEndPoint().getIdleTimeout();
+ }
+
+ @Override
public IOState getIOState()
{
return ioState;
@@ -518,7 +531,7 @@
}
// Initiate close - politely send close frame.
- session.incomingError(new WebSocketTimeoutException("Timeout on Read"));
+ session.notifyError(new WebSocketTimeoutException("Timeout on Read"));
close(StatusCode.SHUTDOWN,"Idle Timeout");
return false;
@@ -545,7 +558,7 @@
EndPoint endPoint = getEndPoint();
try
{
- while (true)
+ while (true) // TODO: should this honor the LogicalConnection.suspend() ?
{
int filled = endPoint.fill(buffer);
if (filled == 0)
@@ -565,6 +578,7 @@
LOG.debug("Filled {} bytes - {}",filled,BufferUtil.toDetailString(buffer));
}
parser.parse(buffer);
+ // TODO: has the end user application already consumed what it was given?
}
}
}
diff --git a/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/io/FramePipes.java b/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/io/FramePipes.java
index c349877..229ea71 100644
--- a/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/io/FramePipes.java
+++ b/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/io/FramePipes.java
@@ -18,7 +18,6 @@
package org.eclipse.jetty.websocket.common.io;
-import org.eclipse.jetty.websocket.api.WebSocketException;
import org.eclipse.jetty.websocket.api.WriteCallback;
import org.eclipse.jetty.websocket.api.extensions.Frame;
import org.eclipse.jetty.websocket.api.extensions.IncomingFrames;
@@ -36,7 +35,7 @@
}
@Override
- public void incomingError(WebSocketException e)
+ public void incomingError(Throwable t)
{
/* cannot send exception on */
}
@@ -60,7 +59,15 @@
@Override
public void outgoingFrame(Frame frame, WriteCallback callback)
{
- this.incoming.incomingFrame(frame);
+ try
+ {
+ this.incoming.incomingFrame(frame);
+ callback.writeSuccess();
+ }
+ catch (Throwable t)
+ {
+ callback.writeFailed(t);
+ }
}
}
diff --git a/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/io/IOState.java b/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/io/IOState.java
index e5bfd08..779a1cf 100644
--- a/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/io/IOState.java
+++ b/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/io/IOState.java
@@ -21,8 +21,6 @@
import java.io.IOException;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
-import java.util.concurrent.atomic.AtomicBoolean;
-import java.util.concurrent.atomic.AtomicReference;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;
@@ -64,12 +62,11 @@
private ConnectionState state;
private final List<ConnectionStateListener> listeners = new CopyOnWriteArrayList<>();
- private final AtomicBoolean inputAvailable;
- private final AtomicBoolean outputAvailable;
- private final AtomicReference<CloseHandshakeSource> closeHandshakeSource;
- private final AtomicReference<CloseInfo> closeInfo;
-
- private final AtomicBoolean cleanClose;
+ private boolean inputAvailable;
+ private boolean outputAvailable;
+ private CloseHandshakeSource closeHandshakeSource;
+ private CloseInfo closeInfo;
+ private boolean cleanClose;
/**
* Create a new IOState, initialized to {@link ConnectionState#CONNECTING}
@@ -77,11 +74,11 @@
public IOState()
{
this.state = ConnectionState.CONNECTING;
- this.inputAvailable = new AtomicBoolean(false);
- this.outputAvailable = new AtomicBoolean(false);
- this.closeHandshakeSource = new AtomicReference<>(CloseHandshakeSource.NONE);
- this.closeInfo = new AtomicReference<>();
- this.cleanClose = new AtomicBoolean(false);
+ this.inputAvailable = false;
+ this.outputAvailable = false;
+ this.closeHandshakeSource = CloseHandshakeSource.NONE;
+ this.closeInfo = null;
+ this.cleanClose = false;
}
public void addListener(ConnectionStateListener listener)
@@ -107,7 +104,7 @@
public CloseInfo getCloseInfo()
{
- return closeInfo.get();
+ return closeInfo;
}
public ConnectionState getConnectionState()
@@ -125,7 +122,7 @@
public boolean isInputAvailable()
{
- return inputAvailable.get();
+ return inputAvailable;
}
public boolean isOpen()
@@ -135,7 +132,7 @@
public boolean isOutputAvailable()
{
- return outputAvailable.get();
+ return outputAvailable;
}
private void notifyStateListeners(ConnectionState state)
@@ -154,7 +151,7 @@
public void onAbnormalClose(CloseInfo close)
{
ConnectionState event = null;
- synchronized (this.state)
+ synchronized (this)
{
if (this.state == ConnectionState.CLOSED)
{
@@ -164,14 +161,15 @@
if (this.state == ConnectionState.OPEN)
{
- this.cleanClose.set(false);
+ this.cleanClose = false;
}
this.state = ConnectionState.CLOSED;
- this.closeInfo.compareAndSet(null,close);
- this.inputAvailable.set(false);
- this.outputAvailable.set(false);
- this.closeHandshakeSource.set(CloseHandshakeSource.ABNORMAL);
+ if (closeInfo == null)
+ this.closeInfo = close;
+ this.inputAvailable = false;
+ this.outputAvailable = false;
+ this.closeHandshakeSource = CloseHandshakeSource.ABNORMAL;
event = this.state;
}
notifyStateListeners(event);
@@ -200,22 +198,26 @@
onOpened();
}
- synchronized (this.state)
+ synchronized (this)
{
- closeInfo.compareAndSet(null,close);
+ if (closeInfo == null)
+ closeInfo = close;
- boolean in = inputAvailable.get();
- boolean out = outputAvailable.get();
- closeHandshakeSource.compareAndSet(CloseHandshakeSource.NONE,CloseHandshakeSource.LOCAL);
+ boolean in = inputAvailable;
+ boolean out = outputAvailable;
+ if (closeHandshakeSource == CloseHandshakeSource.NONE)
+ {
+ closeHandshakeSource = CloseHandshakeSource.LOCAL;
+ }
out = false;
- outputAvailable.set(false);
+ outputAvailable = false;
LOG.debug("onCloseLocal(), input={}, output={}",in,out);
if (!in && !out)
{
LOG.debug("Close Handshake satisfied, disconnecting");
- cleanClose.set(true);
+ cleanClose = true;
this.state = ConnectionState.CLOSED;
event = this.state;
}
@@ -226,7 +228,7 @@
event = this.state;
}
}
-
+
LOG.debug("event = {}",event);
// Only notify on state change events
@@ -239,13 +241,13 @@
if (close.isHarsh())
{
LOG.debug("Harsh close, disconnecting");
- synchronized (this.state)
+ synchronized (this)
{
- this.state = ConnectionState.CLOSED;
- cleanClose.set(false);
- outputAvailable.set(false);
- inputAvailable.set(false);
- this.closeHandshakeSource.set(CloseHandshakeSource.ABNORMAL);
+ state = ConnectionState.CLOSED;
+ cleanClose = false;
+ outputAvailable = false;
+ inputAvailable = false;
+ closeHandshakeSource = CloseHandshakeSource.ABNORMAL;
event = this.state;
}
notifyStateListeners(event);
@@ -261,7 +263,7 @@
{
LOG.debug("onCloseRemote({})",close);
ConnectionState event = null;
- synchronized (this.state)
+ synchronized (this)
{
if (this.state == ConnectionState.CLOSED)
{
@@ -269,21 +271,25 @@
return;
}
- closeInfo.compareAndSet(null,close);
+ if (closeInfo == null)
+ closeInfo = close;
- boolean in = inputAvailable.get();
- boolean out = outputAvailable.get();
- closeHandshakeSource.compareAndSet(CloseHandshakeSource.NONE,CloseHandshakeSource.REMOTE);
+ boolean in = inputAvailable;
+ boolean out = outputAvailable;
+ if (closeHandshakeSource == CloseHandshakeSource.NONE)
+ {
+ closeHandshakeSource = CloseHandshakeSource.REMOTE;
+ }
in = false;
- inputAvailable.set(false);
+ inputAvailable = false;
LOG.debug("onCloseRemote(), input={}, output={}",in,out);
if (!in && !out)
{
LOG.debug("Close Handshake satisfied, disconnecting");
- cleanClose.set(true);
- this.state = ConnectionState.CLOSED;
+ cleanClose = true;
+ state = ConnectionState.CLOSED;
event = this.state;
}
else if (this.state == ConnectionState.OPEN)
@@ -315,11 +321,11 @@
}
ConnectionState event = null;
- synchronized (this.state)
+ synchronized (this)
{
this.state = ConnectionState.CONNECTED;
- this.inputAvailable.set(false); // cannot read (yet)
- this.outputAvailable.set(true); // write allowed
+ inputAvailable = false; // cannot read (yet)
+ outputAvailable = true; // write allowed
event = this.state;
}
notifyStateListeners(event);
@@ -332,12 +338,12 @@
{
assert (this.state == ConnectionState.CONNECTING);
ConnectionState event = null;
- synchronized (this.state)
+ synchronized (this)
{
this.state = ConnectionState.CLOSED;
- this.cleanClose.set(false);
- this.inputAvailable.set(false);
- this.outputAvailable.set(false);
+ cleanClose = false;
+ inputAvailable = false;
+ outputAvailable = false;
event = this.state;
}
notifyStateListeners(event);
@@ -357,11 +363,11 @@
assert (this.state == ConnectionState.CONNECTED);
ConnectionState event = null;
- synchronized (this.state)
+ synchronized (this)
{
this.state = ConnectionState.OPEN;
- this.inputAvailable.set(true);
- this.outputAvailable.set(true);
+ this.inputAvailable = true;
+ this.outputAvailable = true;
event = this.state;
}
notifyStateListeners(event);
@@ -375,7 +381,7 @@
public void onReadEOF()
{
ConnectionState event = null;
- synchronized (this.state)
+ synchronized (this)
{
if (this.state == ConnectionState.CLOSED)
{
@@ -385,12 +391,13 @@
CloseInfo close = new CloseInfo(StatusCode.NO_CLOSE,"Read EOF");
- this.cleanClose.set(false);
+ this.cleanClose = false;
this.state = ConnectionState.CLOSED;
- this.closeInfo.compareAndSet(null,close);
- this.inputAvailable.set(false);
- this.outputAvailable.set(false);
- this.closeHandshakeSource.set(CloseHandshakeSource.ABNORMAL);
+ if (closeInfo == null)
+ this.closeInfo = close;
+ this.inputAvailable = false;
+ this.outputAvailable = false;
+ this.closeHandshakeSource = CloseHandshakeSource.ABNORMAL;
event = this.state;
}
notifyStateListeners(event);
@@ -398,21 +405,21 @@
public boolean wasAbnormalClose()
{
- return closeHandshakeSource.get() == CloseHandshakeSource.ABNORMAL;
+ return closeHandshakeSource == CloseHandshakeSource.ABNORMAL;
}
public boolean wasCleanClose()
{
- return cleanClose.get();
+ return cleanClose;
}
public boolean wasLocalCloseInitiated()
{
- return closeHandshakeSource.get() == CloseHandshakeSource.LOCAL;
+ return closeHandshakeSource == CloseHandshakeSource.LOCAL;
}
public boolean wasRemoteCloseInitiated()
{
- return closeHandshakeSource.get() == CloseHandshakeSource.REMOTE;
+ return closeHandshakeSource == CloseHandshakeSource.REMOTE;
}
}
diff --git a/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/io/WriteCallbackWrapper.java b/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/io/WriteCallbackWrapper.java
index 34b2bf1..e2cb9aa 100644
--- a/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/io/WriteCallbackWrapper.java
+++ b/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/io/WriteCallbackWrapper.java
@@ -22,7 +22,7 @@
import org.eclipse.jetty.websocket.api.WriteCallback;
/**
- * Wraps the exposed {@link WriteCallback} API with a Jetty {@link Callback}.
+ * Wraps the exposed {@link WriteCallback} WebSocket API with a Jetty {@link Callback}.
* <p>
* We don't expose the jetty {@link Callback} object to the webapp, as that makes things complicated for the WebAppContext's Classloader.
*/
diff --git a/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/message/MessageAppender.java b/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/message/MessageAppender.java
index fb6ba7f..96940eb 100644
--- a/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/message/MessageAppender.java
+++ b/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/message/MessageAppender.java
@@ -31,13 +31,17 @@
*
* @param payload
* the payload to append.
+ * @param isLast
+ * flag indicating if this is the last part of the message or not.
* @throws IOException
* if unable to append the payload
*/
- abstract void appendMessage(ByteBuffer payload) throws IOException;
+ abstract void appendMessage(ByteBuffer payload, boolean isLast) throws IOException;
/**
* Notification that message is to be considered complete.
+ * <p>
+ * Any cleanup or final actions should be taken here.
*/
abstract void messageComplete();
}
diff --git a/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/message/MessageInputStream.java b/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/message/MessageInputStream.java
index 21f4ce1..08e03a7 100644
--- a/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/message/MessageInputStream.java
+++ b/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/message/MessageInputStream.java
@@ -21,98 +21,151 @@
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
+import java.util.concurrent.BlockingDeque;
+import java.util.concurrent.LinkedBlockingDeque;
+import java.util.concurrent.atomic.AtomicBoolean;
import org.eclipse.jetty.util.BufferUtil;
-import org.eclipse.jetty.websocket.common.events.AnnotatedEventDriver;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+import org.eclipse.jetty.websocket.common.LogicalConnection;
/**
- * Support class for reading binary message data as an InputStream.
+ * Support class for reading a (single) WebSocket BINARY message via a InputStream.
+ * <p>
+ * An InputStream that can access a queue of ByteBuffer payloads, along with expected InputStream blocking behavior.
*/
public class MessageInputStream extends InputStream implements MessageAppender
{
- private static final int BUFFER_SIZE = 65535;
+ private static final Logger LOG = Log.getLogger(MessageInputStream.class);
/**
- * Threshold (of bytes) to perform compaction at
+ * Used for controlling read suspend/resume behavior if the queue is full, but the read operations haven't caught up yet.
*/
- private static final int COMPACT_THRESHOLD = 5;
- private final AnnotatedEventDriver driver;
- private final ByteBuffer buf;
- private int size;
- private boolean finished;
- private boolean needsNotification;
- private int readPosition;
+ @SuppressWarnings("unused")
+ private final LogicalConnection connection;
+ private final BlockingDeque<ByteBuffer> buffers = new LinkedBlockingDeque<>();
+ private AtomicBoolean closed = new AtomicBoolean(false);
+ // EOB / End of Buffers
+ private AtomicBoolean buffersExhausted = new AtomicBoolean(false);
+ private ByteBuffer activeBuffer = null;
- public MessageInputStream(AnnotatedEventDriver driver)
+ public MessageInputStream(LogicalConnection connection)
{
- this.driver = driver;
- this.buf = ByteBuffer.allocate(BUFFER_SIZE);
- BufferUtil.clearToFill(this.buf);
- size = 0;
- readPosition = this.buf.position();
- finished = false;
- needsNotification = true;
+ this.connection = connection;
}
@Override
- public void appendMessage(ByteBuffer payload) throws IOException
+ public void appendMessage(ByteBuffer payload, boolean isLast) throws IOException
{
- if (finished)
+ if (LOG.isDebugEnabled())
{
- throw new IOException("Cannot append to finished buffer");
+ LOG.debug("appendMessage(ByteBuffer,{}): {}",isLast,BufferUtil.toDetailString(payload));
+ }
+
+ if (buffersExhausted.get())
+ {
+ // This indicates a programming mistake/error and must be bug fixed
+ throw new RuntimeException("Last frame already received");
}
- if (payload == null)
+ // if closed, we should just toss incoming payloads into the bit bucket.
+ if (closed.get())
{
- // empty payload is valid
return;
}
- driver.getPolicy().assertValidMessageSize(size + payload.remaining());
- size += payload.remaining();
-
- synchronized (buf)
+ // Put the payload into the queue
+ try
{
- // TODO: grow buffer till max binary message size?
- // TODO: compact this buffer to fit incoming buffer?
- // TODO: tell connection to suspend if buffer too full?
- BufferUtil.put(payload,buf);
+ buffers.put(payload);
+ if (isLast)
+ {
+ buffersExhausted.set(true);
+ }
}
-
- if (needsNotification)
+ catch (InterruptedException e)
{
- needsNotification = true;
- this.driver.onInputStream(this);
+ throw new IOException(e);
}
}
@Override
public void close() throws IOException
{
- finished = true;
+ closed.set(true);
super.close();
}
@Override
+ public synchronized void mark(int readlimit)
+ {
+ /* do nothing */
+ }
+
+ @Override
+ public boolean markSupported()
+ {
+ return false;
+ }
+
+ @Override
public void messageComplete()
{
- finished = true;
+ LOG.debug("messageComplete()");
+
+ buffersExhausted.set(true);
+ // toss an empty ByteBuffer into queue to let it drain
+ try
+ {
+ buffers.put(ByteBuffer.wrap(new byte[0]));
+ }
+ catch (InterruptedException ignore)
+ {
+ /* ignore */
+ }
}
@Override
public int read() throws IOException
{
- synchronized (buf)
+ LOG.debug("read()");
+
+ try
{
- byte b = buf.get(readPosition);
- readPosition++;
- if (readPosition <= (buf.limit() - COMPACT_THRESHOLD))
+ if (closed.get())
{
- int curPos = buf.position();
- buf.compact();
- int offsetPos = buf.position() - curPos;
- readPosition += offsetPos;
+ return -1;
}
- return b;
+
+ if (activeBuffer == null)
+ {
+ activeBuffer = buffers.take();
+ }
+
+ while (activeBuffer.remaining() <= 0)
+ {
+ if (buffersExhausted.get())
+ {
+ closed.set(true);
+ return -1;
+ }
+ activeBuffer = buffers.take();
+ }
+
+ return activeBuffer.get();
}
+ catch (InterruptedException e)
+ {
+ LOG.warn(e);
+ closed.set(true);
+ return -1;
+// throw new IOException(e);
+ }
+ }
+
+ @Override
+ public synchronized void reset() throws IOException
+ {
+ throw new IOException("reset() not supported");
}
}
diff --git a/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/message/MessageOutputStream.java b/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/message/MessageOutputStream.java
index 29764e6..3b1c182 100644
--- a/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/message/MessageOutputStream.java
+++ b/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/message/MessageOutputStream.java
@@ -20,30 +20,225 @@
import java.io.IOException;
import java.io.OutputStream;
+import java.nio.ByteBuffer;
+import java.util.concurrent.ExecutionException;
+import org.eclipse.jetty.io.ByteBufferPool;
+import org.eclipse.jetty.util.BufferUtil;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+import org.eclipse.jetty.websocket.api.WriteCallback;
import org.eclipse.jetty.websocket.api.extensions.OutgoingFrames;
-import org.eclipse.jetty.websocket.common.LogicalConnection;
+import org.eclipse.jetty.websocket.common.OpCode;
+import org.eclipse.jetty.websocket.common.WebSocketFrame;
+import org.eclipse.jetty.websocket.common.WebSocketSession;
+import org.eclipse.jetty.websocket.common.io.FutureWriteCallback;
+/**
+ * Support for writing a single WebSocket BINARY message via a {@link OutputStream}
+ */
public class MessageOutputStream extends OutputStream
{
- private final LogicalConnection connection;
+ private static final Logger LOG = Log.getLogger(MessageOutputStream.class);
private final OutgoingFrames outgoing;
+ private final ByteBufferPool bufferPool;
+ private long frameCount = 0;
+ private WebSocketFrame frame;
+ private ByteBuffer buffer;
+ private FutureWriteCallback blocker;
+ private WriteCallback callback;
+ private boolean closed = false;
- public MessageOutputStream(LogicalConnection connection, OutgoingFrames outgoing)
+ public MessageOutputStream(OutgoingFrames outgoing, int bufferSize, ByteBufferPool bufferPool)
{
- this.connection = connection;
this.outgoing = outgoing;
+ this.bufferPool = bufferPool;
+ this.buffer = bufferPool.acquire(bufferSize,true);
+ BufferUtil.flipToFill(buffer);
+ this.frame = new WebSocketFrame(OpCode.BINARY);
}
- public boolean isClosed()
+ public MessageOutputStream(WebSocketSession session)
{
- // TODO Auto-generated method stub
- return false;
+ this(session.getOutgoingHandler(),session.getPolicy().getMaxBinaryMessageBufferSize(),session.getBufferPool());
+ }
+
+ private void assertNotClosed() throws IOException
+ {
+ if (closed)
+ {
+ IOException e = new IOException("Stream is closed");
+ notifyFailure(e);
+ throw e;
+ }
}
@Override
- public void write(int b) throws IOException
+ public synchronized void close() throws IOException
{
- // TODO Auto-generated method stub
+ assertNotClosed();
+ LOG.debug("close()");
+
+ // finish sending whatever in the buffer with FIN=true
+ flush(true);
+
+ // close stream
+ LOG.debug("Sent Frame Count: {}",frameCount);
+ closed = true;
+ try
+ {
+ if (callback != null)
+ {
+ callback.writeSuccess();
+ }
+ super.close();
+ bufferPool.release(buffer);
+ LOG.debug("closed");
+ }
+ catch (IOException e)
+ {
+ notifyFailure(e);
+ throw e;
+ }
+ }
+
+ @Override
+ public synchronized void flush() throws IOException
+ {
+ LOG.debug("flush()");
+ assertNotClosed();
+
+ // flush whatever is in the buffer with FIN=false
+ flush(false);
+ try
+ {
+ super.flush();
+ LOG.debug("flushed");
+ }
+ catch (IOException e)
+ {
+ notifyFailure(e);
+ throw e;
+ }
+ }
+
+ /**
+ * Flush whatever is in the buffer.
+ *
+ * @param fin
+ * fin flag
+ * @throws IOException
+ */
+ private synchronized void flush(boolean fin) throws IOException
+ {
+ BufferUtil.flipToFlush(buffer,0);
+ LOG.debug("flush({}): {}",fin,BufferUtil.toDetailString(buffer));
+ frame.setPayload(buffer);
+ frame.setFin(fin);
+
+ try
+ {
+ blocker = new FutureWriteCallback();
+ outgoing.outgoingFrame(frame,blocker);
+ try
+ {
+ // block on write
+ blocker.get();
+ // block success
+ frameCount++;
+ frame.setOpCode(OpCode.CONTINUATION);
+ }
+ catch (ExecutionException e)
+ {
+ Throwable cause = e.getCause();
+ if (cause != null)
+ {
+ if (cause instanceof IOException)
+ {
+ throw (IOException)cause;
+ }
+ else
+ {
+ throw new IOException(cause);
+ }
+ }
+ throw new IOException("Failed to flush",e);
+ }
+ catch (InterruptedException e)
+ {
+ throw new IOException("Failed to flush",e);
+ }
+ }
+ catch (IOException e)
+ {
+ notifyFailure(e);
+ throw e;
+ }
+ }
+
+ private void notifyFailure(IOException e)
+ {
+ if (callback != null)
+ {
+ callback.writeFailed(e);
+ }
+ }
+
+ public void setCallback(WriteCallback callback)
+ {
+ this.callback = callback;
+ }
+
+ @Override
+ public synchronized void write(byte[] b) throws IOException
+ {
+ try
+ {
+ this.write(b,0,b.length);
+ }
+ catch (IOException e)
+ {
+ notifyFailure(e);
+ throw e;
+ }
+ }
+
+ @Override
+ public synchronized void write(byte[] b, int off, int len) throws IOException
+ {
+ LOG.debug("write(byte[{}], {}, {})",b.length,off,len);
+ int left = len; // bytes left to write
+ int offset = off; // offset within provided array
+ while (left > 0)
+ {
+ if (LOG.isDebugEnabled())
+ {
+ LOG.debug("buffer: {}",BufferUtil.toDetailString(buffer));
+ }
+ int space = buffer.remaining();
+ assert (space > 0);
+ int size = Math.min(space,left);
+ buffer.put(b,offset,size);
+ assert (size > 0);
+ left -= size; // decrement bytes left
+ if (left > 0)
+ {
+ flush(false);
+ }
+ offset += size; // increment offset
+ }
+ }
+
+ @Override
+ public synchronized void write(int b) throws IOException
+ {
+ assertNotClosed();
+
+ // buffer up to limit, flush once buffer reached.
+ buffer.put((byte)b);
+ if (buffer.remaining() <= 0)
+ {
+ flush(false);
+ }
}
}
diff --git a/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/message/MessageReader.java b/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/message/MessageReader.java
index e790989..37c065a 100644
--- a/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/message/MessageReader.java
+++ b/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/message/MessageReader.java
@@ -19,79 +19,36 @@
package org.eclipse.jetty.websocket.common.message;
import java.io.IOException;
-import java.io.Reader;
+import java.io.InputStreamReader;
import java.nio.ByteBuffer;
+import java.nio.charset.Charset;
-import org.eclipse.jetty.util.Utf8StringBuilder;
-import org.eclipse.jetty.websocket.common.events.AnnotatedEventDriver;
+import org.eclipse.jetty.util.StringUtil;
/**
- * Support class for reading text message data as an Reader.
+ * Support class for reading a (single) WebSocket TEXT message via a Reader.
* <p>
- * Due to the spec, this reader is forced to use the UTF8 charset.
+ * In compliance to the WebSocket spec, this reader always uses the UTF8 {@link Charset}.
*/
-public class MessageReader extends Reader implements MessageAppender
+public class MessageReader extends InputStreamReader implements MessageAppender
{
- private final AnnotatedEventDriver driver;
- private final Utf8StringBuilder utf;
- private int size;
- private boolean finished;
- private boolean needsNotification;
+ private final MessageInputStream stream;
- public MessageReader(AnnotatedEventDriver driver)
+ public MessageReader(MessageInputStream stream)
{
- this.driver = driver;
- this.utf = new Utf8StringBuilder();
- size = 0;
- finished = false;
- needsNotification = true;
+ super(stream,StringUtil.__UTF8_CHARSET);
+ this.stream = stream;
}
@Override
- public void appendMessage(ByteBuffer payload) throws IOException
+ public void appendMessage(ByteBuffer payload, boolean isLast) throws IOException
{
- if (finished)
- {
- throw new IOException("Cannot append to finished buffer");
- }
-
- if (payload == null)
- {
- // empty payload is valid
- return;
- }
-
- driver.getPolicy().assertValidMessageSize(size + payload.remaining());
- size += payload.remaining();
-
- synchronized (utf)
- {
- utf.append(payload);
- }
-
- if (needsNotification)
- {
- needsNotification = true;
- this.driver.onReader(this);
- }
- }
-
- @Override
- public void close() throws IOException
- {
- finished = true;
+ this.stream.appendMessage(payload,isLast);
}
@Override
public void messageComplete()
{
- finished = true;
- }
-
- @Override
- public int read(char[] cbuf, int off, int len) throws IOException
- {
- // TODO Auto-generated method stub
- return 0;
+ this.stream.messageComplete();
}
}
diff --git a/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/message/MessageWriter.java b/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/message/MessageWriter.java
index 3aed776..f784d48 100644
--- a/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/message/MessageWriter.java
+++ b/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/message/MessageWriter.java
@@ -20,46 +20,204 @@
import java.io.IOException;
import java.io.Writer;
+import java.nio.ByteBuffer;
+import java.util.concurrent.ExecutionException;
+import org.eclipse.jetty.io.ByteBufferPool;
+import org.eclipse.jetty.util.BufferUtil;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+import org.eclipse.jetty.websocket.api.WriteCallback;
import org.eclipse.jetty.websocket.api.extensions.OutgoingFrames;
-import org.eclipse.jetty.websocket.common.LogicalConnection;
+import org.eclipse.jetty.websocket.common.OpCode;
+import org.eclipse.jetty.websocket.common.WebSocketFrame;
+import org.eclipse.jetty.websocket.common.WebSocketSession;
+import org.eclipse.jetty.websocket.common.io.FutureWriteCallback;
+/**
+ * Support for writing a single WebSocket TEXT message via a {@link Writer}
+ * <p>
+ * Note: Per WebSocket spec, all WebSocket TEXT messages must be encoded in UTF-8
+ */
public class MessageWriter extends Writer
{
- private final LogicalConnection connection;
+ private static final Logger LOG = Log.getLogger(MessageWriter.class);
private final OutgoingFrames outgoing;
+ private final ByteBufferPool bufferPool;
+ private long frameCount = 0;
+ private WebSocketFrame frame;
+ private ByteBuffer buffer;
+ private Utf8CharBuffer utf;
+ private FutureWriteCallback blocker;
+ private WriteCallback callback;
+ private boolean closed = false;
- public MessageWriter(LogicalConnection connection, OutgoingFrames outgoing)
+ public MessageWriter(OutgoingFrames outgoing, int bufferSize, ByteBufferPool bufferPool)
{
- this.connection = connection;
this.outgoing = outgoing;
+ this.bufferPool = bufferPool;
+ this.buffer = bufferPool.acquire(bufferSize,true);
+ BufferUtil.flipToFill(buffer);
+ this.utf = Utf8CharBuffer.wrap(buffer);
+ this.frame = new WebSocketFrame(OpCode.TEXT);
+ }
+
+ public MessageWriter(WebSocketSession session)
+ {
+ this(session.getOutgoingHandler(),session.getPolicy().getMaxTextMessageBufferSize(),session.getBufferPool());
+ }
+
+ private void assertNotClosed() throws IOException
+ {
+ if (closed)
+ {
+ IOException e = new IOException("Stream is closed");
+ notifyFailure(e);
+ throw e;
+ }
}
@Override
- public void close() throws IOException
+ public synchronized void close() throws IOException
{
- // TODO Auto-generated method stub
+ assertNotClosed();
+ // finish sending whatever in the buffer with FIN=true
+ flush(true);
+
+ // close stream
+ closed = true;
+ if (callback != null)
+ {
+ callback.writeSuccess();
+ }
+ bufferPool.release(buffer);
+ LOG.debug("closed (frame count={})",frameCount);
}
@Override
public void flush() throws IOException
{
- // TODO Auto-generated method stub
+ assertNotClosed();
+ // flush whatever is in the buffer with FIN=false
+ flush(false);
}
- public boolean isClosed()
+ /**
+ * Flush whatever is in the buffer.
+ *
+ * @param fin
+ * fin flag
+ * @throws IOException
+ */
+ private synchronized void flush(boolean fin) throws IOException
{
- // TODO Auto-generated method stub
- return false;
+ ByteBuffer data = utf.getByteBuffer();
+ frame.setPayload(data);
+ frame.setFin(fin);
+
+ try
+ {
+ blocker = new FutureWriteCallback();
+ outgoing.outgoingFrame(frame,blocker);
+ try
+ {
+ // block on write
+ blocker.get();
+ // write success
+ // clear utf buffer
+ utf.clear();
+ frameCount++;
+ frame.setOpCode(OpCode.CONTINUATION);
+ }
+ catch (ExecutionException e)
+ {
+ Throwable cause = e.getCause();
+ if (cause != null)
+ {
+ if (cause instanceof IOException)
+ {
+ throw (IOException)cause;
+ }
+ else
+ {
+ throw new IOException(cause);
+ }
+ }
+ throw new IOException("Failed to flush",e);
+ }
+ catch (InterruptedException e)
+ {
+ throw new IOException("Failed to flush",e);
+ }
+ }
+ catch (IOException e)
+ {
+ notifyFailure(e);
+ throw e;
+ }
+ }
+
+ private void notifyFailure(IOException e)
+ {
+ if (callback != null)
+ {
+ callback.writeFailed(e);
+ }
+ }
+
+ public void setCallback(WriteCallback callback)
+ {
+ this.callback = callback;
+ }
+
+ @Override
+ public void write(char[] cbuf) throws IOException
+ {
+ try
+ {
+ this.write(cbuf,0,cbuf.length);
+ }
+ catch (IOException e)
+ {
+ notifyFailure(e);
+ throw e;
+ }
}
@Override
public void write(char[] cbuf, int off, int len) throws IOException
{
- // TODO Auto-generated method stub
-
+ assertNotClosed();
+ int left = len; // bytes left to write
+ int offset = off; // offset within provided array
+ while (left > 0)
+ {
+ int space = utf.remaining();
+ int size = Math.min(space,left);
+ assert (space > 0);
+ assert (size > 0);
+ utf.append(cbuf,offset,size); // append with utf logic
+ left -= size; // decrement char left
+ if (left > 0)
+ {
+ flush(false);
+ }
+ offset += size; // increment offset
+ }
}
+ @Override
+ public void write(int c) throws IOException
+ {
+ assertNotClosed();
+
+ // buffer up to limit, flush once buffer reached.
+ utf.append(c); // append with utf logic
+ if (utf.remaining() <= 0)
+ {
+ flush(false);
+ }
+ }
}
diff --git a/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/message/SimpleBinaryMessage.java b/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/message/SimpleBinaryMessage.java
index d8802f8..274f731 100644
--- a/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/message/SimpleBinaryMessage.java
+++ b/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/message/SimpleBinaryMessage.java
@@ -29,9 +29,9 @@
{
private static final int BUFFER_SIZE = 65535;
private final EventDriver onEvent;
- private final ByteArrayOutputStream out;
+ protected final ByteArrayOutputStream out;
private int size;
- private boolean finished;
+ protected boolean finished;
public SimpleBinaryMessage(EventDriver onEvent)
{
@@ -41,7 +41,7 @@
}
@Override
- public void appendMessage(ByteBuffer payload) throws IOException
+ public void appendMessage(ByteBuffer payload, boolean isLast) throws IOException
{
if (finished)
{
@@ -54,7 +54,7 @@
return;
}
- onEvent.getPolicy().assertValidMessageSize(size + payload.remaining());
+ onEvent.getPolicy().assertValidBinaryMessageSize(size + payload.remaining());
size += payload.remaining();
BufferUtil.writeTo(payload,out);
diff --git a/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/message/SimpleTextMessage.java b/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/message/SimpleTextMessage.java
index 8b60aae..e002e12 100644
--- a/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/message/SimpleTextMessage.java
+++ b/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/message/SimpleTextMessage.java
@@ -27,9 +27,9 @@
public class SimpleTextMessage implements MessageAppender
{
private final EventDriver onEvent;
- private final Utf8StringBuilder utf;
+ protected final Utf8StringBuilder utf;
private int size = 0;
- private boolean finished;
+ protected boolean finished;
public SimpleTextMessage(EventDriver onEvent)
{
@@ -40,7 +40,7 @@
}
@Override
- public void appendMessage(ByteBuffer payload) throws IOException
+ public void appendMessage(ByteBuffer payload, boolean isLast) throws IOException
{
if (finished)
{
@@ -53,7 +53,7 @@
return;
}
- onEvent.getPolicy().assertValidMessageSize(size + payload.remaining());
+ onEvent.getPolicy().assertValidTextMessageSize(size + payload.remaining());
size += payload.remaining();
// allow for fast fail of BAD utf (incomplete utf will trigger on messageComplete)
diff --git a/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/message/Utf8CharBuffer.java b/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/message/Utf8CharBuffer.java
new file mode 100644
index 0000000..9d634fb
--- /dev/null
+++ b/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/message/Utf8CharBuffer.java
@@ -0,0 +1,115 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.common.message;
+
+import java.nio.ByteBuffer;
+import java.nio.CharBuffer;
+import java.nio.charset.Charset;
+
+import org.eclipse.jetty.util.BufferUtil;
+import org.eclipse.jetty.util.StringUtil;
+import org.eclipse.jetty.util.Utf8Appendable;
+
+/**
+ * A CharBuffer wrapped with the Utf8Appendable logic.
+ */
+public class Utf8CharBuffer extends Utf8Appendable
+{
+ private static final Charset UTF8 = StringUtil.__UTF8_CHARSET;
+
+ /**
+ * Convenience method to wrap a ByteBuffer with a {@link Utf8CharBuffer}
+ *
+ * @param buffer
+ * the buffer to wrap
+ * @return the Utf8ByteBuffer for the provided ByteBuffer
+ */
+ public static Utf8CharBuffer wrap(ByteBuffer buffer)
+ {
+ return new Utf8CharBuffer(buffer.asCharBuffer());
+ }
+
+ private final CharBuffer buffer;
+
+ private Utf8CharBuffer(CharBuffer buffer)
+ {
+ super(buffer);
+ this.buffer = buffer;
+ }
+
+ public void append(char[] cbuf, int offset, int size)
+ {
+ append(BufferUtil.toDirectBuffer(new String(cbuf,offset,size),UTF8));
+ }
+
+ public void append(int c)
+ {
+ buffer.append((char)c);
+ }
+
+ public void clear()
+ {
+ buffer.position(0);
+ buffer.limit(buffer.capacity());
+ }
+
+ public ByteBuffer getByteBuffer()
+ {
+ // remember settings
+ int limit = buffer.limit();
+ int position = buffer.position();
+
+ // flip to flush
+ buffer.limit(buffer.position());
+ buffer.position(0);
+
+ // get byte buffer
+ ByteBuffer bb = UTF8.encode(buffer);
+
+ // restor settings
+ buffer.limit(limit);
+ buffer.position(position);
+
+ return bb;
+ }
+
+ @Override
+ public int length()
+ {
+ return buffer.capacity();
+ }
+
+ public int remaining()
+ {
+ return buffer.remaining();
+ }
+
+ @Override
+ public String toString()
+ {
+ StringBuilder str = new StringBuilder();
+ str.append("Utf8CharBuffer@").append(hashCode());
+ str.append("[p=").append(buffer.position());
+ str.append(",l=").append(buffer.limit());
+ str.append(",c=").append(buffer.capacity());
+ str.append(",r=").append(buffer.remaining());
+ str.append("]");
+ return str.toString();
+ }
+}
diff --git a/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/util/ReflectUtils.java b/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/util/ReflectUtils.java
new file mode 100644
index 0000000..eb6e3c5
--- /dev/null
+++ b/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/util/ReflectUtils.java
@@ -0,0 +1,395 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.common.util;
+
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.lang.reflect.ParameterizedType;
+import java.lang.reflect.Type;
+import java.lang.reflect.TypeVariable;
+
+public class ReflectUtils
+{
+ private static class GenericRef
+ {
+ // The base class reference lookup started from
+ private final Class<?> baseClass;
+ // The interface that we are interested in
+ private final Class<?> ifaceClass;
+
+ // The actual class generic interface was found on
+ Class<?> genericClass;
+
+ // The found genericType
+ public Type genericType;
+ private int genericIndex;
+
+ public GenericRef(final Class<?> baseClass, final Class<?> ifaceClass)
+ {
+ this.baseClass = baseClass;
+ this.ifaceClass = ifaceClass;
+ }
+
+ public boolean needsUnwrap()
+ {
+ return (genericClass == null) && (genericType != null) && (genericType instanceof TypeVariable<?>);
+ }
+
+ public void setGenericFromType(Type type, int index)
+ {
+ // debug("setGenericFromType(%s,%d)",toShortName(type),index);
+ this.genericType = type;
+ this.genericIndex = index;
+ if (type instanceof Class)
+ {
+ this.genericClass = (Class<?>)type;
+ }
+ }
+
+ @Override
+ public String toString()
+ {
+ StringBuilder builder = new StringBuilder();
+ builder.append("GenericRef [baseClass=");
+ builder.append(baseClass);
+ builder.append(", ifaceClass=");
+ builder.append(ifaceClass);
+ builder.append(", genericType=");
+ builder.append(genericType);
+ builder.append(", genericClass=");
+ builder.append(genericClass);
+ builder.append("]");
+ return builder.toString();
+ }
+ }
+
+ private static StringBuilder appendTypeName(StringBuilder sb, Type type, boolean ellipses)
+ {
+ if (type instanceof Class<?>)
+ {
+ Class<?> ctype = (Class<?>)type;
+ if (ctype.isArray())
+ {
+ try
+ {
+ int dimensions = 0;
+ while (ctype.isArray())
+ {
+ dimensions++;
+ ctype = ctype.getComponentType();
+ }
+ sb.append(ctype.getName());
+ for (int i = 0; i < dimensions; i++)
+ {
+ if (ellipses)
+ {
+ sb.append("...");
+ }
+ else
+ {
+ sb.append("[]");
+ }
+ }
+ return sb;
+ }
+ catch (Throwable ignore)
+ {
+ // ignore
+ }
+ }
+
+ sb.append(ctype.getName());
+ }
+ else
+ {
+ sb.append(type.toString());
+ }
+
+ return sb;
+ }
+
+ /**
+ * Given a Base (concrete) Class, find the interface specified, and return its concrete Generic class declaration.
+ *
+ * @param baseClass
+ * the base (concrete) class to look in
+ * @param ifaceClass
+ * the interface of interest
+ * @return the (concrete) generic class that the interface exposes
+ */
+ public static Class<?> findGenericClassFor(Class<?> baseClass, Class<?> ifaceClass)
+ {
+ GenericRef ref = new GenericRef(baseClass,ifaceClass);
+ if (resolveGenericRef(ref,baseClass))
+ {
+ // debug("Generic Found: %s",ref.genericClass);
+ return ref.genericClass;
+ }
+
+ // debug("Generic not found: %s",ref);
+ return null;
+ }
+
+ private static int findTypeParameterIndex(Class<?> clazz, TypeVariable<?> needVar)
+ {
+ // debug("findTypeParameterIndex(%s, [%s])",toShortName(clazz),toShortName(needVar));
+ TypeVariable<?> params[] = clazz.getTypeParameters();
+ for (int i = 0; i < params.length; i++)
+ {
+ if (params[i].getName().equals(needVar.getName()))
+ {
+ // debug("Type Parameter found at index: [%d]",i);
+ return i;
+ }
+ }
+ // debug("Type Parameter NOT found");
+ return -1;
+ }
+
+ public static boolean isDefaultConstructable(Class<?> clazz)
+ {
+ int mods = clazz.getModifiers();
+ if (Modifier.isAbstract(mods) || !Modifier.isPublic(mods))
+ {
+ // Needs to be public, non-abstract
+ return false;
+ }
+
+ Class<?>[] noargs = new Class<?>[0];
+ try
+ {
+ // Needs to have a no-args constructor
+ Constructor<?> constructor = clazz.getConstructor(noargs);
+ // Constructor needs to be public
+ return Modifier.isPublic(constructor.getModifiers());
+ }
+ catch (NoSuchMethodException | SecurityException e)
+ {
+ return false;
+ }
+ }
+
+ private static boolean resolveGenericRef(GenericRef ref, Class<?> clazz, Type type)
+ {
+ if (type instanceof Class)
+ {
+ if (type == ref.ifaceClass)
+ {
+ // is this a straight ref or a TypeVariable?
+ // debug("Found ref (as class): %s",toShortName(type));
+ ref.setGenericFromType(type,0);
+ return true;
+ }
+ else
+ {
+ // Keep digging
+ return resolveGenericRef(ref,type);
+ }
+ }
+
+ if (type instanceof ParameterizedType)
+ {
+ ParameterizedType ptype = (ParameterizedType)type;
+ Type rawType = ptype.getRawType();
+ if (rawType == ref.ifaceClass)
+ {
+ // debug("Found ref on [%s] as ParameterizedType [%s]",toShortName(clazz),toShortName(ptype));
+ // Always get the raw type parameter, let unwrap() solve for what it is
+ ref.setGenericFromType(ptype.getActualTypeArguments()[0],0);
+ return true;
+ }
+ else
+ {
+ // Keep digging
+ return resolveGenericRef(ref,rawType);
+ }
+ }
+ return false;
+ }
+
+ private static boolean resolveGenericRef(GenericRef ref, Type type)
+ {
+ if ((type == null) || (type == Object.class))
+ {
+ return false;
+ }
+
+ if (type instanceof Class)
+ {
+ Class<?> clazz = (Class<?>)type;
+ // prevent spinning off into Serialization and other parts of the
+ // standard tree that we could care less about
+ if (clazz.getName().matches("^javax*\\..*"))
+ {
+ return false;
+ }
+
+ Type ifaces[] = clazz.getGenericInterfaces();
+ for (Type iface : ifaces)
+ {
+ // debug("resolve %s interface[]: %s",toShortName(clazz),toShortName(iface));
+ if (resolveGenericRef(ref,clazz,iface))
+ {
+ if (ref.needsUnwrap())
+ {
+ // debug("## Unwrap class %s::%s",toShortName(clazz),toShortName(iface));
+ TypeVariable<?> needVar = (TypeVariable<?>)ref.genericType;
+ // debug("needs unwrap of type var [%s] - index [%d]",toShortName(needVar),ref.genericIndex);
+
+ // attempt to find typeParameter on class itself
+ int typeParamIdx = findTypeParameterIndex(clazz,needVar);
+ // debug("type param index for %s[%s] is [%d]",toShortName(clazz),toShortName(needVar),typeParamIdx);
+
+ if (typeParamIdx >= 0)
+ {
+ // found a type parameter, use it
+ // debug("unwrap from class [%s] - typeParameters[%d]",toShortName(clazz),typeParamIdx);
+ TypeVariable<?> params[] = clazz.getTypeParameters();
+ if (params.length >= typeParamIdx)
+ {
+ ref.setGenericFromType(params[typeParamIdx],typeParamIdx);
+ }
+ }
+ else if (iface instanceof ParameterizedType)
+ {
+ // use actual args on interface
+ Type arg = ((ParameterizedType)iface).getActualTypeArguments()[ref.genericIndex];
+ ref.setGenericFromType(arg,ref.genericIndex);
+ }
+ }
+ return true;
+ }
+ }
+
+ type = clazz.getGenericSuperclass();
+ return resolveGenericRef(ref,type);
+ }
+
+ if (type instanceof ParameterizedType)
+ {
+ ParameterizedType ptype = (ParameterizedType)type;
+ Class<?> rawClass = (Class<?>)ptype.getRawType();
+ if (resolveGenericRef(ref,rawClass))
+ {
+ if (ref.needsUnwrap())
+ {
+ // debug("## Unwrap ParameterizedType %s::%s",toShortName(type),toShortName(rawClass));
+ TypeVariable<?> needVar = (TypeVariable<?>)ref.genericType;
+ // debug("needs unwrap of type var [%s] - index [%d]",toShortName(needVar),ref.genericIndex);
+ int typeParamIdx = findTypeParameterIndex(rawClass,needVar);
+ // debug("type paramIdx of %s::%s is index [%d]",toShortName(rawClass),toShortName(needVar),typeParamIdx);
+
+ Type arg = ptype.getActualTypeArguments()[typeParamIdx];
+ ref.setGenericFromType(arg,typeParamIdx);
+ return true;
+ }
+ }
+ }
+
+ return false;
+ }
+
+ public static String toShortName(Type type)
+ {
+ if (type == null)
+ {
+ return "<null>";
+ }
+
+ if (type instanceof Class)
+ {
+ String name = ((Class<?>)type).getName();
+ return trimClassName(name);
+ }
+
+ if (type instanceof ParameterizedType)
+ {
+ ParameterizedType ptype = (ParameterizedType)type;
+ StringBuilder str = new StringBuilder();
+ str.append(trimClassName(((Class<?>)ptype.getRawType()).getName()));
+ str.append("<");
+ Type args[] = ptype.getActualTypeArguments();
+ for (int i = 0; i < args.length; i++)
+ {
+ if (i > 0)
+ {
+ str.append(",");
+ }
+ str.append(args[i]);
+ }
+ str.append(">");
+ return str.toString();
+ }
+
+ return type.toString();
+ }
+
+ public static String toString(Class<?> pojo, Method method)
+ {
+ StringBuilder str = new StringBuilder();
+
+ // method modifiers
+ int mod = method.getModifiers() & Modifier.methodModifiers();
+ if (mod != 0)
+ {
+ str.append(Modifier.toString(mod)).append(' ');
+ }
+
+ // return type
+ Type retType = method.getGenericReturnType();
+ appendTypeName(str,retType,false).append(' ');
+
+ // class name
+ str.append(pojo.getName());
+ str.append("#");
+
+ // method name
+ str.append(method.getName());
+
+ // method parameters
+ str.append('(');
+ Type[] params = method.getGenericParameterTypes();
+ for (int j = 0; j < params.length; j++)
+ {
+ boolean ellipses = method.isVarArgs() && (j == (params.length - 1));
+ appendTypeName(str,params[j],ellipses);
+ if (j < (params.length - 1))
+ {
+ str.append(", ");
+ }
+ }
+ str.append(')');
+
+ // TODO: show exceptions?
+ return str.toString();
+ }
+
+ public static String trimClassName(String name)
+ {
+ int idx = name.lastIndexOf('.');
+ name = name.substring(idx + 1);
+ idx = name.lastIndexOf('$');
+ if (idx >= 0)
+ {
+ name = name.substring(idx + 1);
+ }
+ return name;
+ }
+}
diff --git a/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/util/TextUtil.java b/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/util/TextUtil.java
new file mode 100644
index 0000000..85633b7
--- /dev/null
+++ b/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/util/TextUtil.java
@@ -0,0 +1,84 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.common.util;
+
+/**
+ * Collection of utility methods for Text content
+ */
+public final class TextUtil
+{
+ /**
+ * Create a hint of what the text is like.
+ * <p>
+ * Used by logging and error messages to get a hint of what the text is like.
+ *
+ * @param text
+ * the text to abbreviate, quote, and generally give you a hint of what the value is.
+ * @return the abbreviated text
+ */
+ public static String hint(String text)
+ {
+ if (text == null)
+ {
+ return "<null>";
+ }
+ return '"' + maxStringLength(30,text) + '"';
+ }
+
+ /**
+ * Smash a long string to fit within the max string length, by taking the middle section of the string and replacing them with an ellipsis "..."
+ * <p>
+ *
+ * <pre>
+ * Examples:
+ * .maxStringLength( 9, "Eatagramovabits") == "Eat...its"
+ * .maxStringLength(10, "Eatagramovabits") == "Eat...bits"
+ * .maxStringLength(11, "Eatagramovabits") == "Eata...bits"
+ * </pre>
+ *
+ * @param max
+ * the maximum size of the string (minimum size supported is 9)
+ * @param raw
+ * the raw string to smash
+ * @return the ellipsis'd version of the string.
+ */
+ public static String maxStringLength(int max, String raw)
+ {
+ int length = raw.length();
+ if (length <= max)
+ {
+ // already short enough
+ return raw;
+ }
+
+ if (max < 9)
+ {
+ // minimum supported
+ return raw.substring(0,max);
+ }
+
+ StringBuilder ret = new StringBuilder();
+ int startLen = (int)Math.round((double)max / (double)3);
+ ret.append(raw.substring(0,startLen));
+ ret.append("...");
+ ret.append(raw.substring(length - (max - startLen - 3)));
+
+ return ret.toString();
+ }
+}
diff --git a/jetty-websocket/websocket-common/src/main/resources/META-INF/services/org.eclipse.jetty.websocket.api.extensions.Extension b/jetty-websocket/websocket-common/src/main/resources/META-INF/services/org.eclipse.jetty.websocket.api.extensions.Extension
new file mode 100644
index 0000000..5442966
--- /dev/null
+++ b/jetty-websocket/websocket-common/src/main/resources/META-INF/services/org.eclipse.jetty.websocket.api.extensions.Extension
@@ -0,0 +1,4 @@
+org.eclipse.jetty.websocket.common.extensions.identity.IdentityExtension
+org.eclipse.jetty.websocket.common.extensions.compress.FrameCompressionExtension
+org.eclipse.jetty.websocket.common.extensions.compress.MessageDeflateCompressionExtension
+org.eclipse.jetty.websocket.common.extensions.fragment.FragmentExtension
\ No newline at end of file
diff --git a/jetty-websocket/websocket-common/src/test/java/examples/AnnotatedBinaryStreamSocket.java b/jetty-websocket/websocket-common/src/test/java/examples/AnnotatedBinaryStreamSocket.java
index d0e3b90..114afdf 100644
--- a/jetty-websocket/websocket-common/src/test/java/examples/AnnotatedBinaryStreamSocket.java
+++ b/jetty-websocket/websocket-common/src/test/java/examples/AnnotatedBinaryStreamSocket.java
@@ -35,6 +35,10 @@
@OnWebSocketMessage
public void onBinary(InputStream stream)
{
+ if (stream == null)
+ {
+ new RuntimeException("Stream cannot be null").printStackTrace(System.err);
+ }
capture.add("onBinary(%s)",stream);
}
diff --git a/jetty-websocket/websocket-common/src/test/java/examples/echo/AnnotatedEchoSocket.java b/jetty-websocket/websocket-common/src/test/java/examples/echo/AnnotatedEchoSocket.java
index 0b71bb5..b999452 100644
--- a/jetty-websocket/websocket-common/src/test/java/examples/echo/AnnotatedEchoSocket.java
+++ b/jetty-websocket/websocket-common/src/test/java/examples/echo/AnnotatedEchoSocket.java
@@ -25,7 +25,7 @@
/**
* Example EchoSocket using Annotations.
*/
-@WebSocket(maxMessageSize = 64 * 1024)
+@WebSocket(maxTextMessageSize = 64 * 1024)
public class AnnotatedEchoSocket
{
@OnWebSocketMessage
diff --git a/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/IncomingFramesCapture.java b/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/IncomingFramesCapture.java
index 81d207a..5477b06 100644
--- a/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/IncomingFramesCapture.java
+++ b/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/IncomingFramesCapture.java
@@ -35,7 +35,7 @@
private static final Logger LOG = Log.getLogger(IncomingFramesCapture.class);
private LinkedList<WebSocketFrame> frames = new LinkedList<>();
- private LinkedList<WebSocketException> errors = new LinkedList<>();
+ private LinkedList<Throwable> errors = new LinkedList<>();
public void assertErrorCount(int expectedCount)
{
@@ -86,7 +86,7 @@
public int getErrorCount(Class<? extends WebSocketException> errorType)
{
int count = 0;
- for (WebSocketException error : errors)
+ for (Throwable error : errors)
{
if (errorType.isInstance(error))
{
@@ -96,7 +96,7 @@
return count;
}
- public LinkedList<WebSocketException> getErrors()
+ public LinkedList<Throwable> getErrors()
{
return errors;
}
@@ -120,7 +120,7 @@
}
@Override
- public void incomingError(WebSocketException e)
+ public void incomingError(Throwable e)
{
LOG.debug(e);
errors.add(e);
diff --git a/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/TextPayloadParserTest.java b/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/TextPayloadParserTest.java
index ccfb53f..df0d74f 100644
--- a/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/TextPayloadParserTest.java
+++ b/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/TextPayloadParserTest.java
@@ -38,14 +38,16 @@
{
WebSocketPolicy policy = new WebSocketPolicy(WebSocketBehavior.SERVER);
// Artificially small buffer/payload
- policy.setMaxMessageSize(1024);
+ policy.setInputBufferSize(1024); // read buffer
+ policy.setMaxTextMessageBufferSize(1024); // streaming buffer (not used in this test)
+ policy.setMaxTextMessageSize(1024); // actual maximum text message size policy
byte utf[] = new byte[2048];
Arrays.fill(utf,(byte)'a');
Assert.assertThat("Must be a medium length payload",utf.length,allOf(greaterThan(0x7E),lessThan(0xFFFF)));
ByteBuffer buf = ByteBuffer.allocate(utf.length + 8);
- buf.put((byte)0x81);
+ buf.put((byte)0x81); // text frame, fin = true
buf.put((byte)(0x80 | 0x7E)); // 0x7E == 126 (a 2 byte payload length)
buf.putShort((short)utf.length);
MaskedByteBuffer.putMask(buf);
@@ -80,7 +82,7 @@
Assert.assertThat("Must be a long length payload",utf.length,greaterThan(0xFFFF));
ByteBuffer buf = ByteBuffer.allocate(utf.length + 32);
- buf.put((byte)0x81);
+ buf.put((byte)0x81); // text frame, fin = true
buf.put((byte)(0x80 | 0x7F)); // 0x7F == 127 (a 8 byte payload length)
buf.putLong(utf.length);
MaskedByteBuffer.putMask(buf);
@@ -88,7 +90,7 @@
buf.flip();
WebSocketPolicy policy = WebSocketPolicy.newServerPolicy();
- policy.setMaxMessageSize(100000);
+ policy.setMaxTextMessageSize(100000);
Parser parser = new UnitParser(policy);
IncomingFramesCapture capture = new IncomingFramesCapture();
parser.setIncomingFramesHandler(capture);
diff --git a/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/WebSocketRemoteEndpointTest.java b/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/WebSocketRemoteEndpointTest.java
new file mode 100644
index 0000000..287b78d
--- /dev/null
+++ b/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/WebSocketRemoteEndpointTest.java
@@ -0,0 +1,86 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.common;
+
+import static org.hamcrest.Matchers.*;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+
+import org.eclipse.jetty.websocket.common.io.LocalWebSocketConnection;
+import org.junit.Assert;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TestName;
+
+public class WebSocketRemoteEndpointTest
+{
+ @Rule
+ public TestName testname = new TestName();
+
+ @Test
+ public void testTextBinaryText() throws IOException
+ {
+ LocalWebSocketConnection conn = new LocalWebSocketConnection(testname);
+ OutgoingFramesCapture outgoing = new OutgoingFramesCapture();
+ WebSocketRemoteEndpoint remote = new WebSocketRemoteEndpoint(conn,outgoing);
+ conn.connect();
+ conn.open();
+
+ // Start text message
+ remote.sendPartialString("Hello ",false);
+
+ try
+ {
+ // Attempt to start Binary Message
+ ByteBuffer bytes = ByteBuffer.wrap(new byte[]
+ { 0, 1, 2 });
+ remote.sendPartialBytes(bytes,false);
+ Assert.fail("Expected " + IllegalStateException.class.getName());
+ }
+ catch (IllegalStateException e)
+ {
+ // Expected path
+ Assert.assertThat("Exception",e.getMessage(),containsString("message pending"));
+ }
+
+ // End text message
+ remote.sendPartialString("World!",true);
+ }
+
+ @Test
+ public void testTextPingText() throws IOException
+ {
+ LocalWebSocketConnection conn = new LocalWebSocketConnection(testname);
+ OutgoingFramesCapture outgoing = new OutgoingFramesCapture();
+ WebSocketRemoteEndpoint remote = new WebSocketRemoteEndpoint(conn,outgoing);
+ conn.connect();
+ conn.open();
+
+ // Start text message
+ remote.sendPartialString("Hello ",false);
+
+ // Attempt to send Ping Message
+ remote.sendPing(ByteBuffer.wrap(new byte[]
+ { 0 }));
+
+ // End text message
+ remote.sendPartialString("World!",true);
+ }
+}
diff --git a/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/ab/TestABCase1_1.java b/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/ab/TestABCase1_1.java
index 755c501..73baaf2 100644
--- a/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/ab/TestABCase1_1.java
+++ b/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/ab/TestABCase1_1.java
@@ -451,7 +451,7 @@
expected.flip();
WebSocketPolicy policy = new WebSocketPolicy(WebSocketBehavior.CLIENT);
- policy.setMaxMessageSize(length);
+ policy.setMaxTextMessageSize(length);
Parser parser = new UnitParser(policy);
IncomingFramesCapture capture = new IncomingFramesCapture();
parser.setIncomingFramesHandler(capture);
@@ -488,7 +488,7 @@
expected.flip();
WebSocketPolicy policy = new WebSocketPolicy(WebSocketBehavior.CLIENT);
- policy.setMaxMessageSize(length);
+ policy.setMaxTextMessageSize(length);
Parser parser = new UnitParser(policy);
IncomingFramesCapture capture = new IncomingFramesCapture();
parser.setIncomingFramesHandler(capture);
diff --git a/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/ab/TestABCase1_2.java b/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/ab/TestABCase1_2.java
index dd6d6ea..80be3a7 100644
--- a/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/ab/TestABCase1_2.java
+++ b/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/ab/TestABCase1_2.java
@@ -466,7 +466,7 @@
expected.flip();
WebSocketPolicy policy = new WebSocketPolicy(WebSocketBehavior.CLIENT);
- policy.setMaxMessageSize(length);
+ policy.setMaxBinaryMessageSize(length);
Parser parser = new UnitParser(policy);
IncomingFramesCapture capture = new IncomingFramesCapture();
parser.setIncomingFramesHandler(capture);
@@ -503,7 +503,7 @@
expected.flip();
WebSocketPolicy policy = new WebSocketPolicy(WebSocketBehavior.CLIENT);
- policy.setMaxMessageSize(length);
+ policy.setMaxBinaryMessageSize(length);
Parser parser = new UnitParser(policy);
IncomingFramesCapture capture = new IncomingFramesCapture();
parser.setIncomingFramesHandler(capture);
diff --git a/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/ab/TestABCase4.java b/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/ab/TestABCase4.java
index 562c0b2..1af04d2 100644
--- a/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/ab/TestABCase4.java
+++ b/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/ab/TestABCase4.java
@@ -65,7 +65,7 @@
Assert.assertEquals( "error on undefined opcode", 1, capture.getErrorCount(WebSocketException.class)) ;
- WebSocketException known = capture.getErrors().get(0);
+ Throwable known = capture.getErrors().get(0);
Assert.assertTrue("undefined option should be in message",known.getMessage().contains("Unknown opcode: 11"));
}
@@ -87,7 +87,7 @@
Assert.assertEquals( "error on undefined opcode", 1, capture.getErrorCount(WebSocketException.class)) ;
- WebSocketException known = capture.getErrors().get(0);
+ Throwable known = capture.getErrors().get(0);
Assert.assertTrue("undefined option should be in message",known.getMessage().contains("Unknown opcode: 12"));
}
@@ -110,7 +110,7 @@
Assert.assertEquals( "error on undefined opcode", 1, capture.getErrorCount(WebSocketException.class)) ;
- WebSocketException known = capture.getErrors().get(0);
+ Throwable known = capture.getErrors().get(0);
Assert.assertTrue("undefined option should be in message",known.getMessage().contains("Unknown opcode: 3"));
}
@@ -132,7 +132,7 @@
Assert.assertEquals( "error on undefined opcode", 1, capture.getErrorCount(WebSocketException.class)) ;
- WebSocketException known = capture.getErrors().get(0);
+ Throwable known = capture.getErrors().get(0);
Assert.assertTrue("undefined option should be in message",known.getMessage().contains("Unknown opcode: 4"));
}
diff --git a/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/events/EventCapture.java b/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/events/EventCapture.java
index f4c13fd..363a4ab 100644
--- a/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/events/EventCapture.java
+++ b/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/events/EventCapture.java
@@ -18,31 +18,56 @@
package org.eclipse.jetty.websocket.common.events;
-import java.util.ArrayList;
+import static org.hamcrest.Matchers.*;
+
import java.util.regex.Pattern;
+import org.eclipse.jetty.toolchain.test.EventQueue;
import org.junit.Assert;
-import static org.hamcrest.Matchers.containsString;
-import static org.hamcrest.Matchers.is;
-import static org.hamcrest.Matchers.startsWith;
-
@SuppressWarnings("serial")
-public class EventCapture extends ArrayList<String>
+public class EventCapture extends EventQueue<String>
{
+ public static class Assertable
+ {
+ private final String event;
+
+ public Assertable(String event)
+ {
+ this.event = event;
+ }
+
+ public void assertEventContains(String expected)
+ {
+ Assert.assertThat("Event",event,containsString(expected));
+ }
+
+ public void assertEventRegex(String regex)
+ {
+ Assert.assertTrue("Event: regex:[" + regex + "] in [" + event + "]",Pattern.matches(regex,event));
+ }
+
+ public void assertEventStartsWith(String expected)
+ {
+ Assert.assertThat("Event",event,startsWith(expected));
+ }
+
+ public void assertEvent(String expected)
+ {
+ Assert.assertThat("Event",event,is(expected));
+ }
+ }
+
public void add(String format, Object... args)
{
- super.add(String.format(format,args));
+ String msg = String.format(format,args);
+ System.err.printf("### EVENT: %s%n",msg);
+ super.offer(msg);
}
- public void assertEvent(int eventNum, String expected)
+ public Assertable pop()
{
- Assert.assertThat("Event[" + eventNum + "]",get(eventNum),is(expected));
- }
-
- public void assertEventContains(int eventNum, String expected)
- {
- Assert.assertThat("Event[" + eventNum + "]",get(eventNum),containsString(expected));
+ return new Assertable(super.poll());
}
public void assertEventCount(int expectedCount)
@@ -50,17 +75,6 @@
Assert.assertThat("Event Count",size(),is(expectedCount));
}
- public void assertEventRegex(int eventNum, String regex)
- {
- String event = get(eventNum);
- Assert.assertTrue("Event[" + eventNum + "]: regex:[" + regex + "] in [" + event + "]",Pattern.matches(regex,event));
- }
-
- public void assertEventStartsWith(int eventNum, String expected)
- {
- Assert.assertThat("Event[" + eventNum + "]",get(eventNum),startsWith(expected));
- }
-
public String q(String str)
{
if (str == null)
diff --git a/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/events/EventDriverFactoryTest.java b/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/events/EventDriverFactoryTest.java
index 8ba6bee..baba8e3 100644
--- a/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/events/EventDriverFactoryTest.java
+++ b/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/events/EventDriverFactoryTest.java
@@ -24,41 +24,15 @@
import org.eclipse.jetty.websocket.api.WebSocketListener;
import org.eclipse.jetty.websocket.api.WebSocketPolicy;
import org.eclipse.jetty.websocket.api.annotations.WebSocket;
-import org.eclipse.jetty.websocket.common.annotations.BadBinarySignatureSocket;
-import org.eclipse.jetty.websocket.common.annotations.BadDuplicateBinarySocket;
-import org.eclipse.jetty.websocket.common.annotations.BadDuplicateFrameSocket;
-import org.eclipse.jetty.websocket.common.annotations.BadTextSignatureSocket;
-import org.eclipse.jetty.websocket.common.annotations.FrameSocket;
-import org.eclipse.jetty.websocket.common.annotations.MyEchoBinarySocket;
-import org.eclipse.jetty.websocket.common.annotations.MyEchoSocket;
-import org.eclipse.jetty.websocket.common.annotations.MyStatelessEchoSocket;
-import org.eclipse.jetty.websocket.common.annotations.NoopSocket;
import org.eclipse.jetty.websocket.common.annotations.NotASocket;
import org.junit.Assert;
import org.junit.Test;
import examples.AdapterConnectCloseSocket;
-import examples.AnnotatedBinaryArraySocket;
-import examples.AnnotatedBinaryStreamSocket;
-import examples.AnnotatedTextSocket;
-import examples.AnnotatedTextStreamSocket;
import examples.ListenerBasicSocket;
public class EventDriverFactoryTest
{
- private void assertHasEventMethod(String message, EventMethod actual)
- {
- Assert.assertThat(message + " EventMethod",actual,notNullValue());
-
- Assert.assertThat(message + " EventMethod.pojo",actual.pojo,notNullValue());
- Assert.assertThat(message + " EventMethod.method",actual.method,notNullValue());
- }
-
- private void assertNoEventMethod(String message, EventMethod actual)
- {
- Assert.assertThat(message + "Event method",actual,nullValue());
- }
-
/**
* Test Case for no exceptions and 5 methods (extends WebSocketAdapter)
*/
@@ -70,291 +44,7 @@
EventDriver driver = factory.wrap(socket);
String classId = AdapterConnectCloseSocket.class.getSimpleName();
- Assert.assertThat("EventDriver for " + classId,driver,instanceOf(ListenerEventDriver.class));
- }
-
- /**
- * Test Case for bad declaration (duplicate OnWebSocketBinary declarations)
- */
- @Test
- public void testAnnotatedBadDuplicateBinarySocket()
- {
- EventDriverFactory factory = new EventDriverFactory(WebSocketPolicy.newClientPolicy());
- try
- {
- // Should toss exception
- factory.getMethods(BadDuplicateBinarySocket.class);
- Assert.fail("Should have thrown " + InvalidWebSocketException.class);
- }
- catch (InvalidWebSocketException e)
- {
- // Validate that we have clear error message to the developer
- Assert.assertThat(e.getMessage(),containsString("Duplicate @OnWebSocketMessage declaration"));
- }
- }
-
- /**
- * Test Case for bad declaration (duplicate frame type methods)
- */
- @Test
- public void testAnnotatedBadDuplicateFrameSocket()
- {
- EventDriverFactory factory = new EventDriverFactory(WebSocketPolicy.newClientPolicy());
- try
- {
- // Should toss exception
- factory.getMethods(BadDuplicateFrameSocket.class);
- Assert.fail("Should have thrown " + InvalidWebSocketException.class);
- }
- catch (InvalidWebSocketException e)
- {
- // Validate that we have clear error message to the developer
- Assert.assertThat(e.getMessage(),containsString("Duplicate @OnWebSocketFrame"));
- }
- }
-
- /**
- * Test Case for bad declaration a method with a non-void return type
- */
- @Test
- public void testAnnotatedBadSignature_NonVoidReturn()
- {
- EventDriverFactory factory = new EventDriverFactory(WebSocketPolicy.newClientPolicy());
- try
- {
- // Should toss exception
- factory.getMethods(BadBinarySignatureSocket.class);
- Assert.fail("Should have thrown " + InvalidWebSocketException.class);
- }
- catch (InvalidWebSocketException e)
- {
- // Validate that we have clear error message to the developer
- Assert.assertThat(e.getMessage(),containsString("must be void"));
- }
- }
-
- /**
- * Test Case for bad declaration a method with a public static method
- */
- @Test
- public void testAnnotatedBadSignature_Static()
- {
- EventDriverFactory factory = new EventDriverFactory(WebSocketPolicy.newClientPolicy());
- try
- {
- // Should toss exception
- factory.getMethods(BadTextSignatureSocket.class);
- Assert.fail("Should have thrown " + InvalidWebSocketException.class);
- }
- catch (InvalidWebSocketException e)
- {
- // Validate that we have clear error message to the developer
- Assert.assertThat(e.getMessage(),containsString("may not be static"));
- }
- }
-
- /**
- * Test Case for socket for binary array messages
- */
- @Test
- public void testAnnotatedBinaryArraySocket()
- {
- EventDriverFactory factory = new EventDriverFactory(WebSocketPolicy.newClientPolicy());
- EventMethods methods = factory.getMethods(AnnotatedBinaryArraySocket.class);
-
- String classId = AnnotatedBinaryArraySocket.class.getSimpleName();
-
- Assert.assertThat("EventMethods for " + classId,methods,notNullValue());
-
- assertHasEventMethod(classId + ".onBinary",methods.onBinary);
- assertHasEventMethod(classId + ".onClose",methods.onClose);
- assertHasEventMethod(classId + ".onConnect",methods.onConnect);
- assertNoEventMethod(classId + ".onException",methods.onError);
- assertNoEventMethod(classId + ".onText",methods.onText);
- assertNoEventMethod(classId + ".onFrame",methods.onFrame);
-
- Assert.assertFalse(classId + ".onBinary.hasSession",methods.onBinary.isHasSession());
- Assert.assertFalse(classId + ".onBinary.isStreaming",methods.onBinary.isStreaming());
- }
-
- /**
- * Test Case for socket for binary stream messages
- */
- @Test
- public void testAnnotatedBinaryStreamSocket()
- {
- EventDriverFactory factory = new EventDriverFactory(WebSocketPolicy.newClientPolicy());
- EventMethods methods = factory.getMethods(AnnotatedBinaryStreamSocket.class);
-
- String classId = AnnotatedBinaryStreamSocket.class.getSimpleName();
-
- Assert.assertThat("EventMethods for " + classId,methods,notNullValue());
-
- assertHasEventMethod(classId + ".onBinary",methods.onBinary);
- assertHasEventMethod(classId + ".onClose",methods.onClose);
- assertHasEventMethod(classId + ".onConnect",methods.onConnect);
- assertNoEventMethod(classId + ".onException",methods.onError);
- assertNoEventMethod(classId + ".onText",methods.onText);
- assertNoEventMethod(classId + ".onFrame",methods.onFrame);
-
- Assert.assertFalse(classId + ".onBinary.hasSession",methods.onBinary.isHasSession());
- Assert.assertTrue(classId + ".onBinary.isStreaming",methods.onBinary.isStreaming());
- }
-
- /**
- * Test Case for no exceptions and 4 methods (3 methods from parent)
- */
- @Test
- public void testAnnotatedMyEchoBinarySocket()
- {
- EventDriverFactory factory = new EventDriverFactory(WebSocketPolicy.newClientPolicy());
- EventMethods methods = factory.getMethods(MyEchoBinarySocket.class);
-
- String classId = MyEchoBinarySocket.class.getSimpleName();
-
- Assert.assertThat("EventMethods for " + classId,methods,notNullValue());
-
- assertHasEventMethod(classId + ".onBinary",methods.onBinary);
- assertHasEventMethod(classId + ".onClose",methods.onClose);
- assertHasEventMethod(classId + ".onConnect",methods.onConnect);
- assertNoEventMethod(classId + ".onException",methods.onError);
- assertHasEventMethod(classId + ".onText",methods.onText);
- assertNoEventMethod(classId + ".onFrame",methods.onFrame);
- }
-
- /**
- * Test Case for no exceptions and 3 methods
- */
- @Test
- public void testAnnotatedMyEchoSocket()
- {
- EventDriverFactory factory = new EventDriverFactory(WebSocketPolicy.newClientPolicy());
- EventMethods methods = factory.getMethods(MyEchoSocket.class);
-
- String classId = MyEchoSocket.class.getSimpleName();
-
- Assert.assertThat("EventMethods for " + classId,methods,notNullValue());
-
- assertNoEventMethod(classId + ".onBinary",methods.onBinary);
- assertHasEventMethod(classId + ".onClose",methods.onClose);
- assertHasEventMethod(classId + ".onConnect",methods.onConnect);
- assertNoEventMethod(classId + ".onException",methods.onError);
- assertHasEventMethod(classId + ".onText",methods.onText);
- assertNoEventMethod(classId + ".onFrame",methods.onFrame);
- }
-
- /**
- * Test Case for annotated for text messages w/connection param
- */
- @Test
- public void testAnnotatedMyStatelessEchoSocket()
- {
- EventDriverFactory factory = new EventDriverFactory(WebSocketPolicy.newClientPolicy());
- EventMethods methods = factory.getMethods(MyStatelessEchoSocket.class);
-
- String classId = MyStatelessEchoSocket.class.getSimpleName();
-
- Assert.assertThat("EventMethods for " + classId,methods,notNullValue());
-
- assertNoEventMethod(classId + ".onBinary",methods.onBinary);
- assertNoEventMethod(classId + ".onClose",methods.onClose);
- assertNoEventMethod(classId + ".onConnect",methods.onConnect);
- assertNoEventMethod(classId + ".onException",methods.onError);
- assertHasEventMethod(classId + ".onText",methods.onText);
- assertNoEventMethod(classId + ".onFrame",methods.onFrame);
-
- Assert.assertTrue(classId + ".onText.hasSession",methods.onText.isHasSession());
- Assert.assertFalse(classId + ".onText.isStreaming",methods.onText.isStreaming());
- }
-
- /**
- * Test Case for no exceptions and no methods
- */
- @Test
- public void testAnnotatedNoop()
- {
- EventDriverFactory factory = new EventDriverFactory(WebSocketPolicy.newClientPolicy());
- EventMethods methods = factory.getMethods(NoopSocket.class);
-
- String classId = NoopSocket.class.getSimpleName();
-
- Assert.assertThat("Methods for " + classId,methods,notNullValue());
-
- assertNoEventMethod(classId + ".onBinary",methods.onBinary);
- assertNoEventMethod(classId + ".onClose",methods.onClose);
- assertNoEventMethod(classId + ".onConnect",methods.onConnect);
- assertNoEventMethod(classId + ".onException",methods.onError);
- assertNoEventMethod(classId + ".onText",methods.onText);
- assertNoEventMethod(classId + ".onFrame",methods.onFrame);
- }
-
- /**
- * Test Case for no exceptions and 1 methods
- */
- @Test
- public void testAnnotatedOnFrame()
- {
- EventDriverFactory factory = new EventDriverFactory(WebSocketPolicy.newClientPolicy());
- EventMethods methods = factory.getMethods(FrameSocket.class);
-
- String classId = FrameSocket.class.getSimpleName();
-
- Assert.assertThat("EventMethods for " + classId,methods,notNullValue());
-
- assertNoEventMethod(classId + ".onBinary",methods.onBinary);
- assertNoEventMethod(classId + ".onClose",methods.onClose);
- assertNoEventMethod(classId + ".onConnect",methods.onConnect);
- assertNoEventMethod(classId + ".onException",methods.onError);
- assertNoEventMethod(classId + ".onText",methods.onText);
- assertHasEventMethod(classId + ".onFrame",methods.onFrame);
- }
-
- /**
- * Test Case for socket for simple text messages
- */
- @Test
- public void testAnnotatedTextSocket()
- {
- EventDriverFactory factory = new EventDriverFactory(WebSocketPolicy.newClientPolicy());
- EventMethods methods = factory.getMethods(AnnotatedTextSocket.class);
-
- String classId = AnnotatedTextSocket.class.getSimpleName();
-
- Assert.assertThat("EventMethods for " + classId,methods,notNullValue());
-
- assertNoEventMethod(classId + ".onBinary",methods.onBinary);
- assertHasEventMethod(classId + ".onClose",methods.onClose);
- assertHasEventMethod(classId + ".onConnect",methods.onConnect);
- assertHasEventMethod(classId + ".onException",methods.onError);
- assertHasEventMethod(classId + ".onText",methods.onText);
- assertNoEventMethod(classId + ".onFrame",methods.onFrame);
-
- Assert.assertFalse(classId + ".onText.hasSession",methods.onText.isHasSession());
- Assert.assertFalse(classId + ".onText.isStreaming",methods.onText.isStreaming());
- }
-
- /**
- * Test Case for socket for text stream messages
- */
- @Test
- public void testAnnotatedTextStreamSocket()
- {
- EventDriverFactory factory = new EventDriverFactory(WebSocketPolicy.newClientPolicy());
- EventMethods methods = factory.getMethods(AnnotatedTextStreamSocket.class);
-
- String classId = AnnotatedTextStreamSocket.class.getSimpleName();
-
- Assert.assertThat("EventMethods for " + classId,methods,notNullValue());
-
- assertNoEventMethod(classId + ".onBinary",methods.onBinary);
- assertHasEventMethod(classId + ".onClose",methods.onClose);
- assertHasEventMethod(classId + ".onConnect",methods.onConnect);
- assertNoEventMethod(classId + ".onException",methods.onError);
- assertHasEventMethod(classId + ".onText",methods.onText);
- assertNoEventMethod(classId + ".onFrame",methods.onFrame);
-
- Assert.assertFalse(classId + ".onText.hasSession",methods.onText.isHasSession());
- Assert.assertTrue(classId + ".onText.isStreaming",methods.onText.isStreaming());
+ Assert.assertThat("EventDriver for " + classId,driver,instanceOf(JettyListenerEventDriver.class));
}
/**
@@ -388,6 +78,6 @@
EventDriver driver = factory.wrap(socket);
String classId = ListenerBasicSocket.class.getSimpleName();
- Assert.assertThat("EventDriver for " + classId,driver,instanceOf(ListenerEventDriver.class));
+ Assert.assertThat("EventDriver for " + classId,driver,instanceOf(JettyListenerEventDriver.class));
}
}
diff --git a/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/events/EventDriverTest.java b/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/events/EventDriverTest.java
index ecb5adb..20e4d9d 100644
--- a/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/events/EventDriverTest.java
+++ b/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/events/EventDriverTest.java
@@ -19,6 +19,8 @@
package org.eclipse.jetty.websocket.common.events;
import java.io.IOException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
import org.eclipse.jetty.websocket.api.StatusCode;
import org.eclipse.jetty.websocket.api.WebSocketException;
@@ -61,8 +63,8 @@
driver.incomingFrame(new CloseInfo(StatusCode.NORMAL).asFrame());
socket.capture.assertEventCount(2);
- socket.capture.assertEventStartsWith(0,"onWebSocketConnect");
- socket.capture.assertEventStartsWith(1,"onWebSocketClose");
+ socket.capture.pop().assertEventStartsWith("onWebSocketConnect");
+ socket.capture.pop().assertEventStartsWith("onWebSocketClose");
}
}
@@ -79,9 +81,9 @@
driver.incomingFrame(new CloseInfo(StatusCode.NORMAL).asFrame());
socket.capture.assertEventCount(3);
- socket.capture.assertEventStartsWith(0,"onConnect");
- socket.capture.assertEvent(1,"onBinary([11],0,11)");
- socket.capture.assertEventStartsWith(2,"onClose(1000,");
+ socket.capture.pop().assertEventStartsWith("onConnect");
+ socket.capture.pop().assertEvent("onBinary([11],0,11)");
+ socket.capture.pop().assertEventStartsWith("onClose(1000,");
}
}
@@ -98,9 +100,9 @@
driver.incomingFrame(new CloseInfo(StatusCode.NORMAL).asFrame());
socket.capture.assertEventCount(3);
- socket.capture.assertEventStartsWith(0,"onConnect");
- socket.capture.assertEventStartsWith(1,"onError(WebSocketException: oof)");
- socket.capture.assertEventStartsWith(2,"onClose(1000,");
+ socket.capture.pop().assertEventStartsWith("onConnect");
+ socket.capture.pop().assertEventStartsWith("onError(WebSocketException: oof)");
+ socket.capture.pop().assertEventStartsWith("onClose(1000,");
}
}
@@ -119,17 +121,17 @@
driver.incomingFrame(new CloseInfo(StatusCode.SHUTDOWN,"testcase").asFrame());
socket.capture.assertEventCount(6);
- socket.capture.assertEventStartsWith(0,"onConnect(");
- socket.capture.assertEventStartsWith(1,"onFrame(PING[");
- socket.capture.assertEventStartsWith(2,"onFrame(TEXT[");
- socket.capture.assertEventStartsWith(3,"onFrame(BINARY[");
- socket.capture.assertEventStartsWith(4,"onFrame(CLOSE[");
- socket.capture.assertEventStartsWith(5,"onClose(1001,");
+ socket.capture.pop().assertEventStartsWith("onConnect(");
+ socket.capture.pop().assertEventStartsWith("onFrame(PING[");
+ socket.capture.pop().assertEventStartsWith("onFrame(TEXT[");
+ socket.capture.pop().assertEventStartsWith("onFrame(BINARY[");
+ socket.capture.pop().assertEventStartsWith("onFrame(CLOSE[");
+ socket.capture.pop().assertEventStartsWith("onClose(1001,");
}
}
@Test
- public void testAnnotated_InputStream() throws IOException
+ public void testAnnotated_InputStream() throws IOException, TimeoutException, InterruptedException
{
AnnotatedBinaryStreamSocket socket = new AnnotatedBinaryStreamSocket();
EventDriver driver = wrap(socket);
@@ -139,11 +141,11 @@
conn.open();
driver.incomingFrame(makeBinaryFrame("Hello World",true));
driver.incomingFrame(new CloseInfo(StatusCode.NORMAL).asFrame());
-
+
socket.capture.assertEventCount(3);
- socket.capture.assertEventStartsWith(0,"onConnect");
- socket.capture.assertEventRegex(1,"^onBinary\\(.*InputStream.*");
- socket.capture.assertEventStartsWith(2,"onClose(1000,");
+ socket.capture.pop().assertEventStartsWith("onConnect");
+ socket.capture.pop().assertEventRegex("^onBinary\\(.*InputStream.*");
+ socket.capture.pop().assertEventStartsWith("onClose(1000,");
}
}
@@ -161,9 +163,9 @@
driver.incomingFrame(new CloseInfo(StatusCode.NORMAL).asFrame());
socket.capture.assertEventCount(3);
- socket.capture.assertEventStartsWith(0,"onWebSocketConnect");
- socket.capture.assertEventStartsWith(1,"onWebSocketText(\"Hello World\")");
- socket.capture.assertEventStartsWith(2,"onWebSocketClose(1000,");
+ socket.capture.pop().assertEventStartsWith("onWebSocketConnect");
+ socket.capture.pop().assertEventStartsWith("onWebSocketText(\"Hello World\")");
+ socket.capture.pop().assertEventStartsWith("onWebSocketClose(1000,");
}
}
diff --git a/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/events/JettyAnnotatedScannerTest.java b/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/events/JettyAnnotatedScannerTest.java
new file mode 100644
index 0000000..eab47d1
--- /dev/null
+++ b/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/events/JettyAnnotatedScannerTest.java
@@ -0,0 +1,340 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.common.events;
+
+import static org.hamcrest.Matchers.*;
+
+import org.eclipse.jetty.websocket.api.InvalidWebSocketException;
+import org.eclipse.jetty.websocket.common.annotations.BadBinarySignatureSocket;
+import org.eclipse.jetty.websocket.common.annotations.BadDuplicateBinarySocket;
+import org.eclipse.jetty.websocket.common.annotations.BadDuplicateFrameSocket;
+import org.eclipse.jetty.websocket.common.annotations.BadTextSignatureSocket;
+import org.eclipse.jetty.websocket.common.annotations.FrameSocket;
+import org.eclipse.jetty.websocket.common.annotations.MyEchoBinarySocket;
+import org.eclipse.jetty.websocket.common.annotations.MyEchoSocket;
+import org.eclipse.jetty.websocket.common.annotations.MyStatelessEchoSocket;
+import org.eclipse.jetty.websocket.common.annotations.NoopSocket;
+import org.eclipse.jetty.websocket.common.events.annotated.CallableMethod;
+import org.junit.Assert;
+import org.junit.Test;
+
+import examples.AnnotatedBinaryArraySocket;
+import examples.AnnotatedBinaryStreamSocket;
+import examples.AnnotatedTextSocket;
+import examples.AnnotatedTextStreamSocket;
+
+public class JettyAnnotatedScannerTest
+{
+ private void assertHasEventMethod(String message, CallableMethod actual)
+ {
+ Assert.assertThat(message + " CallableMethod",actual,notNullValue());
+
+ Assert.assertThat(message + " CallableMethod.pojo",actual.getPojo(),notNullValue());
+ Assert.assertThat(message + " CallableMethod.method",actual.getMethod(),notNullValue());
+ }
+
+ private void assertNoEventMethod(String message, CallableMethod actual)
+ {
+ Assert.assertThat(message + " CallableMethod",actual,nullValue());
+ }
+
+ /**
+ * Test Case for bad declaration (duplicate OnWebSocketBinary declarations)
+ */
+ @Test
+ public void testAnnotatedBadDuplicateBinarySocket()
+ {
+ JettyAnnotatedScanner impl = new JettyAnnotatedScanner();
+ try
+ {
+ // Should toss exception
+ impl.scan(BadDuplicateBinarySocket.class);
+ Assert.fail("Should have thrown " + InvalidWebSocketException.class);
+ }
+ catch (InvalidWebSocketException e)
+ {
+ // Validate that we have clear error message to the developer
+ Assert.assertThat(e.getMessage(),containsString("Duplicate @OnWebSocketMessage declaration"));
+ }
+ }
+
+ /**
+ * Test Case for bad declaration (duplicate frame type methods)
+ */
+ @Test
+ public void testAnnotatedBadDuplicateFrameSocket()
+ {
+ JettyAnnotatedScanner impl = new JettyAnnotatedScanner();
+ try
+ {
+ // Should toss exception
+ impl.scan(BadDuplicateFrameSocket.class);
+ Assert.fail("Should have thrown " + InvalidWebSocketException.class);
+ }
+ catch (InvalidWebSocketException e)
+ {
+ // Validate that we have clear error message to the developer
+ Assert.assertThat(e.getMessage(),containsString("Duplicate @OnWebSocketFrame"));
+ }
+ }
+
+ /**
+ * Test Case for bad declaration a method with a non-void return type
+ */
+ @Test
+ public void testAnnotatedBadSignature_NonVoidReturn()
+ {
+ JettyAnnotatedScanner impl = new JettyAnnotatedScanner();
+ try
+ {
+ // Should toss exception
+ impl.scan(BadBinarySignatureSocket.class);
+ Assert.fail("Should have thrown " + InvalidWebSocketException.class);
+ }
+ catch (InvalidWebSocketException e)
+ {
+ // Validate that we have clear error message to the developer
+ Assert.assertThat(e.getMessage(),containsString("must be void"));
+ }
+ }
+
+ /**
+ * Test Case for bad declaration a method with a public static method
+ */
+ @Test
+ public void testAnnotatedBadSignature_Static()
+ {
+ JettyAnnotatedScanner impl = new JettyAnnotatedScanner();
+ try
+ {
+ // Should toss exception
+ impl.scan(BadTextSignatureSocket.class);
+ Assert.fail("Should have thrown " + InvalidWebSocketException.class);
+ }
+ catch (InvalidWebSocketException e)
+ {
+ // Validate that we have clear error message to the developer
+ Assert.assertThat(e.getMessage(),containsString("may not be static"));
+ }
+ }
+
+ /**
+ * Test Case for socket for binary array messages
+ */
+ @Test
+ public void testAnnotatedBinaryArraySocket()
+ {
+ JettyAnnotatedScanner impl = new JettyAnnotatedScanner();
+ JettyAnnotatedMetadata metadata = impl.scan(AnnotatedBinaryArraySocket.class);
+
+ String classId = AnnotatedBinaryArraySocket.class.getSimpleName();
+
+ Assert.assertThat("EventMethods for " + classId,metadata,notNullValue());
+
+ assertHasEventMethod(classId + ".onBinary",metadata.onBinary);
+ assertHasEventMethod(classId + ".onClose",metadata.onClose);
+ assertHasEventMethod(classId + ".onConnect",metadata.onConnect);
+ assertNoEventMethod(classId + ".onException",metadata.onError);
+ assertNoEventMethod(classId + ".onText",metadata.onText);
+ assertNoEventMethod(classId + ".onFrame",metadata.onFrame);
+
+ Assert.assertFalse(classId + ".onBinary.isSessionAware",metadata.onBinary.isSessionAware());
+ Assert.assertFalse(classId + ".onBinary.isStreaming",metadata.onBinary.isStreaming());
+ }
+
+ /**
+ * Test Case for socket for binary stream messages
+ */
+ @Test
+ public void testAnnotatedBinaryStreamSocket()
+ {
+ JettyAnnotatedScanner impl = new JettyAnnotatedScanner();
+ JettyAnnotatedMetadata metadata = impl.scan(AnnotatedBinaryStreamSocket.class);
+
+ String classId = AnnotatedBinaryStreamSocket.class.getSimpleName();
+
+ Assert.assertThat("EventMethods for " + classId,metadata,notNullValue());
+
+ assertHasEventMethod(classId + ".onBinary",metadata.onBinary);
+ assertHasEventMethod(classId + ".onClose",metadata.onClose);
+ assertHasEventMethod(classId + ".onConnect",metadata.onConnect);
+ assertNoEventMethod(classId + ".onException",metadata.onError);
+ assertNoEventMethod(classId + ".onText",metadata.onText);
+ assertNoEventMethod(classId + ".onFrame",metadata.onFrame);
+
+ Assert.assertFalse(classId + ".onBinary.isSessionAware",metadata.onBinary.isSessionAware());
+ Assert.assertTrue(classId + ".onBinary.isStreaming",metadata.onBinary.isStreaming());
+ }
+
+ /**
+ * Test Case for no exceptions and 4 methods (3 methods from parent)
+ */
+ @Test
+ public void testAnnotatedMyEchoBinarySocket()
+ {
+ JettyAnnotatedScanner impl = new JettyAnnotatedScanner();
+ JettyAnnotatedMetadata metadata = impl.scan(MyEchoBinarySocket.class);
+
+ String classId = MyEchoBinarySocket.class.getSimpleName();
+
+ Assert.assertThat("EventMethods for " + classId,metadata,notNullValue());
+
+ assertHasEventMethod(classId + ".onBinary",metadata.onBinary);
+ assertHasEventMethod(classId + ".onClose",metadata.onClose);
+ assertHasEventMethod(classId + ".onConnect",metadata.onConnect);
+ assertNoEventMethod(classId + ".onException",metadata.onError);
+ assertHasEventMethod(classId + ".onText",metadata.onText);
+ assertNoEventMethod(classId + ".onFrame",metadata.onFrame);
+ }
+
+ /**
+ * Test Case for no exceptions and 3 methods
+ */
+ @Test
+ public void testAnnotatedMyEchoSocket()
+ {
+ JettyAnnotatedScanner impl = new JettyAnnotatedScanner();
+ JettyAnnotatedMetadata metadata = impl.scan(MyEchoSocket.class);
+
+ String classId = MyEchoSocket.class.getSimpleName();
+
+ Assert.assertThat("EventMethods for " + classId,metadata,notNullValue());
+
+ assertNoEventMethod(classId + ".onBinary",metadata.onBinary);
+ assertHasEventMethod(classId + ".onClose",metadata.onClose);
+ assertHasEventMethod(classId + ".onConnect",metadata.onConnect);
+ assertNoEventMethod(classId + ".onException",metadata.onError);
+ assertHasEventMethod(classId + ".onText",metadata.onText);
+ assertNoEventMethod(classId + ".onFrame",metadata.onFrame);
+ }
+
+ /**
+ * Test Case for annotated for text messages w/connection param
+ */
+ @Test
+ public void testAnnotatedMyStatelessEchoSocket()
+ {
+ JettyAnnotatedScanner impl = new JettyAnnotatedScanner();
+ JettyAnnotatedMetadata metadata = impl.scan(MyStatelessEchoSocket.class);
+
+ String classId = MyStatelessEchoSocket.class.getSimpleName();
+
+ Assert.assertThat("EventMethods for " + classId,metadata,notNullValue());
+
+ assertNoEventMethod(classId + ".onBinary",metadata.onBinary);
+ assertNoEventMethod(classId + ".onClose",metadata.onClose);
+ assertNoEventMethod(classId + ".onConnect",metadata.onConnect);
+ assertNoEventMethod(classId + ".onException",metadata.onError);
+ assertHasEventMethod(classId + ".onText",metadata.onText);
+ assertNoEventMethod(classId + ".onFrame",metadata.onFrame);
+
+ Assert.assertTrue(classId + ".onText.isSessionAware",metadata.onText.isSessionAware());
+ Assert.assertFalse(classId + ".onText.isStreaming",metadata.onText.isStreaming());
+ }
+
+ /**
+ * Test Case for no exceptions and no methods
+ */
+ @Test
+ public void testAnnotatedNoop()
+ {
+ JettyAnnotatedScanner impl = new JettyAnnotatedScanner();
+ JettyAnnotatedMetadata metadata = impl.scan(NoopSocket.class);
+
+ String classId = NoopSocket.class.getSimpleName();
+
+ Assert.assertThat("Methods for " + classId,metadata,notNullValue());
+
+ assertNoEventMethod(classId + ".onBinary",metadata.onBinary);
+ assertNoEventMethod(classId + ".onClose",metadata.onClose);
+ assertNoEventMethod(classId + ".onConnect",metadata.onConnect);
+ assertNoEventMethod(classId + ".onException",metadata.onError);
+ assertNoEventMethod(classId + ".onText",metadata.onText);
+ assertNoEventMethod(classId + ".onFrame",metadata.onFrame);
+ }
+
+ /**
+ * Test Case for no exceptions and 1 methods
+ */
+ @Test
+ public void testAnnotatedOnFrame()
+ {
+ JettyAnnotatedScanner impl = new JettyAnnotatedScanner();
+ JettyAnnotatedMetadata metadata = impl.scan(FrameSocket.class);
+
+ String classId = FrameSocket.class.getSimpleName();
+
+ Assert.assertThat("EventMethods for " + classId,metadata,notNullValue());
+
+ assertNoEventMethod(classId + ".onBinary",metadata.onBinary);
+ assertNoEventMethod(classId + ".onClose",metadata.onClose);
+ assertNoEventMethod(classId + ".onConnect",metadata.onConnect);
+ assertNoEventMethod(classId + ".onException",metadata.onError);
+ assertNoEventMethod(classId + ".onText",metadata.onText);
+ assertHasEventMethod(classId + ".onFrame",metadata.onFrame);
+ }
+
+ /**
+ * Test Case for socket for simple text messages
+ */
+ @Test
+ public void testAnnotatedTextSocket()
+ {
+ JettyAnnotatedScanner impl = new JettyAnnotatedScanner();
+ JettyAnnotatedMetadata metadata = impl.scan(AnnotatedTextSocket.class);
+
+ String classId = AnnotatedTextSocket.class.getSimpleName();
+
+ Assert.assertThat("EventMethods for " + classId,metadata,notNullValue());
+
+ assertNoEventMethod(classId + ".onBinary",metadata.onBinary);
+ assertHasEventMethod(classId + ".onClose",metadata.onClose);
+ assertHasEventMethod(classId + ".onConnect",metadata.onConnect);
+ assertHasEventMethod(classId + ".onException",metadata.onError);
+ assertHasEventMethod(classId + ".onText",metadata.onText);
+ assertNoEventMethod(classId + ".onFrame",metadata.onFrame);
+
+ Assert.assertFalse(classId + ".onText.isSessionAware",metadata.onText.isSessionAware());
+ Assert.assertFalse(classId + ".onText.isStreaming",metadata.onText.isStreaming());
+ }
+
+ /**
+ * Test Case for socket for text stream messages
+ */
+ @Test
+ public void testAnnotatedTextStreamSocket()
+ {
+ JettyAnnotatedScanner impl = new JettyAnnotatedScanner();
+ JettyAnnotatedMetadata metadata = impl.scan(AnnotatedTextStreamSocket.class);
+
+ String classId = AnnotatedTextStreamSocket.class.getSimpleName();
+
+ Assert.assertThat("EventMethods for " + classId,metadata,notNullValue());
+
+ assertNoEventMethod(classId + ".onBinary",metadata.onBinary);
+ assertHasEventMethod(classId + ".onClose",metadata.onClose);
+ assertHasEventMethod(classId + ".onConnect",metadata.onConnect);
+ assertNoEventMethod(classId + ".onException",metadata.onError);
+ assertHasEventMethod(classId + ".onText",metadata.onText);
+ assertNoEventMethod(classId + ".onFrame",metadata.onFrame);
+
+ Assert.assertFalse(classId + ".onText.isSessionAware",metadata.onText.isSessionAware());
+ Assert.assertTrue(classId + ".onText.isStreaming",metadata.onText.isStreaming());
+ }
+}
diff --git a/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/extensions/DummyIncomingFrames.java b/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/extensions/DummyIncomingFrames.java
index 39cfc7d..b9e045e 100644
--- a/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/extensions/DummyIncomingFrames.java
+++ b/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/extensions/DummyIncomingFrames.java
@@ -20,7 +20,6 @@
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;
-import org.eclipse.jetty.websocket.api.WebSocketException;
import org.eclipse.jetty.websocket.api.extensions.Frame;
import org.eclipse.jetty.websocket.api.extensions.IncomingFrames;
@@ -38,7 +37,7 @@
}
@Override
- public void incomingError(WebSocketException e)
+ public void incomingError(Throwable e)
{
LOG.debug("incomingError()",e);
}
diff --git a/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/extensions/compress/MessageCompressionExtensionTest.java b/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/extensions/compress/MessageCompressionExtensionTest.java
index 3f8c96d..6f0a9e8 100644
--- a/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/extensions/compress/MessageCompressionExtensionTest.java
+++ b/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/extensions/compress/MessageCompressionExtensionTest.java
@@ -49,7 +49,7 @@
WebSocketPolicy policy = WebSocketPolicy.newServerPolicy();
// Setup extension
- MessageCompressionExtension ext = new MessageCompressionExtension();
+ MessageDeflateCompressionExtension ext = new MessageDeflateCompressionExtension();
ext.setBufferPool(new MappedByteBufferPool());
ext.setPolicy(policy);
ExtensionConfig config = ExtensionConfig.parse("permessage-compress");
@@ -162,7 +162,7 @@
*/
@Test
public void testIncomingPing() {
- MessageCompressionExtension ext = new MessageCompressionExtension();
+ MessageDeflateCompressionExtension ext = new MessageDeflateCompressionExtension();
ext.setBufferPool(new MappedByteBufferPool());
ext.setPolicy(WebSocketPolicy.newServerPolicy());
ExtensionConfig config = ExtensionConfig.parse("permessage-compress");
@@ -199,7 +199,7 @@
@Test
public void testIncomingUncompressedFrames()
{
- MessageCompressionExtension ext = new MessageCompressionExtension();
+ MessageDeflateCompressionExtension ext = new MessageDeflateCompressionExtension();
ext.setBufferPool(new MappedByteBufferPool());
ext.setPolicy(WebSocketPolicy.newServerPolicy());
ExtensionConfig config = ExtensionConfig.parse("permessage-compress");
@@ -255,7 +255,7 @@
@Test
public void testOutgoingFrames() throws IOException
{
- MessageCompressionExtension ext = new MessageCompressionExtension();
+ MessageDeflateCompressionExtension ext = new MessageDeflateCompressionExtension();
ext.setBufferPool(new MappedByteBufferPool());
ext.setPolicy(WebSocketPolicy.newServerPolicy());
ExtensionConfig config = ExtensionConfig.parse("permessage-compress");
@@ -325,7 +325,7 @@
@Test
public void testOutgoingPing() throws IOException
{
- MessageCompressionExtension ext = new MessageCompressionExtension();
+ MessageDeflateCompressionExtension ext = new MessageDeflateCompressionExtension();
ext.setBufferPool(new MappedByteBufferPool());
ext.setPolicy(WebSocketPolicy.newServerPolicy());
ExtensionConfig config = ExtensionConfig.parse("permessage-compress");
diff --git a/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/extensions/mux/MuxDecoder.java b/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/extensions/mux/MuxDecoder.java
deleted file mode 100644
index c06a741..0000000
--- a/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/extensions/mux/MuxDecoder.java
+++ /dev/null
@@ -1,45 +0,0 @@
-//
-// ========================================================================
-// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
-// ------------------------------------------------------------------------
-// All rights reserved. This program and the accompanying materials
-// are made available under the terms of the Eclipse Public License v1.0
-// and Apache License v2.0 which accompanies this distribution.
-//
-// The Eclipse Public License is available at
-// http://www.eclipse.org/legal/epl-v10.html
-//
-// The Apache License v2.0 is available at
-// http://www.opensource.org/licenses/apache2.0.php
-//
-// You may elect to redistribute this code under either of these licenses.
-// ========================================================================
-//
-
-package org.eclipse.jetty.websocket.common.extensions.mux;
-
-import org.eclipse.jetty.websocket.api.WriteCallback;
-import org.eclipse.jetty.websocket.api.extensions.Frame;
-import org.eclipse.jetty.websocket.api.extensions.OutgoingFrames;
-
-/**
- * Helpful utility class to parse arbitrary mux events from a physical connection's OutgoingFrames.
- *
- * @see MuxEncoder
- */
-public class MuxDecoder extends MuxEventCapture implements OutgoingFrames
-{
- private MuxParser parser;
-
- public MuxDecoder()
- {
- parser = new MuxParser();
- parser.setEvents(this);
- }
-
- @Override
- public void outgoingFrame(Frame frame, WriteCallback callback)
- {
- parser.parse(frame);
- }
-}
diff --git a/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/extensions/mux/MuxEncoder.java b/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/extensions/mux/MuxEncoder.java
deleted file mode 100644
index 079d5e4..0000000
--- a/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/extensions/mux/MuxEncoder.java
+++ /dev/null
@@ -1,64 +0,0 @@
-//
-// ========================================================================
-// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
-// ------------------------------------------------------------------------
-// All rights reserved. This program and the accompanying materials
-// are made available under the terms of the Eclipse Public License v1.0
-// and Apache License v2.0 which accompanies this distribution.
-//
-// The Eclipse Public License is available at
-// http://www.eclipse.org/legal/epl-v10.html
-//
-// The Apache License v2.0 is available at
-// http://www.opensource.org/licenses/apache2.0.php
-//
-// You may elect to redistribute this code under either of these licenses.
-// ========================================================================
-//
-
-package org.eclipse.jetty.websocket.common.extensions.mux;
-
-import java.io.IOException;
-
-import org.eclipse.jetty.websocket.api.WriteCallback;
-import org.eclipse.jetty.websocket.api.extensions.IncomingFrames;
-import org.eclipse.jetty.websocket.api.extensions.OutgoingFrames;
-import org.eclipse.jetty.websocket.common.WebSocketFrame;
-import org.eclipse.jetty.websocket.common.io.FramePipes;
-
-/**
- * Helpful utility class to send arbitrary mux events into a physical connection's IncomingFrames.
- *
- * @see MuxDecoder
- */
-public class MuxEncoder
-{
- public static MuxEncoder toIncoming(IncomingFrames incoming)
- {
- return new MuxEncoder(FramePipes.to(incoming));
- }
-
- public static MuxEncoder toOutgoing(OutgoingFrames outgoing)
- {
- return new MuxEncoder(outgoing);
- }
-
- private MuxGenerator generator;
-
- private MuxEncoder(OutgoingFrames outgoing)
- {
- this.generator = new MuxGenerator();
- this.generator.setOutgoing(outgoing);
- }
-
- public void frame(long channelId, WebSocketFrame frame) throws IOException
- {
- this.generator.generate(channelId,frame,null);
- }
-
- public void op(MuxControlBlock op) throws IOException
- {
- WriteCallback callback = null;
- this.generator.generate(callback,op);
- }
-}
diff --git a/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/extensions/mux/MuxEventCapture.java b/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/extensions/mux/MuxEventCapture.java
deleted file mode 100644
index f9cfc91..0000000
--- a/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/extensions/mux/MuxEventCapture.java
+++ /dev/null
@@ -1,141 +0,0 @@
-//
-// ========================================================================
-// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
-// ------------------------------------------------------------------------
-// All rights reserved. This program and the accompanying materials
-// are made available under the terms of the Eclipse Public License v1.0
-// and Apache License v2.0 which accompanies this distribution.
-//
-// The Eclipse Public License is available at
-// http://www.eclipse.org/legal/epl-v10.html
-//
-// The Apache License v2.0 is available at
-// http://www.opensource.org/licenses/apache2.0.php
-//
-// You may elect to redistribute this code under either of these licenses.
-// ========================================================================
-//
-
-package org.eclipse.jetty.websocket.common.extensions.mux;
-
-import static org.hamcrest.Matchers.*;
-
-import java.util.LinkedList;
-
-import org.eclipse.jetty.util.log.Log;
-import org.eclipse.jetty.util.log.Logger;
-import org.eclipse.jetty.websocket.common.OpCode;
-import org.eclipse.jetty.websocket.common.extensions.mux.MuxControlBlock;
-import org.eclipse.jetty.websocket.common.extensions.mux.MuxException;
-import org.eclipse.jetty.websocket.common.extensions.mux.MuxParser;
-import org.eclipse.jetty.websocket.common.extensions.mux.MuxedFrame;
-import org.eclipse.jetty.websocket.common.extensions.mux.op.MuxAddChannelRequest;
-import org.eclipse.jetty.websocket.common.extensions.mux.op.MuxAddChannelResponse;
-import org.eclipse.jetty.websocket.common.extensions.mux.op.MuxDropChannel;
-import org.eclipse.jetty.websocket.common.extensions.mux.op.MuxFlowControl;
-import org.eclipse.jetty.websocket.common.extensions.mux.op.MuxNewChannelSlot;
-import org.junit.Assert;
-
-public class MuxEventCapture implements MuxParser.Listener
-{
- private static final Logger LOG = Log.getLogger(MuxEventCapture.class);
-
- private LinkedList<MuxedFrame> frames = new LinkedList<>();
- private LinkedList<MuxControlBlock> ops = new LinkedList<>();
- private LinkedList<MuxException> errors = new LinkedList<>();
-
- public void assertFrameCount(int expected)
- {
- Assert.assertThat("Frame Count",frames.size(), is(expected));
- }
-
- public void assertHasFrame(byte opcode, long channelId, int expectedCount)
- {
- int actualCount = 0;
-
- for (MuxedFrame frame : frames)
- {
- if (frame.getChannelId() == channelId)
- {
- if (frame.getOpCode() == opcode)
- {
- actualCount++;
- }
- }
- }
-
- Assert.assertThat("Expected Count of " + OpCode.name(opcode) + " frames on Channel ID " + channelId,actualCount,is(expectedCount));
- }
-
- public void assertHasOp(byte opCode, int expectedCount)
- {
- int actualCount = 0;
- for (MuxControlBlock block : ops)
- {
- if (block.getOpCode() == opCode)
- {
- actualCount++;
- }
- }
- Assert.assertThat("Op[" + opCode + "] count",actualCount,is(expectedCount));
- }
-
- public LinkedList<MuxedFrame> getFrames()
- {
- return frames;
- }
-
- public LinkedList<MuxControlBlock> getOps()
- {
- return ops;
- }
-
- @Override
- public void onMuxAddChannelRequest(MuxAddChannelRequest request)
- {
- ops.add(request);
- }
-
- @Override
- public void onMuxAddChannelResponse(MuxAddChannelResponse response)
- {
- ops.add(response);
- }
-
- @Override
- public void onMuxDropChannel(MuxDropChannel drop)
- {
- ops.add(drop);
- }
-
- @Override
- public void onMuxedFrame(MuxedFrame frame)
- {
- frames.add(new MuxedFrame(frame));
- }
-
- @Override
- public void onMuxException(MuxException e)
- {
- LOG.debug(e);
- errors.add(e);
- }
-
- @Override
- public void onMuxFlowControl(MuxFlowControl flow)
- {
- ops.add(flow);
- }
-
- @Override
- public void onMuxNewChannelSlot(MuxNewChannelSlot slot)
- {
- ops.add(slot);
- }
-
- public void reset()
- {
- frames.clear();
- ops.clear();
- }
-}
diff --git a/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/extensions/mux/MuxGeneratorWrite139SizeTest.java b/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/extensions/mux/MuxGeneratorWrite139SizeTest.java
deleted file mode 100644
index acdc204..0000000
--- a/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/extensions/mux/MuxGeneratorWrite139SizeTest.java
+++ /dev/null
@@ -1,98 +0,0 @@
-//
-// ========================================================================
-// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
-// ------------------------------------------------------------------------
-// All rights reserved. This program and the accompanying materials
-// are made available under the terms of the Eclipse Public License v1.0
-// and Apache License v2.0 which accompanies this distribution.
-//
-// The Eclipse Public License is available at
-// http://www.eclipse.org/legal/epl-v10.html
-//
-// The Apache License v2.0 is available at
-// http://www.opensource.org/licenses/apache2.0.php
-//
-// You may elect to redistribute this code under either of these licenses.
-// ========================================================================
-//
-
-package org.eclipse.jetty.websocket.common.extensions.mux;
-
-import static org.hamcrest.Matchers.*;
-
-import java.nio.ByteBuffer;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.List;
-import java.util.Locale;
-
-import org.eclipse.jetty.util.BufferUtil;
-import org.eclipse.jetty.util.TypeUtil;
-import org.eclipse.jetty.websocket.common.extensions.mux.MuxGenerator;
-import org.junit.Assert;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.rules.TestName;
-import org.junit.runner.RunWith;
-import org.junit.runners.Parameterized;
-import org.junit.runners.Parameterized.Parameters;
-
-@RunWith(Parameterized.class)
-public class MuxGeneratorWrite139SizeTest
-{
- private static MuxGenerator generator = new MuxGenerator();
-
- @Parameters
- public static Collection<Object[]> data()
- {
- // Various good 1/3/9 encodings
- List<Object[]> data = new ArrayList<>();
-
- // @formatter:off
- // - 1 byte tests
- data.add(new Object[]{ 0L, "00"});
- data.add(new Object[]{ 1L, "01"});
- data.add(new Object[]{ 2L, "02"});
- data.add(new Object[]{ 55L, "37"});
- data.add(new Object[]{125L, "7D"});
-
- // - 3 byte tests
- data.add(new Object[]{0x00_80L, "7E0080"});
- data.add(new Object[]{0x00_ABL, "7E00AB"});
- data.add(new Object[]{0x00_FFL, "7E00FF"});
- data.add(new Object[]{0x3F_FFL, "7E3FFF"});
-
- // - 9 byte tests
- data.add(new Object[]{0x00_00_01_FF_FFL, "7F000000000001FFFF"});
- data.add(new Object[]{0x00_00_FF_FF_FFL, "7F0000000000FFFFFF"});
- data.add(new Object[]{0x00_FF_FF_FF_FFL, "7F00000000FFFFFFFF"});
- data.add(new Object[]{0xFF_FF_FF_FF_FFL, "7F000000FFFFFFFFFF"});
- // @formatter:on
-
- return data;
- }
-
- @Rule
- public TestName testname = new TestName();
-
- private long value;
- private String expectedHex;
-
- public MuxGeneratorWrite139SizeTest(long value, String expectedHex)
- {
- this.value = value;
- this.expectedHex = expectedHex;
- }
-
- @Test
- public void testWrite139Size()
- {
- System.err.printf("Running %s.%s - value: %,d%n",this.getClass().getName(),testname.getMethodName(),value);
- ByteBuffer bbuf = ByteBuffer.allocate(10);
- generator.write139Size(bbuf,value);
- BufferUtil.flipToFlush(bbuf,0);
- byte actual[] = BufferUtil.toArray(bbuf);
- String actualHex = TypeUtil.toHexString(actual).toUpperCase(Locale.ENGLISH);
- Assert.assertThat("1/3/9 encoded size of [" + value + "]",actualHex,is(expectedHex));
- }
-}
diff --git a/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/extensions/mux/MuxGeneratorWriteChannelIdTest.java b/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/extensions/mux/MuxGeneratorWriteChannelIdTest.java
deleted file mode 100644
index 4fe5fdb..0000000
--- a/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/extensions/mux/MuxGeneratorWriteChannelIdTest.java
+++ /dev/null
@@ -1,102 +0,0 @@
-//
-// ========================================================================
-// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
-// ------------------------------------------------------------------------
-// All rights reserved. This program and the accompanying materials
-// are made available under the terms of the Eclipse Public License v1.0
-// and Apache License v2.0 which accompanies this distribution.
-//
-// The Eclipse Public License is available at
-// http://www.eclipse.org/legal/epl-v10.html
-//
-// The Apache License v2.0 is available at
-// http://www.opensource.org/licenses/apache2.0.php
-//
-// You may elect to redistribute this code under either of these licenses.
-// ========================================================================
-//
-
-package org.eclipse.jetty.websocket.common.extensions.mux;
-
-import static org.hamcrest.Matchers.*;
-
-import java.nio.ByteBuffer;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.List;
-import java.util.Locale;
-
-import org.eclipse.jetty.util.BufferUtil;
-import org.eclipse.jetty.util.TypeUtil;
-import org.eclipse.jetty.websocket.common.extensions.mux.MuxGenerator;
-import org.junit.Assert;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.rules.TestName;
-import org.junit.runner.RunWith;
-import org.junit.runners.Parameterized;
-import org.junit.runners.Parameterized.Parameters;
-
-/**
- * Tests of valid ChannelID generation
- */
-@RunWith(Parameterized.class)
-public class MuxGeneratorWriteChannelIdTest
-{
- private static MuxGenerator generator = new MuxGenerator();
-
- @Parameters
- public static Collection<Object[]> data()
- {
- // Various good Channel IDs
- List<Object[]> data = new ArrayList<>();
-
- // @formatter:off
- // - 1 byte tests
- data.add(new Object[]{ 0L, "00"});
- data.add(new Object[]{ 1L, "01"});
- data.add(new Object[]{ 2L, "02"});
- data.add(new Object[]{ 55L, "37"});
- data.add(new Object[]{127L, "7F"});
-
- // - 2 byte tests
- data.add(new Object[]{0x00_80L, "8080"});
- data.add(new Object[]{0x00_FFL, "80FF"});
- data.add(new Object[]{0x3F_FFL, "BFFF"});
-
- // - 3 byte tests
- data.add(new Object[]{0x00_FF_FFL, "C0FFFF"});
- data.add(new Object[]{0x1F_FF_FFL, "DFFFFF"});
-
- // - 3 byte tests
- data.add(new Object[]{0x00_FF_FF_FFL, "E0FFFFFF"});
- data.add(new Object[]{0x1F_FF_FF_FFL, "FFFFFFFF"});
-
- // @formatter:on
- return data;
- }
-
- @Rule
- public TestName testname = new TestName();
-
- private long channelId;
- private String expectedHex;
-
- public MuxGeneratorWriteChannelIdTest(long channelId, String expectedHex)
- {
- this.channelId = channelId;
- this.expectedHex = expectedHex;
- }
-
- @Test
- public void testReadChannelId()
- {
- System.err.printf("Running %s.%s - channelId: %,d%n",this.getClass().getName(),testname.getMethodName(),channelId);
- ByteBuffer bbuf = ByteBuffer.allocate(10);
- generator.writeChannelId(bbuf,channelId);
- BufferUtil.flipToFlush(bbuf,0);
- byte actual[] = BufferUtil.toArray(bbuf);
- String actualHex = TypeUtil.toHexString(actual).toUpperCase(Locale.ENGLISH);
- Assert.assertThat("Channel ID [" + channelId + "]",actualHex,is(expectedHex));
- }
-}
diff --git a/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/extensions/mux/MuxParserRFCTest.java b/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/extensions/mux/MuxParserRFCTest.java
deleted file mode 100644
index 332fa4e..0000000
--- a/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/extensions/mux/MuxParserRFCTest.java
+++ /dev/null
@@ -1,244 +0,0 @@
-//
-// ========================================================================
-// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
-// ------------------------------------------------------------------------
-// All rights reserved. This program and the accompanying materials
-// are made available under the terms of the Eclipse Public License v1.0
-// and Apache License v2.0 which accompanies this distribution.
-//
-// The Eclipse Public License is available at
-// http://www.eclipse.org/legal/epl-v10.html
-//
-// The Apache License v2.0 is available at
-// http://www.opensource.org/licenses/apache2.0.php
-//
-// You may elect to redistribute this code under either of these licenses.
-// ========================================================================
-//
-
-package org.eclipse.jetty.websocket.common.extensions.mux;
-
-import static org.hamcrest.Matchers.*;
-
-import java.io.ByteArrayOutputStream;
-import java.io.IOException;
-import java.nio.ByteBuffer;
-import java.util.Collections;
-import java.util.LinkedList;
-import java.util.List;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-
-import org.eclipse.jetty.util.StringUtil;
-import org.eclipse.jetty.util.TypeUtil;
-import org.eclipse.jetty.websocket.api.WebSocketPolicy;
-import org.eclipse.jetty.websocket.api.extensions.Frame;
-import org.eclipse.jetty.websocket.common.IncomingFramesCapture;
-import org.eclipse.jetty.websocket.common.OpCode;
-import org.eclipse.jetty.websocket.common.Parser;
-import org.eclipse.jetty.websocket.common.UnitParser;
-import org.eclipse.jetty.websocket.common.WebSocketFrame;
-import org.eclipse.jetty.websocket.common.extensions.AbstractExtension;
-import org.junit.Assert;
-import org.junit.Test;
-
-public class MuxParserRFCTest
-{
- public static class DummyMuxExtension extends AbstractMuxExtension
- {
- @Override
- public void configureMuxer(Muxer muxer)
- {
- /* nothing to do */
- }
- }
-
- private LinkedList<WebSocketFrame> asFrames(byte[] buf)
- {
- IncomingFramesCapture capture = new IncomingFramesCapture();
- WebSocketPolicy policy = WebSocketPolicy.newClientPolicy();
- Parser parser = new UnitParser(policy);
- parser.setIncomingFramesHandler(capture);
- List<? extends AbstractExtension> muxList = Collections.singletonList(new DummyMuxExtension());
- parser.configureFromExtensions(muxList);
- ByteBuffer bbuf = ByteBuffer.wrap(buf);
- parser.parse(bbuf);
-
- return capture.getFrames();
- }
-
- private boolean isHexOnly(String part)
- {
- Pattern bytePat = Pattern.compile("(\\s*0x[0-9A-Fa-f]{2}+){1,}+");
- Matcher mat = bytePat.matcher(part);
- return mat.matches();
- }
-
- private MuxEventCapture parseMuxFrames(LinkedList<WebSocketFrame> frames)
- {
- MuxParser parser = new MuxParser();
- MuxEventCapture capture = new MuxEventCapture();
- parser.setEvents(capture);
- for(Frame frame: frames) {
- parser.parse(frame);
- }
- return capture;
- }
-
- @Test
- public void testIsHexOnly()
- {
- Assert.assertTrue(isHexOnly("0x00"));
- Assert.assertTrue(isHexOnly("0x00 0xaF"));
- Assert.assertFalse(isHexOnly("Hello World"));
- }
-
- @Test
- public void testRFCExample1() throws IOException
- {
- // Create RFC detailed frames
- byte buf[] = toByteArray("0x82 0x0d 0x01 0x81","Hello world");
- LinkedList<WebSocketFrame> frames = asFrames(buf);
- Assert.assertThat("Frame count",frames.size(),is(1));
-
- // Have mux parse frames
- MuxEventCapture capture = parseMuxFrames(frames);
- capture.assertFrameCount(1);
-
- MuxedFrame mux;
-
- mux = capture.getFrames().pop();
- String prefix = "MuxFrame[0]";
- Assert.assertThat(prefix + ".channelId",mux.getChannelId(),is(1L));
- Assert.assertThat(prefix + ".fin",mux.isFin(),is(true));
- Assert.assertThat(prefix + ".rsv1",mux.isRsv1(),is(false));
- Assert.assertThat(prefix + ".rsv2",mux.isRsv2(),is(false));
- Assert.assertThat(prefix + ".rsv3",mux.isRsv3(),is(false));
- Assert.assertThat(prefix + ".masked",mux.isMasked(),is(false));
- Assert.assertThat(prefix + ".opcode",mux.getOpCode(),is(OpCode.TEXT));
-
- String payload = mux.getPayloadAsUTF8();
- Assert.assertThat(prefix + ".payload/text",payload,is("Hello world"));
- }
-
- @Test
- public void testRFCExample2() throws IOException
- {
- // Create RFC detailed frames
- byte buf[] = toByteArray("0x02 0x07 0x01 0x81","Hello","0x80 0x06"," world");
- LinkedList<WebSocketFrame> frames = asFrames(buf);
- Assert.assertThat("Frame count",frames.size(),is(2));
-
- // Have mux parse frames
- MuxEventCapture capture = parseMuxFrames(frames);
- capture.assertFrameCount(2);
-
- MuxedFrame mux;
-
- // Text Frame
- mux = capture.getFrames().get(0);
- String prefix = "MuxFrame[0]";
- Assert.assertThat(prefix + ".channelId",mux.getChannelId(),is(1L));
- // (BUG IN DRAFT) Assert.assertThat(prefix + ".fin",mux.isFin(),is(false));
- Assert.assertThat(prefix + ".rsv1",mux.isRsv1(),is(false));
- Assert.assertThat(prefix + ".rsv2",mux.isRsv2(),is(false));
- Assert.assertThat(prefix + ".rsv3",mux.isRsv3(),is(false));
- Assert.assertThat(prefix + ".masked",mux.isMasked(),is(false));
- Assert.assertThat(prefix + ".opcode",mux.getOpCode(),is(OpCode.TEXT));
-
- String payload = mux.getPayloadAsUTF8();
- Assert.assertThat(prefix + ".payload/text",payload,is("Hello"));
-
- // Continuation Frame
- mux = capture.getFrames().get(1);
- prefix = "MuxFrame[1]";
- // (BUG IN DRAFT) Assert.assertThat(prefix + ".channelId",mux.getChannelId(),is(1L));
- // (BUG IN DRAFT) Assert.assertThat(prefix + ".fin",mux.isFin(),is(true));
- Assert.assertThat(prefix + ".rsv1",mux.isRsv1(),is(false));
- Assert.assertThat(prefix + ".rsv2",mux.isRsv2(),is(false));
- Assert.assertThat(prefix + ".rsv3",mux.isRsv3(),is(false));
- Assert.assertThat(prefix + ".masked",mux.isMasked(),is(false));
- Assert.assertThat(prefix + ".continuation",mux.isContinuation(),is(true));
- // (BUG IN DRAFT) Assert.assertThat(prefix + ".opcode",mux.getOpCode(),is(OpCode.BINARY));
-
- payload = mux.getPayloadAsUTF8();
- Assert.assertThat(prefix + ".payload/text",payload,is(" world"));
- }
-
- @Test
- public void testRFCExample3() throws IOException
- {
- // Create RFC detailed frames
- byte buf[] = toByteArray("0x82 0x07 0x01 0x01","Hello","0x82 0x05 0x02 0x81","bye","0x82 0x08 0x01 0x80"," world");
- LinkedList<WebSocketFrame> frames = asFrames(buf);
- Assert.assertThat("Frame count",frames.size(),is(3));
-
- // Have mux parse frames
- MuxEventCapture capture = parseMuxFrames(frames);
- capture.assertFrameCount(3);
-
- MuxedFrame mux;
-
- // Text Frame (Message 1)
- mux = capture.getFrames().pop();
- String prefix = "MuxFrame[0]";
- Assert.assertThat(prefix + ".channelId",mux.getChannelId(),is(1L));
- Assert.assertThat(prefix + ".fin",mux.isFin(),is(false));
- Assert.assertThat(prefix + ".rsv1",mux.isRsv1(),is(false));
- Assert.assertThat(prefix + ".rsv2",mux.isRsv2(),is(false));
- Assert.assertThat(prefix + ".rsv3",mux.isRsv3(),is(false));
- Assert.assertThat(prefix + ".masked",mux.isMasked(),is(false));
- Assert.assertThat(prefix + ".continuation",mux.isContinuation(),is(false));
- Assert.assertThat(prefix + ".opcode",mux.getOpCode(),is(OpCode.TEXT));
-
- String payload = mux.getPayloadAsUTF8();
- Assert.assertThat(prefix + ".payload/text",payload,is("Hello"));
-
- // Text Frame (Message 2)
- mux = capture.getFrames().pop();
- prefix = "MuxFrame[1]";
- Assert.assertThat(prefix + ".channelId",mux.getChannelId(),is(2L));
- Assert.assertThat(prefix + ".fin",mux.isFin(),is(true));
- Assert.assertThat(prefix + ".rsv1",mux.isRsv1(),is(false));
- Assert.assertThat(prefix + ".rsv2",mux.isRsv2(),is(false));
- Assert.assertThat(prefix + ".rsv3",mux.isRsv3(),is(false));
- Assert.assertThat(prefix + ".masked",mux.isMasked(),is(false));
- Assert.assertThat(prefix + ".continuation",mux.isContinuation(),is(false));
- Assert.assertThat(prefix + ".opcode",mux.getOpCode(),is(OpCode.TEXT));
-
- payload = mux.getPayloadAsUTF8();
- Assert.assertThat(prefix + ".payload/text",payload,is("bye"));
-
- // Continuation Frame (Message 1)
- mux = capture.getFrames().pop();
- prefix = "MuxFrame[2]";
- Assert.assertThat(prefix + ".channelId",mux.getChannelId(),is(1L));
- Assert.assertThat(prefix + ".fin",mux.isFin(),is(true));
- Assert.assertThat(prefix + ".rsv1",mux.isRsv1(),is(false));
- Assert.assertThat(prefix + ".rsv2",mux.isRsv2(),is(false));
- Assert.assertThat(prefix + ".rsv3",mux.isRsv3(),is(false));
- Assert.assertThat(prefix + ".masked",mux.isMasked(),is(false));
- Assert.assertThat(prefix + ".continuation",mux.isContinuation(),is(true));
- Assert.assertThat(prefix + ".opcode",mux.getOpCode(),is(OpCode.TEXT));
-
- payload = mux.getPayloadAsUTF8();
- Assert.assertThat(prefix + ".payload/text",payload,is(" world"));
- }
-
- private byte[] toByteArray(String... parts) throws IOException
- {
- ByteArrayOutputStream out = new ByteArrayOutputStream();
- for(String part: parts) {
- if (isHexOnly(part))
- {
- String hexonly = part.replaceAll("\\s*0x","");
- out.write(TypeUtil.fromHexString(hexonly));
- }
- else
- {
- out.write(part.getBytes(StringUtil.__UTF8_CHARSET));
- }
- }
- return out.toByteArray();
- }
-}
diff --git a/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/extensions/mux/MuxParserRead139Size_BadEncodingTest.java b/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/extensions/mux/MuxParserRead139Size_BadEncodingTest.java
deleted file mode 100644
index 15c2e06..0000000
--- a/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/extensions/mux/MuxParserRead139Size_BadEncodingTest.java
+++ /dev/null
@@ -1,106 +0,0 @@
-//
-// ========================================================================
-// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
-// ------------------------------------------------------------------------
-// All rights reserved. This program and the accompanying materials
-// are made available under the terms of the Eclipse Public License v1.0
-// and Apache License v2.0 which accompanies this distribution.
-//
-// The Eclipse Public License is available at
-// http://www.eclipse.org/legal/epl-v10.html
-//
-// The Apache License v2.0 is available at
-// http://www.opensource.org/licenses/apache2.0.php
-//
-// You may elect to redistribute this code under either of these licenses.
-// ========================================================================
-//
-
-package org.eclipse.jetty.websocket.common.extensions.mux;
-
-import static org.hamcrest.Matchers.*;
-
-import java.nio.ByteBuffer;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.List;
-
-import org.eclipse.jetty.util.TypeUtil;
-import org.eclipse.jetty.websocket.common.extensions.mux.MuxException;
-import org.eclipse.jetty.websocket.common.extensions.mux.MuxParser;
-import org.junit.Assert;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.rules.TestName;
-import org.junit.runner.RunWith;
-import org.junit.runners.Parameterized;
-import org.junit.runners.Parameterized.Parameters;
-
-/**
- * Tests for bad 1/3/9 size encoding.
- */
-@RunWith(Parameterized.class)
-public class MuxParserRead139Size_BadEncodingTest
-{
- private static MuxParser parser = new MuxParser();
-
- @Parameters
- public static Collection<Object[]> data()
- {
- // Various bad 1/3/9 encodings
- // Violating "minimal number of bytes necessary" rule.
- List<Object[]> data = new ArrayList<>();
-
- // @formatter:off
- // - 1 byte tests
- // all known 1 byte tests are valid
-
- // - 3 byte tests
- data.add(new Object[]{"7E0000"});
- data.add(new Object[]{"7E0001"});
- data.add(new Object[]{"7E0012"});
- data.add(new Object[]{"7E0059"});
- // extra bytes (not related to 1/3/9 size)
- data.add(new Object[]{"7E0012345678"});
-
- // - 9 byte tests
- data.add(new Object[]{"7F0000000000000000"});
- data.add(new Object[]{"7F0000000000000001"});
- data.add(new Object[]{"7F0000000000000012"});
- data.add(new Object[]{"7F0000000000001234"});
- data.add(new Object[]{"7F000000000000FFFF"});
-
- // @formatter:on
- return data;
- }
-
- @Rule
- public TestName testname = new TestName();
-
- private String rawhex;
- private byte buf[];
-
- public MuxParserRead139Size_BadEncodingTest(String rawhex)
- {
- this.rawhex = rawhex;
- this.buf = TypeUtil.fromHexString(rawhex);
- }
-
- @Test
- public void testRead139EncodedSize()
- {
- System.err.printf("Running %s.%s - hex: %s%n",this.getClass().getName(),testname.getMethodName(),rawhex);
- ByteBuffer bbuf = ByteBuffer.wrap(buf);
- try
- {
- parser.read139EncodedSize(bbuf);
- // unexpected path
- Assert.fail("Should have failed with an invalid parse");
- }
- catch (MuxException e)
- {
- // expected path
- Assert.assertThat(e.getMessage(),containsString("Invalid 1/3/9 length"));
- }
- }
-}
diff --git a/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/extensions/mux/MuxParserRead139Size_GoodTest.java b/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/extensions/mux/MuxParserRead139Size_GoodTest.java
deleted file mode 100644
index eb450fc..0000000
--- a/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/extensions/mux/MuxParserRead139Size_GoodTest.java
+++ /dev/null
@@ -1,100 +0,0 @@
-//
-// ========================================================================
-// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
-// ------------------------------------------------------------------------
-// All rights reserved. This program and the accompanying materials
-// are made available under the terms of the Eclipse Public License v1.0
-// and Apache License v2.0 which accompanies this distribution.
-//
-// The Eclipse Public License is available at
-// http://www.eclipse.org/legal/epl-v10.html
-//
-// The Apache License v2.0 is available at
-// http://www.opensource.org/licenses/apache2.0.php
-//
-// You may elect to redistribute this code under either of these licenses.
-// ========================================================================
-//
-
-package org.eclipse.jetty.websocket.common.extensions.mux;
-
-import static org.hamcrest.Matchers.*;
-
-import java.nio.ByteBuffer;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.List;
-
-import org.eclipse.jetty.util.TypeUtil;
-import org.eclipse.jetty.websocket.common.extensions.mux.MuxParser;
-import org.junit.Assert;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.rules.TestName;
-import org.junit.runner.RunWith;
-import org.junit.runners.Parameterized;
-import org.junit.runners.Parameterized.Parameters;
-
-@RunWith(Parameterized.class)
-public class MuxParserRead139Size_GoodTest
-{
- private static MuxParser parser = new MuxParser();
-
- @Parameters
- public static Collection<Object[]> data()
- {
- // Various good 1/3/9 encodings
- List<Object[]> data = new ArrayList<>();
-
- // @formatter:off
- // - 1 byte tests
- data.add(new Object[]{"00", 0L});
- data.add(new Object[]{"01", 1L});
- data.add(new Object[]{"02", 2L});
- data.add(new Object[]{"37", 55L});
- data.add(new Object[]{"7D", 125L});
- // extra bytes (not related to 1/3/9 size)
- data.add(new Object[]{"37FF", 55L});
- data.add(new Object[]{"0123456789", 0x01L});
-
- // - 3 byte tests
- data.add(new Object[]{"7E0080", 0x00_80L});
- data.add(new Object[]{"7E00AB", 0x00_ABL});
- data.add(new Object[]{"7E00FF", 0x00_FFL});
- data.add(new Object[]{"7E3FFF", 0x3F_FFL});
- // extra bytes (not related to 1/3/9 size)
- data.add(new Object[]{"7E0123456789", 0x01_23L});
-
- // - 9 byte tests
- data.add(new Object[]{"7F000000000001FFFF", 0x00_00_01_FF_FFL});
- data.add(new Object[]{"7F0000000000FFFFFF", 0x00_00_FF_FF_FFL});
- data.add(new Object[]{"7F00000000FFFFFFFF", 0x00_FF_FF_FF_FFL});
- data.add(new Object[]{"7F000000FFFFFFFFFF", 0xFF_FF_FF_FF_FFL});
-
- // @formatter:on
- return data;
- }
-
- @Rule
- public TestName testname = new TestName();
-
- private String rawhex;
- private byte buf[];
- private long expected;
-
- public MuxParserRead139Size_GoodTest(String rawhex, long expected)
- {
- this.rawhex = rawhex;
- this.buf = TypeUtil.fromHexString(rawhex);
- this.expected = expected;
- }
-
- @Test
- public void testRead139EncodedSize()
- {
- System.err.printf("Running %s.%s - hex: %s%n",this.getClass().getName(),testname.getMethodName(),rawhex);
- ByteBuffer bbuf = ByteBuffer.wrap(buf);
- long actual = parser.read139EncodedSize(bbuf);
- Assert.assertThat("1/3/9 size from buffer [" + rawhex + "]",actual,is(expected));
- }
-}
diff --git a/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/extensions/mux/MuxParserReadChannelId_BadEncodingTest.java b/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/extensions/mux/MuxParserReadChannelId_BadEncodingTest.java
deleted file mode 100644
index 57c17a3..0000000
--- a/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/extensions/mux/MuxParserReadChannelId_BadEncodingTest.java
+++ /dev/null
@@ -1,108 +0,0 @@
-//
-// ========================================================================
-// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
-// ------------------------------------------------------------------------
-// All rights reserved. This program and the accompanying materials
-// are made available under the terms of the Eclipse Public License v1.0
-// and Apache License v2.0 which accompanies this distribution.
-//
-// The Eclipse Public License is available at
-// http://www.eclipse.org/legal/epl-v10.html
-//
-// The Apache License v2.0 is available at
-// http://www.opensource.org/licenses/apache2.0.php
-//
-// You may elect to redistribute this code under either of these licenses.
-// ========================================================================
-//
-
-package org.eclipse.jetty.websocket.common.extensions.mux;
-
-import static org.hamcrest.Matchers.*;
-
-import java.nio.ByteBuffer;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.List;
-
-import org.eclipse.jetty.util.TypeUtil;
-import org.eclipse.jetty.websocket.common.extensions.mux.MuxException;
-import org.eclipse.jetty.websocket.common.extensions.mux.MuxParser;
-import org.junit.Assert;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.rules.TestName;
-import org.junit.runner.RunWith;
-import org.junit.runners.Parameterized;
-import org.junit.runners.Parameterized.Parameters;
-
-/**
- * Tests of Invalid ChannelID parsing
- */
-@RunWith(Parameterized.class)
-public class MuxParserReadChannelId_BadEncodingTest
-{
- private static MuxParser parser = new MuxParser();
-
- @Parameters
- public static Collection<Object[]> data()
- {
- // Various Invalid Encoded Channel IDs.
- // Violating "minimal number of bytes necessary" rule.
- List<Object[]> data = new ArrayList<>();
-
- // @formatter:off
- // - 1 byte tests
- // all known 1 byte tests are valid
-
- // - 2 byte tests
- data.add(new Object[]{"8000"});
- data.add(new Object[]{"8001"});
- data.add(new Object[]{"807F"});
- // extra bytes (not related to channelId)
- data.add(new Object[]{"8023456789"});
-
- // - 3 byte tests
- data.add(new Object[]{"C00000"});
- data.add(new Object[]{"C01234"});
- data.add(new Object[]{"C03FFF"});
-
- // - 3 byte tests
- data.add(new Object[]{"E0000000"});
- data.add(new Object[]{"E0000001"});
- data.add(new Object[]{"E01FFFFF"});
-
- // @formatter:on
- return data;
- }
-
- @Rule
- public TestName testname = new TestName();
-
- private String rawhex;
- private byte buf[];
-
- public MuxParserReadChannelId_BadEncodingTest(String rawhex)
- {
- this.rawhex = rawhex;
- this.buf = TypeUtil.fromHexString(rawhex);
- }
-
- @Test
- public void testBadEncoding()
- {
- System.err.printf("Running %s.%s - hex: %s%n",this.getClass().getName(),testname.getMethodName(),rawhex);
- ByteBuffer bbuf = ByteBuffer.wrap(buf);
- try
- {
- parser.readChannelId(bbuf);
- // unexpected path
- Assert.fail("Should have failed with an invalid parse");
- }
- catch (MuxException e)
- {
- // expected path
- Assert.assertThat(e.getMessage(),containsString("Invalid Channel ID"));
- }
- }
-}
diff --git a/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/extensions/mux/MuxParserReadChannelId_GoodTest.java b/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/extensions/mux/MuxParserReadChannelId_GoodTest.java
deleted file mode 100644
index 5a1ad5e..0000000
--- a/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/extensions/mux/MuxParserReadChannelId_GoodTest.java
+++ /dev/null
@@ -1,107 +0,0 @@
-//
-// ========================================================================
-// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
-// ------------------------------------------------------------------------
-// All rights reserved. This program and the accompanying materials
-// are made available under the terms of the Eclipse Public License v1.0
-// and Apache License v2.0 which accompanies this distribution.
-//
-// The Eclipse Public License is available at
-// http://www.eclipse.org/legal/epl-v10.html
-//
-// The Apache License v2.0 is available at
-// http://www.opensource.org/licenses/apache2.0.php
-//
-// You may elect to redistribute this code under either of these licenses.
-// ========================================================================
-//
-
-package org.eclipse.jetty.websocket.common.extensions.mux;
-
-import static org.hamcrest.Matchers.*;
-
-import java.nio.ByteBuffer;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.List;
-
-import org.eclipse.jetty.util.TypeUtil;
-import org.eclipse.jetty.websocket.common.extensions.mux.MuxParser;
-import org.junit.Assert;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.rules.TestName;
-import org.junit.runner.RunWith;
-import org.junit.runners.Parameterized;
-import org.junit.runners.Parameterized.Parameters;
-
-/**
- * Tests of valid ChannelID parsing
- */
-@RunWith(Parameterized.class)
-public class MuxParserReadChannelId_GoodTest
-{
- private static MuxParser parser = new MuxParser();
-
- @Parameters
- public static Collection<Object[]> data()
- {
- // Various good Channel IDs
- List<Object[]> data = new ArrayList<>();
-
- // @formatter:off
- // - 1 byte tests
- data.add(new Object[]{"00", 0L});
- data.add(new Object[]{"01", 1L});
- data.add(new Object[]{"02", 2L});
- data.add(new Object[]{"7F", 127L});
- // extra bytes (not related to channelId)
- data.add(new Object[]{"37FF", 55L});
- data.add(new Object[]{"0123456789", 0x01L});
-
- // - 2 byte tests
- data.add(new Object[]{"8080", 0x00_80L});
- data.add(new Object[]{"80FF", 0x00_FFL});
- data.add(new Object[]{"BFFF", 0x3F_FFL});
- // extra bytes (not related to channelId)
- data.add(new Object[]{"8123456789", 0x01_23L});
-
- // - 3 byte tests
- data.add(new Object[]{"C0FFFF", 0x00_FF_FFL});
- data.add(new Object[]{"DFFFFF", 0x1F_FF_FFL});
- // extra bytes (not related to channelId)
- data.add(new Object[]{"C123456789", 0x01_23_45L});
-
- // - 3 byte tests
- data.add(new Object[]{"E0FFFFFF", 0x00_FF_FF_FFL});
- data.add(new Object[]{"FFFFFFFF", 0x1F_FF_FF_FFL});
- // extra bytes (not related to channelId)
- data.add(new Object[]{"E123456789", 0x01_23_45_67L});
-
- // @formatter:on
- return data;
- }
-
- @Rule
- public TestName testname = new TestName();
-
- private String rawhex;
- private byte buf[];
- private long expected;
-
- public MuxParserReadChannelId_GoodTest(String rawhex, long expected)
- {
- this.rawhex = rawhex;
- this.buf = TypeUtil.fromHexString(rawhex);
- this.expected = expected;
- }
-
- @Test
- public void testReadChannelId()
- {
- System.err.printf("Running %s.%s - hex: %s%n",this.getClass().getName(),testname.getMethodName(),rawhex);
- ByteBuffer bbuf = ByteBuffer.wrap(buf);
- long actual = parser.readChannelId(bbuf);
- Assert.assertThat("Channel ID from buffer [" + rawhex + "]",actual,is(expected));
- }
-}
diff --git a/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/extensions/mux/add/DummyMuxAddClient.java b/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/extensions/mux/add/DummyMuxAddClient.java
deleted file mode 100644
index 81ccc3d..0000000
--- a/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/extensions/mux/add/DummyMuxAddClient.java
+++ /dev/null
@@ -1,32 +0,0 @@
-//
-// ========================================================================
-// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
-// ------------------------------------------------------------------------
-// All rights reserved. This program and the accompanying materials
-// are made available under the terms of the Eclipse Public License v1.0
-// and Apache License v2.0 which accompanies this distribution.
-//
-// The Eclipse Public License is available at
-// http://www.eclipse.org/legal/epl-v10.html
-//
-// The Apache License v2.0 is available at
-// http://www.opensource.org/licenses/apache2.0.php
-//
-// You may elect to redistribute this code under either of these licenses.
-// ========================================================================
-//
-
-package org.eclipse.jetty.websocket.common.extensions.mux.add;
-
-import org.eclipse.jetty.websocket.common.WebSocketSession;
-import org.eclipse.jetty.websocket.common.extensions.mux.op.MuxAddChannelResponse;
-
-public class DummyMuxAddClient implements MuxAddClient
-{
- @Override
- public WebSocketSession createSession(MuxAddChannelResponse response)
- {
- // TODO Auto-generated method stub
- return null;
- }
-}
diff --git a/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/extensions/mux/add/DummyMuxAddServer.java b/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/extensions/mux/add/DummyMuxAddServer.java
deleted file mode 100644
index 49b4fcb..0000000
--- a/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/extensions/mux/add/DummyMuxAddServer.java
+++ /dev/null
@@ -1,98 +0,0 @@
-//
-// ========================================================================
-// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
-// ------------------------------------------------------------------------
-// All rights reserved. This program and the accompanying materials
-// are made available under the terms of the Eclipse Public License v1.0
-// and Apache License v2.0 which accompanies this distribution.
-//
-// The Eclipse Public License is available at
-// http://www.eclipse.org/legal/epl-v10.html
-//
-// The Apache License v2.0 is available at
-// http://www.opensource.org/licenses/apache2.0.php
-//
-// You may elect to redistribute this code under either of these licenses.
-// ========================================================================
-//
-
-package org.eclipse.jetty.websocket.common.extensions.mux.add;
-
-import java.io.IOException;
-
-import org.eclipse.jetty.util.log.Log;
-import org.eclipse.jetty.util.log.Logger;
-import org.eclipse.jetty.websocket.api.UpgradeRequest;
-import org.eclipse.jetty.websocket.api.UpgradeResponse;
-import org.eclipse.jetty.websocket.api.WebSocketPolicy;
-import org.eclipse.jetty.websocket.common.WebSocketSession;
-import org.eclipse.jetty.websocket.common.events.EventDriver;
-import org.eclipse.jetty.websocket.common.events.EventDriverFactory;
-import org.eclipse.jetty.websocket.common.extensions.mux.MuxChannel;
-import org.eclipse.jetty.websocket.common.extensions.mux.MuxException;
-import org.eclipse.jetty.websocket.common.extensions.mux.Muxer;
-import org.eclipse.jetty.websocket.common.extensions.mux.op.MuxAddChannelResponse;
-
-import examples.echo.AdapterEchoSocket;
-
-/**
- * Dummy impl of MuxAddServer
- */
-public class DummyMuxAddServer implements MuxAddServer
-{
- @SuppressWarnings("unused")
- private static final Logger LOG = Log.getLogger(DummyMuxAddServer.class);
- private AdapterEchoSocket echo;
- private WebSocketPolicy policy;
- private EventDriverFactory eventDriverFactory;
-
- public DummyMuxAddServer()
- {
- this.policy = WebSocketPolicy.newServerPolicy();
- this.eventDriverFactory = new EventDriverFactory(policy);
- this.echo = new AdapterEchoSocket();
- }
-
- @Override
- public UpgradeRequest getPhysicalHandshakeRequest()
- {
- // TODO Auto-generated method stub
- return null;
- }
-
- @Override
- public UpgradeResponse getPhysicalHandshakeResponse()
- {
- // TODO Auto-generated method stub
- return null;
- }
-
- @Override
- public void handshake(Muxer muxer, MuxChannel channel, UpgradeRequest request) throws MuxException, IOException
- {
- StringBuilder response = new StringBuilder();
- response.append("HTTP/1.1 101 Switching Protocols\r\n");
- response.append("Connection: upgrade\r\n");
- // not meaningful (per Draft 08) hresp.append("Upgrade: websocket\r\n");
- // not meaningful (per Draft 08) hresp.append("Sec-WebSocket-Accept: Kgo85/8KVE8YPONSeyhgL3GwqhI=\r\n");
- response.append("\r\n");
-
- EventDriver websocket = this.eventDriverFactory.wrap(echo);
- WebSocketSession session = new WebSocketSession(request.getRequestURI(),websocket,channel);
- UpgradeResponse uresponse = new UpgradeResponse();
- uresponse.setAcceptedSubProtocol("echo");
- session.setUpgradeResponse(uresponse);
- channel.setSession(session);
- channel.setSubProtocol("echo");
- channel.onOpen();
- session.open();
-
- MuxAddChannelResponse addChannelResponse = new MuxAddChannelResponse();
- addChannelResponse.setChannelId(channel.getChannelId());
- addChannelResponse.setEncoding(MuxAddChannelResponse.IDENTITY_ENCODING);
- addChannelResponse.setFailed(false);
- addChannelResponse.setHandshake(response.toString());
-
- muxer.output(addChannelResponse);
- }
-}
diff --git a/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/extensions/mux/add/MuxerAddClientTest.java b/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/extensions/mux/add/MuxerAddClientTest.java
deleted file mode 100644
index 2b87ff2..0000000
--- a/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/extensions/mux/add/MuxerAddClientTest.java
+++ /dev/null
@@ -1,105 +0,0 @@
-//
-// ========================================================================
-// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
-// ------------------------------------------------------------------------
-// All rights reserved. This program and the accompanying materials
-// are made available under the terms of the Eclipse Public License v1.0
-// and Apache License v2.0 which accompanies this distribution.
-//
-// The Eclipse Public License is available at
-// http://www.eclipse.org/legal/epl-v10.html
-//
-// The Apache License v2.0 is available at
-// http://www.opensource.org/licenses/apache2.0.php
-//
-// You may elect to redistribute this code under either of these licenses.
-// ========================================================================
-//
-
-package org.eclipse.jetty.websocket.common.extensions.mux.add;
-
-import org.eclipse.jetty.websocket.api.WebSocketPolicy;
-import org.eclipse.jetty.websocket.common.extensions.mux.MuxChannel;
-import org.eclipse.jetty.websocket.common.extensions.mux.MuxDecoder;
-import org.eclipse.jetty.websocket.common.extensions.mux.MuxEncoder;
-import org.eclipse.jetty.websocket.common.extensions.mux.MuxOp;
-import org.eclipse.jetty.websocket.common.extensions.mux.Muxer;
-import org.eclipse.jetty.websocket.common.extensions.mux.op.MuxAddChannelRequest;
-import org.eclipse.jetty.websocket.common.extensions.mux.op.MuxAddChannelResponse;
-import org.eclipse.jetty.websocket.common.io.LocalWebSocketConnection;
-import org.junit.Ignore;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.rules.TestName;
-
-public class MuxerAddClientTest
-{
- @Rule
- public TestName testname = new TestName();
-
- @Test
- @Ignore("Interrim, not functional yet")
- public void testAddChannel_Client() throws Exception
- {
- // Client side physical socket
- LocalWebSocketConnection physical = new LocalWebSocketConnection(testname);
- physical.setPolicy(WebSocketPolicy.newClientPolicy());
- physical.onOpen();
-
- // Server Reader
- MuxDecoder serverRead = new MuxDecoder();
-
- // Client side Muxer
- Muxer muxer = new Muxer(physical);
- DummyMuxAddClient addClient = new DummyMuxAddClient();
- muxer.setAddClient(addClient);
- muxer.setOutgoingFramesHandler(serverRead);
-
- // Server Writer
- MuxEncoder serverWrite = MuxEncoder.toIncoming(physical);
-
- // Build AddChannelRequest handshake data
- StringBuilder request = new StringBuilder();
- request.append("GET /echo HTTP/1.1\r\n");
- request.append("Host: localhost\r\n");
- request.append("Upgrade: websocket\r\n");
- request.append("Connection: Upgrade\r\n");
- request.append("Sec-WebSocket-Key: ZDTIRU5vU9xOfkg8JAgN3A==\r\n");
- request.append("Sec-WebSocket-Version: 13\r\n");
- request.append("\r\n");
-
- // Build AddChannelRequest
- long channelId = 1L;
- MuxAddChannelRequest req = new MuxAddChannelRequest();
- req.setChannelId(channelId);
- req.setEncoding((byte)0);
- req.setHandshake(request.toString());
-
- // Have client sent AddChannelRequest
- MuxChannel channel = muxer.getChannel(channelId,true);
- MuxEncoder clientWrite = MuxEncoder.toOutgoing(channel);
- clientWrite.op(req);
-
- // Have server read request
- serverRead.assertHasOp(MuxOp.ADD_CHANNEL_REQUEST,1);
-
- // prepare AddChannelResponse
- StringBuilder response = new StringBuilder();
- response.append("HTTP/1.1 101 Switching Protocols\r\n");
- response.append("Upgrade: websocket\r\n");
- response.append("Connection: upgrade\r\n");
- response.append("Sec-WebSocket-Accept: Kgo85/8KVE8YPONSeyhgL3GwqhI=\r\n");
- response.append("\r\n");
-
- MuxAddChannelResponse resp = new MuxAddChannelResponse();
- resp.setChannelId(channelId);
- resp.setFailed(false);
- resp.setEncoding((byte)0);
- resp.setHandshake(resp.toString());
-
- // Server writes add channel response
- serverWrite.op(resp);
-
- // TODO: handle the upgrade on client side.
- }
-}
diff --git a/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/extensions/mux/add/MuxerAddServerTest.java b/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/extensions/mux/add/MuxerAddServerTest.java
deleted file mode 100644
index 0dfffa1..0000000
--- a/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/extensions/mux/add/MuxerAddServerTest.java
+++ /dev/null
@@ -1,105 +0,0 @@
-//
-// ========================================================================
-// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
-// ------------------------------------------------------------------------
-// All rights reserved. This program and the accompanying materials
-// are made available under the terms of the Eclipse Public License v1.0
-// and Apache License v2.0 which accompanies this distribution.
-//
-// The Eclipse Public License is available at
-// http://www.eclipse.org/legal/epl-v10.html
-//
-// The Apache License v2.0 is available at
-// http://www.opensource.org/licenses/apache2.0.php
-//
-// You may elect to redistribute this code under either of these licenses.
-// ========================================================================
-//
-
-package org.eclipse.jetty.websocket.common.extensions.mux.add;
-
-import static org.hamcrest.Matchers.*;
-
-import org.eclipse.jetty.websocket.api.WebSocketPolicy;
-import org.eclipse.jetty.websocket.common.OpCode;
-import org.eclipse.jetty.websocket.common.WebSocketFrame;
-import org.eclipse.jetty.websocket.common.extensions.mux.MuxDecoder;
-import org.eclipse.jetty.websocket.common.extensions.mux.MuxEncoder;
-import org.eclipse.jetty.websocket.common.extensions.mux.MuxOp;
-import org.eclipse.jetty.websocket.common.extensions.mux.Muxer;
-import org.eclipse.jetty.websocket.common.extensions.mux.op.MuxAddChannelRequest;
-import org.eclipse.jetty.websocket.common.extensions.mux.op.MuxAddChannelResponse;
-import org.eclipse.jetty.websocket.common.io.LocalWebSocketConnection;
-import org.junit.Assert;
-import org.junit.Ignore;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.rules.TestName;
-
-public class MuxerAddServerTest
-{
- @Rule
- public TestName testname = new TestName();
-
- @Test
- @Ignore("Interrim, not functional yet")
- public void testAddChannel_Server() throws Exception
- {
- // Server side physical connection
- LocalWebSocketConnection physical = new LocalWebSocketConnection(testname);
- physical.setPolicy(WebSocketPolicy.newServerPolicy());
- physical.onOpen();
-
- // Client reader
- MuxDecoder clientRead = new MuxDecoder();
-
- // Build up server side muxer.
- Muxer muxer = new Muxer(physical);
- DummyMuxAddServer addServer = new DummyMuxAddServer();
- muxer.setAddServer(addServer);
- muxer.setOutgoingFramesHandler(clientRead);
-
- // Wire up physical connection to forward incoming frames to muxer
- physical.setNextIncomingFrames(muxer);
-
- // Client simulator
- // Can inject mux encapsulated frames into physical connection as if from
- // physical connection.
- MuxEncoder clientWrite = MuxEncoder.toIncoming(physical);
-
- // Build AddChannelRequest handshake data
- StringBuilder request = new StringBuilder();
- request.append("GET /echo HTTP/1.1\r\n");
- request.append("Host: localhost\r\n");
- request.append("Upgrade: websocket\r\n");
- request.append("Connection: Upgrade\r\n");
- request.append("Sec-WebSocket-Key: ZDTIRU5vU9xOfkg8JAgN3A==\r\n");
- request.append("Sec-WebSocket-Version: 13\r\n");
- request.append("\r\n");
-
- // Build AddChannelRequest
- MuxAddChannelRequest req = new MuxAddChannelRequest();
- req.setChannelId(1);
- req.setEncoding((byte)0);
- req.setHandshake(request.toString());
-
- // Have client sent AddChannelRequest
- clientWrite.op(req);
-
- // Make sure client got AddChannelResponse
- clientRead.assertHasOp(MuxOp.ADD_CHANNEL_RESPONSE,1);
- MuxAddChannelResponse response = (MuxAddChannelResponse)clientRead.getOps().pop();
- Assert.assertThat("AddChannelResponse.channelId",response.getChannelId(),is(1L));
- Assert.assertThat("AddChannelResponse.failed",response.isFailed(),is(false));
- Assert.assertThat("AddChannelResponse.handshake",response.getHandshake(),notNullValue());
- Assert.assertThat("AddChannelResponse.handshakeSize",response.getHandshakeSize(),is(57L));
-
- clientRead.reset();
-
- // Send simple echo request
- clientWrite.frame(1,WebSocketFrame.text("Hello World"));
-
- // Test for echo response (is there a user echo websocket connected to the sub-channel?)
- clientRead.assertHasFrame(OpCode.TEXT,1L,1);
- }
-}
diff --git a/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/io/LocalWebSocketConnection.java b/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/io/LocalWebSocketConnection.java
index 92e5e77..9494c3b 100644
--- a/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/io/LocalWebSocketConnection.java
+++ b/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/io/LocalWebSocketConnection.java
@@ -19,12 +19,15 @@
package org.eclipse.jetty.websocket.common.io;
import java.net.InetSocketAddress;
+import java.util.concurrent.Executor;
+import org.eclipse.jetty.io.ByteBufferPool;
+import org.eclipse.jetty.io.MappedByteBufferPool;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;
+import org.eclipse.jetty.util.thread.ExecutorThreadPool;
import org.eclipse.jetty.websocket.api.StatusCode;
import org.eclipse.jetty.websocket.api.SuspendToken;
-import org.eclipse.jetty.websocket.api.WebSocketException;
import org.eclipse.jetty.websocket.api.WebSocketPolicy;
import org.eclipse.jetty.websocket.api.WriteCallback;
import org.eclipse.jetty.websocket.api.extensions.Frame;
@@ -40,6 +43,8 @@
{
private static final Logger LOG = Log.getLogger(LocalWebSocketConnection.class);
private final String id;
+ private final ByteBufferPool bufferPool;
+ private final Executor executor;
private WebSocketPolicy policy = WebSocketPolicy.newServerPolicy();
private IncomingFrames incoming;
private IOState ioState = new IOState();
@@ -52,13 +57,20 @@
public LocalWebSocketConnection(String id)
{
this.id = id;
+ this.bufferPool = new MappedByteBufferPool();
+ this.executor = new ExecutorThreadPool();
this.ioState.addListener(this);
}
public LocalWebSocketConnection(TestName testname)
{
- this.id = testname.getMethodName();
- this.ioState.addListener(this);
+ this(testname.getMethodName());
+ }
+
+ @Override
+ public Executor getExecutor()
+ {
+ return executor;
}
@Override
@@ -75,12 +87,30 @@
ioState.onCloseLocal(close);
}
+ public void connect()
+ {
+ LOG.debug("connect()");
+ ioState.onConnected();
+ }
+
@Override
public void disconnect()
{
LOG.debug("disconnect()");
}
+ @Override
+ public ByteBufferPool getBufferPool()
+ {
+ return this.bufferPool;
+ }
+
+ @Override
+ public long getIdleTimeout()
+ {
+ return 0;
+ }
+
public IncomingFrames getIncoming()
{
return incoming;
@@ -101,7 +131,6 @@
@Override
public long getMaxIdleTimeout()
{
- // TODO Auto-generated method stub
return 0;
}
@@ -124,7 +153,7 @@
}
@Override
- public void incomingError(WebSocketException e)
+ public void incomingError(Throwable e)
{
incoming.incomingError(e);
}
@@ -169,8 +198,9 @@
}
}
- public void onOpen() {
- LOG.debug("onOpen()");
+ public void open()
+ {
+ LOG.debug("open()");
ioState.onOpened();
}
@@ -187,8 +217,6 @@
@Override
public void setMaxIdleTimeout(long ms)
{
- // TODO Auto-generated method stub
-
}
@Override
diff --git a/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/io/LocalWebSocketSession.java b/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/io/LocalWebSocketSession.java
index f1c123c..d19eed0 100644
--- a/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/io/LocalWebSocketSession.java
+++ b/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/io/LocalWebSocketSession.java
@@ -29,30 +29,27 @@
{
private String id;
private OutgoingFramesCapture outgoingCapture;
- private LocalWebSocketConnection lwsconnection;
public LocalWebSocketSession(TestName testname, EventDriver driver)
{
super(URI.create("ws://localhost/LocalWebSocketSesssion/" + testname.getMethodName()),driver,new LocalWebSocketConnection(testname));
this.id = testname.getMethodName();
- this.lwsconnection = (LocalWebSocketConnection)getConnection();
outgoingCapture = new OutgoingFramesCapture();
setOutgoingHandler(outgoingCapture);
}
+ @Override
+ public void dispatch(Runnable runnable)
+ {
+ runnable.run();
+ }
+
public OutgoingFramesCapture getOutgoingCapture()
{
return outgoingCapture;
}
@Override
- public void open()
- {
- lwsconnection.onOpen();
- super.open();
- }
-
- @Override
public String toString()
{
return String.format("%s[%s]",LocalWebSocketSession.class.getSimpleName(),id);
diff --git a/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/io/http/HttpResponseParseCapture.java b/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/io/http/HttpResponseParseCapture.java
index a7973e6..168a633 100644
--- a/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/io/http/HttpResponseParseCapture.java
+++ b/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/io/http/HttpResponseParseCapture.java
@@ -19,26 +19,25 @@
package org.eclipse.jetty.websocket.common.io.http;
import java.nio.ByteBuffer;
-import java.util.HashMap;
-import java.util.Locale;
import java.util.Map;
+import java.util.TreeMap;
public class HttpResponseParseCapture implements HttpResponseHeaderParseListener
{
private int statusCode;
private String statusReason;
- private Map<String, String> headers = new HashMap<>();
+ private Map<String, String> headers = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
private ByteBuffer remainingBuffer;
@Override
public void addHeader(String name, String value)
{
- headers.put(name.toLowerCase(Locale.ENGLISH),value);
+ headers.put(name,value);
}
public String getHeader(String name)
{
- return headers.get(name.toLowerCase(Locale.ENGLISH));
+ return headers.get(name);
}
public ByteBuffer getRemainingBuffer()
diff --git a/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/message/DummySocket.java b/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/message/DummySocket.java
new file mode 100644
index 0000000..20e7208
--- /dev/null
+++ b/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/message/DummySocket.java
@@ -0,0 +1,29 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.common.message;
+
+import org.eclipse.jetty.websocket.api.WebSocketAdapter;
+
+/**
+ * Do nothing Dummy Socket, used in testing.
+ */
+public class DummySocket extends WebSocketAdapter
+{
+ /* intentionally empty */
+}
diff --git a/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/message/MessageDebug.java b/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/message/MessageDebug.java
new file mode 100644
index 0000000..72436ca
--- /dev/null
+++ b/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/message/MessageDebug.java
@@ -0,0 +1,60 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.common.message;
+
+import java.nio.ByteBuffer;
+
+public class MessageDebug
+{
+ public static String toDetailHint(byte[] data, int offset, int len)
+ {
+ StringBuilder buf = new StringBuilder();
+ ByteBuffer buffer = ByteBuffer.wrap(data,offset,len);
+
+ buf.append("byte[").append(data.length);
+ buf.append("](o=").append(offset);
+ buf.append(",len=").append(len);
+
+ buf.append(")<<<");
+ for (int i = buffer.position(); i < buffer.limit(); i++)
+ {
+ char c = (char)buffer.get(i);
+ if ((c >= ' ') && (c <= 127))
+ {
+ buf.append(c);
+ }
+ else if ((c == '\r') || (c == '\n'))
+ {
+ buf.append('|');
+ }
+ else
+ {
+ buf.append('\ufffd');
+ }
+ if ((i == (buffer.position() + 16)) && (buffer.limit() > (buffer.position() + 32)))
+ {
+ buf.append("...");
+ i = buffer.limit() - 16;
+ }
+ }
+ buf.append(">>>");
+
+ return buf.toString();
+ }
+}
diff --git a/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/message/MessageInputStreamTest.java b/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/message/MessageInputStreamTest.java
new file mode 100644
index 0000000..a3334b3
--- /dev/null
+++ b/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/message/MessageInputStreamTest.java
@@ -0,0 +1,187 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.common.message;
+
+import static org.hamcrest.Matchers.*;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.charset.Charset;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import org.eclipse.jetty.util.BufferUtil;
+import org.eclipse.jetty.util.StringUtil;
+import org.eclipse.jetty.websocket.common.io.LocalWebSocketConnection;
+import org.junit.Assert;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TestName;
+
+public class MessageInputStreamTest
+{
+ private static final Charset UTF8 = StringUtil.__UTF8_CHARSET;
+
+ @Rule
+ public TestName testname = new TestName();
+
+ @Test(timeout=10000)
+ public void testBasicAppendRead() throws IOException
+ {
+ LocalWebSocketConnection conn = new LocalWebSocketConnection(testname);
+
+ try (MessageInputStream stream = new MessageInputStream(conn))
+ {
+ // Append a message (simple, short)
+ ByteBuffer payload = BufferUtil.toBuffer("Hello World",UTF8);
+ System.out.printf("payload = %s%n",BufferUtil.toDetailString(payload));
+ boolean fin = true;
+ stream.appendMessage(payload,fin);
+
+ // Read it from the stream.
+ byte buf[] = new byte[32];
+ int len = stream.read(buf);
+ String message = new String(buf,0,len,UTF8);
+
+ // Test it
+ Assert.assertThat("Message",message,is("Hello World"));
+ }
+ }
+
+ @Test(timeout=10000)
+ public void testBlockOnRead() throws IOException
+ {
+ LocalWebSocketConnection conn = new LocalWebSocketConnection(testname);
+
+ try (MessageInputStream stream = new MessageInputStream(conn))
+ {
+ final AtomicBoolean hadError = new AtomicBoolean(false);
+
+ new Thread(new Runnable()
+ {
+ @Override
+ public void run()
+ {
+ try
+ {
+ boolean fin = false;
+ TimeUnit.MILLISECONDS.sleep(200);
+ stream.appendMessage(BufferUtil.toBuffer("Saved",UTF8),fin);
+ TimeUnit.MILLISECONDS.sleep(200);
+ stream.appendMessage(BufferUtil.toBuffer(" by ",UTF8),fin);
+ fin = true;
+ TimeUnit.MILLISECONDS.sleep(200);
+ stream.appendMessage(BufferUtil.toBuffer("Zero",UTF8),fin);
+ }
+ catch (IOException | InterruptedException e)
+ {
+ hadError.set(true);
+ e.printStackTrace(System.err);
+ }
+ }
+ }).start();
+
+ // Read it from the stream.
+ byte buf[] = new byte[32];
+ int len = stream.read(buf);
+ String message = new String(buf,0,len,UTF8);
+
+ // Test it
+ Assert.assertThat("Error when appending",hadError.get(),is(false));
+ Assert.assertThat("Message",message,is("Saved by Zero"));
+ }
+ }
+
+ @Test(timeout=10000)
+ public void testBlockOnReadInitial() throws IOException
+ {
+ LocalWebSocketConnection conn = new LocalWebSocketConnection(testname);
+
+ try (MessageInputStream stream = new MessageInputStream(conn))
+ {
+ final AtomicBoolean hadError = new AtomicBoolean(false);
+
+ new Thread(new Runnable()
+ {
+ @Override
+ public void run()
+ {
+ try
+ {
+ boolean fin = true;
+ // wait for a little bit before populating buffers
+ TimeUnit.MILLISECONDS.sleep(400);
+ stream.appendMessage(BufferUtil.toBuffer("I will conquer",UTF8),fin);
+ }
+ catch (IOException | InterruptedException e)
+ {
+ hadError.set(true);
+ e.printStackTrace(System.err);
+ }
+ }
+ }).start();
+
+ // Read byte from stream.
+ int b = stream.read();
+ // Should be a byte, blocking till byte received.
+
+ // Test it
+ Assert.assertThat("Error when appending",hadError.get(),is(false));
+ Assert.assertThat("Initial byte",b,is((int)'I'));
+ }
+ }
+
+ @Test(timeout=10000)
+ public void testReadByteNoBuffersClosed() throws IOException
+ {
+ LocalWebSocketConnection conn = new LocalWebSocketConnection(testname);
+
+ try (MessageInputStream stream = new MessageInputStream(conn))
+ {
+ final AtomicBoolean hadError = new AtomicBoolean(false);
+
+ new Thread(new Runnable()
+ {
+ @Override
+ public void run()
+ {
+ try
+ {
+ // wait for a little bit before sending input closed
+ TimeUnit.MILLISECONDS.sleep(400);
+ stream.messageComplete();
+ }
+ catch (InterruptedException e)
+ {
+ hadError.set(true);
+ e.printStackTrace(System.err);
+ }
+ }
+ }).start();
+
+ // Read byte from stream.
+ int b = stream.read();
+ // Should be a -1, indicating the end of the stream.
+
+ // Test it
+ Assert.assertThat("Error when appending",hadError.get(),is(false));
+ Assert.assertThat("Initial byte",b,is(-1));
+ }
+ }
+}
diff --git a/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/message/MessageOutputStreamTest.java b/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/message/MessageOutputStreamTest.java
new file mode 100644
index 0000000..e501936
--- /dev/null
+++ b/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/message/MessageOutputStreamTest.java
@@ -0,0 +1,133 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.common.message;
+
+import static org.hamcrest.Matchers.*;
+
+import java.util.Arrays;
+
+import org.eclipse.jetty.toolchain.test.TestTracker;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+import org.eclipse.jetty.websocket.api.WebSocketPolicy;
+import org.eclipse.jetty.websocket.api.extensions.OutgoingFrames;
+import org.eclipse.jetty.websocket.common.events.EventDriver;
+import org.eclipse.jetty.websocket.common.events.EventDriverFactory;
+import org.eclipse.jetty.websocket.common.io.FramePipes;
+import org.eclipse.jetty.websocket.common.io.LocalWebSocketSession;
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TestName;
+
+public class MessageOutputStreamTest
+{
+ private static final Logger LOG = Log.getLogger(MessageOutputStreamTest.class);
+
+ @Rule
+ public TestTracker testtracker = new TestTracker();
+
+ @Rule
+ public TestName testname = new TestName();
+
+ private WebSocketPolicy policy;
+ private TrackingSocket socket;
+ private LocalWebSocketSession session;
+
+ @After
+ public void closeSession()
+ {
+ session.close();
+ }
+
+ @Before
+ public void setupSession()
+ {
+ policy = WebSocketPolicy.newServerPolicy();
+ policy.setInputBufferSize(1024);
+ policy.setMaxBinaryMessageBufferSize(1024);
+
+ // Event Driver factory
+ EventDriverFactory factory = new EventDriverFactory(policy);
+
+ // local socket
+ EventDriver driver = factory.wrap(new TrackingSocket("local"));
+
+ // remote socket
+ socket = new TrackingSocket("remote");
+ OutgoingFrames socketPipe = FramePipes.to(factory.wrap(socket));
+
+ session = new LocalWebSocketSession(testname,driver);
+
+ session.setPolicy(policy);
+ // talk to our remote socket
+ session.setOutgoingHandler(socketPipe);
+ // open connection
+ session.open();
+ }
+
+ @Test
+ public void testMultipleWrites() throws Exception
+ {
+ try (MessageOutputStream stream = new MessageOutputStream(session))
+ {
+ stream.write("Hello".getBytes("UTF-8"));
+ stream.write(" ".getBytes("UTF-8"));
+ stream.write("World".getBytes("UTF-8"));
+ }
+
+ Assert.assertThat("Socket.messageQueue.size",socket.messageQueue.size(),is(1));
+ String msg = socket.messageQueue.poll();
+ Assert.assertThat("Message",msg,allOf(containsString("byte[11]"),containsString("Hello World")));
+ }
+
+ @Test
+ public void testSingleWrite() throws Exception
+ {
+ try (MessageOutputStream stream = new MessageOutputStream(session))
+ {
+ stream.write("Hello World".getBytes("UTF-8"));
+ }
+
+ Assert.assertThat("Socket.messageQueue.size",socket.messageQueue.size(),is(1));
+ String msg = socket.messageQueue.poll();
+ Assert.assertThat("Message",msg,allOf(containsString("byte[11]"),containsString("Hello World")));
+ }
+
+ @Test
+ public void testWriteMultipleBuffers() throws Exception
+ {
+ int bufsize = (int)(policy.getMaxBinaryMessageBufferSize() * 2.5);
+ byte buf[] = new byte[bufsize];
+ LOG.debug("Buffer size: {}",bufsize);
+ Arrays.fill(buf,(byte)'x');
+ buf[bufsize - 1] = (byte)'o'; // mark last entry for debugging
+
+ try (MessageOutputStream stream = new MessageOutputStream(session))
+ {
+ stream.write(buf);
+ }
+
+ Assert.assertThat("Socket.messageQueue.size",socket.messageQueue.size(),is(1));
+ String msg = socket.messageQueue.poll();
+ Assert.assertThat("Message",msg,allOf(containsString("byte[" + bufsize + "]"),containsString("xxxo>>>")));
+ }
+}
diff --git a/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/message/MessageWriterTest.java b/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/message/MessageWriterTest.java
new file mode 100644
index 0000000..86d7f5d
--- /dev/null
+++ b/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/message/MessageWriterTest.java
@@ -0,0 +1,134 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.common.message;
+
+import static org.hamcrest.Matchers.*;
+
+import java.util.Arrays;
+
+import org.eclipse.jetty.toolchain.test.TestTracker;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+import org.eclipse.jetty.websocket.api.WebSocketPolicy;
+import org.eclipse.jetty.websocket.api.extensions.OutgoingFrames;
+import org.eclipse.jetty.websocket.common.events.EventDriver;
+import org.eclipse.jetty.websocket.common.events.EventDriverFactory;
+import org.eclipse.jetty.websocket.common.io.FramePipes;
+import org.eclipse.jetty.websocket.common.io.LocalWebSocketSession;
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TestName;
+
+public class MessageWriterTest
+{
+ private static final Logger LOG = Log.getLogger(MessageWriterTest.class);
+
+ @Rule
+ public TestTracker testtracker = new TestTracker();
+
+ @Rule
+ public TestName testname = new TestName();
+
+ private WebSocketPolicy policy;
+ private TrackingSocket socket;
+ private LocalWebSocketSession session;
+
+ @After
+ public void closeSession()
+ {
+ session.close();
+ }
+
+ @Before
+ public void setupSession()
+ {
+ policy = WebSocketPolicy.newServerPolicy();
+ policy.setInputBufferSize(1024);
+ policy.setMaxTextMessageBufferSize(1024);
+
+ // Event Driver factory
+ EventDriverFactory factory = new EventDriverFactory(policy);
+
+ // local socket
+ EventDriver driver = factory.wrap(new TrackingSocket("local"));
+
+ // remote socket
+ socket = new TrackingSocket("remote");
+ OutgoingFrames socketPipe = FramePipes.to(factory.wrap(socket));
+
+ session = new LocalWebSocketSession(testname,driver);
+
+ session.setPolicy(policy);
+ // talk to our remote socket
+ session.setOutgoingHandler(socketPipe);
+ // open connection
+ session.open();
+ }
+
+ @Test
+ public void testMultipleWrites() throws Exception
+ {
+ try (MessageWriter stream = new MessageWriter(session))
+ {
+ stream.write("Hello");
+ stream.write(" ");
+ stream.write("World");
+ }
+
+ Assert.assertThat("Socket.messageQueue.size",socket.messageQueue.size(),is(1));
+ String msg = socket.messageQueue.poll();
+ Assert.assertThat("Message",msg,is("Hello World"));
+ }
+
+ @Test
+ public void testSingleWrite() throws Exception
+ {
+ try (MessageWriter stream = new MessageWriter(session))
+ {
+ stream.append("Hello World");
+ }
+
+ Assert.assertThat("Socket.messageQueue.size",socket.messageQueue.size(),is(1));
+ String msg = socket.messageQueue.poll();
+ Assert.assertThat("Message",msg,is("Hello World"));
+ }
+
+ @Test
+ public void testWriteMultipleBuffers() throws Exception
+ {
+ int bufsize = (int)(policy.getMaxTextMessageBufferSize() * 2.5);
+ char buf[] = new char[bufsize];
+ LOG.debug("Buffer size: {}",bufsize);
+ Arrays.fill(buf,'x');
+ buf[bufsize - 1] = 'o'; // mark last entry for debugging
+
+ try (MessageWriter stream = new MessageWriter(session))
+ {
+ stream.write(buf);
+ }
+
+ Assert.assertThat("Socket.messageQueue.size",socket.messageQueue.size(),is(1));
+ String msg = socket.messageQueue.poll();
+ String expected = new String(buf);
+ Assert.assertThat("Message",msg,is(expected));
+ }
+}
diff --git a/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/message/TrackingInputStreamSocket.java b/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/message/TrackingInputStreamSocket.java
new file mode 100644
index 0000000..d49fd94
--- /dev/null
+++ b/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/message/TrackingInputStreamSocket.java
@@ -0,0 +1,110 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.common.message;
+
+import static org.hamcrest.Matchers.*;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+import org.eclipse.jetty.toolchain.test.EventQueue;
+import org.eclipse.jetty.util.IO;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+import org.eclipse.jetty.websocket.api.annotations.OnWebSocketClose;
+import org.eclipse.jetty.websocket.api.annotations.OnWebSocketError;
+import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage;
+import org.eclipse.jetty.websocket.api.annotations.WebSocket;
+import org.junit.Assert;
+
+@WebSocket
+public class TrackingInputStreamSocket
+{
+ private static final Logger LOG = Log.getLogger(TrackingInputStreamSocket.class);
+ private final String id;
+ public int closeCode = -1;
+ public StringBuilder closeMessage = new StringBuilder();
+ public CountDownLatch closeLatch = new CountDownLatch(1);
+ public EventQueue<String> messageQueue = new EventQueue<>();
+ public EventQueue<Throwable> errorQueue = new EventQueue<>();
+
+ public TrackingInputStreamSocket()
+ {
+ this("socket");
+ }
+
+ public TrackingInputStreamSocket(String id)
+ {
+ this.id = id;
+ }
+
+ public void assertClose(int expectedStatusCode, String expectedReason) throws InterruptedException
+ {
+ assertCloseCode(expectedStatusCode);
+ assertCloseReason(expectedReason);
+ }
+
+ public void assertCloseCode(int expectedCode) throws InterruptedException
+ {
+ Assert.assertThat("Was Closed",closeLatch.await(50,TimeUnit.MILLISECONDS),is(true));
+ Assert.assertThat("Close Code",closeCode,is(expectedCode));
+ }
+
+ private void assertCloseReason(String expectedReason)
+ {
+ Assert.assertThat("Close Reason",closeMessage.toString(),is(expectedReason));
+ }
+
+ @OnWebSocketClose
+ public void onClose(int statusCode, String reason)
+ {
+ LOG.debug("{} onClose({},{})",id,statusCode,reason);
+ closeCode = statusCode;
+ closeMessage.append(reason);
+ closeLatch.countDown();
+ }
+
+ @OnWebSocketError
+ public void onError(Throwable cause)
+ {
+ errorQueue.add(cause);
+ }
+
+ @OnWebSocketMessage
+ public void onInputStream(InputStream stream)
+ {
+ LOG.debug("{} onInputStream({})",id,stream);
+ try
+ {
+ String msg = IO.toString(stream);
+ messageQueue.add(msg);
+ }
+ catch (IOException e)
+ {
+ errorQueue.add(e);
+ }
+ }
+
+ public void waitForClose(int timeoutDuration, TimeUnit timeoutUnit) throws InterruptedException
+ {
+ Assert.assertThat("Client Socket Closed",closeLatch.await(timeoutDuration,timeoutUnit),is(true));
+ }
+}
diff --git a/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/message/TrackingSocket.java b/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/message/TrackingSocket.java
new file mode 100644
index 0000000..73865a2
--- /dev/null
+++ b/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/message/TrackingSocket.java
@@ -0,0 +1,169 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.common.message;
+
+import static org.hamcrest.Matchers.*;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+import org.eclipse.jetty.toolchain.test.EventQueue;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+import org.eclipse.jetty.websocket.api.Session;
+import org.eclipse.jetty.websocket.api.WebSocketAdapter;
+import org.junit.Assert;
+
+/**
+ * Testing Socket used on client side WebSocket testing.
+ */
+public class TrackingSocket extends WebSocketAdapter
+{
+ private static final Logger LOG = Log.getLogger(TrackingSocket.class);
+
+ private final String id;
+ public int closeCode = -1;
+ public StringBuilder closeMessage = new StringBuilder();
+ public CountDownLatch openLatch = new CountDownLatch(1);
+ public CountDownLatch closeLatch = new CountDownLatch(1);
+ public CountDownLatch dataLatch = new CountDownLatch(1);
+ public EventQueue<String> messageQueue = new EventQueue<>();
+ public EventQueue<Throwable> errorQueue = new EventQueue<>();
+
+ public TrackingSocket()
+ {
+ this("socket");
+ }
+
+ public TrackingSocket(String id)
+ {
+ this.id = id;
+ }
+
+ public void assertClose(int expectedStatusCode, String expectedReason) throws InterruptedException
+ {
+ assertCloseCode(expectedStatusCode);
+ assertCloseReason(expectedReason);
+ }
+
+ public void assertCloseCode(int expectedCode) throws InterruptedException
+ {
+ Assert.assertThat("Was Closed",closeLatch.await(50,TimeUnit.MILLISECONDS),is(true));
+ Assert.assertThat("Close Code",closeCode,is(expectedCode));
+ }
+
+ private void assertCloseReason(String expectedReason)
+ {
+ Assert.assertThat("Close Reason",closeMessage.toString(),is(expectedReason));
+ }
+
+ public void assertIsOpen() throws InterruptedException
+ {
+ assertWasOpened();
+ assertNotClosed();
+ }
+
+ public void assertMessage(String expected)
+ {
+ String actual = messageQueue.poll();
+ Assert.assertEquals("Message",expected,actual);
+ }
+
+ public void assertNotClosed()
+ {
+ Assert.assertThat("Closed Latch",closeLatch.getCount(),greaterThanOrEqualTo(1L));
+ }
+
+ public void assertNotOpened()
+ {
+ Assert.assertThat("Open Latch",openLatch.getCount(),greaterThanOrEqualTo(1L));
+ }
+
+ public void assertWasOpened() throws InterruptedException
+ {
+ Assert.assertThat("Was Opened",openLatch.await(500,TimeUnit.MILLISECONDS),is(true));
+ }
+
+ public void awaitMessage(int expectedMessageCount, TimeUnit timeoutUnit, int timeoutDuration) throws TimeoutException, InterruptedException
+ {
+ messageQueue.awaitEventCount(expectedMessageCount,timeoutDuration,timeoutUnit);
+ }
+
+ public void clear()
+ {
+ messageQueue.clear();
+ }
+
+ @Override
+ public void onWebSocketBinary(byte[] payload, int offset, int len)
+ {
+ LOG.debug("{} onWebSocketBinary(byte[{}],{},{})",id,payload.length,offset,len);
+ messageQueue.offer(MessageDebug.toDetailHint(payload,offset,len));
+ dataLatch.countDown();
+ }
+
+ @Override
+ public void onWebSocketClose(int statusCode, String reason)
+ {
+ LOG.debug("{} onWebSocketClose({},{})",id,statusCode,reason);
+ super.onWebSocketClose(statusCode,reason);
+ closeCode = statusCode;
+ closeMessage.append(reason);
+ closeLatch.countDown();
+ }
+
+ @Override
+ public void onWebSocketConnect(Session session)
+ {
+ super.onWebSocketConnect(session);
+ openLatch.countDown();
+ }
+
+ @Override
+ public void onWebSocketError(Throwable cause)
+ {
+ LOG.debug("{} onWebSocketError",id,cause);
+ Assert.assertThat("Error capture",errorQueue.offer(cause),is(true));
+ }
+
+ @Override
+ public void onWebSocketText(String message)
+ {
+ LOG.debug("{} onWebSocketText({})",id,message);
+ messageQueue.offer(message);
+ dataLatch.countDown();
+ }
+
+ public void waitForClose(int timeoutDuration, TimeUnit timeoutUnit) throws InterruptedException
+ {
+ Assert.assertThat("Client Socket Closed",closeLatch.await(timeoutDuration,timeoutUnit),is(true));
+ }
+
+ public void waitForConnected(int timeoutDuration, TimeUnit timeoutUnit) throws InterruptedException
+ {
+ Assert.assertThat("Client Socket Connected",openLatch.await(timeoutDuration,timeoutUnit),is(true));
+ }
+
+ public void waitForMessage(int timeoutDuration, TimeUnit timeoutUnit) throws InterruptedException
+ {
+ LOG.debug("{} Waiting for message",id);
+ Assert.assertThat("Message Received",dataLatch.await(timeoutDuration,timeoutUnit),is(true));
+ }
+}
diff --git a/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/message/Utf8CharBufferTest.java b/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/message/Utf8CharBufferTest.java
new file mode 100644
index 0000000..1ec0eb0
--- /dev/null
+++ b/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/message/Utf8CharBufferTest.java
@@ -0,0 +1,125 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.common.message;
+
+import static org.hamcrest.Matchers.*;
+
+import java.nio.ByteBuffer;
+
+import org.eclipse.jetty.util.BufferUtil;
+import org.eclipse.jetty.util.StringUtil;
+import org.junit.Assert;
+import org.junit.Test;
+
+public class Utf8CharBufferTest
+{
+ private static String asString(ByteBuffer buffer)
+ {
+ return BufferUtil.toUTF8String(buffer);
+ }
+
+ private static byte[] asUTF(String str)
+ {
+ return str.getBytes(StringUtil.__UTF8_CHARSET);
+ }
+
+ @Test
+ public void testAppendGetAppendGet()
+ {
+ ByteBuffer buf = ByteBuffer.allocate(128);
+ Utf8CharBuffer utf = Utf8CharBuffer.wrap(buf);
+
+ byte hellobytes[] = asUTF("Hello ");
+ byte worldbytes[] = asUTF("World!");
+
+ utf.append(hellobytes, 0, hellobytes.length);
+ ByteBuffer hellobuf = utf.getByteBuffer();
+ utf.append(worldbytes, 0, worldbytes.length);
+ ByteBuffer worldbuf = utf.getByteBuffer();
+
+ Assert.assertThat("Hello buffer",asString(hellobuf),is("Hello "));
+ Assert.assertThat("World buffer",asString(worldbuf),is("Hello World!"));
+ }
+
+ @Test
+ public void testAppendGetClearAppendGet()
+ {
+ int bufsize = 128;
+ ByteBuffer buf = ByteBuffer.allocate(bufsize);
+ Utf8CharBuffer utf = Utf8CharBuffer.wrap(buf);
+
+ int expectedSize = bufsize / 2;
+ Assert.assertThat("Remaining (initial)",utf.remaining(),is(expectedSize));
+
+ byte hellobytes[] = asUTF("Hello World");
+
+ utf.append(hellobytes,0,hellobytes.length);
+ ByteBuffer hellobuf = utf.getByteBuffer();
+
+ Assert.assertThat("Remaining (after append)",utf.remaining(),is(expectedSize - hellobytes.length));
+ Assert.assertThat("Hello buffer",asString(hellobuf),is("Hello World"));
+
+ utf.clear();
+
+ Assert.assertThat("Remaining (after clear)",utf.remaining(),is(expectedSize));
+
+ byte whatnowbytes[] = asUTF("What Now?");
+ utf.append(whatnowbytes,0,whatnowbytes.length);
+ ByteBuffer whatnowbuf = utf.getByteBuffer();
+
+ Assert.assertThat("Remaining (after 2nd append)",utf.remaining(),is(expectedSize - whatnowbytes.length));
+ Assert.assertThat("What buffer",asString(whatnowbuf),is("What Now?"));
+ }
+
+ @Test
+ public void testAppendUnicodeGetBuffer()
+ {
+ ByteBuffer buf = ByteBuffer.allocate(64);
+ Utf8CharBuffer utf = Utf8CharBuffer.wrap(buf);
+
+ byte bb[] = asUTF("Hello A\u00ea\u00f1\u00fcC");
+ utf.append(bb,0,bb.length);
+
+ ByteBuffer actual = utf.getByteBuffer();
+ Assert.assertThat("Buffer length should be retained",actual.remaining(),is(bb.length));
+ Assert.assertThat("Message",asString(actual),is("Hello A\u00ea\u00f1\u00fcC"));
+ }
+
+ @Test
+ public void testSimpleGetBuffer()
+ {
+ int bufsize = 64;
+ ByteBuffer buf = ByteBuffer.allocate(bufsize);
+ Utf8CharBuffer utf = Utf8CharBuffer.wrap(buf);
+
+ int expectedSize = bufsize / 2;
+ Assert.assertThat("Remaining (initial)",utf.remaining(),is(expectedSize));
+
+ byte bb[] = asUTF("Hello World");
+ utf.append(bb,0,bb.length);
+
+ expectedSize -= bb.length;
+ Assert.assertThat("Remaining (after append)",utf.remaining(),is(expectedSize));
+
+ ByteBuffer actual = utf.getByteBuffer();
+ Assert.assertThat("Buffer length",actual.remaining(),is(bb.length));
+
+ Assert.assertThat("Message",asString(actual),is("Hello World"));
+ }
+}
diff --git a/jetty-websocket/websocket-mux-extension/pom.xml b/jetty-websocket/websocket-mux-extension/pom.xml
new file mode 100644
index 0000000..6a7b867
--- /dev/null
+++ b/jetty-websocket/websocket-mux-extension/pom.xml
@@ -0,0 +1,56 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+ <parent>
+ <groupId>org.eclipse.jetty.websocket</groupId>
+ <artifactId>websocket-parent</artifactId>
+ <version>9.1.0-SNAPSHOT</version>
+ </parent>
+
+ <modelVersion>4.0.0</modelVersion>
+ <artifactId>websocket-mux-extension</artifactId>
+ <name>Jetty :: Websocket :: Mux Extension</name>
+
+ <properties>
+ <bundle-symbolic-name>${project.groupId}.mux</bundle-symbolic-name>
+ </properties>
+
+ <dependencies>
+ <dependency>
+ <groupId>org.eclipse.jetty</groupId>
+ <artifactId>jetty-server</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.eclipse.jetty.websocket</groupId>
+ <artifactId>websocket-client</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.eclipse.jetty.websocket</groupId>
+ <artifactId>websocket-server</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.eclipse.jetty.toolchain</groupId>
+ <artifactId>jetty-test-helper</artifactId>
+ <scope>test</scope>
+ </dependency>
+ </dependencies>
+
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-jar-plugin</artifactId>
+ <executions>
+ <execution>
+ <id>tests-jar</id>
+ <goals>
+ <goal>test-jar</goal>
+ </goals>
+ </execution>
+ </executions>
+ </plugin>
+ </plugins>
+ </build>
+</project>
diff --git a/jetty-websocket/websocket-mux-extension/src/main/java/org/eclipse/jetty/websocket/mux/AbstractMuxExtension.java b/jetty-websocket/websocket-mux-extension/src/main/java/org/eclipse/jetty/websocket/mux/AbstractMuxExtension.java
new file mode 100644
index 0000000..d57559b
--- /dev/null
+++ b/jetty-websocket/websocket-mux-extension/src/main/java/org/eclipse/jetty/websocket/mux/AbstractMuxExtension.java
@@ -0,0 +1,65 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.mux;
+
+import org.eclipse.jetty.websocket.api.WriteCallback;
+import org.eclipse.jetty.websocket.api.extensions.Frame;
+import org.eclipse.jetty.websocket.common.LogicalConnection;
+import org.eclipse.jetty.websocket.common.extensions.AbstractExtension;
+
+/**
+ * Multiplexing Extension for WebSockets.
+ * <p>
+ * Supporting <a href="https://tools.ietf.org/html/draft-ietf-hybi-websocket-multiplexing-08">draft-ietf-hybi-websocket-multiplexing-08</a> Specification.
+ */
+public abstract class AbstractMuxExtension extends AbstractExtension
+{
+ private Muxer muxer;
+
+ public AbstractMuxExtension()
+ {
+ super();
+ }
+
+ public abstract void configureMuxer(Muxer muxer);
+
+ @Override
+ public void incomingFrame(Frame frame)
+ {
+ this.muxer.incomingFrame(frame);
+ }
+
+ @Override
+ public void outgoingFrame(Frame frame, WriteCallback callback)
+ {
+ /* do nothing here, allow Muxer to handle this aspect */
+ }
+
+ @Override
+ public void setConnection(LogicalConnection connection)
+ {
+ super.setConnection(connection);
+ if (muxer != null)
+ {
+ throw new RuntimeException("Cannot reset muxer physical connection once established");
+ }
+ this.muxer = new Muxer(connection);
+ configureMuxer(this.muxer);
+ }
+}
diff --git a/jetty-websocket/websocket-mux-extension/src/main/java/org/eclipse/jetty/websocket/mux/MuxChannel.java b/jetty-websocket/websocket-mux-extension/src/main/java/org/eclipse/jetty/websocket/mux/MuxChannel.java
new file mode 100644
index 0000000..41014b7
--- /dev/null
+++ b/jetty-websocket/websocket-mux-extension/src/main/java/org/eclipse/jetty/websocket/mux/MuxChannel.java
@@ -0,0 +1,275 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.mux;
+
+import java.net.InetSocketAddress;
+import java.util.concurrent.Executor;
+import java.util.concurrent.Future;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import org.eclipse.jetty.io.ByteBufferPool;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+import org.eclipse.jetty.websocket.api.StatusCode;
+import org.eclipse.jetty.websocket.api.SuspendToken;
+import org.eclipse.jetty.websocket.api.WebSocketPolicy;
+import org.eclipse.jetty.websocket.api.WriteCallback;
+import org.eclipse.jetty.websocket.api.extensions.Frame;
+import org.eclipse.jetty.websocket.api.extensions.IncomingFrames;
+import org.eclipse.jetty.websocket.common.CloseInfo;
+import org.eclipse.jetty.websocket.common.ConnectionState;
+import org.eclipse.jetty.websocket.common.LogicalConnection;
+import org.eclipse.jetty.websocket.common.WebSocketFrame;
+import org.eclipse.jetty.websocket.common.WebSocketSession;
+import org.eclipse.jetty.websocket.common.io.FutureWriteCallback;
+import org.eclipse.jetty.websocket.common.io.IOState;
+import org.eclipse.jetty.websocket.common.io.IOState.ConnectionStateListener;
+
+/**
+ * MuxChannel, acts as WebSocketConnection for specific sub-channel.
+ */
+public class MuxChannel implements LogicalConnection, IncomingFrames, SuspendToken, ConnectionStateListener
+{
+ private static final Logger LOG = Log.getLogger(MuxChannel.class);
+
+ private final long channelId;
+ private final Muxer muxer;
+ private final AtomicBoolean inputClosed;
+ private final AtomicBoolean outputClosed;
+ private final AtomicBoolean suspendToken;
+ private IOState ioState;
+ private WebSocketPolicy policy;
+ private WebSocketSession session;
+ private IncomingFrames incoming;
+ private String subProtocol;
+
+ public MuxChannel(long channelId, Muxer muxer)
+ {
+ this.channelId = channelId;
+ this.muxer = muxer;
+ this.policy = muxer.getPolicy().clonePolicy();
+
+ this.suspendToken = new AtomicBoolean(false);
+ this.ioState = new IOState();
+ this.ioState.addListener(this);
+
+ this.inputClosed = new AtomicBoolean(false);
+ this.outputClosed = new AtomicBoolean(false);
+ }
+
+ @Override
+ public Executor getExecutor()
+ {
+ // TODO Auto-generated method stub
+ return null;
+ }
+
+ @Override
+ public void close()
+ {
+ close(StatusCode.NORMAL,null);
+ }
+
+ @Override
+ public void close(int statusCode, String reason)
+ {
+ CloseInfo close = new CloseInfo(statusCode,reason);
+ // TODO: disconnect callback?
+ outgoingFrame(close.asFrame(),null);
+ }
+
+ @Override
+ public void disconnect()
+ {
+ // TODO: disconnect the virtual end-point?
+ }
+
+ @Override
+ public ByteBufferPool getBufferPool()
+ {
+ // TODO Auto-generated method stub
+ return null;
+ }
+
+ public long getChannelId()
+ {
+ return channelId;
+ }
+
+ @Override
+ public long getIdleTimeout()
+ {
+ // TODO Auto-generated method stub
+ return 0;
+ }
+
+ @Override
+ public IOState getIOState()
+ {
+ // TODO Auto-generated method stub
+ return null;
+ }
+
+ @Override
+ public InetSocketAddress getLocalAddress()
+ {
+ // TODO Auto-generated method stub
+ return null;
+ }
+
+ @Override
+ public long getMaxIdleTimeout()
+ {
+ // TODO Auto-generated method stub
+ return 0;
+ }
+
+ @Override
+ public WebSocketPolicy getPolicy()
+ {
+ return policy;
+ }
+
+ @Override
+ public InetSocketAddress getRemoteAddress()
+ {
+ return muxer.getRemoteAddress();
+ }
+
+ @Override
+ public WebSocketSession getSession()
+ {
+ return session;
+ }
+
+ /**
+ * Incoming exceptions from Muxer.
+ */
+ @Override
+ public void incomingError(Throwable e)
+ {
+ incoming.incomingError(e);
+ }
+
+ /**
+ * Incoming frames from Muxer
+ */
+ @Override
+ public void incomingFrame(Frame frame)
+ {
+ incoming.incomingFrame(frame);
+ }
+
+ public boolean isActive()
+ {
+ return (ioState.isOpen());
+ }
+
+ @Override
+ public boolean isOpen()
+ {
+ return isActive() && muxer.isOpen();
+ }
+
+ @Override
+ public boolean isReading()
+ {
+ return true;
+ }
+
+ public void onClose()
+ {
+ }
+
+ @Override
+ public void onConnectionStateChange(ConnectionState state)
+ {
+ // TODO Auto-generated method stub
+
+ }
+
+ public void onOpen()
+ {
+ this.ioState.onOpened();
+ }
+
+ /**
+ * Internal
+ *
+ * @param frame the frame to write
+ * @return the future for the network write of the frame
+ */
+ private Future<Void> outgoingAsyncFrame(WebSocketFrame frame)
+ {
+ FutureWriteCallback future = new FutureWriteCallback();
+ outgoingFrame(frame,future);
+ return future;
+ }
+
+ /**
+ * Frames destined for the Muxer
+ */
+ @Override
+ public void outgoingFrame(Frame frame, WriteCallback callback)
+ {
+ muxer.output(channelId,frame,callback);
+ }
+
+ @Override
+ public void resume()
+ {
+ if (suspendToken.getAndSet(false))
+ {
+ // TODO: Start reading again. (how?)
+ }
+ }
+
+ @Override
+ public void setMaxIdleTimeout(long ms)
+ {
+ // TODO Auto-generated method stub
+
+ }
+
+ @Override
+ public void setNextIncomingFrames(IncomingFrames incoming)
+ {
+ this.incoming = incoming;
+ }
+
+ @Override
+ public void setSession(WebSocketSession session)
+ {
+ this.session = session;
+ // session.setOutgoing(this);
+ }
+
+ public void setSubProtocol(String subProtocol)
+ {
+ this.subProtocol = subProtocol;
+ }
+
+ @Override
+ public SuspendToken suspend()
+ {
+ suspendToken.set(true);
+ // TODO: how to suspend reading?
+ return this;
+ }
+}
diff --git a/jetty-websocket/websocket-mux-extension/src/main/java/org/eclipse/jetty/websocket/mux/MuxControlBlock.java b/jetty-websocket/websocket-mux-extension/src/main/java/org/eclipse/jetty/websocket/mux/MuxControlBlock.java
new file mode 100644
index 0000000..230a867
--- /dev/null
+++ b/jetty-websocket/websocket-mux-extension/src/main/java/org/eclipse/jetty/websocket/mux/MuxControlBlock.java
@@ -0,0 +1,24 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.mux;
+
+public interface MuxControlBlock
+{
+ public int getOpCode();
+}
diff --git a/jetty-websocket/websocket-mux-extension/src/main/java/org/eclipse/jetty/websocket/mux/MuxException.java b/jetty-websocket/websocket-mux-extension/src/main/java/org/eclipse/jetty/websocket/mux/MuxException.java
new file mode 100644
index 0000000..162361f
--- /dev/null
+++ b/jetty-websocket/websocket-mux-extension/src/main/java/org/eclipse/jetty/websocket/mux/MuxException.java
@@ -0,0 +1,40 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.mux;
+
+import org.eclipse.jetty.websocket.api.WebSocketException;
+
+@SuppressWarnings("serial")
+public class MuxException extends WebSocketException
+{
+ public MuxException(String message)
+ {
+ super(message);
+ }
+
+ public MuxException(String message, Throwable cause)
+ {
+ super(message,cause);
+ }
+
+ public MuxException(Throwable cause)
+ {
+ super(cause);
+ }
+}
diff --git a/jetty-websocket/websocket-mux-extension/src/main/java/org/eclipse/jetty/websocket/mux/MuxGenerator.java b/jetty-websocket/websocket-mux-extension/src/main/java/org/eclipse/jetty/websocket/mux/MuxGenerator.java
new file mode 100644
index 0000000..7b86cbe
--- /dev/null
+++ b/jetty-websocket/websocket-mux-extension/src/main/java/org/eclipse/jetty/websocket/mux/MuxGenerator.java
@@ -0,0 +1,271 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.mux;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+
+import org.eclipse.jetty.io.ArrayByteBufferPool;
+import org.eclipse.jetty.io.ByteBufferPool;
+import org.eclipse.jetty.util.BufferUtil;
+import org.eclipse.jetty.websocket.api.WriteCallback;
+import org.eclipse.jetty.websocket.api.extensions.Frame;
+import org.eclipse.jetty.websocket.api.extensions.OutgoingFrames;
+import org.eclipse.jetty.websocket.common.WebSocketFrame;
+import org.eclipse.jetty.websocket.mux.op.MuxAddChannelRequest;
+import org.eclipse.jetty.websocket.mux.op.MuxAddChannelResponse;
+import org.eclipse.jetty.websocket.mux.op.MuxDropChannel;
+import org.eclipse.jetty.websocket.mux.op.MuxFlowControl;
+import org.eclipse.jetty.websocket.mux.op.MuxNewChannelSlot;
+
+/**
+ * Generate Mux frames destined for the physical connection.
+ */
+public class MuxGenerator
+{
+ private static final int CONTROL_BUFFER_SIZE = 2 * 1024;
+ /** 4 bytes for channel ID + 1 for fin/rsv/opcode */
+ private static final int DATA_FRAME_OVERHEAD = 5;
+ private ByteBufferPool bufferPool;
+ private OutgoingFrames outgoing;
+
+ public MuxGenerator()
+ {
+ this(new ArrayByteBufferPool());
+ }
+
+ public MuxGenerator(ByteBufferPool bufferPool)
+ {
+ this.bufferPool = bufferPool;
+ }
+
+ public void generate(long channelId, Frame frame, WriteCallback callback)
+ {
+ ByteBuffer muxPayload = bufferPool.acquire(frame.getPayloadLength() + DATA_FRAME_OVERHEAD,false);
+ BufferUtil.flipToFill(muxPayload);
+
+ // start building mux payload
+ writeChannelId(muxPayload,channelId);
+ byte b = (byte)(frame.isFin()?0x80:0x00); // fin
+ b |= (byte)(frame.isRsv1()?0x40:0x00); // rsv1
+ b |= (byte)(frame.isRsv2()?0x20:0x00); // rsv2
+ b |= (byte)(frame.isRsv3()?0x10:0x00); // rsv3
+ b |= (byte)(frame.getType().getOpCode() & 0x0F); // opcode
+ muxPayload.put(b);
+ BufferUtil.put(frame.getPayload(),muxPayload);
+
+ // build muxed frame
+ WebSocketFrame muxFrame = WebSocketFrame.binary();
+ BufferUtil.flipToFlush(muxPayload,0);
+ muxFrame.setPayload(muxPayload);
+ // NOTE: the physical connection will handle masking rules for this frame.
+
+ // release original buffer (no longer needed)
+ bufferPool.release(frame.getPayload());
+
+ // send muxed frame down to the physical connection.
+ outgoing.outgoingFrame(muxFrame,callback);
+ }
+
+ public void generate(WriteCallback callback,MuxControlBlock... blocks) throws IOException
+ {
+ if ((blocks == null) || (blocks.length <= 0))
+ {
+ return; // nothing to do
+ }
+
+ ByteBuffer payload = bufferPool.acquire(CONTROL_BUFFER_SIZE,false);
+ BufferUtil.flipToFill(payload);
+
+ writeChannelId(payload,0); // control channel
+
+ for (MuxControlBlock block : blocks)
+ {
+ switch (block.getOpCode())
+ {
+ case MuxOp.ADD_CHANNEL_REQUEST:
+ {
+ MuxAddChannelRequest op = (MuxAddChannelRequest)block;
+ byte b = (byte)((op.getOpCode() & 0x07) << 5); // opcode
+ b |= (byte)((op.getRsv() & 0x07) << 2); // rsv
+ b |= (op.getEncoding() & 0x03); // enc
+ payload.put(b); // opcode + rsv + enc
+ writeChannelId(payload,op.getChannelId());
+ write139Buffer(payload,op.getHandshake());
+ break;
+ }
+ case MuxOp.ADD_CHANNEL_RESPONSE:
+ {
+ MuxAddChannelResponse op = (MuxAddChannelResponse)block;
+ byte b = (byte)((op.getOpCode() & 0x07) << 5); // opcode
+ b |= (op.isFailed()?0x10:0x00); // failure bit
+ b |= (byte)((op.getRsv() & 0x03) << 2); // rsv
+ b |= (op.getEncoding() & 0x03); // enc
+ payload.put(b); // opcode + f + rsv + enc
+ writeChannelId(payload,op.getChannelId());
+ if (op.getHandshake() != null)
+ {
+ write139Buffer(payload,op.getHandshake());
+ }
+ else
+ {
+ // no handshake details
+ write139Size(payload,0);
+ }
+ break;
+ }
+ case MuxOp.DROP_CHANNEL:
+ {
+ MuxDropChannel op = (MuxDropChannel)block;
+ byte b = (byte)((op.getOpCode() & 0x07) << 5); // opcode
+ b |= (byte)(op.getRsv() & 0x1F); // rsv
+ payload.put(b); // opcode + rsv
+ writeChannelId(payload,op.getChannelId());
+ write139Buffer(payload,op.asReasonBuffer());
+ break;
+ }
+ case MuxOp.FLOW_CONTROL:
+ {
+ MuxFlowControl op = (MuxFlowControl)block;
+ byte b = (byte)((op.getOpCode() & 0x07) << 5); // opcode
+ b |= (byte)(op.getRsv() & 0x1F); // rsv
+ payload.put(b); // opcode + rsv
+ writeChannelId(payload,op.getChannelId());
+ write139Size(payload,op.getSendQuotaSize());
+ break;
+ }
+ case MuxOp.NEW_CHANNEL_SLOT:
+ {
+ MuxNewChannelSlot op = (MuxNewChannelSlot)block;
+ byte b = (byte)((op.getOpCode() & 0x07) << 5); // opcode
+ b |= (byte)(op.getRsv() & 0x0F) << 1; // rsv
+ b |= (byte)(op.isFallback()?0x01:0x00); // fallback bit
+ payload.put(b); // opcode + rsv + fallback bit
+ write139Size(payload,op.getNumberOfSlots());
+ write139Size(payload,op.getInitialSendQuota());
+ break;
+ }
+ }
+ }
+ BufferUtil.flipToFlush(payload,0);
+ WebSocketFrame frame = WebSocketFrame.binary();
+ frame.setPayload(payload);
+ outgoing.outgoingFrame(frame,callback);
+ }
+
+ public OutgoingFrames getOutgoing()
+ {
+ return outgoing;
+ }
+
+ public void setOutgoing(OutgoingFrames outgoing)
+ {
+ this.outgoing = outgoing;
+ }
+
+ /**
+ * Write a 1/3/9 encoded size, then a byte buffer of that size.
+ *
+ * @param payload
+ * @param buffer
+ */
+ public void write139Buffer(ByteBuffer payload, ByteBuffer buffer)
+ {
+ write139Size(payload,buffer.remaining());
+ writeBuffer(payload,buffer);
+ }
+
+ /**
+ * Write a 1/3/9 encoded size.
+ *
+ * @param payload
+ * @param size
+ */
+ public void write139Size(ByteBuffer payload, long size)
+ {
+ if (size > 0xFF_FF)
+ {
+ // 9 byte encoded
+ payload.put((byte)0x7F);
+ payload.putLong(size);
+ return;
+ }
+
+ if (size >= 0x7E)
+ {
+ // 3 byte encoded
+ payload.put((byte)0x7E);
+ payload.put((byte)(size >> 8));
+ payload.put((byte)(size & 0xFF));
+ return;
+ }
+
+ // 1 byte (7 bit) encoded
+ payload.put((byte)(size & 0x7F));
+ }
+
+ public void writeBuffer(ByteBuffer payload, ByteBuffer buffer)
+ {
+ BufferUtil.put(buffer,payload);
+ }
+
+ /**
+ * Write multiplexing channel id, using logical channel id encoding (of 1,2,3, or 4 octets)
+ *
+ * @param payload
+ * @param channelId
+ */
+ public void writeChannelId(ByteBuffer payload, long channelId)
+ {
+ if (channelId > 0x1F_FF_FF_FF)
+ {
+ throw new MuxException("Illegal Channel ID: too big");
+ }
+
+ if (channelId > 0x1F_FF_FF)
+ {
+ // 29 bit channel id (4 bytes)
+ payload.put((byte)(0xE0 | ((channelId >> 24) & 0x1F)));
+ payload.put((byte)((channelId >> 16) & 0xFF));
+ payload.put((byte)((channelId >> 8) & 0xFF));
+ payload.put((byte)(channelId & 0xFF));
+ return;
+ }
+
+ if (channelId > 0x3F_FF)
+ {
+ // 21 bit channel id (3 bytes)
+ payload.put((byte)(0xC0 | ((channelId >> 16) & 0x1F)));
+ payload.put((byte)((channelId >> 8) & 0xFF));
+ payload.put((byte)(channelId & 0xFF));
+ return;
+ }
+
+ if (channelId > 0x7F)
+ {
+ // 14 bit channel id (2 bytes)
+ payload.put((byte)(0x80 | ((channelId >> 8) & 0x3F)));
+ payload.put((byte)(channelId & 0xFF));
+ return;
+ }
+
+ // 7 bit channel id
+ payload.put((byte)(channelId & 0x7F));
+ }
+}
diff --git a/jetty-websocket/websocket-mux-extension/src/main/java/org/eclipse/jetty/websocket/mux/MuxOp.java b/jetty-websocket/websocket-mux-extension/src/main/java/org/eclipse/jetty/websocket/mux/MuxOp.java
new file mode 100644
index 0000000..17b61a7
--- /dev/null
+++ b/jetty-websocket/websocket-mux-extension/src/main/java/org/eclipse/jetty/websocket/mux/MuxOp.java
@@ -0,0 +1,28 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.mux;
+
+public final class MuxOp
+{
+ public static final byte ADD_CHANNEL_REQUEST = 0;
+ public static final byte ADD_CHANNEL_RESPONSE = 1;
+ public static final byte FLOW_CONTROL = 2;
+ public static final byte DROP_CHANNEL = 3;
+ public static final byte NEW_CHANNEL_SLOT = 4;
+}
diff --git a/jetty-websocket/websocket-mux-extension/src/main/java/org/eclipse/jetty/websocket/mux/MuxParser.java b/jetty-websocket/websocket-mux-extension/src/main/java/org/eclipse/jetty/websocket/mux/MuxParser.java
new file mode 100644
index 0000000..f060ec1
--- /dev/null
+++ b/jetty-websocket/websocket-mux-extension/src/main/java/org/eclipse/jetty/websocket/mux/MuxParser.java
@@ -0,0 +1,410 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.mux;
+
+import java.nio.ByteBuffer;
+
+import org.eclipse.jetty.util.BufferUtil;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+import org.eclipse.jetty.websocket.api.extensions.Frame;
+import org.eclipse.jetty.websocket.common.OpCode;
+import org.eclipse.jetty.websocket.common.WebSocketFrame;
+import org.eclipse.jetty.websocket.mux.op.MuxAddChannelRequest;
+import org.eclipse.jetty.websocket.mux.op.MuxAddChannelResponse;
+import org.eclipse.jetty.websocket.mux.op.MuxDropChannel;
+import org.eclipse.jetty.websocket.mux.op.MuxFlowControl;
+import org.eclipse.jetty.websocket.mux.op.MuxNewChannelSlot;
+
+public class MuxParser
+{
+ public static interface Listener
+ {
+ public void onMuxAddChannelRequest(MuxAddChannelRequest request);
+
+ public void onMuxAddChannelResponse(MuxAddChannelResponse response);
+
+ public void onMuxDropChannel(MuxDropChannel drop);
+
+ public void onMuxedFrame(MuxedFrame frame);
+
+ public void onMuxException(MuxException e);
+
+ public void onMuxFlowControl(MuxFlowControl flow);
+
+ public void onMuxNewChannelSlot(MuxNewChannelSlot slot);
+ }
+
+ private final static Logger LOG = Log.getLogger(MuxParser.class);
+
+ private MuxedFrame muxframe = new MuxedFrame();
+ private MuxParser.Listener events;
+ private long channelId;
+
+ public MuxParser.Listener getEvents()
+ {
+ return events;
+ }
+
+ /**
+ * Parse the raw {@link WebSocketFrame} payload data for various Mux frames.
+ *
+ * @param frame
+ * the WebSocketFrame to parse for mux payload
+ */
+ public synchronized void parse(Frame frame)
+ {
+ if (events == null)
+ {
+ throw new RuntimeException("No " + MuxParser.Listener.class + " specified");
+ }
+
+ if (!frame.hasPayload())
+ {
+ LOG.debug("No payload data, skipping");
+ return; // nothing to parse
+ }
+
+ if (frame.getType().getOpCode() != OpCode.BINARY)
+ {
+ LOG.debug("Not a binary opcode (base frame), skipping");
+ return; // not a binary opcode
+ }
+
+ LOG.debug("Parsing Mux Payload of {}",frame);
+
+ try
+ {
+ ByteBuffer buffer = frame.getPayload().slice();
+
+ if (buffer.remaining() <= 0)
+ {
+ return;
+ }
+
+ if (frame.isContinuation())
+ {
+ muxframe.reset();
+ muxframe.setFin(frame.isFin());
+ muxframe.setFin(frame.isRsv1());
+ muxframe.setFin(frame.isRsv2());
+ muxframe.setFin(frame.isRsv3());
+ muxframe.setContinuation(true);
+ parseDataFramePayload(buffer);
+ }
+ else
+ {
+ // new frame
+ channelId = readChannelId(buffer);
+ if (channelId == 0)
+ {
+ parseControlBlocks(buffer);
+ }
+ else
+ {
+ parseDataFrame(buffer);
+ }
+ }
+ }
+ catch (MuxException e)
+ {
+ events.onMuxException(e);
+ }
+ catch (Throwable t)
+ {
+ events.onMuxException(new MuxException(t));
+ }
+ }
+
+ private void parseControlBlocks(ByteBuffer buffer)
+ {
+ // process the remaining buffer here.
+ while (buffer.remaining() > 0)
+ {
+ byte b = buffer.get();
+ byte opc = (byte)((byte)(b >> 5) & 0xFF);
+ b = (byte)(b & 0x1F);
+
+ try {
+ switch (opc)
+ {
+ case MuxOp.ADD_CHANNEL_REQUEST:
+ {
+ MuxAddChannelRequest op = new MuxAddChannelRequest();
+ op.setRsv((byte)((b & 0x1C) >> 2));
+ op.setEncoding((byte)(b & 0x03));
+ op.setChannelId(readChannelId(buffer));
+ long handshakeSize = read139EncodedSize(buffer);
+ op.setHandshake(readBlock(buffer,handshakeSize));
+ events.onMuxAddChannelRequest(op);
+ break;
+ }
+ case MuxOp.ADD_CHANNEL_RESPONSE:
+ {
+ MuxAddChannelResponse op = new MuxAddChannelResponse();
+ op.setFailed((b & 0x10) != 0);
+ op.setRsv((byte)((byte)(b & 0x0C) >> 2));
+ op.setEncoding((byte)(b & 0x03));
+ op.setChannelId(readChannelId(buffer));
+ long handshakeSize = read139EncodedSize(buffer);
+ op.setHandshake(readBlock(buffer,handshakeSize));
+ events.onMuxAddChannelResponse(op);
+ break;
+ }
+ case MuxOp.DROP_CHANNEL:
+ {
+ int rsv = (b & 0x1F);
+ long channelId = readChannelId(buffer);
+ long reasonSize = read139EncodedSize(buffer);
+ ByteBuffer reasonBuf = readBlock(buffer,reasonSize);
+ MuxDropChannel op = MuxDropChannel.parse(channelId,reasonBuf);
+ op.setRsv(rsv);
+ events.onMuxDropChannel(op);
+ break;
+ }
+ case MuxOp.FLOW_CONTROL:
+ {
+ MuxFlowControl op = new MuxFlowControl();
+ op.setRsv((byte)(b & 0x1F));
+ op.setChannelId(readChannelId(buffer));
+ op.setSendQuotaSize(read139EncodedSize(buffer));
+ events.onMuxFlowControl(op);
+ break;
+ }
+ case MuxOp.NEW_CHANNEL_SLOT:
+ {
+ MuxNewChannelSlot op = new MuxNewChannelSlot();
+ op.setRsv((byte)((b & 0x1E) >> 1));
+ op.setFallback((b & 0x01) != 0);
+ op.setNumberOfSlots(read139EncodedSize(buffer));
+ op.setInitialSendQuota(read139EncodedSize(buffer));
+ events.onMuxNewChannelSlot(op);
+ break;
+ }
+ default:
+ {
+ String err = String.format("Unknown Mux Control Code OPC [0x%X]",opc);
+ throw new MuxException(err);
+ }
+ }
+ }
+ catch (Throwable t)
+ {
+ LOG.warn(t);
+ throw new MuxException(t);
+ }
+ }
+ }
+
+ private void parseDataFrame(ByteBuffer buffer)
+ {
+ byte b = buffer.get();
+ boolean fin = ((b & 0x80) != 0);
+ boolean rsv1 = ((b & 0x40) != 0);
+ boolean rsv2 = ((b & 0x20) != 0);
+ boolean rsv3 = ((b & 0x10) != 0);
+ byte opcode = (byte)(b & 0x0F);
+
+ if (opcode == OpCode.CONTINUATION)
+ {
+ muxframe.setContinuation(true);
+ }
+ else
+ {
+ muxframe.reset();
+ muxframe.setOpCode(opcode);
+ }
+
+ muxframe.setChannelId(channelId);
+ muxframe.setFin(fin);
+ muxframe.setRsv1(rsv1);
+ muxframe.setRsv2(rsv2);
+ muxframe.setRsv3(rsv3);
+
+ parseDataFramePayload(buffer);
+ }
+
+ private void parseDataFramePayload(ByteBuffer buffer)
+ {
+ int capacity = buffer.remaining();
+ ByteBuffer payload = ByteBuffer.allocate(capacity);
+ payload.put(buffer);
+ BufferUtil.flipToFlush(payload,0);
+ muxframe.setPayload(payload);
+ try
+ {
+ LOG.debug("notifyFrame() - {}",muxframe);
+ events.onMuxedFrame(muxframe);
+ }
+ catch (Throwable t)
+ {
+ LOG.warn(t);
+ }
+ }
+
+ /**
+ * Per section <a href="https://tools.ietf.org/html/draft-ietf-hybi-websocket-multiplexing#section-9.1">9.1. Number Encoding in Multiplex Control
+ * Blocks</a>, read the 1/3/9 byte length using <a href="https://tools.ietf.org/html/rfc6455#section-5.2">Section 5.2 of RFC 6455</a>.
+ *
+ * @param buffer
+ * the buffer to read from
+ * @return the decoded size
+ * @throws MuxException
+ * when the encoding does not make sense per the spec, or it is a value above {@link Long#MAX_VALUE}
+ */
+ public long read139EncodedSize(ByteBuffer buffer)
+ {
+ long ret = -1;
+ long minValue = 0x00; // used to validate minimum # of bytes (per spec)
+ int cursor = 0;
+
+ byte b = buffer.get();
+ ret = (b & 0x7F);
+
+ if (ret == 0x7F)
+ {
+ // 9 byte length
+ ret = 0;
+ minValue = 0xFF_FF;
+ cursor = 8;
+ }
+ else if (ret == 0x7E)
+ {
+ // 3 byte length
+ ret = 0;
+ minValue = 0x7F;
+ cursor = 2;
+ }
+ else
+ {
+ // 1 byte length
+ // no validation of minimum bytes needed here
+ return ret;
+ }
+
+ // parse multi-byte length
+ while (cursor > 0)
+ {
+ ret = ret << 8;
+ b = buffer.get();
+ ret |= (b & 0xFF);
+ --cursor;
+ }
+
+ // validate minimum value per spec.
+ if (ret <= minValue)
+ {
+ String err = String.format("Invalid 1/3/9 length 0x%X (minimum value for chosen encoding is 0x%X)",ret,minValue);
+ throw new MuxException(err);
+ }
+
+ return ret;
+ }
+
+ private ByteBuffer readBlock(ByteBuffer buffer, long size)
+ {
+ if (size == 0)
+ {
+ return null;
+ }
+
+ if (size > buffer.remaining())
+ {
+ String err = String.format("Truncated data, expected %,d byte(s), but only %,d byte(s) remain",size,buffer.remaining());
+ throw new MuxException(err);
+ }
+
+ if (size > Integer.MAX_VALUE)
+ {
+ String err = String.format("[Int-Sane!] Buffer size %,d is too large to be supported (max allowed is %,d)",size,Integer.MAX_VALUE);
+ throw new MuxException(err);
+ }
+
+ ByteBuffer ret = ByteBuffer.allocate((int)size);
+ BufferUtil.put(buffer,ret);
+ BufferUtil.flipToFlush(ret,0);
+ return ret;
+ }
+
+ /**
+ * Read Channel ID using <a href="https://tools.ietf.org/html/draft-ietf-hybi-websocket-multiplexing#section-7">Section 7. Framing</a> techniques
+ *
+ * @param buffer
+ * the buffer to parse from.
+ * @return the channel Id
+ * @throws MuxException
+ * when the encoding does not make sense per the spec.
+ */
+ public long readChannelId(ByteBuffer buffer)
+ {
+ long id = -1;
+ long minValue = 0x00; // used to validate minimum # of bytes (per spec)
+ byte b = buffer.get();
+ int cursor = -1;
+ if ((b & 0x80) == 0)
+ {
+ // 7 bit channel id
+ // no validation of minimum bytes needed here
+ return (b & 0x7F);
+ }
+ else if ((b & 0x40) == 0)
+ {
+ // 14 bit channel id
+ id = (b & 0x3F);
+ minValue = 0x7F;
+ cursor = 1;
+ }
+ else if ((b & 0x20) == 0)
+ {
+ // 21 bit channel id
+ id = (b & 0x1F);
+ minValue = 0x3F_FF;
+ cursor = 2;
+ }
+ else
+ {
+ // 29 bit channel id
+ id = (b & 0x1F);
+ minValue = 0x1F_FF_FF;
+ cursor = 3;
+ }
+
+ while (cursor > 0)
+ {
+ id = id << 8;
+ b = buffer.get();
+ id |= (b & 0xFF);
+ --cursor;
+ }
+
+ // validate minimum value per spec.
+ if (id <= minValue)
+ {
+ String err = String.format("Invalid Channel ID 0x%X (minimum value for chosen encoding is 0x%X)",id,minValue);
+ throw new MuxException(err);
+ }
+
+ return id;
+ }
+
+ public void setEvents(MuxParser.Listener events)
+ {
+ this.events = events;
+ }
+}
diff --git a/jetty-websocket/websocket-mux-extension/src/main/java/org/eclipse/jetty/websocket/mux/MuxPhysicalConnectionException.java b/jetty-websocket/websocket-mux-extension/src/main/java/org/eclipse/jetty/websocket/mux/MuxPhysicalConnectionException.java
new file mode 100644
index 0000000..d808dfe
--- /dev/null
+++ b/jetty-websocket/websocket-mux-extension/src/main/java/org/eclipse/jetty/websocket/mux/MuxPhysicalConnectionException.java
@@ -0,0 +1,44 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.mux;
+
+import org.eclipse.jetty.websocket.mux.op.MuxDropChannel;
+
+public class MuxPhysicalConnectionException extends MuxException
+{
+ private static final long serialVersionUID = 1L;
+ private MuxDropChannel drop;
+
+ public MuxPhysicalConnectionException(MuxDropChannel.Reason code, String phrase)
+ {
+ super(phrase);
+ drop = new MuxDropChannel(0,code,phrase);
+ }
+
+ public MuxPhysicalConnectionException(MuxDropChannel.Reason code, String phrase, Throwable t)
+ {
+ super(phrase,t);
+ drop = new MuxDropChannel(0,code,phrase);
+ }
+
+ public MuxDropChannel getMuxDropChannel()
+ {
+ return drop;
+ }
+}
diff --git a/jetty-websocket/websocket-mux-extension/src/main/java/org/eclipse/jetty/websocket/mux/MuxRequest.java b/jetty-websocket/websocket-mux-extension/src/main/java/org/eclipse/jetty/websocket/mux/MuxRequest.java
new file mode 100644
index 0000000..deb623f
--- /dev/null
+++ b/jetty-websocket/websocket-mux-extension/src/main/java/org/eclipse/jetty/websocket/mux/MuxRequest.java
@@ -0,0 +1,63 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.mux;
+
+import java.nio.ByteBuffer;
+
+import org.eclipse.jetty.websocket.api.UpgradeRequest;
+
+public class MuxRequest extends UpgradeRequest
+{
+ public static final String HEADER_VALUE_DELIM="\"\\\n\r\t\f\b%+ ;=";
+
+ public static UpgradeRequest merge(UpgradeRequest baseReq, UpgradeRequest deltaReq)
+ {
+ MuxRequest req = new MuxRequest(baseReq);
+
+ // TODO: finish
+
+ return req;
+ }
+
+ private static String overlay(String val, String defVal)
+ {
+ if (val == null)
+ {
+ return defVal;
+ }
+ return val;
+ }
+
+ public static UpgradeRequest parse(ByteBuffer handshake)
+ {
+ MuxRequest req = new MuxRequest();
+ // TODO Auto-generated method stub
+ return req;
+ }
+
+ public MuxRequest()
+ {
+ super();
+ }
+
+ public MuxRequest(UpgradeRequest copy)
+ {
+ super();
+ }
+}
diff --git a/jetty-websocket/websocket-mux-extension/src/main/java/org/eclipse/jetty/websocket/mux/MuxResponse.java b/jetty-websocket/websocket-mux-extension/src/main/java/org/eclipse/jetty/websocket/mux/MuxResponse.java
new file mode 100644
index 0000000..cd2eaf6
--- /dev/null
+++ b/jetty-websocket/websocket-mux-extension/src/main/java/org/eclipse/jetty/websocket/mux/MuxResponse.java
@@ -0,0 +1,26 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.mux;
+
+import org.eclipse.jetty.websocket.api.UpgradeResponse;
+
+public class MuxResponse extends UpgradeResponse
+{
+
+}
diff --git a/jetty-websocket/websocket-mux-extension/src/main/java/org/eclipse/jetty/websocket/mux/MuxedFrame.java b/jetty-websocket/websocket-mux-extension/src/main/java/org/eclipse/jetty/websocket/mux/MuxedFrame.java
new file mode 100644
index 0000000..aef64d2
--- /dev/null
+++ b/jetty-websocket/websocket-mux-extension/src/main/java/org/eclipse/jetty/websocket/mux/MuxedFrame.java
@@ -0,0 +1,73 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.mux;
+
+import org.eclipse.jetty.websocket.common.OpCode;
+import org.eclipse.jetty.websocket.common.WebSocketFrame;
+
+public class MuxedFrame extends WebSocketFrame
+{
+ private long channelId = -1;
+
+ public MuxedFrame()
+ {
+ super();
+ }
+
+ public MuxedFrame(MuxedFrame frame)
+ {
+ super(frame);
+ this.channelId = frame.channelId;
+ }
+
+ public long getChannelId()
+ {
+ return channelId;
+ }
+
+ @Override
+ public void reset()
+ {
+ super.reset();
+ this.channelId = -1;
+ }
+
+ public void setChannelId(long channelId)
+ {
+ this.channelId = channelId;
+ }
+
+ @Override
+ public String toString()
+ {
+ StringBuilder b = new StringBuilder();
+ b.append(OpCode.name(getOpCode()));
+ b.append('[');
+ b.append("channel=").append(channelId);
+ b.append(",len=").append(getPayloadLength());
+ b.append(",fin=").append(isFin());
+ b.append(",rsv=");
+ b.append(isRsv1()?'1':'.');
+ b.append(isRsv2()?'1':'.');
+ b.append(isRsv3()?'1':'.');
+ b.append(",continuation=").append(isContinuation());
+ b.append(']');
+ return b.toString();
+ }
+}
diff --git a/jetty-websocket/websocket-mux-extension/src/main/java/org/eclipse/jetty/websocket/mux/Muxer.java b/jetty-websocket/websocket-mux-extension/src/main/java/org/eclipse/jetty/websocket/mux/Muxer.java
new file mode 100644
index 0000000..28bed38
--- /dev/null
+++ b/jetty-websocket/websocket-mux-extension/src/main/java/org/eclipse/jetty/websocket/mux/Muxer.java
@@ -0,0 +1,439 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.mux;
+
+import java.io.IOException;
+import java.net.InetSocketAddress;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.eclipse.jetty.util.StringUtil;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+import org.eclipse.jetty.websocket.api.StatusCode;
+import org.eclipse.jetty.websocket.api.UpgradeRequest;
+import org.eclipse.jetty.websocket.api.UpgradeResponse;
+import org.eclipse.jetty.websocket.api.WebSocketBehavior;
+import org.eclipse.jetty.websocket.api.WebSocketPolicy;
+import org.eclipse.jetty.websocket.api.WriteCallback;
+import org.eclipse.jetty.websocket.api.extensions.Frame;
+import org.eclipse.jetty.websocket.api.extensions.IncomingFrames;
+import org.eclipse.jetty.websocket.api.extensions.OutgoingFrames;
+import org.eclipse.jetty.websocket.common.LogicalConnection;
+import org.eclipse.jetty.websocket.common.WebSocketFrame;
+import org.eclipse.jetty.websocket.mux.add.MuxAddClient;
+import org.eclipse.jetty.websocket.mux.add.MuxAddServer;
+import org.eclipse.jetty.websocket.mux.op.MuxAddChannelRequest;
+import org.eclipse.jetty.websocket.mux.op.MuxAddChannelResponse;
+import org.eclipse.jetty.websocket.mux.op.MuxDropChannel;
+import org.eclipse.jetty.websocket.mux.op.MuxFlowControl;
+import org.eclipse.jetty.websocket.mux.op.MuxNewChannelSlot;
+
+/**
+ * Muxer responsible for managing sub-channels.
+ * <p>
+ * Maintains a 1 (incoming and outgoing mux encapsulated frames) to many (per-channel incoming/outgoing standard websocket frames) relationship, along with
+ * routing of {@link MuxControlBlock} events.
+ * <p>
+ * Control Channel events (channel ID == 0) are handled by the Muxer.
+ */
+public class Muxer implements IncomingFrames, MuxParser.Listener
+{
+ private static final int CONTROL_CHANNEL_ID = 0;
+
+ private static final Logger LOG = Log.getLogger(Muxer.class);
+
+ /**
+ * Map of sub-channels, key is the channel Id.
+ */
+ private Map<Long, MuxChannel> channels = new HashMap<Long, MuxChannel>();
+
+ private final WebSocketPolicy policy;
+ private final LogicalConnection physicalConnection;
+ private InetSocketAddress remoteAddress;
+ /** Parsing frames destined for sub-channels */
+ private MuxParser parser;
+ /** Generating frames destined for physical connection */
+ private MuxGenerator generator;
+ private MuxAddServer addServer;
+ private MuxAddClient addClient;
+ /** The original request headers, used for delta encoded AddChannelRequest blocks */
+ private UpgradeRequest physicalRequestHeaders;
+ /** The original response headers, used for delta encoded AddChannelResponse blocks */
+ private UpgradeResponse physicalResponseHeaders;
+
+ public Muxer(final LogicalConnection connection)
+ {
+ this.physicalConnection = connection;
+ this.policy = connection.getPolicy().clonePolicy();
+ this.parser = new MuxParser();
+ this.parser.setEvents(this);
+ this.generator = new MuxGenerator();
+ }
+
+ public MuxAddClient getAddClient()
+ {
+ return addClient;
+ }
+
+ public MuxAddServer getAddServer()
+ {
+ return addServer;
+ }
+
+ public MuxChannel getChannel(long channelId, boolean create)
+ {
+ if (channelId == CONTROL_CHANNEL_ID)
+ {
+ throw new MuxPhysicalConnectionException(MuxDropChannel.Reason.UNKNOWN_MUX_CONTROL_BLOCK,"Invalid Channel ID");
+ }
+
+ MuxChannel channel = channels.get(channelId);
+ if (channel == null)
+ {
+ if (create)
+ {
+ channel = new MuxChannel(channelId,this);
+ channels.put(channelId,channel);
+ }
+ else
+ {
+ throw new MuxPhysicalConnectionException(MuxDropChannel.Reason.UNKNOWN_MUX_CONTROL_BLOCK,"Unknown Channel ID");
+ }
+ }
+ return channel;
+ }
+
+ public WebSocketPolicy getPolicy()
+ {
+ return policy;
+ }
+
+ /**
+ * Get the remote address of the physical connection.
+ *
+ * @return the remote address of the physical connection
+ */
+ public InetSocketAddress getRemoteAddress()
+ {
+ return this.remoteAddress;
+ }
+
+ /**
+ * Incoming parser errors
+ */
+ @Override
+ public void incomingError(Throwable e)
+ {
+ MuxDropChannel.Reason reason = MuxDropChannel.Reason.PHYSICAL_CONNECTION_FAILED;
+ String phrase = String.format("%s: %s", e.getClass().getName(), e.getMessage());
+ mustFailPhysicalConnection(new MuxPhysicalConnectionException(reason,phrase));
+ }
+
+ /**
+ * Incoming mux encapsulated frames.
+ */
+ @Override
+ public void incomingFrame(Frame frame)
+ {
+ parser.parse(frame);
+ }
+
+ /**
+ * Is the muxer and the physical connection still open?
+ *
+ * @return true if open
+ */
+ public boolean isOpen()
+ {
+ return physicalConnection.isOpen();
+ }
+
+ public String mergeHeaders(List<String> physicalHeaders, String deltaHeaders)
+ {
+ // TODO Auto-generated method stub
+ return null;
+ }
+
+ /**
+ * Per spec, the physical connection must be failed.
+ * <p>
+ * <a href="https://tools.ietf.org/html/draft-ietf-hybi-websocket-multiplexing-08#section-18">Section 18. Fail the Physical Connection.</a>
+ *
+ * <blockquote> To _Fail the Physical Connection_, an endpoint MUST send a DropChannel multiplex control block with objective channel ID of 0 and drop
+ * reason code in the range of 2000-2999, and then _Fail the WebSocket Connection_ on the physical connection with status code of 1011. </blockquote>
+ */
+ private void mustFailPhysicalConnection(MuxPhysicalConnectionException muxe)
+ {
+ // TODO: stop muxer from receiving incoming sub-channel traffic.
+
+ MuxDropChannel drop = muxe.getMuxDropChannel();
+ LOG.warn(muxe);
+ try
+ {
+ generator.generate(null,drop);
+ }
+ catch (IOException ioe)
+ {
+ LOG.warn("Unable to send mux DropChannel",ioe);
+ }
+
+ String reason = "Mux[MUST FAIL]" + drop.getPhrase();
+ reason = StringUtil.truncate(reason,WebSocketFrame.MAX_CONTROL_PAYLOAD);
+ this.physicalConnection.close(StatusCode.SERVER_ERROR,reason);
+
+ // TODO: trigger abnormal close for all sub-channels.
+ }
+
+ /**
+ * Incoming mux control block, destined for the control channel (id 0)
+ */
+ @Override
+ public void onMuxAddChannelRequest(MuxAddChannelRequest request)
+ {
+ if (policy.getBehavior() == WebSocketBehavior.CLIENT)
+ {
+ throw new MuxPhysicalConnectionException(MuxDropChannel.Reason.UNKNOWN_MUX_CONTROL_BLOCK,"AddChannelRequest not allowed per spec");
+ }
+
+ if (request.getRsv() != 0)
+ {
+ throw new MuxPhysicalConnectionException(MuxDropChannel.Reason.UNKNOWN_REQUEST_ENCODING,"RSV Not allowed to be set");
+ }
+
+ // Pre-allocate channel.
+ long channelId = request.getChannelId();
+ MuxChannel channel = getChannel(channelId, true);
+
+ // submit to upgrade handshake process
+ try
+ {
+ switch (request.getEncoding())
+ {
+ case MuxAddChannelRequest.IDENTITY_ENCODING:
+ {
+ UpgradeRequest idenReq = MuxRequest.parse(request.getHandshake());
+ addServer.handshake(this,channel,idenReq);
+ break;
+ }
+ case MuxAddChannelRequest.DELTA_ENCODING:
+ {
+ UpgradeRequest baseReq = addServer.getPhysicalHandshakeRequest();
+ UpgradeRequest deltaReq = MuxRequest.parse(request.getHandshake());
+ UpgradeRequest mergedReq = MuxRequest.merge(baseReq,deltaReq);
+
+ addServer.handshake(this,channel,mergedReq);
+ break;
+ }
+ default:
+ {
+ throw new MuxPhysicalConnectionException(MuxDropChannel.Reason.BAD_REQUEST,"Unrecognized request encoding");
+ }
+ }
+ }
+ catch (MuxPhysicalConnectionException e)
+ {
+ throw e;
+ }
+ catch (Throwable t)
+ {
+ throw new MuxPhysicalConnectionException(MuxDropChannel.Reason.BAD_REQUEST,"Unable to parse request",t);
+ }
+ }
+
+ /**
+ * Incoming mux control block, destined for the control channel (id 0)
+ */
+ @Override
+ public void onMuxAddChannelResponse(MuxAddChannelResponse response)
+ {
+ if (policy.getBehavior() == WebSocketBehavior.SERVER)
+ {
+ throw new MuxPhysicalConnectionException(MuxDropChannel.Reason.UNKNOWN_MUX_CONTROL_BLOCK,"AddChannelResponse not allowed per spec");
+ }
+
+ if (response.getRsv() != 0)
+ {
+ throw new MuxPhysicalConnectionException(MuxDropChannel.Reason.UNKNOWN_RESPONSE_ENCODING,"RSV Not allowed to be set");
+ }
+
+ // Process channel
+ long channelId = response.getChannelId();
+ MuxChannel channel = getChannel(channelId,false);
+
+ // Process Response headers
+ try
+ {
+ // Parse Response
+
+ // TODO: Sec-WebSocket-Accept header
+ // TODO: Sec-WebSocket-Extensions header
+ // TODO: Setup extensions
+ // TODO: Setup sessions
+
+ // Trigger channel open
+ channel.onOpen();
+ }
+ catch (Throwable t)
+ {
+ throw new MuxPhysicalConnectionException(MuxDropChannel.Reason.BAD_RESPONSE,"Unable to parse response",t);
+ }
+ }
+
+ /**
+ * Incoming mux control block, destined for the control channel (id 0)
+ */
+ @Override
+ public void onMuxDropChannel(MuxDropChannel drop)
+ {
+ // Process channel
+ long channelId = drop.getChannelId();
+ MuxChannel channel = getChannel(channelId,false);
+
+ String reason = "Mux " + drop.toString();
+ reason = StringUtil.truncate(reason,(WebSocketFrame.MAX_CONTROL_PAYLOAD - 2));
+ channel.close(StatusCode.PROTOCOL,reason);
+ // TODO: set channel to inactive?
+ }
+
+ /**
+ * Incoming mux-unwrapped frames, destined for a sub-channel
+ */
+ @Override
+ public void onMuxedFrame(MuxedFrame frame)
+ {
+ MuxChannel subchannel = channels.get(frame.getChannelId());
+ subchannel.incomingFrame(frame);
+ }
+
+ @Override
+ public void onMuxException(MuxException e)
+ {
+ if (e instanceof MuxPhysicalConnectionException)
+ {
+ mustFailPhysicalConnection((MuxPhysicalConnectionException)e);
+ }
+
+ LOG.warn(e);
+ // TODO: handle other (non physical) mux exceptions how?
+ }
+
+ /**
+ * Incoming mux control block, destined for the control channel (id 0)
+ */
+ @Override
+ public void onMuxFlowControl(MuxFlowControl flow)
+ {
+ if (flow.getSendQuotaSize() > 0x7F_FF_FF_FF_FF_FF_FF_FFL)
+ {
+ throw new MuxPhysicalConnectionException(MuxDropChannel.Reason.SEND_QUOTA_OVERFLOW,"Send Quota Overflow");
+ }
+
+ // Process channel
+ long channelId = flow.getChannelId();
+ MuxChannel channel = getChannel(channelId,false);
+
+ // TODO: set channel quota
+ }
+
+ /**
+ * Incoming mux control block, destined for the control channel (id 0)
+ */
+ @Override
+ public void onMuxNewChannelSlot(MuxNewChannelSlot slot)
+ {
+ if (policy.getBehavior() == WebSocketBehavior.SERVER)
+ {
+ throw new MuxPhysicalConnectionException(MuxDropChannel.Reason.UNKNOWN_MUX_CONTROL_BLOCK,"NewChannelSlot not allowed per spec");
+ }
+
+ if (slot.isFallback())
+ {
+ if (slot.getNumberOfSlots() == 0)
+ {
+ throw new MuxPhysicalConnectionException(MuxDropChannel.Reason.UNKNOWN_MUX_CONTROL_BLOCK,"Cannot have 0 number of slots during fallback");
+ }
+ if (slot.getInitialSendQuota() == 0)
+ {
+ throw new MuxPhysicalConnectionException(MuxDropChannel.Reason.UNKNOWN_MUX_CONTROL_BLOCK,"Cannot have 0 initial send quota during fallback");
+ }
+ }
+
+ // TODO: handle channel slot
+ }
+
+ /**
+ * Outgoing frame, without mux encapsulated payload.
+ */
+ public void output(long channelId, Frame frame, WriteCallback callback)
+ {
+ if (LOG.isDebugEnabled())
+ {
+ LOG.debug("output({}, {})",channelId,frame,callback);
+ }
+ generator.generate(channelId,frame,callback);
+ }
+
+ /**
+ * Write an OP out the physical connection.
+ *
+ * @param op
+ * the mux operation to write
+ * @throws IOException
+ */
+ public void output(MuxControlBlock op) throws IOException
+ {
+ generator.generate(null,op);
+ }
+
+ public void setAddClient(MuxAddClient addClient)
+ {
+ this.addClient = addClient;
+ }
+
+ public void setAddServer(MuxAddServer addServer)
+ {
+ this.addServer = addServer;
+ }
+
+ public void setOutgoingFramesHandler(OutgoingFrames outgoing)
+ {
+ this.generator.setOutgoing(outgoing);
+ }
+
+ /**
+ * Set the remote address of the physical connection.
+ * <p>
+ * This address made available to sub-channels.
+ *
+ * @param remoteAddress
+ * the remote address
+ */
+ public void setRemoteAddress(InetSocketAddress remoteAddress)
+ {
+ this.remoteAddress = remoteAddress;
+ }
+
+ @Override
+ public String toString()
+ {
+ return String.format("Muxer[subChannels.size=%d]",channels.size());
+ }
+}
diff --git a/jetty-websocket/websocket-mux-extension/src/main/java/org/eclipse/jetty/websocket/mux/add/MuxAddClient.java b/jetty-websocket/websocket-mux-extension/src/main/java/org/eclipse/jetty/websocket/mux/add/MuxAddClient.java
new file mode 100644
index 0000000..6fa1d35
--- /dev/null
+++ b/jetty-websocket/websocket-mux-extension/src/main/java/org/eclipse/jetty/websocket/mux/add/MuxAddClient.java
@@ -0,0 +1,30 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.mux.add;
+
+import org.eclipse.jetty.websocket.common.WebSocketSession;
+import org.eclipse.jetty.websocket.mux.op.MuxAddChannelResponse;
+
+/**
+ * Interface for Mux Client to handle receiving a AddChannelResponse
+ */
+public interface MuxAddClient
+{
+ WebSocketSession createSession(MuxAddChannelResponse response);
+}
diff --git a/jetty-websocket/websocket-mux-extension/src/main/java/org/eclipse/jetty/websocket/mux/add/MuxAddServer.java b/jetty-websocket/websocket-mux-extension/src/main/java/org/eclipse/jetty/websocket/mux/add/MuxAddServer.java
new file mode 100644
index 0000000..89980af
--- /dev/null
+++ b/jetty-websocket/websocket-mux-extension/src/main/java/org/eclipse/jetty/websocket/mux/add/MuxAddServer.java
@@ -0,0 +1,52 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.mux.add;
+
+import java.io.IOException;
+
+import org.eclipse.jetty.websocket.api.UpgradeRequest;
+import org.eclipse.jetty.websocket.api.UpgradeResponse;
+import org.eclipse.jetty.websocket.common.WebSocketSession;
+import org.eclipse.jetty.websocket.mux.MuxChannel;
+import org.eclipse.jetty.websocket.mux.MuxException;
+import org.eclipse.jetty.websocket.mux.Muxer;
+
+/**
+ * Server interface, for dealing with incoming AddChannelRequest / AddChannelResponse flows.
+ */
+public interface MuxAddServer
+{
+ public UpgradeRequest getPhysicalHandshakeRequest();
+
+ public UpgradeResponse getPhysicalHandshakeResponse();
+
+ /**
+ * Perform the handshake.
+ *
+ * @param channel
+ * the channel to attach the {@link WebSocketSession} to.
+ * @param requestHandshake
+ * the request handshake (request headers)
+ * @throws AbstractMuxException
+ * if unable to handshake
+ * @throws IOException
+ * if unable to parse request headers
+ */
+ void handshake(Muxer muxer, MuxChannel channel, UpgradeRequest request) throws MuxException, IOException;
+}
diff --git a/jetty-websocket/websocket-mux-extension/src/main/java/org/eclipse/jetty/websocket/mux/add/package-info.java b/jetty-websocket/websocket-mux-extension/src/main/java/org/eclipse/jetty/websocket/mux/add/package-info.java
new file mode 100644
index 0000000..1834e97
--- /dev/null
+++ b/jetty-websocket/websocket-mux-extension/src/main/java/org/eclipse/jetty/websocket/mux/add/package-info.java
@@ -0,0 +1,23 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+/**
+ * Jetty WebSocket Common : MUX Extension Add Channel Handling [<em>Unstable Early Draft</em>]
+ */
+package org.eclipse.jetty.websocket.mux.add;
+
diff --git a/jetty-websocket/websocket-mux-extension/src/main/java/org/eclipse/jetty/websocket/mux/client/MuxClientAddHandler.java b/jetty-websocket/websocket-mux-extension/src/main/java/org/eclipse/jetty/websocket/mux/client/MuxClientAddHandler.java
new file mode 100644
index 0000000..daa0ac3
--- /dev/null
+++ b/jetty-websocket/websocket-mux-extension/src/main/java/org/eclipse/jetty/websocket/mux/client/MuxClientAddHandler.java
@@ -0,0 +1,33 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.mux.client;
+
+import org.eclipse.jetty.websocket.common.WebSocketSession;
+import org.eclipse.jetty.websocket.mux.add.MuxAddClient;
+import org.eclipse.jetty.websocket.mux.op.MuxAddChannelResponse;
+
+public class MuxClientAddHandler implements MuxAddClient
+{
+ @Override
+ public WebSocketSession createSession(MuxAddChannelResponse response)
+ {
+ // TODO Auto-generated method stub
+ return null;
+ }
+}
diff --git a/jetty-websocket/websocket-mux-extension/src/main/java/org/eclipse/jetty/websocket/mux/client/MuxClientExtension.java b/jetty-websocket/websocket-mux-extension/src/main/java/org/eclipse/jetty/websocket/mux/client/MuxClientExtension.java
new file mode 100644
index 0000000..fd03392
--- /dev/null
+++ b/jetty-websocket/websocket-mux-extension/src/main/java/org/eclipse/jetty/websocket/mux/client/MuxClientExtension.java
@@ -0,0 +1,31 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.mux.client;
+
+import org.eclipse.jetty.websocket.mux.AbstractMuxExtension;
+import org.eclipse.jetty.websocket.mux.Muxer;
+
+public class MuxClientExtension extends AbstractMuxExtension
+{
+ @Override
+ public void configureMuxer(Muxer muxer)
+ {
+ muxer.setAddClient(new MuxClientAddHandler());
+ }
+}
diff --git a/jetty-websocket/websocket-mux-extension/src/main/java/org/eclipse/jetty/websocket/mux/client/package-info.java b/jetty-websocket/websocket-mux-extension/src/main/java/org/eclipse/jetty/websocket/mux/client/package-info.java
new file mode 100644
index 0000000..e8d7942
--- /dev/null
+++ b/jetty-websocket/websocket-mux-extension/src/main/java/org/eclipse/jetty/websocket/mux/client/package-info.java
@@ -0,0 +1,23 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+/**
+ * Jetty WebSocket Client : MUX Extension [<em>Unstable Early Draft</em>]
+ */
+package org.eclipse.jetty.websocket.mux.client;
+
diff --git a/jetty-websocket/websocket-mux-extension/src/main/java/org/eclipse/jetty/websocket/mux/op/MuxAddChannelRequest.java b/jetty-websocket/websocket-mux-extension/src/main/java/org/eclipse/jetty/websocket/mux/op/MuxAddChannelRequest.java
new file mode 100644
index 0000000..4647c5b
--- /dev/null
+++ b/jetty-websocket/websocket-mux-extension/src/main/java/org/eclipse/jetty/websocket/mux/op/MuxAddChannelRequest.java
@@ -0,0 +1,114 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.mux.op;
+
+import java.nio.ByteBuffer;
+
+import org.eclipse.jetty.util.BufferUtil;
+import org.eclipse.jetty.util.StringUtil;
+import org.eclipse.jetty.websocket.mux.MuxControlBlock;
+import org.eclipse.jetty.websocket.mux.MuxOp;
+
+public class MuxAddChannelRequest implements MuxControlBlock
+{
+ public static final byte IDENTITY_ENCODING = (byte)0x00;
+ public static final byte DELTA_ENCODING = (byte)0x01;
+
+ private long channelId = -1;
+ private byte encoding;
+ private ByteBuffer handshake;
+ private byte rsv;
+
+ public long getChannelId()
+ {
+ return channelId;
+ }
+
+ public byte getEncoding()
+ {
+ return encoding;
+ }
+
+ public ByteBuffer getHandshake()
+ {
+ return handshake;
+ }
+
+ public long getHandshakeSize()
+ {
+ if (handshake == null)
+ {
+ return 0;
+ }
+ return handshake.remaining();
+ }
+
+ @Override
+ public int getOpCode()
+ {
+ return MuxOp.ADD_CHANNEL_REQUEST;
+ }
+
+ public byte getRsv()
+ {
+ return rsv;
+ }
+
+ public boolean isDeltaEncoded()
+ {
+ return (encoding == DELTA_ENCODING);
+ }
+
+ public boolean isIdentityEncoded()
+ {
+ return (encoding == IDENTITY_ENCODING);
+ }
+
+ public void setChannelId(long channelId)
+ {
+ this.channelId = channelId;
+ }
+
+ public void setEncoding(byte enc)
+ {
+ this.encoding = enc;
+ }
+
+ public void setHandshake(ByteBuffer handshake)
+ {
+ if (handshake == null)
+ {
+ this.handshake = null;
+ }
+ else
+ {
+ this.handshake = handshake.slice();
+ }
+ }
+
+ public void setHandshake(String rawstring)
+ {
+ setHandshake(BufferUtil.toBuffer(rawstring,StringUtil.__UTF8_CHARSET));
+ }
+
+ public void setRsv(byte rsv)
+ {
+ this.rsv = rsv;
+ }
+}
diff --git a/jetty-websocket/websocket-mux-extension/src/main/java/org/eclipse/jetty/websocket/mux/op/MuxAddChannelResponse.java b/jetty-websocket/websocket-mux-extension/src/main/java/org/eclipse/jetty/websocket/mux/op/MuxAddChannelResponse.java
new file mode 100644
index 0000000..b2a2dd6
--- /dev/null
+++ b/jetty-websocket/websocket-mux-extension/src/main/java/org/eclipse/jetty/websocket/mux/op/MuxAddChannelResponse.java
@@ -0,0 +1,125 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.mux.op;
+
+import java.nio.ByteBuffer;
+
+import org.eclipse.jetty.util.BufferUtil;
+import org.eclipse.jetty.util.StringUtil;
+import org.eclipse.jetty.websocket.mux.MuxControlBlock;
+import org.eclipse.jetty.websocket.mux.MuxOp;
+
+public class MuxAddChannelResponse implements MuxControlBlock
+{
+ public static final byte IDENTITY_ENCODING = (byte)0x00;
+ public static final byte DELTA_ENCODING = (byte)0x01;
+
+ private long channelId;
+ private byte encoding;
+ private byte rsv;
+ private boolean failed = false;
+ private ByteBuffer handshake;
+
+ public long getChannelId()
+ {
+ return channelId;
+ }
+
+ public byte getEncoding()
+ {
+ return encoding;
+ }
+
+ public ByteBuffer getHandshake()
+ {
+ return handshake;
+ }
+
+ public long getHandshakeSize()
+ {
+ if (handshake == null)
+ {
+ return 0;
+ }
+ return handshake.remaining();
+ }
+
+ @Override
+ public int getOpCode()
+ {
+ return MuxOp.ADD_CHANNEL_RESPONSE;
+ }
+
+ public byte getRsv()
+ {
+ return rsv;
+ }
+
+ public boolean isDeltaEncoded()
+ {
+ return (encoding == DELTA_ENCODING);
+ }
+
+ public boolean isFailed()
+ {
+ return failed;
+ }
+
+ public boolean isIdentityEncoded()
+ {
+ return (encoding == IDENTITY_ENCODING);
+ }
+
+ public void setChannelId(long channelId)
+ {
+ this.channelId = channelId;
+ }
+
+ public void setEncoding(byte enc)
+ {
+ this.encoding = enc;
+ }
+
+ public void setFailed(boolean failed)
+ {
+ this.failed = failed;
+ }
+
+ public void setHandshake(ByteBuffer handshake)
+ {
+ if (handshake == null)
+ {
+ this.handshake = null;
+ }
+ else
+ {
+ this.handshake = handshake.slice();
+ }
+ }
+
+ public void setHandshake(String responseHandshake)
+ {
+ setHandshake(BufferUtil.toBuffer(responseHandshake,StringUtil.__UTF8_CHARSET));
+ }
+
+ public void setRsv(byte rsv)
+ {
+ this.rsv = rsv;
+ }
+}
diff --git a/jetty-websocket/websocket-mux-extension/src/main/java/org/eclipse/jetty/websocket/mux/op/MuxDropChannel.java b/jetty-websocket/websocket-mux-extension/src/main/java/org/eclipse/jetty/websocket/mux/op/MuxDropChannel.java
new file mode 100644
index 0000000..edd186f
--- /dev/null
+++ b/jetty-websocket/websocket-mux-extension/src/main/java/org/eclipse/jetty/websocket/mux/op/MuxDropChannel.java
@@ -0,0 +1,183 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.mux.op;
+
+import java.nio.ByteBuffer;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.eclipse.jetty.websocket.mux.MuxControlBlock;
+import org.eclipse.jetty.websocket.mux.MuxOp;
+
+public class MuxDropChannel implements MuxControlBlock
+{
+ /**
+ * Outlined in <a href="https://tools.ietf.org/html/draft-ietf-hybi-websocket-multiplexing-05#section-9.4.1">Section 9.4.1. Drop Reason Codes</a>
+ */
+ public static enum Reason
+ {
+ // Normal Close : (1000-1999)
+ NORMAL_CLOSURE(1000),
+
+ // Failures in Physical Connection : (2000-2999)
+ PHYSICAL_CONNECTION_FAILED(2000),
+ INVALID_ENCAPSULATING_MESSAGE(2001),
+ CHANNEL_ID_TRUNCATED(2002),
+ ENCAPSULATED_FRAME_TRUNCATED(2003),
+ UNKNOWN_MUX_CONTROL_OPC(2004),
+ UNKNOWN_MUX_CONTROL_BLOCK(2005),
+ CHANNEL_ALREADY_EXISTS(2006),
+ NEW_CHANNEL_SLOT_VIOLATION(2007),
+ NEW_CHANNEL_SLOT_OVERFLOW(2008),
+ BAD_REQUEST(2009),
+ UNKNOWN_REQUEST_ENCODING(2010),
+ BAD_RESPONSE(2011),
+ UNKNOWN_RESPONSE_ENCODING(2012),
+
+ // Failures in Logical Connections : (3000-3999)
+ LOGICAL_CHANNEL_FAILED(3000),
+ SEND_QUOTA_VIOLATION(3005),
+ SEND_QUOTA_OVERFLOW(3006),
+ IDLE_TIMEOUT(3007),
+ DROP_CHANNEL_ACK(3008),
+
+ // Other Peer Actions : (4000-4999)
+ USE_ANOTHER_PHYSICAL_CONNECTION(4001),
+ BUSY(4002);
+
+ private static final Map<Integer, Reason> codeMap;
+
+ static
+ {
+ codeMap = new HashMap<>();
+ for (Reason r : values())
+ {
+ codeMap.put(r.getValue(),r);
+ }
+ }
+
+ public static Reason valueOf(int code)
+ {
+ return codeMap.get(code);
+ }
+
+ private final int code;
+
+ private Reason(int code)
+ {
+ this.code = code;
+ }
+
+ public int getValue()
+ {
+ return code;
+ }
+ }
+
+ public static MuxDropChannel parse(long channelId, ByteBuffer payload)
+ {
+ // TODO Auto-generated method stub
+ return null;
+ }
+
+ private final long channelId;
+ private final Reason code;
+ private String phrase;
+ private int rsv;
+
+ /**
+ * Normal Drop. no reason Phrase.
+ *
+ * @param channelId
+ * the logical channel Id to perform drop against.
+ */
+ public MuxDropChannel(long channelId)
+ {
+ this(channelId,Reason.NORMAL_CLOSURE,null);
+ }
+
+ /**
+ * Drop with reason code and optional phrase
+ *
+ * @param channelId
+ * the logical channel Id to perform drop against.
+ * @param code
+ * reason code
+ * @param phrase
+ * optional human readable phrase
+ */
+ public MuxDropChannel(long channelId, int code, String phrase)
+ {
+ this(channelId, Reason.valueOf(code), phrase);
+ }
+
+ /**
+ * Drop with reason code and optional phrase
+ *
+ * @param channelId
+ * the logical channel Id to perform drop against.
+ * @param code
+ * reason code
+ * @param phrase
+ * optional human readable phrase
+ */
+ public MuxDropChannel(long channelId, Reason code, String phrase)
+ {
+ this.channelId = channelId;
+ this.code = code;
+ this.phrase = phrase;
+ }
+
+ public ByteBuffer asReasonBuffer()
+ {
+ // TODO: convert to reason buffer
+ return null;
+ }
+
+ public long getChannelId()
+ {
+ return channelId;
+ }
+
+ public Reason getCode()
+ {
+ return code;
+ }
+
+ @Override
+ public int getOpCode()
+ {
+ return MuxOp.DROP_CHANNEL;
+ }
+
+ public String getPhrase()
+ {
+ return phrase;
+ }
+
+ public int getRsv()
+ {
+ return rsv;
+ }
+
+ public void setRsv(int rsv)
+ {
+ this.rsv = rsv;
+ }
+}
diff --git a/jetty-websocket/websocket-mux-extension/src/main/java/org/eclipse/jetty/websocket/mux/op/MuxFlowControl.java b/jetty-websocket/websocket-mux-extension/src/main/java/org/eclipse/jetty/websocket/mux/op/MuxFlowControl.java
new file mode 100644
index 0000000..c238c6f
--- /dev/null
+++ b/jetty-websocket/websocket-mux-extension/src/main/java/org/eclipse/jetty/websocket/mux/op/MuxFlowControl.java
@@ -0,0 +1,65 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.mux.op;
+
+import org.eclipse.jetty.websocket.mux.MuxControlBlock;
+import org.eclipse.jetty.websocket.mux.MuxOp;
+
+public class MuxFlowControl implements MuxControlBlock
+{
+ private long channelId;
+ private byte rsv;
+ private long sendQuotaSize;
+
+ public long getChannelId()
+ {
+ return channelId;
+ }
+
+ @Override
+ public int getOpCode()
+ {
+ return MuxOp.FLOW_CONTROL;
+ }
+
+ public byte getRsv()
+ {
+ return rsv;
+ }
+
+ public long getSendQuotaSize()
+ {
+ return sendQuotaSize;
+ }
+
+ public void setChannelId(long channelId)
+ {
+ this.channelId = channelId;
+ }
+
+ public void setRsv(byte rsv)
+ {
+ this.rsv = rsv;
+ }
+
+ public void setSendQuotaSize(long sendQuotaSize)
+ {
+ this.sendQuotaSize = sendQuotaSize;
+ }
+}
diff --git a/jetty-websocket/websocket-mux-extension/src/main/java/org/eclipse/jetty/websocket/mux/op/MuxNewChannelSlot.java b/jetty-websocket/websocket-mux-extension/src/main/java/org/eclipse/jetty/websocket/mux/op/MuxNewChannelSlot.java
new file mode 100644
index 0000000..4fae241
--- /dev/null
+++ b/jetty-websocket/websocket-mux-extension/src/main/java/org/eclipse/jetty/websocket/mux/op/MuxNewChannelSlot.java
@@ -0,0 +1,76 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.mux.op;
+
+import org.eclipse.jetty.websocket.mux.MuxControlBlock;
+import org.eclipse.jetty.websocket.mux.MuxOp;
+
+public class MuxNewChannelSlot implements MuxControlBlock
+{
+ private boolean fallback;
+ private long initialSendQuota;
+ private long numberOfSlots;
+ private byte rsv;
+
+ public long getInitialSendQuota()
+ {
+ return initialSendQuota;
+ }
+
+ public long getNumberOfSlots()
+ {
+ return numberOfSlots;
+ }
+
+ @Override
+ public int getOpCode()
+ {
+ return MuxOp.NEW_CHANNEL_SLOT;
+ }
+
+ public byte getRsv()
+ {
+ return rsv;
+ }
+
+ public boolean isFallback()
+ {
+ return fallback;
+ }
+
+ public void setFallback(boolean fallback)
+ {
+ this.fallback = fallback;
+ }
+
+ public void setInitialSendQuota(long initialSendQuota)
+ {
+ this.initialSendQuota = initialSendQuota;
+ }
+
+ public void setNumberOfSlots(long numberOfSlots)
+ {
+ this.numberOfSlots = numberOfSlots;
+ }
+
+ public void setRsv(byte rsv)
+ {
+ this.rsv = rsv;
+ }
+}
diff --git a/jetty-websocket/websocket-mux-extension/src/main/java/org/eclipse/jetty/websocket/mux/op/package-info.java b/jetty-websocket/websocket-mux-extension/src/main/java/org/eclipse/jetty/websocket/mux/op/package-info.java
new file mode 100644
index 0000000..a01e182
--- /dev/null
+++ b/jetty-websocket/websocket-mux-extension/src/main/java/org/eclipse/jetty/websocket/mux/op/package-info.java
@@ -0,0 +1,23 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+/**
+ * Jetty WebSocket Common : MUX Extension OpCode Handling [<em>Unstable Early Draft</em>]
+ */
+package org.eclipse.jetty.websocket.mux.op;
+
diff --git a/jetty-websocket/websocket-mux-extension/src/main/java/org/eclipse/jetty/websocket/mux/package-info.java b/jetty-websocket/websocket-mux-extension/src/main/java/org/eclipse/jetty/websocket/mux/package-info.java
new file mode 100644
index 0000000..b025466
--- /dev/null
+++ b/jetty-websocket/websocket-mux-extension/src/main/java/org/eclipse/jetty/websocket/mux/package-info.java
@@ -0,0 +1,23 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+/**
+ * Jetty WebSocket Common : MUX Extension Core [<em>Unstable Early Draft</em>]
+ */
+package org.eclipse.jetty.websocket.mux;
+
diff --git a/jetty-websocket/websocket-mux-extension/src/main/java/org/eclipse/jetty/websocket/mux/server/EmptyHttpInput.java b/jetty-websocket/websocket-mux-extension/src/main/java/org/eclipse/jetty/websocket/mux/server/EmptyHttpInput.java
new file mode 100644
index 0000000..f34c873
--- /dev/null
+++ b/jetty-websocket/websocket-mux-extension/src/main/java/org/eclipse/jetty/websocket/mux/server/EmptyHttpInput.java
@@ -0,0 +1,44 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.mux.server;
+
+import java.nio.ByteBuffer;
+
+import org.eclipse.jetty.server.ByteBufferQueuedHttpInput;
+import org.eclipse.jetty.server.HttpChannel;
+import org.eclipse.jetty.server.HttpInput;
+import org.eclipse.jetty.server.QueuedHttpInput;
+
+/**
+ * HttpInput for Empty Http body sections.
+ */
+public class EmptyHttpInput extends ByteBufferQueuedHttpInput
+{
+ @Override
+ protected int get(ByteBuffer item, byte[] buffer, int offset, int length)
+ {
+ return 0;
+ }
+
+ @Override
+ protected int remaining(ByteBuffer item)
+ {
+ return 0;
+ }
+}
diff --git a/jetty-websocket/websocket-mux-extension/src/main/java/org/eclipse/jetty/websocket/mux/server/HttpChannelOverMux.java b/jetty-websocket/websocket-mux-extension/src/main/java/org/eclipse/jetty/websocket/mux/server/HttpChannelOverMux.java
new file mode 100644
index 0000000..5234550
--- /dev/null
+++ b/jetty-websocket/websocket-mux-extension/src/main/java/org/eclipse/jetty/websocket/mux/server/HttpChannelOverMux.java
@@ -0,0 +1,40 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.mux.server;
+
+import java.nio.ByteBuffer;
+
+import org.eclipse.jetty.io.EndPoint;
+import org.eclipse.jetty.server.Connector;
+import org.eclipse.jetty.server.HttpChannel;
+import org.eclipse.jetty.server.HttpConfiguration;
+import org.eclipse.jetty.server.HttpInput;
+import org.eclipse.jetty.server.HttpTransport;
+
+/**
+ * Process incoming AddChannelRequest headers within the existing Jetty framework. Benefiting from Server container knowledge and various webapp configuration
+ * knowledge.
+ */
+public class HttpChannelOverMux extends HttpChannel<ByteBuffer>
+{
+ public HttpChannelOverMux(Connector connector, HttpConfiguration configuration, EndPoint endPoint, HttpTransport transport, HttpInput<ByteBuffer> input)
+ {
+ super(connector,configuration,endPoint,transport,input);
+ }
+}
diff --git a/jetty-websocket/websocket-mux-extension/src/main/java/org/eclipse/jetty/websocket/mux/server/HttpTransportOverMux.java b/jetty-websocket/websocket-mux-extension/src/main/java/org/eclipse/jetty/websocket/mux/server/HttpTransportOverMux.java
new file mode 100644
index 0000000..80addb6
--- /dev/null
+++ b/jetty-websocket/websocket-mux-extension/src/main/java/org/eclipse/jetty/websocket/mux/server/HttpTransportOverMux.java
@@ -0,0 +1,85 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.mux.server;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+
+import org.eclipse.jetty.http.HttpGenerator.ResponseInfo;
+import org.eclipse.jetty.io.EofException;
+import org.eclipse.jetty.server.HttpTransport;
+import org.eclipse.jetty.util.BlockingCallback;
+import org.eclipse.jetty.util.Callback;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+import org.eclipse.jetty.websocket.mux.MuxChannel;
+import org.eclipse.jetty.websocket.mux.Muxer;
+
+/**
+ * Take {@link ResponseInfo} objects and convert to bytes for response.
+ */
+public class HttpTransportOverMux implements HttpTransport
+{
+ private static final Logger LOG = Log.getLogger(HttpTransportOverMux.class);
+ private final BlockingCallback streamBlocker = new BlockingCallback();
+
+ public HttpTransportOverMux(Muxer muxer, MuxChannel channel)
+ {
+ // TODO Auto-generated constructor stub
+ }
+
+ @Override
+ public void completed()
+ {
+ LOG.debug("completed");
+ }
+
+ /**
+ * Process ResponseInfo object into AddChannelResponse
+ */
+ @Override
+ public void send(ResponseInfo info, ByteBuffer responseBodyContent, boolean lastContent) throws IOException
+ {
+ send(info,responseBodyContent,lastContent,streamBlocker);
+ streamBlocker.block();
+ }
+
+ @Override
+ public void send(ResponseInfo info, ByteBuffer responseBodyContent, boolean lastContent, Callback callback)
+ {
+ if (lastContent == false)
+ {
+ // throw error
+ }
+
+ if (info.getContentLength() > 0)
+ {
+ // throw error
+ }
+
+ // prepare the AddChannelResponse
+ // TODO: look at HttpSender in jetty-client for generator loop logic
+ }
+
+ @Override
+ public void send(ByteBuffer responseBodyContent, boolean lastContent, Callback callback)
+ {
+ send(null,responseBodyContent, lastContent, callback);
+ }
+}
diff --git a/jetty-websocket/websocket-mux-extension/src/main/java/org/eclipse/jetty/websocket/mux/server/MuxAddHandler.java b/jetty-websocket/websocket-mux-extension/src/main/java/org/eclipse/jetty/websocket/mux/server/MuxAddHandler.java
new file mode 100644
index 0000000..15b88cf
--- /dev/null
+++ b/jetty-websocket/websocket-mux-extension/src/main/java/org/eclipse/jetty/websocket/mux/server/MuxAddHandler.java
@@ -0,0 +1,112 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.mux.server;
+
+import java.io.IOException;
+
+import org.eclipse.jetty.http.HttpField;
+import org.eclipse.jetty.http.HttpHeader;
+import org.eclipse.jetty.http.HttpMethod;
+import org.eclipse.jetty.http.HttpVersion;
+import org.eclipse.jetty.io.EndPoint;
+import org.eclipse.jetty.server.Connector;
+import org.eclipse.jetty.server.HttpConfiguration;
+import org.eclipse.jetty.util.BufferUtil;
+import org.eclipse.jetty.websocket.api.UpgradeRequest;
+import org.eclipse.jetty.websocket.api.UpgradeResponse;
+import org.eclipse.jetty.websocket.mux.MuxChannel;
+import org.eclipse.jetty.websocket.mux.MuxException;
+import org.eclipse.jetty.websocket.mux.Muxer;
+import org.eclipse.jetty.websocket.mux.add.MuxAddServer;
+
+/**
+ * Handler for incoming MuxAddChannel requests.
+ */
+public class MuxAddHandler implements MuxAddServer
+{
+ /** Represents physical connector */
+ private Connector connector;
+
+ /** Used for local address */
+ private EndPoint endPoint;
+
+ /** The original request handshake */
+ private UpgradeRequest baseHandshakeRequest;
+
+ /** The original request handshake */
+ private UpgradeResponse baseHandshakeResponse;
+
+ private int maximumHeaderSize = 32 * 1024;
+
+ @Override
+ public UpgradeRequest getPhysicalHandshakeRequest()
+ {
+ // TODO Auto-generated method stub
+ return null;
+ }
+
+ @Override
+ public UpgradeResponse getPhysicalHandshakeResponse()
+ {
+ // TODO Auto-generated method stub
+ return null;
+ }
+
+ /**
+ * An incoming MuxAddChannel request.
+ *
+ * @param the
+ * channel this request should be bound to
+ * @param request
+ * the incoming request headers (complete and merged if delta encoded)
+ * @return the outgoing response headers
+ */
+ @Override
+ public void handshake(Muxer muxer, MuxChannel channel, UpgradeRequest request) throws MuxException, IOException
+ {
+ // Need to call into HttpChannel to get the websocket properly setup.
+ HttpTransportOverMux transport = new HttpTransportOverMux(muxer,channel);
+ EmptyHttpInput input = new EmptyHttpInput();
+ HttpConfiguration configuration = new HttpConfiguration();
+
+ HttpChannelOverMux httpChannel = new HttpChannelOverMux(//
+ connector,configuration,endPoint,transport,input);
+
+ HttpMethod method = HttpMethod.fromString(request.getMethod());
+ HttpVersion version = HttpVersion.fromString(request.getHttpVersion());
+ httpChannel.startRequest(method,request.getMethod(),BufferUtil.toBuffer(request.getRequestURI().toASCIIString()),version);
+
+ for (String headerName : request.getHeaders().keySet())
+ {
+ HttpHeader header = HttpHeader.CACHE.getBest(headerName.getBytes(),0,headerName.length());
+ for (String value : request.getHeaders().get(headerName))
+ {
+ httpChannel.parsedHeader(new HttpField(header,value));
+ }
+ }
+
+ httpChannel.headerComplete();
+ httpChannel.messageComplete();
+ httpChannel.run(); // calls into server for appropriate resource
+
+ // TODO: what's in request handshake is not enough to process the request.
+ // like a partial http request. (consider this a AddChannelRequest failure)
+ throw new MuxException("Not a valid request");
+ }
+}
diff --git a/jetty-websocket/websocket-mux-extension/src/main/java/org/eclipse/jetty/websocket/mux/server/MuxServerExtension.java b/jetty-websocket/websocket-mux-extension/src/main/java/org/eclipse/jetty/websocket/mux/server/MuxServerExtension.java
new file mode 100644
index 0000000..61562b2
--- /dev/null
+++ b/jetty-websocket/websocket-mux-extension/src/main/java/org/eclipse/jetty/websocket/mux/server/MuxServerExtension.java
@@ -0,0 +1,31 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.mux.server;
+
+import org.eclipse.jetty.websocket.mux.AbstractMuxExtension;
+import org.eclipse.jetty.websocket.mux.Muxer;
+
+public class MuxServerExtension extends AbstractMuxExtension
+{
+ @Override
+ public void configureMuxer(Muxer muxer)
+ {
+ muxer.setAddServer(new MuxAddHandler());
+ }
+}
diff --git a/jetty-websocket/websocket-mux-extension/src/main/java/org/eclipse/jetty/websocket/mux/server/package-info.java b/jetty-websocket/websocket-mux-extension/src/main/java/org/eclipse/jetty/websocket/mux/server/package-info.java
new file mode 100644
index 0000000..5e44702
--- /dev/null
+++ b/jetty-websocket/websocket-mux-extension/src/main/java/org/eclipse/jetty/websocket/mux/server/package-info.java
@@ -0,0 +1,23 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+/**
+ * Jetty WebSocket Server : MUX Extension [<em>Unstable Early Draft</em>]
+ */
+package org.eclipse.jetty.websocket.mux.server;
+
diff --git a/jetty-websocket/websocket-mux-extension/src/main/resources/META-INF/services/org.eclipse.jetty.websocket.client.ClientExtension b/jetty-websocket/websocket-mux-extension/src/main/resources/META-INF/services/org.eclipse.jetty.websocket.client.ClientExtension
new file mode 100644
index 0000000..8c39ac7
--- /dev/null
+++ b/jetty-websocket/websocket-mux-extension/src/main/resources/META-INF/services/org.eclipse.jetty.websocket.client.ClientExtension
@@ -0,0 +1 @@
+org.eclipse.jetty.websocket.mux.client.MuxClientExtension
\ No newline at end of file
diff --git a/jetty-websocket/websocket-mux-extension/src/main/resources/META-INF/services/org.eclipse.jetty.websocket.server.ServerExtension b/jetty-websocket/websocket-mux-extension/src/main/resources/META-INF/services/org.eclipse.jetty.websocket.server.ServerExtension
new file mode 100644
index 0000000..05c9974
--- /dev/null
+++ b/jetty-websocket/websocket-mux-extension/src/main/resources/META-INF/services/org.eclipse.jetty.websocket.server.ServerExtension
@@ -0,0 +1 @@
+org.eclipse.jetty.websocket.mux.client.MuxServerExtension
\ No newline at end of file
diff --git a/jetty-websocket/websocket-mux-extension/src/test/java/examples/echo/AdapterEchoSocket.java b/jetty-websocket/websocket-mux-extension/src/test/java/examples/echo/AdapterEchoSocket.java
new file mode 100644
index 0000000..ad6b3cf
--- /dev/null
+++ b/jetty-websocket/websocket-mux-extension/src/test/java/examples/echo/AdapterEchoSocket.java
@@ -0,0 +1,47 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package examples.echo;
+
+import java.io.IOException;
+
+import org.eclipse.jetty.websocket.api.WebSocketAdapter;
+
+/**
+ * Example EchoSocket using Adapter.
+ */
+public class AdapterEchoSocket extends WebSocketAdapter
+{
+ @Override
+ public void onWebSocketText(String message)
+ {
+ if (isConnected())
+ {
+ try
+ {
+ System.out.printf("Echoing back message [%s]%n",message);
+ // echo the message back
+ getRemote().sendString(message);
+ }
+ catch (IOException e)
+ {
+ e.printStackTrace(System.err);
+ }
+ }
+ }
+}
diff --git a/jetty-websocket/websocket-mux-extension/src/test/java/org/eclipse/jetty/websocket/mux/MuxDecoder.java b/jetty-websocket/websocket-mux-extension/src/test/java/org/eclipse/jetty/websocket/mux/MuxDecoder.java
new file mode 100644
index 0000000..bc00408
--- /dev/null
+++ b/jetty-websocket/websocket-mux-extension/src/test/java/org/eclipse/jetty/websocket/mux/MuxDecoder.java
@@ -0,0 +1,46 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.mux;
+
+import org.eclipse.jetty.websocket.api.WriteCallback;
+import org.eclipse.jetty.websocket.api.extensions.Frame;
+import org.eclipse.jetty.websocket.api.extensions.OutgoingFrames;
+import org.eclipse.jetty.websocket.mux.MuxParser;
+
+/**
+ * Helpful utility class to parse arbitrary mux events from a physical connection's OutgoingFrames.
+ *
+ * @see MuxEncoder
+ */
+public class MuxDecoder extends MuxEventCapture implements OutgoingFrames
+{
+ private MuxParser parser;
+
+ public MuxDecoder()
+ {
+ parser = new MuxParser();
+ parser.setEvents(this);
+ }
+
+ @Override
+ public void outgoingFrame(Frame frame, WriteCallback callback)
+ {
+ parser.parse(frame);
+ }
+}
diff --git a/jetty-websocket/websocket-mux-extension/src/test/java/org/eclipse/jetty/websocket/mux/MuxEncoder.java b/jetty-websocket/websocket-mux-extension/src/test/java/org/eclipse/jetty/websocket/mux/MuxEncoder.java
new file mode 100644
index 0000000..17e4768
--- /dev/null
+++ b/jetty-websocket/websocket-mux-extension/src/test/java/org/eclipse/jetty/websocket/mux/MuxEncoder.java
@@ -0,0 +1,66 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.mux;
+
+import java.io.IOException;
+
+import org.eclipse.jetty.websocket.api.WriteCallback;
+import org.eclipse.jetty.websocket.api.extensions.IncomingFrames;
+import org.eclipse.jetty.websocket.api.extensions.OutgoingFrames;
+import org.eclipse.jetty.websocket.common.WebSocketFrame;
+import org.eclipse.jetty.websocket.common.io.FramePipes;
+import org.eclipse.jetty.websocket.mux.MuxControlBlock;
+import org.eclipse.jetty.websocket.mux.MuxGenerator;
+
+/**
+ * Helpful utility class to send arbitrary mux events into a physical connection's IncomingFrames.
+ *
+ * @see MuxDecoder
+ */
+public class MuxEncoder
+{
+ public static MuxEncoder toIncoming(IncomingFrames incoming)
+ {
+ return new MuxEncoder(FramePipes.to(incoming));
+ }
+
+ public static MuxEncoder toOutgoing(OutgoingFrames outgoing)
+ {
+ return new MuxEncoder(outgoing);
+ }
+
+ private MuxGenerator generator;
+
+ private MuxEncoder(OutgoingFrames outgoing)
+ {
+ this.generator = new MuxGenerator();
+ this.generator.setOutgoing(outgoing);
+ }
+
+ public void frame(long channelId, WebSocketFrame frame) throws IOException
+ {
+ this.generator.generate(channelId,frame,null);
+ }
+
+ public void op(MuxControlBlock op) throws IOException
+ {
+ WriteCallback callback = null;
+ this.generator.generate(callback,op);
+ }
+}
diff --git a/jetty-websocket/websocket-mux-extension/src/test/java/org/eclipse/jetty/websocket/mux/MuxEventCapture.java b/jetty-websocket/websocket-mux-extension/src/test/java/org/eclipse/jetty/websocket/mux/MuxEventCapture.java
new file mode 100644
index 0000000..f13b4ce
--- /dev/null
+++ b/jetty-websocket/websocket-mux-extension/src/test/java/org/eclipse/jetty/websocket/mux/MuxEventCapture.java
@@ -0,0 +1,141 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.mux;
+
+import static org.hamcrest.Matchers.*;
+
+import java.util.LinkedList;
+
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+import org.eclipse.jetty.websocket.common.OpCode;
+import org.eclipse.jetty.websocket.mux.MuxControlBlock;
+import org.eclipse.jetty.websocket.mux.MuxException;
+import org.eclipse.jetty.websocket.mux.MuxParser;
+import org.eclipse.jetty.websocket.mux.MuxedFrame;
+import org.eclipse.jetty.websocket.mux.op.MuxAddChannelRequest;
+import org.eclipse.jetty.websocket.mux.op.MuxAddChannelResponse;
+import org.eclipse.jetty.websocket.mux.op.MuxDropChannel;
+import org.eclipse.jetty.websocket.mux.op.MuxFlowControl;
+import org.eclipse.jetty.websocket.mux.op.MuxNewChannelSlot;
+import org.junit.Assert;
+
+public class MuxEventCapture implements MuxParser.Listener
+{
+ private static final Logger LOG = Log.getLogger(MuxEventCapture.class);
+
+ private LinkedList<MuxedFrame> frames = new LinkedList<>();
+ private LinkedList<MuxControlBlock> ops = new LinkedList<>();
+ private LinkedList<MuxException> errors = new LinkedList<>();
+
+ public void assertFrameCount(int expected)
+ {
+ Assert.assertThat("Frame Count",frames.size(), is(expected));
+ }
+
+ public void assertHasFrame(byte opcode, long channelId, int expectedCount)
+ {
+ int actualCount = 0;
+
+ for (MuxedFrame frame : frames)
+ {
+ if (frame.getChannelId() == channelId)
+ {
+ if (frame.getOpCode() == opcode)
+ {
+ actualCount++;
+ }
+ }
+ }
+
+ Assert.assertThat("Expected Count of " + OpCode.name(opcode) + " frames on Channel ID " + channelId,actualCount,is(expectedCount));
+ }
+
+ public void assertHasOp(byte opCode, int expectedCount)
+ {
+ int actualCount = 0;
+ for (MuxControlBlock block : ops)
+ {
+ if (block.getOpCode() == opCode)
+ {
+ actualCount++;
+ }
+ }
+ Assert.assertThat("Op[" + opCode + "] count",actualCount,is(expectedCount));
+ }
+
+ public LinkedList<MuxedFrame> getFrames()
+ {
+ return frames;
+ }
+
+ public LinkedList<MuxControlBlock> getOps()
+ {
+ return ops;
+ }
+
+ @Override
+ public void onMuxAddChannelRequest(MuxAddChannelRequest request)
+ {
+ ops.add(request);
+ }
+
+ @Override
+ public void onMuxAddChannelResponse(MuxAddChannelResponse response)
+ {
+ ops.add(response);
+ }
+
+ @Override
+ public void onMuxDropChannel(MuxDropChannel drop)
+ {
+ ops.add(drop);
+ }
+
+ @Override
+ public void onMuxedFrame(MuxedFrame frame)
+ {
+ frames.add(new MuxedFrame(frame));
+ }
+
+ @Override
+ public void onMuxException(MuxException e)
+ {
+ LOG.debug(e);
+ errors.add(e);
+ }
+
+ @Override
+ public void onMuxFlowControl(MuxFlowControl flow)
+ {
+ ops.add(flow);
+ }
+
+ @Override
+ public void onMuxNewChannelSlot(MuxNewChannelSlot slot)
+ {
+ ops.add(slot);
+ }
+
+ public void reset()
+ {
+ frames.clear();
+ ops.clear();
+ }
+}
diff --git a/jetty-websocket/websocket-mux-extension/src/test/java/org/eclipse/jetty/websocket/mux/MuxGeneratorWrite139SizeTest.java b/jetty-websocket/websocket-mux-extension/src/test/java/org/eclipse/jetty/websocket/mux/MuxGeneratorWrite139SizeTest.java
new file mode 100644
index 0000000..822cd07
--- /dev/null
+++ b/jetty-websocket/websocket-mux-extension/src/test/java/org/eclipse/jetty/websocket/mux/MuxGeneratorWrite139SizeTest.java
@@ -0,0 +1,98 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.mux;
+
+import static org.hamcrest.Matchers.*;
+
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Locale;
+
+import org.eclipse.jetty.util.BufferUtil;
+import org.eclipse.jetty.util.TypeUtil;
+import org.eclipse.jetty.websocket.mux.MuxGenerator;
+import org.junit.Assert;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TestName;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class MuxGeneratorWrite139SizeTest
+{
+ private static MuxGenerator generator = new MuxGenerator();
+
+ @Parameters
+ public static Collection<Object[]> data()
+ {
+ // Various good 1/3/9 encodings
+ List<Object[]> data = new ArrayList<>();
+
+ // @formatter:off
+ // - 1 byte tests
+ data.add(new Object[]{ 0L, "00"});
+ data.add(new Object[]{ 1L, "01"});
+ data.add(new Object[]{ 2L, "02"});
+ data.add(new Object[]{ 55L, "37"});
+ data.add(new Object[]{125L, "7D"});
+
+ // - 3 byte tests
+ data.add(new Object[]{0x00_80L, "7E0080"});
+ data.add(new Object[]{0x00_ABL, "7E00AB"});
+ data.add(new Object[]{0x00_FFL, "7E00FF"});
+ data.add(new Object[]{0x3F_FFL, "7E3FFF"});
+
+ // - 9 byte tests
+ data.add(new Object[]{0x00_00_01_FF_FFL, "7F000000000001FFFF"});
+ data.add(new Object[]{0x00_00_FF_FF_FFL, "7F0000000000FFFFFF"});
+ data.add(new Object[]{0x00_FF_FF_FF_FFL, "7F00000000FFFFFFFF"});
+ data.add(new Object[]{0xFF_FF_FF_FF_FFL, "7F000000FFFFFFFFFF"});
+ // @formatter:on
+
+ return data;
+ }
+
+ @Rule
+ public TestName testname = new TestName();
+
+ private long value;
+ private String expectedHex;
+
+ public MuxGeneratorWrite139SizeTest(long value, String expectedHex)
+ {
+ this.value = value;
+ this.expectedHex = expectedHex;
+ }
+
+ @Test
+ public void testWrite139Size()
+ {
+ System.err.printf("Running %s.%s - value: %,d%n",this.getClass().getName(),testname.getMethodName(),value);
+ ByteBuffer bbuf = ByteBuffer.allocate(10);
+ generator.write139Size(bbuf,value);
+ BufferUtil.flipToFlush(bbuf,0);
+ byte actual[] = BufferUtil.toArray(bbuf);
+ String actualHex = TypeUtil.toHexString(actual).toUpperCase(Locale.ENGLISH);
+ Assert.assertThat("1/3/9 encoded size of [" + value + "]",actualHex,is(expectedHex));
+ }
+}
diff --git a/jetty-websocket/websocket-mux-extension/src/test/java/org/eclipse/jetty/websocket/mux/MuxGeneratorWriteChannelIdTest.java b/jetty-websocket/websocket-mux-extension/src/test/java/org/eclipse/jetty/websocket/mux/MuxGeneratorWriteChannelIdTest.java
new file mode 100644
index 0000000..aa795d1
--- /dev/null
+++ b/jetty-websocket/websocket-mux-extension/src/test/java/org/eclipse/jetty/websocket/mux/MuxGeneratorWriteChannelIdTest.java
@@ -0,0 +1,102 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.mux;
+
+import static org.hamcrest.Matchers.*;
+
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Locale;
+
+import org.eclipse.jetty.util.BufferUtil;
+import org.eclipse.jetty.util.TypeUtil;
+import org.eclipse.jetty.websocket.mux.MuxGenerator;
+import org.junit.Assert;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TestName;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+/**
+ * Tests of valid ChannelID generation
+ */
+@RunWith(Parameterized.class)
+public class MuxGeneratorWriteChannelIdTest
+{
+ private static MuxGenerator generator = new MuxGenerator();
+
+ @Parameters
+ public static Collection<Object[]> data()
+ {
+ // Various good Channel IDs
+ List<Object[]> data = new ArrayList<>();
+
+ // @formatter:off
+ // - 1 byte tests
+ data.add(new Object[]{ 0L, "00"});
+ data.add(new Object[]{ 1L, "01"});
+ data.add(new Object[]{ 2L, "02"});
+ data.add(new Object[]{ 55L, "37"});
+ data.add(new Object[]{127L, "7F"});
+
+ // - 2 byte tests
+ data.add(new Object[]{0x00_80L, "8080"});
+ data.add(new Object[]{0x00_FFL, "80FF"});
+ data.add(new Object[]{0x3F_FFL, "BFFF"});
+
+ // - 3 byte tests
+ data.add(new Object[]{0x00_FF_FFL, "C0FFFF"});
+ data.add(new Object[]{0x1F_FF_FFL, "DFFFFF"});
+
+ // - 3 byte tests
+ data.add(new Object[]{0x00_FF_FF_FFL, "E0FFFFFF"});
+ data.add(new Object[]{0x1F_FF_FF_FFL, "FFFFFFFF"});
+
+ // @formatter:on
+ return data;
+ }
+
+ @Rule
+ public TestName testname = new TestName();
+
+ private long channelId;
+ private String expectedHex;
+
+ public MuxGeneratorWriteChannelIdTest(long channelId, String expectedHex)
+ {
+ this.channelId = channelId;
+ this.expectedHex = expectedHex;
+ }
+
+ @Test
+ public void testReadChannelId()
+ {
+ System.err.printf("Running %s.%s - channelId: %,d%n",this.getClass().getName(),testname.getMethodName(),channelId);
+ ByteBuffer bbuf = ByteBuffer.allocate(10);
+ generator.writeChannelId(bbuf,channelId);
+ BufferUtil.flipToFlush(bbuf,0);
+ byte actual[] = BufferUtil.toArray(bbuf);
+ String actualHex = TypeUtil.toHexString(actual).toUpperCase(Locale.ENGLISH);
+ Assert.assertThat("Channel ID [" + channelId + "]",actualHex,is(expectedHex));
+ }
+}
diff --git a/jetty-websocket/websocket-mux-extension/src/test/java/org/eclipse/jetty/websocket/mux/MuxParserRFCTest.java b/jetty-websocket/websocket-mux-extension/src/test/java/org/eclipse/jetty/websocket/mux/MuxParserRFCTest.java
new file mode 100644
index 0000000..edea733
--- /dev/null
+++ b/jetty-websocket/websocket-mux-extension/src/test/java/org/eclipse/jetty/websocket/mux/MuxParserRFCTest.java
@@ -0,0 +1,244 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.mux;
+
+import static org.hamcrest.Matchers.*;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.util.Collections;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.eclipse.jetty.util.StringUtil;
+import org.eclipse.jetty.util.TypeUtil;
+import org.eclipse.jetty.websocket.api.WebSocketPolicy;
+import org.eclipse.jetty.websocket.api.extensions.Frame;
+import org.eclipse.jetty.websocket.common.OpCode;
+import org.eclipse.jetty.websocket.common.Parser;
+import org.eclipse.jetty.websocket.common.WebSocketFrame;
+import org.eclipse.jetty.websocket.common.extensions.AbstractExtension;
+import org.eclipse.jetty.websocket.mux.helper.IncomingFramesCapture;
+import org.eclipse.jetty.websocket.mux.helper.UnitParser;
+import org.junit.Assert;
+import org.junit.Test;
+
+public class MuxParserRFCTest
+{
+ public static class DummyMuxExtension extends AbstractMuxExtension
+ {
+ @Override
+ public void configureMuxer(Muxer muxer)
+ {
+ /* nothing to do */
+ }
+ }
+
+ private LinkedList<WebSocketFrame> asFrames(byte[] buf)
+ {
+ IncomingFramesCapture capture = new IncomingFramesCapture();
+ WebSocketPolicy policy = WebSocketPolicy.newClientPolicy();
+ Parser parser = new UnitParser(policy);
+ parser.setIncomingFramesHandler(capture);
+ List<? extends AbstractExtension> muxList = Collections.singletonList(new DummyMuxExtension());
+ parser.configureFromExtensions(muxList);
+ ByteBuffer bbuf = ByteBuffer.wrap(buf);
+ parser.parse(bbuf);
+
+ return capture.getFrames();
+ }
+
+ private boolean isHexOnly(String part)
+ {
+ Pattern bytePat = Pattern.compile("(\\s*0x[0-9A-Fa-f]{2}+){1,}+");
+ Matcher mat = bytePat.matcher(part);
+ return mat.matches();
+ }
+
+ private MuxEventCapture parseMuxFrames(LinkedList<WebSocketFrame> frames)
+ {
+ MuxParser parser = new MuxParser();
+ MuxEventCapture capture = new MuxEventCapture();
+ parser.setEvents(capture);
+ for(Frame frame: frames) {
+ parser.parse(frame);
+ }
+ return capture;
+ }
+
+ @Test
+ public void testIsHexOnly()
+ {
+ Assert.assertTrue(isHexOnly("0x00"));
+ Assert.assertTrue(isHexOnly("0x00 0xaF"));
+ Assert.assertFalse(isHexOnly("Hello World"));
+ }
+
+ @Test
+ public void testRFCExample1() throws IOException
+ {
+ // Create RFC detailed frames
+ byte buf[] = toByteArray("0x82 0x0d 0x01 0x81","Hello world");
+ LinkedList<WebSocketFrame> frames = asFrames(buf);
+ Assert.assertThat("Frame count",frames.size(),is(1));
+
+ // Have mux parse frames
+ MuxEventCapture capture = parseMuxFrames(frames);
+ capture.assertFrameCount(1);
+
+ MuxedFrame mux;
+
+ mux = capture.getFrames().pop();
+ String prefix = "MuxFrame[0]";
+ Assert.assertThat(prefix + ".channelId",mux.getChannelId(),is(1L));
+ Assert.assertThat(prefix + ".fin",mux.isFin(),is(true));
+ Assert.assertThat(prefix + ".rsv1",mux.isRsv1(),is(false));
+ Assert.assertThat(prefix + ".rsv2",mux.isRsv2(),is(false));
+ Assert.assertThat(prefix + ".rsv3",mux.isRsv3(),is(false));
+ Assert.assertThat(prefix + ".masked",mux.isMasked(),is(false));
+ Assert.assertThat(prefix + ".opcode",mux.getOpCode(),is(OpCode.TEXT));
+
+ String payload = mux.getPayloadAsUTF8();
+ Assert.assertThat(prefix + ".payload/text",payload,is("Hello world"));
+ }
+
+ @Test
+ public void testRFCExample2() throws IOException
+ {
+ // Create RFC detailed frames
+ byte buf[] = toByteArray("0x02 0x07 0x01 0x81","Hello","0x80 0x06"," world");
+ LinkedList<WebSocketFrame> frames = asFrames(buf);
+ Assert.assertThat("Frame count",frames.size(),is(2));
+
+ // Have mux parse frames
+ MuxEventCapture capture = parseMuxFrames(frames);
+ capture.assertFrameCount(2);
+
+ MuxedFrame mux;
+
+ // Text Frame
+ mux = capture.getFrames().get(0);
+ String prefix = "MuxFrame[0]";
+ Assert.assertThat(prefix + ".channelId",mux.getChannelId(),is(1L));
+ // (BUG IN DRAFT) Assert.assertThat(prefix + ".fin",mux.isFin(),is(false));
+ Assert.assertThat(prefix + ".rsv1",mux.isRsv1(),is(false));
+ Assert.assertThat(prefix + ".rsv2",mux.isRsv2(),is(false));
+ Assert.assertThat(prefix + ".rsv3",mux.isRsv3(),is(false));
+ Assert.assertThat(prefix + ".masked",mux.isMasked(),is(false));
+ Assert.assertThat(prefix + ".opcode",mux.getOpCode(),is(OpCode.TEXT));
+
+ String payload = mux.getPayloadAsUTF8();
+ Assert.assertThat(prefix + ".payload/text",payload,is("Hello"));
+
+ // Continuation Frame
+ mux = capture.getFrames().get(1);
+ prefix = "MuxFrame[1]";
+ // (BUG IN DRAFT) Assert.assertThat(prefix + ".channelId",mux.getChannelId(),is(1L));
+ // (BUG IN DRAFT) Assert.assertThat(prefix + ".fin",mux.isFin(),is(true));
+ Assert.assertThat(prefix + ".rsv1",mux.isRsv1(),is(false));
+ Assert.assertThat(prefix + ".rsv2",mux.isRsv2(),is(false));
+ Assert.assertThat(prefix + ".rsv3",mux.isRsv3(),is(false));
+ Assert.assertThat(prefix + ".masked",mux.isMasked(),is(false));
+ Assert.assertThat(prefix + ".continuation",mux.isContinuation(),is(true));
+ // (BUG IN DRAFT) Assert.assertThat(prefix + ".opcode",mux.getOpCode(),is(OpCode.BINARY));
+
+ payload = mux.getPayloadAsUTF8();
+ Assert.assertThat(prefix + ".payload/text",payload,is(" world"));
+ }
+
+ @Test
+ public void testRFCExample3() throws IOException
+ {
+ // Create RFC detailed frames
+ byte buf[] = toByteArray("0x82 0x07 0x01 0x01","Hello","0x82 0x05 0x02 0x81","bye","0x82 0x08 0x01 0x80"," world");
+ LinkedList<WebSocketFrame> frames = asFrames(buf);
+ Assert.assertThat("Frame count",frames.size(),is(3));
+
+ // Have mux parse frames
+ MuxEventCapture capture = parseMuxFrames(frames);
+ capture.assertFrameCount(3);
+
+ MuxedFrame mux;
+
+ // Text Frame (Message 1)
+ mux = capture.getFrames().pop();
+ String prefix = "MuxFrame[0]";
+ Assert.assertThat(prefix + ".channelId",mux.getChannelId(),is(1L));
+ Assert.assertThat(prefix + ".fin",mux.isFin(),is(false));
+ Assert.assertThat(prefix + ".rsv1",mux.isRsv1(),is(false));
+ Assert.assertThat(prefix + ".rsv2",mux.isRsv2(),is(false));
+ Assert.assertThat(prefix + ".rsv3",mux.isRsv3(),is(false));
+ Assert.assertThat(prefix + ".masked",mux.isMasked(),is(false));
+ Assert.assertThat(prefix + ".continuation",mux.isContinuation(),is(false));
+ Assert.assertThat(prefix + ".opcode",mux.getOpCode(),is(OpCode.TEXT));
+
+ String payload = mux.getPayloadAsUTF8();
+ Assert.assertThat(prefix + ".payload/text",payload,is("Hello"));
+
+ // Text Frame (Message 2)
+ mux = capture.getFrames().pop();
+ prefix = "MuxFrame[1]";
+ Assert.assertThat(prefix + ".channelId",mux.getChannelId(),is(2L));
+ Assert.assertThat(prefix + ".fin",mux.isFin(),is(true));
+ Assert.assertThat(prefix + ".rsv1",mux.isRsv1(),is(false));
+ Assert.assertThat(prefix + ".rsv2",mux.isRsv2(),is(false));
+ Assert.assertThat(prefix + ".rsv3",mux.isRsv3(),is(false));
+ Assert.assertThat(prefix + ".masked",mux.isMasked(),is(false));
+ Assert.assertThat(prefix + ".continuation",mux.isContinuation(),is(false));
+ Assert.assertThat(prefix + ".opcode",mux.getOpCode(),is(OpCode.TEXT));
+
+ payload = mux.getPayloadAsUTF8();
+ Assert.assertThat(prefix + ".payload/text",payload,is("bye"));
+
+ // Continuation Frame (Message 1)
+ mux = capture.getFrames().pop();
+ prefix = "MuxFrame[2]";
+ Assert.assertThat(prefix + ".channelId",mux.getChannelId(),is(1L));
+ Assert.assertThat(prefix + ".fin",mux.isFin(),is(true));
+ Assert.assertThat(prefix + ".rsv1",mux.isRsv1(),is(false));
+ Assert.assertThat(prefix + ".rsv2",mux.isRsv2(),is(false));
+ Assert.assertThat(prefix + ".rsv3",mux.isRsv3(),is(false));
+ Assert.assertThat(prefix + ".masked",mux.isMasked(),is(false));
+ Assert.assertThat(prefix + ".continuation",mux.isContinuation(),is(true));
+ Assert.assertThat(prefix + ".opcode",mux.getOpCode(),is(OpCode.TEXT));
+
+ payload = mux.getPayloadAsUTF8();
+ Assert.assertThat(prefix + ".payload/text",payload,is(" world"));
+ }
+
+ private byte[] toByteArray(String... parts) throws IOException
+ {
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ for(String part: parts) {
+ if (isHexOnly(part))
+ {
+ String hexonly = part.replaceAll("\\s*0x","");
+ out.write(TypeUtil.fromHexString(hexonly));
+ }
+ else
+ {
+ out.write(part.getBytes(StringUtil.__UTF8_CHARSET));
+ }
+ }
+ return out.toByteArray();
+ }
+}
diff --git a/jetty-websocket/websocket-mux-extension/src/test/java/org/eclipse/jetty/websocket/mux/MuxParserRead139Size_BadEncodingTest.java b/jetty-websocket/websocket-mux-extension/src/test/java/org/eclipse/jetty/websocket/mux/MuxParserRead139Size_BadEncodingTest.java
new file mode 100644
index 0000000..1ed2f7d
--- /dev/null
+++ b/jetty-websocket/websocket-mux-extension/src/test/java/org/eclipse/jetty/websocket/mux/MuxParserRead139Size_BadEncodingTest.java
@@ -0,0 +1,106 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.mux;
+
+import static org.hamcrest.Matchers.*;
+
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+import org.eclipse.jetty.util.TypeUtil;
+import org.eclipse.jetty.websocket.mux.MuxException;
+import org.eclipse.jetty.websocket.mux.MuxParser;
+import org.junit.Assert;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TestName;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+/**
+ * Tests for bad 1/3/9 size encoding.
+ */
+@RunWith(Parameterized.class)
+public class MuxParserRead139Size_BadEncodingTest
+{
+ private static MuxParser parser = new MuxParser();
+
+ @Parameters
+ public static Collection<Object[]> data()
+ {
+ // Various bad 1/3/9 encodings
+ // Violating "minimal number of bytes necessary" rule.
+ List<Object[]> data = new ArrayList<>();
+
+ // @formatter:off
+ // - 1 byte tests
+ // all known 1 byte tests are valid
+
+ // - 3 byte tests
+ data.add(new Object[]{"7E0000"});
+ data.add(new Object[]{"7E0001"});
+ data.add(new Object[]{"7E0012"});
+ data.add(new Object[]{"7E0059"});
+ // extra bytes (not related to 1/3/9 size)
+ data.add(new Object[]{"7E0012345678"});
+
+ // - 9 byte tests
+ data.add(new Object[]{"7F0000000000000000"});
+ data.add(new Object[]{"7F0000000000000001"});
+ data.add(new Object[]{"7F0000000000000012"});
+ data.add(new Object[]{"7F0000000000001234"});
+ data.add(new Object[]{"7F000000000000FFFF"});
+
+ // @formatter:on
+ return data;
+ }
+
+ @Rule
+ public TestName testname = new TestName();
+
+ private String rawhex;
+ private byte buf[];
+
+ public MuxParserRead139Size_BadEncodingTest(String rawhex)
+ {
+ this.rawhex = rawhex;
+ this.buf = TypeUtil.fromHexString(rawhex);
+ }
+
+ @Test
+ public void testRead139EncodedSize()
+ {
+ System.err.printf("Running %s.%s - hex: %s%n",this.getClass().getName(),testname.getMethodName(),rawhex);
+ ByteBuffer bbuf = ByteBuffer.wrap(buf);
+ try
+ {
+ parser.read139EncodedSize(bbuf);
+ // unexpected path
+ Assert.fail("Should have failed with an invalid parse");
+ }
+ catch (MuxException e)
+ {
+ // expected path
+ Assert.assertThat(e.getMessage(),containsString("Invalid 1/3/9 length"));
+ }
+ }
+}
diff --git a/jetty-websocket/websocket-mux-extension/src/test/java/org/eclipse/jetty/websocket/mux/MuxParserRead139Size_GoodTest.java b/jetty-websocket/websocket-mux-extension/src/test/java/org/eclipse/jetty/websocket/mux/MuxParserRead139Size_GoodTest.java
new file mode 100644
index 0000000..d8f3158
--- /dev/null
+++ b/jetty-websocket/websocket-mux-extension/src/test/java/org/eclipse/jetty/websocket/mux/MuxParserRead139Size_GoodTest.java
@@ -0,0 +1,100 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.mux;
+
+import static org.hamcrest.Matchers.*;
+
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+import org.eclipse.jetty.util.TypeUtil;
+import org.eclipse.jetty.websocket.mux.MuxParser;
+import org.junit.Assert;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TestName;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class MuxParserRead139Size_GoodTest
+{
+ private static MuxParser parser = new MuxParser();
+
+ @Parameters
+ public static Collection<Object[]> data()
+ {
+ // Various good 1/3/9 encodings
+ List<Object[]> data = new ArrayList<>();
+
+ // @formatter:off
+ // - 1 byte tests
+ data.add(new Object[]{"00", 0L});
+ data.add(new Object[]{"01", 1L});
+ data.add(new Object[]{"02", 2L});
+ data.add(new Object[]{"37", 55L});
+ data.add(new Object[]{"7D", 125L});
+ // extra bytes (not related to 1/3/9 size)
+ data.add(new Object[]{"37FF", 55L});
+ data.add(new Object[]{"0123456789", 0x01L});
+
+ // - 3 byte tests
+ data.add(new Object[]{"7E0080", 0x00_80L});
+ data.add(new Object[]{"7E00AB", 0x00_ABL});
+ data.add(new Object[]{"7E00FF", 0x00_FFL});
+ data.add(new Object[]{"7E3FFF", 0x3F_FFL});
+ // extra bytes (not related to 1/3/9 size)
+ data.add(new Object[]{"7E0123456789", 0x01_23L});
+
+ // - 9 byte tests
+ data.add(new Object[]{"7F000000000001FFFF", 0x00_00_01_FF_FFL});
+ data.add(new Object[]{"7F0000000000FFFFFF", 0x00_00_FF_FF_FFL});
+ data.add(new Object[]{"7F00000000FFFFFFFF", 0x00_FF_FF_FF_FFL});
+ data.add(new Object[]{"7F000000FFFFFFFFFF", 0xFF_FF_FF_FF_FFL});
+
+ // @formatter:on
+ return data;
+ }
+
+ @Rule
+ public TestName testname = new TestName();
+
+ private String rawhex;
+ private byte buf[];
+ private long expected;
+
+ public MuxParserRead139Size_GoodTest(String rawhex, long expected)
+ {
+ this.rawhex = rawhex;
+ this.buf = TypeUtil.fromHexString(rawhex);
+ this.expected = expected;
+ }
+
+ @Test
+ public void testRead139EncodedSize()
+ {
+ System.err.printf("Running %s.%s - hex: %s%n",this.getClass().getName(),testname.getMethodName(),rawhex);
+ ByteBuffer bbuf = ByteBuffer.wrap(buf);
+ long actual = parser.read139EncodedSize(bbuf);
+ Assert.assertThat("1/3/9 size from buffer [" + rawhex + "]",actual,is(expected));
+ }
+}
diff --git a/jetty-websocket/websocket-mux-extension/src/test/java/org/eclipse/jetty/websocket/mux/MuxParserReadChannelId_BadEncodingTest.java b/jetty-websocket/websocket-mux-extension/src/test/java/org/eclipse/jetty/websocket/mux/MuxParserReadChannelId_BadEncodingTest.java
new file mode 100644
index 0000000..2d3e051
--- /dev/null
+++ b/jetty-websocket/websocket-mux-extension/src/test/java/org/eclipse/jetty/websocket/mux/MuxParserReadChannelId_BadEncodingTest.java
@@ -0,0 +1,108 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.mux;
+
+import static org.hamcrest.Matchers.*;
+
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+import org.eclipse.jetty.util.TypeUtil;
+import org.eclipse.jetty.websocket.mux.MuxException;
+import org.eclipse.jetty.websocket.mux.MuxParser;
+import org.junit.Assert;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TestName;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+/**
+ * Tests of Invalid ChannelID parsing
+ */
+@RunWith(Parameterized.class)
+public class MuxParserReadChannelId_BadEncodingTest
+{
+ private static MuxParser parser = new MuxParser();
+
+ @Parameters
+ public static Collection<Object[]> data()
+ {
+ // Various Invalid Encoded Channel IDs.
+ // Violating "minimal number of bytes necessary" rule.
+ List<Object[]> data = new ArrayList<>();
+
+ // @formatter:off
+ // - 1 byte tests
+ // all known 1 byte tests are valid
+
+ // - 2 byte tests
+ data.add(new Object[]{"8000"});
+ data.add(new Object[]{"8001"});
+ data.add(new Object[]{"807F"});
+ // extra bytes (not related to channelId)
+ data.add(new Object[]{"8023456789"});
+
+ // - 3 byte tests
+ data.add(new Object[]{"C00000"});
+ data.add(new Object[]{"C01234"});
+ data.add(new Object[]{"C03FFF"});
+
+ // - 3 byte tests
+ data.add(new Object[]{"E0000000"});
+ data.add(new Object[]{"E0000001"});
+ data.add(new Object[]{"E01FFFFF"});
+
+ // @formatter:on
+ return data;
+ }
+
+ @Rule
+ public TestName testname = new TestName();
+
+ private String rawhex;
+ private byte buf[];
+
+ public MuxParserReadChannelId_BadEncodingTest(String rawhex)
+ {
+ this.rawhex = rawhex;
+ this.buf = TypeUtil.fromHexString(rawhex);
+ }
+
+ @Test
+ public void testBadEncoding()
+ {
+ System.err.printf("Running %s.%s - hex: %s%n",this.getClass().getName(),testname.getMethodName(),rawhex);
+ ByteBuffer bbuf = ByteBuffer.wrap(buf);
+ try
+ {
+ parser.readChannelId(bbuf);
+ // unexpected path
+ Assert.fail("Should have failed with an invalid parse");
+ }
+ catch (MuxException e)
+ {
+ // expected path
+ Assert.assertThat(e.getMessage(),containsString("Invalid Channel ID"));
+ }
+ }
+}
diff --git a/jetty-websocket/websocket-mux-extension/src/test/java/org/eclipse/jetty/websocket/mux/MuxParserReadChannelId_GoodTest.java b/jetty-websocket/websocket-mux-extension/src/test/java/org/eclipse/jetty/websocket/mux/MuxParserReadChannelId_GoodTest.java
new file mode 100644
index 0000000..62402cf
--- /dev/null
+++ b/jetty-websocket/websocket-mux-extension/src/test/java/org/eclipse/jetty/websocket/mux/MuxParserReadChannelId_GoodTest.java
@@ -0,0 +1,107 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.mux;
+
+import static org.hamcrest.Matchers.*;
+
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+import org.eclipse.jetty.util.TypeUtil;
+import org.eclipse.jetty.websocket.mux.MuxParser;
+import org.junit.Assert;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TestName;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+/**
+ * Tests of valid ChannelID parsing
+ */
+@RunWith(Parameterized.class)
+public class MuxParserReadChannelId_GoodTest
+{
+ private static MuxParser parser = new MuxParser();
+
+ @Parameters
+ public static Collection<Object[]> data()
+ {
+ // Various good Channel IDs
+ List<Object[]> data = new ArrayList<>();
+
+ // @formatter:off
+ // - 1 byte tests
+ data.add(new Object[]{"00", 0L});
+ data.add(new Object[]{"01", 1L});
+ data.add(new Object[]{"02", 2L});
+ data.add(new Object[]{"7F", 127L});
+ // extra bytes (not related to channelId)
+ data.add(new Object[]{"37FF", 55L});
+ data.add(new Object[]{"0123456789", 0x01L});
+
+ // - 2 byte tests
+ data.add(new Object[]{"8080", 0x00_80L});
+ data.add(new Object[]{"80FF", 0x00_FFL});
+ data.add(new Object[]{"BFFF", 0x3F_FFL});
+ // extra bytes (not related to channelId)
+ data.add(new Object[]{"8123456789", 0x01_23L});
+
+ // - 3 byte tests
+ data.add(new Object[]{"C0FFFF", 0x00_FF_FFL});
+ data.add(new Object[]{"DFFFFF", 0x1F_FF_FFL});
+ // extra bytes (not related to channelId)
+ data.add(new Object[]{"C123456789", 0x01_23_45L});
+
+ // - 3 byte tests
+ data.add(new Object[]{"E0FFFFFF", 0x00_FF_FF_FFL});
+ data.add(new Object[]{"FFFFFFFF", 0x1F_FF_FF_FFL});
+ // extra bytes (not related to channelId)
+ data.add(new Object[]{"E123456789", 0x01_23_45_67L});
+
+ // @formatter:on
+ return data;
+ }
+
+ @Rule
+ public TestName testname = new TestName();
+
+ private String rawhex;
+ private byte buf[];
+ private long expected;
+
+ public MuxParserReadChannelId_GoodTest(String rawhex, long expected)
+ {
+ this.rawhex = rawhex;
+ this.buf = TypeUtil.fromHexString(rawhex);
+ this.expected = expected;
+ }
+
+ @Test
+ public void testReadChannelId()
+ {
+ System.err.printf("Running %s.%s - hex: %s%n",this.getClass().getName(),testname.getMethodName(),rawhex);
+ ByteBuffer bbuf = ByteBuffer.wrap(buf);
+ long actual = parser.readChannelId(bbuf);
+ Assert.assertThat("Channel ID from buffer [" + rawhex + "]",actual,is(expected));
+ }
+}
diff --git a/jetty-websocket/websocket-mux-extension/src/test/java/org/eclipse/jetty/websocket/mux/add/DummyMuxAddClient.java b/jetty-websocket/websocket-mux-extension/src/test/java/org/eclipse/jetty/websocket/mux/add/DummyMuxAddClient.java
new file mode 100644
index 0000000..a7920cb
--- /dev/null
+++ b/jetty-websocket/websocket-mux-extension/src/test/java/org/eclipse/jetty/websocket/mux/add/DummyMuxAddClient.java
@@ -0,0 +1,33 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.mux.add;
+
+import org.eclipse.jetty.websocket.common.WebSocketSession;
+import org.eclipse.jetty.websocket.mux.add.MuxAddClient;
+import org.eclipse.jetty.websocket.mux.op.MuxAddChannelResponse;
+
+public class DummyMuxAddClient implements MuxAddClient
+{
+ @Override
+ public WebSocketSession createSession(MuxAddChannelResponse response)
+ {
+ // TODO Auto-generated method stub
+ return null;
+ }
+}
diff --git a/jetty-websocket/websocket-mux-extension/src/test/java/org/eclipse/jetty/websocket/mux/add/DummyMuxAddServer.java b/jetty-websocket/websocket-mux-extension/src/test/java/org/eclipse/jetty/websocket/mux/add/DummyMuxAddServer.java
new file mode 100644
index 0000000..8f997e3
--- /dev/null
+++ b/jetty-websocket/websocket-mux-extension/src/test/java/org/eclipse/jetty/websocket/mux/add/DummyMuxAddServer.java
@@ -0,0 +1,99 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.mux.add;
+
+import java.io.IOException;
+
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+import org.eclipse.jetty.websocket.api.UpgradeRequest;
+import org.eclipse.jetty.websocket.api.UpgradeResponse;
+import org.eclipse.jetty.websocket.api.WebSocketPolicy;
+import org.eclipse.jetty.websocket.common.WebSocketSession;
+import org.eclipse.jetty.websocket.common.events.EventDriver;
+import org.eclipse.jetty.websocket.common.events.EventDriverFactory;
+import org.eclipse.jetty.websocket.mux.MuxChannel;
+import org.eclipse.jetty.websocket.mux.MuxException;
+import org.eclipse.jetty.websocket.mux.Muxer;
+import org.eclipse.jetty.websocket.mux.add.MuxAddServer;
+import org.eclipse.jetty.websocket.mux.op.MuxAddChannelResponse;
+
+import examples.echo.AdapterEchoSocket;
+
+/**
+ * Dummy impl of MuxAddServer
+ */
+public class DummyMuxAddServer implements MuxAddServer
+{
+ @SuppressWarnings("unused")
+ private static final Logger LOG = Log.getLogger(DummyMuxAddServer.class);
+ private AdapterEchoSocket echo;
+ private WebSocketPolicy policy;
+ private EventDriverFactory eventDriverFactory;
+
+ public DummyMuxAddServer()
+ {
+ this.policy = WebSocketPolicy.newServerPolicy();
+ this.eventDriverFactory = new EventDriverFactory(policy);
+ this.echo = new AdapterEchoSocket();
+ }
+
+ @Override
+ public UpgradeRequest getPhysicalHandshakeRequest()
+ {
+ // TODO Auto-generated method stub
+ return null;
+ }
+
+ @Override
+ public UpgradeResponse getPhysicalHandshakeResponse()
+ {
+ // TODO Auto-generated method stub
+ return null;
+ }
+
+ @Override
+ public void handshake(Muxer muxer, MuxChannel channel, UpgradeRequest request) throws MuxException, IOException
+ {
+ StringBuilder response = new StringBuilder();
+ response.append("HTTP/1.1 101 Switching Protocols\r\n");
+ response.append("Connection: upgrade\r\n");
+ // not meaningful (per Draft 08) hresp.append("Upgrade: websocket\r\n");
+ // not meaningful (per Draft 08) hresp.append("Sec-WebSocket-Accept: Kgo85/8KVE8YPONSeyhgL3GwqhI=\r\n");
+ response.append("\r\n");
+
+ EventDriver websocket = this.eventDriverFactory.wrap(echo);
+ WebSocketSession session = new WebSocketSession(request.getRequestURI(),websocket,channel);
+ UpgradeResponse uresponse = new UpgradeResponse();
+ uresponse.setAcceptedSubProtocol("echo");
+ session.setUpgradeResponse(uresponse);
+ channel.setSession(session);
+ channel.setSubProtocol("echo");
+ channel.onOpen();
+ session.open();
+
+ MuxAddChannelResponse addChannelResponse = new MuxAddChannelResponse();
+ addChannelResponse.setChannelId(channel.getChannelId());
+ addChannelResponse.setEncoding(MuxAddChannelResponse.IDENTITY_ENCODING);
+ addChannelResponse.setFailed(false);
+ addChannelResponse.setHandshake(response.toString());
+
+ muxer.output(addChannelResponse);
+ }
+}
diff --git a/jetty-websocket/websocket-mux-extension/src/test/java/org/eclipse/jetty/websocket/mux/add/MuxerAddClientTest.java b/jetty-websocket/websocket-mux-extension/src/test/java/org/eclipse/jetty/websocket/mux/add/MuxerAddClientTest.java
new file mode 100644
index 0000000..80d922c
--- /dev/null
+++ b/jetty-websocket/websocket-mux-extension/src/test/java/org/eclipse/jetty/websocket/mux/add/MuxerAddClientTest.java
@@ -0,0 +1,105 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.mux.add;
+
+import org.eclipse.jetty.websocket.api.WebSocketPolicy;
+import org.eclipse.jetty.websocket.mux.MuxChannel;
+import org.eclipse.jetty.websocket.mux.MuxDecoder;
+import org.eclipse.jetty.websocket.mux.MuxEncoder;
+import org.eclipse.jetty.websocket.mux.MuxOp;
+import org.eclipse.jetty.websocket.mux.Muxer;
+import org.eclipse.jetty.websocket.mux.helper.LocalWebSocketConnection;
+import org.eclipse.jetty.websocket.mux.op.MuxAddChannelRequest;
+import org.eclipse.jetty.websocket.mux.op.MuxAddChannelResponse;
+import org.junit.Ignore;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TestName;
+
+public class MuxerAddClientTest
+{
+ @Rule
+ public TestName testname = new TestName();
+
+ @Test
+ @Ignore("Interrim, not functional yet")
+ public void testAddChannel_Client() throws Exception
+ {
+ // Client side physical socket
+ LocalWebSocketConnection physical = new LocalWebSocketConnection(testname);
+ physical.setPolicy(WebSocketPolicy.newClientPolicy());
+ physical.open();
+
+ // Server Reader
+ MuxDecoder serverRead = new MuxDecoder();
+
+ // Client side Muxer
+ Muxer muxer = new Muxer(physical);
+ DummyMuxAddClient addClient = new DummyMuxAddClient();
+ muxer.setAddClient(addClient);
+ muxer.setOutgoingFramesHandler(serverRead);
+
+ // Server Writer
+ MuxEncoder serverWrite = MuxEncoder.toIncoming(physical);
+
+ // Build AddChannelRequest handshake data
+ StringBuilder request = new StringBuilder();
+ request.append("GET /echo HTTP/1.1\r\n");
+ request.append("Host: localhost\r\n");
+ request.append("Upgrade: websocket\r\n");
+ request.append("Connection: Upgrade\r\n");
+ request.append("Sec-WebSocket-Key: ZDTIRU5vU9xOfkg8JAgN3A==\r\n");
+ request.append("Sec-WebSocket-Version: 13\r\n");
+ request.append("\r\n");
+
+ // Build AddChannelRequest
+ long channelId = 1L;
+ MuxAddChannelRequest req = new MuxAddChannelRequest();
+ req.setChannelId(channelId);
+ req.setEncoding((byte)0);
+ req.setHandshake(request.toString());
+
+ // Have client sent AddChannelRequest
+ MuxChannel channel = muxer.getChannel(channelId,true);
+ MuxEncoder clientWrite = MuxEncoder.toOutgoing(channel);
+ clientWrite.op(req);
+
+ // Have server read request
+ serverRead.assertHasOp(MuxOp.ADD_CHANNEL_REQUEST,1);
+
+ // prepare AddChannelResponse
+ StringBuilder response = new StringBuilder();
+ response.append("HTTP/1.1 101 Switching Protocols\r\n");
+ response.append("Upgrade: websocket\r\n");
+ response.append("Connection: upgrade\r\n");
+ response.append("Sec-WebSocket-Accept: Kgo85/8KVE8YPONSeyhgL3GwqhI=\r\n");
+ response.append("\r\n");
+
+ MuxAddChannelResponse resp = new MuxAddChannelResponse();
+ resp.setChannelId(channelId);
+ resp.setFailed(false);
+ resp.setEncoding((byte)0);
+ resp.setHandshake(resp.toString());
+
+ // Server writes add channel response
+ serverWrite.op(resp);
+
+ // TODO: handle the upgrade on client side.
+ }
+}
diff --git a/jetty-websocket/websocket-mux-extension/src/test/java/org/eclipse/jetty/websocket/mux/add/MuxerAddServerTest.java b/jetty-websocket/websocket-mux-extension/src/test/java/org/eclipse/jetty/websocket/mux/add/MuxerAddServerTest.java
new file mode 100644
index 0000000..c543c43
--- /dev/null
+++ b/jetty-websocket/websocket-mux-extension/src/test/java/org/eclipse/jetty/websocket/mux/add/MuxerAddServerTest.java
@@ -0,0 +1,105 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.mux.add;
+
+import static org.hamcrest.Matchers.*;
+
+import org.eclipse.jetty.websocket.api.WebSocketPolicy;
+import org.eclipse.jetty.websocket.common.OpCode;
+import org.eclipse.jetty.websocket.common.WebSocketFrame;
+import org.eclipse.jetty.websocket.mux.MuxDecoder;
+import org.eclipse.jetty.websocket.mux.MuxEncoder;
+import org.eclipse.jetty.websocket.mux.MuxOp;
+import org.eclipse.jetty.websocket.mux.Muxer;
+import org.eclipse.jetty.websocket.mux.helper.LocalWebSocketConnection;
+import org.eclipse.jetty.websocket.mux.op.MuxAddChannelRequest;
+import org.eclipse.jetty.websocket.mux.op.MuxAddChannelResponse;
+import org.junit.Assert;
+import org.junit.Ignore;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TestName;
+
+public class MuxerAddServerTest
+{
+ @Rule
+ public TestName testname = new TestName();
+
+ @Test
+ @Ignore("Interrim, not functional yet")
+ public void testAddChannel_Server() throws Exception
+ {
+ // Server side physical connection
+ LocalWebSocketConnection physical = new LocalWebSocketConnection(testname);
+ physical.setPolicy(WebSocketPolicy.newServerPolicy());
+ physical.open();
+
+ // Client reader
+ MuxDecoder clientRead = new MuxDecoder();
+
+ // Build up server side muxer.
+ Muxer muxer = new Muxer(physical);
+ DummyMuxAddServer addServer = new DummyMuxAddServer();
+ muxer.setAddServer(addServer);
+ muxer.setOutgoingFramesHandler(clientRead);
+
+ // Wire up physical connection to forward incoming frames to muxer
+ physical.setNextIncomingFrames(muxer);
+
+ // Client simulator
+ // Can inject mux encapsulated frames into physical connection as if from
+ // physical connection.
+ MuxEncoder clientWrite = MuxEncoder.toIncoming(physical);
+
+ // Build AddChannelRequest handshake data
+ StringBuilder request = new StringBuilder();
+ request.append("GET /echo HTTP/1.1\r\n");
+ request.append("Host: localhost\r\n");
+ request.append("Upgrade: websocket\r\n");
+ request.append("Connection: Upgrade\r\n");
+ request.append("Sec-WebSocket-Key: ZDTIRU5vU9xOfkg8JAgN3A==\r\n");
+ request.append("Sec-WebSocket-Version: 13\r\n");
+ request.append("\r\n");
+
+ // Build AddChannelRequest
+ MuxAddChannelRequest req = new MuxAddChannelRequest();
+ req.setChannelId(1);
+ req.setEncoding((byte)0);
+ req.setHandshake(request.toString());
+
+ // Have client sent AddChannelRequest
+ clientWrite.op(req);
+
+ // Make sure client got AddChannelResponse
+ clientRead.assertHasOp(MuxOp.ADD_CHANNEL_RESPONSE,1);
+ MuxAddChannelResponse response = (MuxAddChannelResponse)clientRead.getOps().pop();
+ Assert.assertThat("AddChannelResponse.channelId",response.getChannelId(),is(1L));
+ Assert.assertThat("AddChannelResponse.failed",response.isFailed(),is(false));
+ Assert.assertThat("AddChannelResponse.handshake",response.getHandshake(),notNullValue());
+ Assert.assertThat("AddChannelResponse.handshakeSize",response.getHandshakeSize(),is(57L));
+
+ clientRead.reset();
+
+ // Send simple echo request
+ clientWrite.frame(1,WebSocketFrame.text("Hello World"));
+
+ // Test for echo response (is there a user echo websocket connected to the sub-channel?)
+ clientRead.assertHasFrame(OpCode.TEXT,1L,1);
+ }
+}
diff --git a/jetty-websocket/websocket-mux-extension/src/test/java/org/eclipse/jetty/websocket/mux/helper/IncomingFramesCapture.java b/jetty-websocket/websocket-mux-extension/src/test/java/org/eclipse/jetty/websocket/mux/helper/IncomingFramesCapture.java
new file mode 100644
index 0000000..7d34c22
--- /dev/null
+++ b/jetty-websocket/websocket-mux-extension/src/test/java/org/eclipse/jetty/websocket/mux/helper/IncomingFramesCapture.java
@@ -0,0 +1,142 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.mux.helper;
+
+import static org.hamcrest.Matchers.*;
+
+import java.util.LinkedList;
+
+import org.eclipse.jetty.util.BufferUtil;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+import org.eclipse.jetty.websocket.api.WebSocketException;
+import org.eclipse.jetty.websocket.api.extensions.Frame;
+import org.eclipse.jetty.websocket.api.extensions.IncomingFrames;
+import org.eclipse.jetty.websocket.common.OpCode;
+import org.eclipse.jetty.websocket.common.WebSocketFrame;
+import org.junit.Assert;
+
+public class IncomingFramesCapture implements IncomingFrames
+{
+ private static final Logger LOG = Log.getLogger(IncomingFramesCapture.class);
+
+ private LinkedList<WebSocketFrame> frames = new LinkedList<>();
+ private LinkedList<Throwable> errors = new LinkedList<>();
+
+ public void assertErrorCount(int expectedCount)
+ {
+ Assert.assertThat("Captured error count",errors.size(),is(expectedCount));
+ }
+
+ public void assertFrameCount(int expectedCount)
+ {
+ Assert.assertThat("Captured frame count",frames.size(),is(expectedCount));
+ }
+
+ public void assertHasErrors(Class<? extends WebSocketException> errorType, int expectedCount)
+ {
+ Assert.assertThat(errorType.getSimpleName(),getErrorCount(errorType),is(expectedCount));
+ }
+
+ public void assertHasFrame(byte op)
+ {
+ Assert.assertThat(OpCode.name(op),getFrameCount(op),greaterThanOrEqualTo(1));
+ }
+
+ public void assertHasFrame(byte op, int expectedCount)
+ {
+ Assert.assertThat(OpCode.name(op),getFrameCount(op),is(expectedCount));
+ }
+
+ public void assertHasNoFrames()
+ {
+ Assert.assertThat("Has no frames",frames.size(),is(0));
+ }
+
+ public void assertNoErrors()
+ {
+ Assert.assertThat("Has no errors",errors.size(),is(0));
+ }
+
+ public void dump()
+ {
+ System.err.printf("Captured %d incoming frames%n",frames.size());
+ for (int i = 0; i < frames.size(); i++)
+ {
+ Frame frame = frames.get(i);
+ System.err.printf("[%3d] %s%n",i,frame);
+ System.err.printf(" %s%n",BufferUtil.toDetailString(frame.getPayload()));
+ }
+ }
+
+ public int getErrorCount(Class<? extends WebSocketException> errorType)
+ {
+ int count = 0;
+ for (Throwable error : errors)
+ {
+ if (errorType.isInstance(error))
+ {
+ count++;
+ }
+ }
+ return count;
+ }
+
+ public LinkedList<Throwable> getErrors()
+ {
+ return errors;
+ }
+
+ public int getFrameCount(byte op)
+ {
+ int count = 0;
+ for (WebSocketFrame frame : frames)
+ {
+ if (frame.getOpCode() == op)
+ {
+ count++;
+ }
+ }
+ return count;
+ }
+
+ public LinkedList<WebSocketFrame> getFrames()
+ {
+ return frames;
+ }
+
+ @Override
+ public void incomingError(Throwable e)
+ {
+ LOG.debug(e);
+ errors.add(e);
+ }
+
+ @Override
+ public void incomingFrame(Frame frame)
+ {
+ WebSocketFrame copy = new WebSocketFrame(frame);
+ frames.add(copy);
+ }
+
+ public int size()
+ {
+ return frames.size();
+ }
+}
diff --git a/jetty-websocket/websocket-mux-extension/src/test/java/org/eclipse/jetty/websocket/mux/helper/LocalWebSocketConnection.java b/jetty-websocket/websocket-mux-extension/src/test/java/org/eclipse/jetty/websocket/mux/helper/LocalWebSocketConnection.java
new file mode 100644
index 0000000..cd86267
--- /dev/null
+++ b/jetty-websocket/websocket-mux-extension/src/test/java/org/eclipse/jetty/websocket/mux/helper/LocalWebSocketConnection.java
@@ -0,0 +1,250 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.mux.helper;
+
+import java.net.InetSocketAddress;
+import java.util.concurrent.Executor;
+
+import org.eclipse.jetty.io.ByteBufferPool;
+import org.eclipse.jetty.io.MappedByteBufferPool;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+import org.eclipse.jetty.util.thread.ExecutorThreadPool;
+import org.eclipse.jetty.websocket.api.StatusCode;
+import org.eclipse.jetty.websocket.api.SuspendToken;
+import org.eclipse.jetty.websocket.api.WebSocketPolicy;
+import org.eclipse.jetty.websocket.api.WriteCallback;
+import org.eclipse.jetty.websocket.api.extensions.Frame;
+import org.eclipse.jetty.websocket.api.extensions.IncomingFrames;
+import org.eclipse.jetty.websocket.common.CloseInfo;
+import org.eclipse.jetty.websocket.common.ConnectionState;
+import org.eclipse.jetty.websocket.common.LogicalConnection;
+import org.eclipse.jetty.websocket.common.WebSocketSession;
+import org.eclipse.jetty.websocket.common.io.IOState;
+import org.eclipse.jetty.websocket.common.io.IOState.ConnectionStateListener;
+import org.junit.rules.TestName;
+
+public class LocalWebSocketConnection implements LogicalConnection, IncomingFrames, ConnectionStateListener
+{
+ private static final Logger LOG = Log.getLogger(LocalWebSocketConnection.class);
+ private final String id;
+ private final ByteBufferPool bufferPool;
+ private final Executor executor;
+ private WebSocketPolicy policy = WebSocketPolicy.newServerPolicy();
+ private IncomingFrames incoming;
+ private IOState ioState = new IOState();
+
+ public LocalWebSocketConnection()
+ {
+ this("anon");
+ }
+
+ public LocalWebSocketConnection(String id)
+ {
+ this.id = id;
+ this.bufferPool = new MappedByteBufferPool();
+ this.executor = new ExecutorThreadPool();
+ this.ioState.addListener(this);
+ }
+
+ public LocalWebSocketConnection(TestName testname)
+ {
+ this(testname.getMethodName());
+ }
+
+ @Override
+ public Executor getExecutor()
+ {
+ return executor;
+ }
+
+ @Override
+ public void close()
+ {
+ close(StatusCode.NORMAL,null);
+ }
+
+ @Override
+ public void close(int statusCode, String reason)
+ {
+ LOG.debug("close({}, {})",statusCode,reason);
+ CloseInfo close = new CloseInfo(statusCode,reason);
+ ioState.onCloseLocal(close);
+ }
+
+ public void connect()
+ {
+ LOG.debug("connect()");
+ ioState.onConnected();
+ }
+
+ @Override
+ public void disconnect()
+ {
+ LOG.debug("disconnect()");
+ }
+
+ @Override
+ public ByteBufferPool getBufferPool()
+ {
+ return this.bufferPool;
+ }
+
+ @Override
+ public long getIdleTimeout()
+ {
+ return 0;
+ }
+
+ public IncomingFrames getIncoming()
+ {
+ return incoming;
+ }
+
+ @Override
+ public IOState getIOState()
+ {
+ return ioState;
+ }
+
+ @Override
+ public InetSocketAddress getLocalAddress()
+ {
+ return null;
+ }
+
+ @Override
+ public long getMaxIdleTimeout()
+ {
+ return 0;
+ }
+
+ @Override
+ public WebSocketPolicy getPolicy()
+ {
+ return policy;
+ }
+
+ @Override
+ public InetSocketAddress getRemoteAddress()
+ {
+ return null;
+ }
+
+ @Override
+ public WebSocketSession getSession()
+ {
+ return null;
+ }
+
+ @Override
+ public void incomingError(Throwable e)
+ {
+ incoming.incomingError(e);
+ }
+
+ @Override
+ public void incomingFrame(Frame frame)
+ {
+ incoming.incomingFrame(frame);
+ }
+
+ @Override
+ public boolean isOpen()
+ {
+ return getIOState().isOpen();
+ }
+
+ @Override
+ public boolean isReading()
+ {
+ return false;
+ }
+
+ @Override
+ public void onConnectionStateChange(ConnectionState state)
+ {
+ LOG.debug("Connection State Change: {}",state);
+ switch (state)
+ {
+ case CLOSED:
+ this.disconnect();
+ break;
+ case CLOSING:
+ if (ioState.wasRemoteCloseInitiated())
+ {
+ // send response close frame
+ CloseInfo close = ioState.getCloseInfo();
+ LOG.debug("write close frame: {}",close);
+ ioState.onCloseLocal(close);
+ }
+ default:
+ break;
+ }
+ }
+
+ public void open()
+ {
+ LOG.debug("open()");
+ ioState.onOpened();
+ }
+
+ @Override
+ public void outgoingFrame(Frame frame, WriteCallback callback)
+ {
+ }
+
+ @Override
+ public void resume()
+ {
+ }
+
+ @Override
+ public void setMaxIdleTimeout(long ms)
+ {
+ }
+
+ @Override
+ public void setNextIncomingFrames(IncomingFrames incoming)
+ {
+ this.incoming = incoming;
+ }
+
+ public void setPolicy(WebSocketPolicy policy)
+ {
+ this.policy = policy;
+ }
+
+ @Override
+ public void setSession(WebSocketSession session)
+ {
+ }
+
+ @Override
+ public SuspendToken suspend()
+ {
+ return null;
+ }
+
+ @Override
+ public String toString()
+ {
+ return String.format("%s[%s]",LocalWebSocketConnection.class.getSimpleName(),id);
+ }
+}
diff --git a/jetty-websocket/websocket-mux-extension/src/test/java/org/eclipse/jetty/websocket/mux/helper/LocalWebSocketSession.java b/jetty-websocket/websocket-mux-extension/src/test/java/org/eclipse/jetty/websocket/mux/helper/LocalWebSocketSession.java
new file mode 100644
index 0000000..bc8ede1
--- /dev/null
+++ b/jetty-websocket/websocket-mux-extension/src/test/java/org/eclipse/jetty/websocket/mux/helper/LocalWebSocketSession.java
@@ -0,0 +1,50 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.mux.helper;
+
+import java.net.URI;
+
+import org.eclipse.jetty.websocket.common.WebSocketSession;
+import org.eclipse.jetty.websocket.common.events.EventDriver;
+import org.junit.rules.TestName;
+
+public class LocalWebSocketSession extends WebSocketSession
+{
+ private String id;
+ private OutgoingFramesCapture outgoingCapture;
+
+ public LocalWebSocketSession(TestName testname, EventDriver driver)
+ {
+ super(URI.create("ws://localhost/LocalWebSocketSesssion/" + testname.getMethodName()),driver,new LocalWebSocketConnection(testname));
+ this.id = testname.getMethodName();
+ outgoingCapture = new OutgoingFramesCapture();
+ setOutgoingHandler(outgoingCapture);
+ }
+
+ public OutgoingFramesCapture getOutgoingCapture()
+ {
+ return outgoingCapture;
+ }
+
+ @Override
+ public String toString()
+ {
+ return String.format("%s[%s]",LocalWebSocketSession.class.getSimpleName(),id);
+ }
+}
diff --git a/jetty-websocket/websocket-mux-extension/src/test/java/org/eclipse/jetty/websocket/mux/helper/OutgoingFramesCapture.java b/jetty-websocket/websocket-mux-extension/src/test/java/org/eclipse/jetty/websocket/mux/helper/OutgoingFramesCapture.java
new file mode 100644
index 0000000..4ca586c
--- /dev/null
+++ b/jetty-websocket/websocket-mux-extension/src/test/java/org/eclipse/jetty/websocket/mux/helper/OutgoingFramesCapture.java
@@ -0,0 +1,96 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.mux.helper;
+
+import static org.hamcrest.Matchers.*;
+
+import java.util.LinkedList;
+
+import org.eclipse.jetty.util.BufferUtil;
+import org.eclipse.jetty.websocket.api.WriteCallback;
+import org.eclipse.jetty.websocket.api.extensions.Frame;
+import org.eclipse.jetty.websocket.api.extensions.OutgoingFrames;
+import org.eclipse.jetty.websocket.common.OpCode;
+import org.eclipse.jetty.websocket.common.WebSocketFrame;
+import org.junit.Assert;
+
+public class OutgoingFramesCapture implements OutgoingFrames
+{
+ private LinkedList<WebSocketFrame> frames = new LinkedList<>();
+
+ public void assertFrameCount(int expectedCount)
+ {
+ Assert.assertThat("Captured frame count",frames.size(),is(expectedCount));
+ }
+
+ public void assertHasFrame(byte op)
+ {
+ Assert.assertThat(OpCode.name(op),getFrameCount(op),greaterThanOrEqualTo(1));
+ }
+
+ public void assertHasFrame(byte op, int expectedCount)
+ {
+ Assert.assertThat(OpCode.name(op),getFrameCount(op),is(expectedCount));
+ }
+
+ public void assertHasNoFrames()
+ {
+ Assert.assertThat("Has no frames",frames.size(),is(0));
+ }
+
+ public void dump()
+ {
+ System.out.printf("Captured %d outgoing writes%n",frames.size());
+ for (int i = 0; i < frames.size(); i++)
+ {
+ Frame frame = frames.get(i);
+ System.out.printf("[%3d] %s%n",i,frame);
+ System.out.printf(" %s%n",BufferUtil.toDetailString(frame.getPayload()));
+ }
+ }
+
+ public int getFrameCount(byte op)
+ {
+ int count = 0;
+ for (WebSocketFrame frame : frames)
+ {
+ if (frame.getOpCode() == op)
+ {
+ count++;
+ }
+ }
+ return count;
+ }
+
+ public LinkedList<WebSocketFrame> getFrames()
+ {
+ return frames;
+ }
+
+ @Override
+ public void outgoingFrame(Frame frame, WriteCallback callback)
+ {
+ WebSocketFrame copy = new WebSocketFrame(frame);
+ frames.add(copy);
+ if (callback != null)
+ {
+ callback.writeSuccess();
+ }
+ }
+}
diff --git a/jetty-websocket/websocket-mux-extension/src/test/java/org/eclipse/jetty/websocket/mux/helper/UnitParser.java b/jetty-websocket/websocket-mux-extension/src/test/java/org/eclipse/jetty/websocket/mux/helper/UnitParser.java
new file mode 100644
index 0000000..7976515
--- /dev/null
+++ b/jetty-websocket/websocket-mux-extension/src/test/java/org/eclipse/jetty/websocket/mux/helper/UnitParser.java
@@ -0,0 +1,78 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.mux.helper;
+
+import java.nio.ByteBuffer;
+
+import org.eclipse.jetty.io.ByteBufferPool;
+import org.eclipse.jetty.io.MappedByteBufferPool;
+import org.eclipse.jetty.util.log.StacklessLogging;
+import org.eclipse.jetty.websocket.api.WebSocketPolicy;
+import org.eclipse.jetty.websocket.common.Parser;
+
+public class UnitParser extends Parser
+{
+ public UnitParser()
+ {
+ this(WebSocketPolicy.newServerPolicy());
+ }
+
+ public UnitParser(ByteBufferPool bufferPool, WebSocketPolicy policy)
+ {
+ super(policy,bufferPool);
+ }
+
+ public UnitParser(WebSocketPolicy policy)
+ {
+ this(new MappedByteBufferPool(),policy);
+ }
+
+ private void parsePartial(ByteBuffer buf, int numBytes)
+ {
+ int len = Math.min(numBytes,buf.remaining());
+ byte arr[] = new byte[len];
+ buf.get(arr,0,len);
+ this.parse(ByteBuffer.wrap(arr));
+ }
+
+ /**
+ * Parse a buffer, but do so in a quiet fashion, squelching stacktraces if encountered.
+ * <p>
+ * Use if you know the parse will cause an exception and just don't wnat to make the test console all noisy.
+ */
+ public void parseQuietly(ByteBuffer buf)
+ {
+ try (StacklessLogging supress = new StacklessLogging(Parser.class))
+ {
+ parse(buf);
+ }
+ catch (Exception ignore)
+ {
+ /* ignore */
+ }
+ }
+
+ public void parseSlowly(ByteBuffer buf, int segmentSize)
+ {
+ while (buf.remaining() > 0)
+ {
+ parsePartial(buf,segmentSize);
+ }
+ }
+}
diff --git a/jetty-websocket/websocket-mux-extension/src/test/resources/jetty-logging.properties b/jetty-websocket/websocket-mux-extension/src/test/resources/jetty-logging.properties
new file mode 100644
index 0000000..4d43210
--- /dev/null
+++ b/jetty-websocket/websocket-mux-extension/src/test/resources/jetty-logging.properties
@@ -0,0 +1,7 @@
+org.eclipse.jetty.util.log.class=org.eclipse.jetty.util.log.StdErrLog
+org.eclipse.jetty.websocket.LEVEL=WARN
+# org.eclipse.jetty.websocket.LEVEL=DEBUG
+# org.eclipse.jetty.websocket.protocol.Parser.LEVEL=DEBUG
+# org.eclipse.jetty.websocket.protocol.LEVEL=DEBUG
+# org.eclipse.jetty.websocket.io.payload.LEVEL=DEBUG
+# org.eclipse.jetty.websocket.core.extensions.compress.LEVEL=DEBUG
diff --git a/jetty-websocket/websocket-server/pom.xml b/jetty-websocket/websocket-server/pom.xml
index a2bae1e..84bb147 100644
--- a/jetty-websocket/websocket-server/pom.xml
+++ b/jetty-websocket/websocket-server/pom.xml
@@ -3,7 +3,7 @@
<parent>
<groupId>org.eclipse.jetty.websocket</groupId>
<artifactId>websocket-parent</artifactId>
- <version>9.0.5-SNAPSHOT</version>
+ <version>9.1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
@@ -48,8 +48,13 @@
<version>${project.version}</version>
</dependency>
<dependency>
- <groupId>org.eclipse.jetty.orbit</groupId>
- <artifactId>javax.servlet</artifactId>
+ <groupId>org.eclipse.jetty</groupId>
+ <artifactId>jetty-servlet</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>javax.servlet</groupId>
+ <artifactId>javax.servlet-api</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
@@ -64,12 +69,6 @@
<scope>provided</scope>
</dependency>
<dependency>
- <groupId>org.eclipse.jetty</groupId>
- <artifactId>jetty-servlet</artifactId>
- <version>${project.version}</version>
- <scope>test</scope>
- </dependency>
- <dependency>
<groupId>org.eclipse.jetty.toolchain</groupId>
<artifactId>jetty-test-helper</artifactId>
<scope>test</scope>
diff --git a/jetty-websocket/websocket-server/src/main/java/org/eclipse/jetty/websocket/server/MappedWebSocketCreator.java b/jetty-websocket/websocket-server/src/main/java/org/eclipse/jetty/websocket/server/MappedWebSocketCreator.java
new file mode 100644
index 0000000..580116d
--- /dev/null
+++ b/jetty-websocket/websocket-server/src/main/java/org/eclipse/jetty/websocket/server/MappedWebSocketCreator.java
@@ -0,0 +1,33 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.server;
+
+import org.eclipse.jetty.websocket.server.pathmap.PathMappings;
+import org.eclipse.jetty.websocket.server.pathmap.PathSpec;
+import org.eclipse.jetty.websocket.servlet.WebSocketCreator;
+
+/**
+ * Common interface for MappedWebSocketCreator
+ */
+public interface MappedWebSocketCreator
+{
+ public void addMapping(PathSpec spec, WebSocketCreator creator);
+
+ public PathMappings<WebSocketCreator> getMappings();
+}
diff --git a/jetty-websocket/websocket-server/src/main/java/org/eclipse/jetty/websocket/server/WebSocketServerConnection.java b/jetty-websocket/websocket-server/src/main/java/org/eclipse/jetty/websocket/server/WebSocketServerConnection.java
index f899941..3879335 100644
--- a/jetty-websocket/websocket-server/src/main/java/org/eclipse/jetty/websocket/server/WebSocketServerConnection.java
+++ b/jetty-websocket/websocket-server/src/main/java/org/eclipse/jetty/websocket/server/WebSocketServerConnection.java
@@ -27,7 +27,6 @@
import org.eclipse.jetty.util.thread.Scheduler;
import org.eclipse.jetty.websocket.api.WebSocketPolicy;
import org.eclipse.jetty.websocket.api.extensions.IncomingFrames;
-import org.eclipse.jetty.websocket.common.ConnectionState;
import org.eclipse.jetty.websocket.common.io.AbstractWebSocketConnection;
public class WebSocketServerConnection extends AbstractWebSocketConnection
diff --git a/jetty-websocket/websocket-server/src/main/java/org/eclipse/jetty/websocket/server/WebSocketServerFactory.java b/jetty-websocket/websocket-server/src/main/java/org/eclipse/jetty/websocket/server/WebSocketServerFactory.java
index fbbb0b5..47869d1 100644
--- a/jetty-websocket/websocket-server/src/main/java/org/eclipse/jetty/websocket/server/WebSocketServerFactory.java
+++ b/jetty-websocket/websocket-server/src/main/java/org/eclipse/jetty/websocket/server/WebSocketServerFactory.java
@@ -19,10 +19,12 @@
package org.eclipse.jetty.websocket.server;
import java.io.IOException;
+import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
+import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Queue;
@@ -42,13 +44,15 @@
import org.eclipse.jetty.util.log.Logger;
import org.eclipse.jetty.util.thread.ScheduledExecutorScheduler;
import org.eclipse.jetty.util.thread.Scheduler;
-import org.eclipse.jetty.websocket.api.UpgradeRequest;
-import org.eclipse.jetty.websocket.api.UpgradeResponse;
+import org.eclipse.jetty.websocket.api.InvalidWebSocketException;
import org.eclipse.jetty.websocket.api.WebSocketException;
import org.eclipse.jetty.websocket.api.WebSocketPolicy;
import org.eclipse.jetty.websocket.api.extensions.ExtensionFactory;
+import org.eclipse.jetty.websocket.api.util.QuoteUtil;
import org.eclipse.jetty.websocket.common.LogicalConnection;
+import org.eclipse.jetty.websocket.common.SessionFactory;
import org.eclipse.jetty.websocket.common.WebSocketSession;
+import org.eclipse.jetty.websocket.common.WebSocketSessionFactory;
import org.eclipse.jetty.websocket.common.events.EventDriver;
import org.eclipse.jetty.websocket.common.events.EventDriverFactory;
import org.eclipse.jetty.websocket.common.extensions.ExtensionStack;
@@ -64,7 +68,6 @@
public class WebSocketServerFactory extends ContainerLifeCycle implements WebSocketCreator, WebSocketServletFactory
{
private static final Logger LOG = Log.getLogger(WebSocketServerFactory.class);
-
private static final ThreadLocal<UpgradeContext> ACTIVE_CONTEXT = new ThreadLocal<>();
public static UpgradeContext getActiveUpgradeContext()
@@ -82,15 +85,16 @@
handshakes.put(HandshakeRFC6455.VERSION,new HandshakeRFC6455());
}
- private final Queue<WebSocketSession> sessions = new ConcurrentLinkedQueue<>();
/**
* Have the factory maintain 1 and only 1 scheduler. All connections share this scheduler.
*/
private final Scheduler scheduler = new ScheduledExecutorScheduler();
+ private final Queue<WebSocketSession> sessions = new ConcurrentLinkedQueue<>();
private final String supportedVersions;
- private final WebSocketPolicy basePolicy;
+ private final WebSocketPolicy defaultPolicy;
private final EventDriverFactory eventDriverFactory;
private final WebSocketExtensionFactory extensionFactory;
+ private List<SessionFactory> sessionFactories;
private WebSocketCreator creator;
private List<Class<?>> registeredSocketClasses;
@@ -111,9 +115,11 @@
this.registeredSocketClasses = new ArrayList<>();
- this.basePolicy = policy;
- this.eventDriverFactory = new EventDriverFactory(basePolicy);
- this.extensionFactory = new WebSocketExtensionFactory(basePolicy,bufferPool);
+ this.defaultPolicy = policy;
+ this.eventDriverFactory = new EventDriverFactory(defaultPolicy);
+ this.extensionFactory = new WebSocketExtensionFactory(defaultPolicy,bufferPool);
+ this.sessionFactories = new ArrayList<>();
+ this.sessionFactories.add(new WebSocketSessionFactory());
this.creator = this;
// Create supportedVersions
@@ -138,16 +144,16 @@
@Override
public boolean acceptWebSocket(HttpServletRequest request, HttpServletResponse response) throws IOException
{
+ return acceptWebSocket(getCreator(),request,response);
+ }
+
+ @Override
+ public boolean acceptWebSocket(WebSocketCreator creator, HttpServletRequest request, HttpServletResponse response) throws IOException
+ {
try
{
- // TODO: use ServletUpgradeRequest in Jetty 9.1
- @SuppressWarnings("deprecation")
- ServletWebSocketRequest sockreq = new ServletWebSocketRequest(request);
- // TODO: use ServletUpgradeResponse in Jetty 9.1
- @SuppressWarnings("deprecation")
- ServletWebSocketResponse sockresp = new ServletWebSocketResponse(response);
-
- WebSocketCreator creator = getCreator();
+ ServletUpgradeRequest sockreq = new ServletUpgradeRequest(request);
+ ServletUpgradeResponse sockresp = new ServletUpgradeResponse(response);
UpgradeContext context = getActiveUpgradeContext();
if (context == null)
@@ -155,6 +161,7 @@
context = new UpgradeContext();
setActiveUpgradeContext(context);
}
+
context.setRequest(sockreq);
context.setResponse(sockresp);
@@ -183,6 +190,15 @@
}
}
+ public void addSessionFactory(SessionFactory sessionFactory)
+ {
+ if (sessionFactories.contains(sessionFactory))
+ {
+ return;
+ }
+ this.sessionFactories.add(sessionFactory);
+ }
+
@Override
public void cleanup()
{
@@ -200,14 +216,7 @@
{
for (WebSocketSession session : sessions)
{
- try
- {
- session.close();
- }
- catch (IOException e)
- {
- LOG.warn("CloseAllConnections Close failure",e);
- }
+ session.close();
}
sessions.clear();
}
@@ -218,11 +227,36 @@
return new WebSocketServerFactory(policy);
}
+ private WebSocketSession createSession(URI requestURI, EventDriver websocket, LogicalConnection connection)
+ {
+ if (websocket == null)
+ {
+ throw new InvalidWebSocketException("Unable to create Session from null websocket");
+ }
+
+ for (SessionFactory impl : sessionFactories)
+ {
+ if (impl.supports(websocket))
+ {
+ try
+ {
+ return impl.createSession(requestURI,websocket,connection);
+ }
+ catch (Throwable e)
+ {
+ throw new InvalidWebSocketException("Unable to create Session",e);
+ }
+ }
+ }
+
+ throw new InvalidWebSocketException("Unable to create Session: unrecognized internal EventDriver type: " + websocket.getClass().getName());
+ }
+
/**
* Default Creator logic
*/
@Override
- public Object createWebSocket(UpgradeRequest req, UpgradeResponse resp)
+ public Object createWebSocket(ServletUpgradeRequest req, ServletUpgradeResponse resp)
{
if (registeredSocketClasses.size() < 1)
{
@@ -258,6 +292,11 @@
return this.creator;
}
+ public EventDriverFactory getEventDriverFactory()
+ {
+ return eventDriverFactory;
+ }
+
@Override
public ExtensionFactory getExtensionFactory()
{
@@ -267,34 +306,67 @@
@Override
public WebSocketPolicy getPolicy()
{
- return basePolicy;
+ return defaultPolicy;
}
@Override
public void init() throws Exception
{
- start();
+ start(); // start lifecycle
}
@Override
public boolean isUpgradeRequest(HttpServletRequest request, HttpServletResponse response)
{
+ if (!"GET".equalsIgnoreCase(request.getMethod()))
+ {
+ // not a "GET" request (not a websocket upgrade)
+ return false;
+ }
+
+ String connection = request.getHeader("connection");
+ if (connection == null)
+ {
+ // no "Connection: upgrade" header present.
+ LOG.debug("No 'Connection' header found");
+ return false;
+ }
+
+ // Test for "Upgrade" token
+ boolean foundUpgradeToken = false;
+ Iterator<String> iter = QuoteUtil.splitAt(connection,",");
+ while (iter.hasNext())
+ {
+ String token = iter.next();
+ if ("upgrade".equalsIgnoreCase(token))
+ {
+ foundUpgradeToken = true;
+ break;
+ }
+ }
+
+ if (!foundUpgradeToken)
+ {
+ LOG.debug("No a `Upgrade` token found in 'Connection' header (was [Connection: {}])",request.getHeader("connection"));
+ return false;
+ }
+
String upgrade = request.getHeader("Upgrade");
if (upgrade == null)
{
- // Quietly fail
+ // no "Upgrade: websocket" header present.
return false;
}
if (!"websocket".equalsIgnoreCase(upgrade))
{
- LOG.warn("Not a 'Upgrade: WebSocket' (was [Upgrade: " + upgrade + "])");
+ LOG.debug("Not a 'Upgrade: WebSocket' (was [Upgrade: " + upgrade + "])");
return false;
}
if (!"HTTP/1.1".equals(request.getProtocol()))
{
- LOG.warn("Not a 'HTTP/1.1' request (was [" + request.getProtocol() + "])");
+ LOG.debug("Not a 'HTTP/1.1' request (was [" + request.getProtocol() + "])");
return false;
}
@@ -320,11 +392,6 @@
return protocols;
}
- /*
- * (non-Javadoc)
- *
- * @see org.eclipse.jetty.websocket.server.WebSocketServletFactory#register(java.lang.Class)
- */
@Override
public void register(Class<?> websocketPojo)
{
@@ -426,8 +493,8 @@
}
// Setup Session
- WebSocketSession session = new WebSocketSession(request.getRequestURI(),driver,connection);
- session.setPolicy(getPolicy().clonePolicy());
+ WebSocketSession session = createSession(request.getRequestURI(),driver,connection);
+ session.setPolicy(driver.getPolicy());
session.setUpgradeRequest(request);
response.setExtensions(extensionStack.getNegotiatedExtensions());
session.setUpgradeResponse(response);
diff --git a/jetty-websocket/websocket-server/src/main/java/org/eclipse/jetty/websocket/server/WebSocketUpgradeFilter.java b/jetty-websocket/websocket-server/src/main/java/org/eclipse/jetty/websocket/server/WebSocketUpgradeFilter.java
new file mode 100644
index 0000000..1ed0749
--- /dev/null
+++ b/jetty-websocket/websocket-server/src/main/java/org/eclipse/jetty/websocket/server/WebSocketUpgradeFilter.java
@@ -0,0 +1,224 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.server;
+
+import java.io.IOException;
+import java.util.EnumSet;
+
+import javax.servlet.DispatcherType;
+import javax.servlet.Filter;
+import javax.servlet.FilterChain;
+import javax.servlet.FilterConfig;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.eclipse.jetty.servlet.FilterHolder;
+import org.eclipse.jetty.servlet.ServletContextHandler;
+import org.eclipse.jetty.util.annotation.ManagedAttribute;
+import org.eclipse.jetty.util.annotation.ManagedObject;
+import org.eclipse.jetty.util.component.ContainerLifeCycle;
+import org.eclipse.jetty.util.component.Dumpable;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+import org.eclipse.jetty.websocket.api.WebSocketBehavior;
+import org.eclipse.jetty.websocket.api.WebSocketPolicy;
+import org.eclipse.jetty.websocket.server.pathmap.PathMappings;
+import org.eclipse.jetty.websocket.server.pathmap.PathMappings.MappedResource;
+import org.eclipse.jetty.websocket.server.pathmap.PathSpec;
+import org.eclipse.jetty.websocket.servlet.WebSocketCreator;
+
+/**
+ * Inline Servlet Filter to capture WebSocket upgrade requests and perform path mappings to {@link WebSocketCreator} objects.
+ */
+@ManagedObject("WebSocket Upgrade Filter")
+public class WebSocketUpgradeFilter extends ContainerLifeCycle implements Filter, MappedWebSocketCreator, Dumpable
+{
+ private static final Logger LOG = Log.getLogger(WebSocketUpgradeFilter.class);
+
+ public static WebSocketUpgradeFilter configureContext(ServletContextHandler context)
+ {
+ WebSocketPolicy policy = new WebSocketPolicy(WebSocketBehavior.SERVER);
+
+ WebSocketUpgradeFilter filter = new WebSocketUpgradeFilter(policy);
+ FilterHolder fholder = new FilterHolder(filter);
+ fholder.setName("Jetty_WebSocketUpgradeFilter");
+ fholder.setDisplayName("WebSocket Upgrade Filter");
+ String pathSpec = "/*";
+ context.addFilter(fholder,pathSpec,EnumSet.of(DispatcherType.REQUEST));
+ LOG.debug("Adding {} mapped to {} to {}",filter,pathSpec,context);
+
+ // Store reference to the WebSocketUpgradeFilter
+ context.setAttribute(WebSocketUpgradeFilter.class.getName(),filter);
+
+ return filter;
+ }
+
+ private final WebSocketServerFactory factory;
+ private final PathMappings<WebSocketCreator> pathmap = new PathMappings<>();
+
+ public WebSocketUpgradeFilter(WebSocketPolicy policy)
+ {
+ factory = new WebSocketServerFactory(policy);
+ addBean(factory,true);
+ }
+
+ @Override
+ public void addMapping(PathSpec spec, WebSocketCreator creator)
+ {
+ pathmap.put(spec,creator);
+ }
+
+ @Override
+ public void destroy()
+ {
+ factory.cleanup();
+ pathmap.reset();
+ super.destroy();
+ }
+
+ @Override
+ public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException
+ {
+ if (factory == null)
+ {
+ // no factory, cannot operate
+ LOG.debug("WebSocketUpgradeFilter is not operational - no WebSocketServletFactory configured");
+ chain.doFilter(request,response);
+ return;
+ }
+
+ if ((request instanceof HttpServletRequest) && (response instanceof HttpServletResponse))
+ {
+ HttpServletRequest httpreq = (HttpServletRequest)request;
+ HttpServletResponse httpresp = (HttpServletResponse)response;
+ String target = httpreq.getServletPath();
+ LOG.debug("target = [{}]",target);
+
+ if (factory.isUpgradeRequest(httpreq,httpresp))
+ {
+ MappedResource<WebSocketCreator> resource = pathmap.getMatch(target);
+ if (resource == null)
+ {
+ LOG.debug("WebSocket Upgrade on {} has no associated endpoint",target);
+ // no match.
+ chain.doFilter(request,response);
+ return;
+ }
+ LOG.debug("WebSocket Upgrade detected on {} for endpoint {}",target,resource);
+
+ WebSocketCreator creator = resource.getResource();
+
+ // Store PathSpec resource mapping as request attribute
+ httpreq.setAttribute(PathSpec.class.getName(),resource.getPathSpec());
+
+ // We have an upgrade request
+ if (factory.acceptWebSocket(creator,httpreq,httpresp))
+ {
+ // We have a socket instance created
+ return;
+ }
+
+ // If we reach this point, it means we had an incoming request to upgrade
+ // but it was either not a proper websocket upgrade, or it was possibly rejected
+ // due to incoming request constraints (controlled by WebSocketCreator)
+ if (response.isCommitted())
+ {
+ // not much we can do at this point.
+ return;
+ }
+ }
+ }
+
+ // not an Upgrade request
+ chain.doFilter(request,response);
+ }
+
+ @Override
+ public String dump()
+ {
+ return ContainerLifeCycle.dump(this);
+ }
+
+ @Override
+ public void dump(Appendable out, String indent) throws IOException
+ {
+ out.append(indent).append(" +- pathmap=").append(pathmap.toString()).append("\n");
+ pathmap.dump(out,indent + " ");
+ }
+
+ public WebSocketServerFactory getFactory()
+ {
+ return factory;
+ }
+
+ @ManagedAttribute(value = "mappings", readonly = true)
+ @Override
+ public PathMappings<WebSocketCreator> getMappings()
+ {
+ return pathmap;
+ }
+
+ @Override
+ public void init(FilterConfig config) throws ServletException
+ {
+ try
+ {
+ WebSocketPolicy policy = factory.getPolicy();
+
+ String max = config.getInitParameter("maxIdleTime");
+ if (max != null)
+ {
+ policy.setIdleTimeout(Long.parseLong(max));
+ }
+
+ max = config.getInitParameter("maxTextMessageSize");
+ if (max != null)
+ {
+ policy.setMaxTextMessageSize(Integer.parseInt(max));
+ }
+
+ max = config.getInitParameter("maxBinaryMessageSize");
+ if (max != null)
+ {
+ policy.setMaxBinaryMessageSize(Integer.parseInt(max));
+ }
+
+ max = config.getInitParameter("inputBufferSize");
+ if (max != null)
+ {
+ policy.setInputBufferSize(Integer.parseInt(max));
+ }
+
+ factory.start();
+ }
+ catch (Exception x)
+ {
+ throw new ServletException(x);
+ }
+ }
+
+ @Override
+ public String toString()
+ {
+ return String.format("%s[factory=%s,pathmap=%s]",this.getClass().getSimpleName(),factory,pathmap);
+ }
+}
diff --git a/jetty-websocket/websocket-server/src/main/java/org/eclipse/jetty/websocket/server/WebSocketUpgradeHandlerWrapper.java b/jetty-websocket/websocket-server/src/main/java/org/eclipse/jetty/websocket/server/WebSocketUpgradeHandlerWrapper.java
new file mode 100644
index 0000000..8599968
--- /dev/null
+++ b/jetty-websocket/websocket-server/src/main/java/org/eclipse/jetty/websocket/server/WebSocketUpgradeHandlerWrapper.java
@@ -0,0 +1,92 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.server;
+
+import java.io.IOException;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.eclipse.jetty.server.Request;
+import org.eclipse.jetty.server.handler.HandlerWrapper;
+import org.eclipse.jetty.websocket.server.pathmap.PathMappings;
+import org.eclipse.jetty.websocket.server.pathmap.PathMappings.MappedResource;
+import org.eclipse.jetty.websocket.server.pathmap.PathSpec;
+import org.eclipse.jetty.websocket.servlet.WebSocketCreator;
+
+public class WebSocketUpgradeHandlerWrapper extends HandlerWrapper implements MappedWebSocketCreator
+{
+ private PathMappings<WebSocketCreator> pathmap = new PathMappings<>();
+ private final WebSocketServerFactory factory;
+
+ public WebSocketUpgradeHandlerWrapper()
+ {
+ factory = new WebSocketServerFactory();
+ }
+
+ @Override
+ public void addMapping(PathSpec spec, WebSocketCreator creator)
+ {
+ pathmap.put(spec,creator);
+ }
+
+ @Override
+ public PathMappings<WebSocketCreator> getMappings()
+ {
+ return pathmap;
+ }
+
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
+ {
+ if (factory.isUpgradeRequest(request,response))
+ {
+ MappedResource<WebSocketCreator> resource = pathmap.getMatch(target);
+ if (resource == null)
+ {
+ // no match.
+ response.sendError(HttpServletResponse.SC_NOT_FOUND,"No websocket endpoint matching path: " + target);
+ return;
+ }
+
+ WebSocketCreator creator = resource.getResource();
+
+ // Store PathSpec resource mapping as request attribute
+ request.setAttribute(PathSpec.class.getName(),resource);
+
+ // We have an upgrade request
+ if (factory.acceptWebSocket(creator,request,response))
+ {
+ // We have a socket instance created
+ return;
+ }
+
+ // If we reach this point, it means we had an incoming request to upgrade
+ // but it was either not a proper websocket upgrade, or it was possibly rejected
+ // due to incoming request constraints (controlled by WebSocketCreator)
+ if (response.isCommitted())
+ {
+ // not much we can do at this point.
+ return;
+ }
+ }
+ super.handle(target,baseRequest,request,response);
+ }
+}
diff --git a/jetty-websocket/websocket-server/src/main/java/org/eclipse/jetty/websocket/server/mux/EmptyHttpInput.java b/jetty-websocket/websocket-server/src/main/java/org/eclipse/jetty/websocket/server/mux/EmptyHttpInput.java
deleted file mode 100644
index 3084ac4..0000000
--- a/jetty-websocket/websocket-server/src/main/java/org/eclipse/jetty/websocket/server/mux/EmptyHttpInput.java
+++ /dev/null
@@ -1,47 +0,0 @@
-//
-// ========================================================================
-// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
-// ------------------------------------------------------------------------
-// All rights reserved. This program and the accompanying materials
-// are made available under the terms of the Eclipse Public License v1.0
-// and Apache License v2.0 which accompanies this distribution.
-//
-// The Eclipse Public License is available at
-// http://www.eclipse.org/legal/epl-v10.html
-//
-// The Apache License v2.0 is available at
-// http://www.opensource.org/licenses/apache2.0.php
-//
-// You may elect to redistribute this code under either of these licenses.
-// ========================================================================
-//
-
-package org.eclipse.jetty.websocket.server.mux;
-
-import java.nio.ByteBuffer;
-
-import org.eclipse.jetty.server.HttpInput;
-
-/**
- * HttpInput for Empty Http body sections.
- */
-public class EmptyHttpInput extends HttpInput<ByteBuffer>
-{
- @Override
- protected int get(ByteBuffer item, byte[] buffer, int offset, int length)
- {
- return 0;
- }
-
- @Override
- protected void onContentConsumed(ByteBuffer item)
- {
- // do nothing
- }
-
- @Override
- protected int remaining(ByteBuffer item)
- {
- return 0;
- }
-}
diff --git a/jetty-websocket/websocket-server/src/main/java/org/eclipse/jetty/websocket/server/mux/HttpChannelOverMux.java b/jetty-websocket/websocket-server/src/main/java/org/eclipse/jetty/websocket/server/mux/HttpChannelOverMux.java
deleted file mode 100644
index a3c953f..0000000
--- a/jetty-websocket/websocket-server/src/main/java/org/eclipse/jetty/websocket/server/mux/HttpChannelOverMux.java
+++ /dev/null
@@ -1,40 +0,0 @@
-//
-// ========================================================================
-// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
-// ------------------------------------------------------------------------
-// All rights reserved. This program and the accompanying materials
-// are made available under the terms of the Eclipse Public License v1.0
-// and Apache License v2.0 which accompanies this distribution.
-//
-// The Eclipse Public License is available at
-// http://www.eclipse.org/legal/epl-v10.html
-//
-// The Apache License v2.0 is available at
-// http://www.opensource.org/licenses/apache2.0.php
-//
-// You may elect to redistribute this code under either of these licenses.
-// ========================================================================
-//
-
-package org.eclipse.jetty.websocket.server.mux;
-
-import java.nio.ByteBuffer;
-
-import org.eclipse.jetty.io.EndPoint;
-import org.eclipse.jetty.server.Connector;
-import org.eclipse.jetty.server.HttpChannel;
-import org.eclipse.jetty.server.HttpConfiguration;
-import org.eclipse.jetty.server.HttpInput;
-import org.eclipse.jetty.server.HttpTransport;
-
-/**
- * Process incoming AddChannelRequest headers within the existing Jetty framework. Benefiting from Server container knowledge and various webapp configuration
- * knowledge.
- */
-public class HttpChannelOverMux extends HttpChannel<ByteBuffer>
-{
- public HttpChannelOverMux(Connector connector, HttpConfiguration configuration, EndPoint endPoint, HttpTransport transport, HttpInput<ByteBuffer> input)
- {
- super(connector,configuration,endPoint,transport,input);
- }
-}
diff --git a/jetty-websocket/websocket-server/src/main/java/org/eclipse/jetty/websocket/server/mux/HttpTransportOverMux.java b/jetty-websocket/websocket-server/src/main/java/org/eclipse/jetty/websocket/server/mux/HttpTransportOverMux.java
deleted file mode 100644
index cc442b1..0000000
--- a/jetty-websocket/websocket-server/src/main/java/org/eclipse/jetty/websocket/server/mux/HttpTransportOverMux.java
+++ /dev/null
@@ -1,85 +0,0 @@
-//
-// ========================================================================
-// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
-// ------------------------------------------------------------------------
-// All rights reserved. This program and the accompanying materials
-// are made available under the terms of the Eclipse Public License v1.0
-// and Apache License v2.0 which accompanies this distribution.
-//
-// The Eclipse Public License is available at
-// http://www.eclipse.org/legal/epl-v10.html
-//
-// The Apache License v2.0 is available at
-// http://www.opensource.org/licenses/apache2.0.php
-//
-// You may elect to redistribute this code under either of these licenses.
-// ========================================================================
-//
-
-package org.eclipse.jetty.websocket.server.mux;
-
-import java.io.IOException;
-import java.nio.ByteBuffer;
-
-import org.eclipse.jetty.http.HttpGenerator.ResponseInfo;
-import org.eclipse.jetty.io.EofException;
-import org.eclipse.jetty.server.HttpTransport;
-import org.eclipse.jetty.util.BlockingCallback;
-import org.eclipse.jetty.util.Callback;
-import org.eclipse.jetty.util.log.Log;
-import org.eclipse.jetty.util.log.Logger;
-import org.eclipse.jetty.websocket.common.extensions.mux.MuxChannel;
-import org.eclipse.jetty.websocket.common.extensions.mux.Muxer;
-
-/**
- * Take {@link ResponseInfo} objects and convert to bytes for response.
- */
-public class HttpTransportOverMux implements HttpTransport
-{
- private static final Logger LOG = Log.getLogger(HttpTransportOverMux.class);
- private final BlockingCallback streamBlocker = new BlockingCallback();
-
- public HttpTransportOverMux(Muxer muxer, MuxChannel channel)
- {
- // TODO Auto-generated constructor stub
- }
-
- @Override
- public void completed()
- {
- LOG.debug("completed");
- }
-
- /**
- * Process ResponseInfo object into AddChannelResponse
- */
- @Override
- public void send(ResponseInfo info, ByteBuffer responseBodyContent, boolean lastContent) throws IOException
- {
- send(info,responseBodyContent,lastContent,streamBlocker);
- streamBlocker.block();
- }
-
- @Override
- public void send(ResponseInfo info, ByteBuffer responseBodyContent, boolean lastContent, Callback callback)
- {
- if (lastContent == false)
- {
- // throw error
- }
-
- if (info.getContentLength() > 0)
- {
- // throw error
- }
-
- // prepare the AddChannelResponse
- // TODO: look at HttpSender in jetty-client for generator loop logic
- }
-
- @Override
- public void send(ByteBuffer responseBodyContent, boolean lastContent, Callback callback)
- {
- send(null,responseBodyContent, lastContent, callback);
- }
-}
diff --git a/jetty-websocket/websocket-server/src/main/java/org/eclipse/jetty/websocket/server/mux/MuxAddHandler.java b/jetty-websocket/websocket-server/src/main/java/org/eclipse/jetty/websocket/server/mux/MuxAddHandler.java
deleted file mode 100644
index 4f46b9d..0000000
--- a/jetty-websocket/websocket-server/src/main/java/org/eclipse/jetty/websocket/server/mux/MuxAddHandler.java
+++ /dev/null
@@ -1,112 +0,0 @@
-//
-// ========================================================================
-// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
-// ------------------------------------------------------------------------
-// All rights reserved. This program and the accompanying materials
-// are made available under the terms of the Eclipse Public License v1.0
-// and Apache License v2.0 which accompanies this distribution.
-//
-// The Eclipse Public License is available at
-// http://www.eclipse.org/legal/epl-v10.html
-//
-// The Apache License v2.0 is available at
-// http://www.opensource.org/licenses/apache2.0.php
-//
-// You may elect to redistribute this code under either of these licenses.
-// ========================================================================
-//
-
-package org.eclipse.jetty.websocket.server.mux;
-
-import java.io.IOException;
-
-import org.eclipse.jetty.http.HttpField;
-import org.eclipse.jetty.http.HttpHeader;
-import org.eclipse.jetty.http.HttpMethod;
-import org.eclipse.jetty.http.HttpVersion;
-import org.eclipse.jetty.io.EndPoint;
-import org.eclipse.jetty.server.Connector;
-import org.eclipse.jetty.server.HttpConfiguration;
-import org.eclipse.jetty.util.BufferUtil;
-import org.eclipse.jetty.websocket.api.UpgradeRequest;
-import org.eclipse.jetty.websocket.api.UpgradeResponse;
-import org.eclipse.jetty.websocket.common.extensions.mux.MuxChannel;
-import org.eclipse.jetty.websocket.common.extensions.mux.MuxException;
-import org.eclipse.jetty.websocket.common.extensions.mux.Muxer;
-import org.eclipse.jetty.websocket.common.extensions.mux.add.MuxAddServer;
-
-/**
- * Handler for incoming MuxAddChannel requests.
- */
-public class MuxAddHandler implements MuxAddServer
-{
- /** Represents physical connector */
- private Connector connector;
-
- /** Used for local address */
- private EndPoint endPoint;
-
- /** The original request handshake */
- private UpgradeRequest baseHandshakeRequest;
-
- /** The original request handshake */
- private UpgradeResponse baseHandshakeResponse;
-
- private int maximumHeaderSize = 32 * 1024;
-
- @Override
- public UpgradeRequest getPhysicalHandshakeRequest()
- {
- // TODO Auto-generated method stub
- return null;
- }
-
- @Override
- public UpgradeResponse getPhysicalHandshakeResponse()
- {
- // TODO Auto-generated method stub
- return null;
- }
-
- /**
- * An incoming MuxAddChannel request.
- *
- * @param the
- * channel this request should be bound to
- * @param request
- * the incoming request headers (complete and merged if delta encoded)
- * @return the outgoing response headers
- */
- @Override
- public void handshake(Muxer muxer, MuxChannel channel, UpgradeRequest request) throws MuxException, IOException
- {
- // Need to call into HttpChannel to get the websocket properly setup.
- HttpTransportOverMux transport = new HttpTransportOverMux(muxer,channel);
- EmptyHttpInput input = new EmptyHttpInput();
- HttpConfiguration configuration = new HttpConfiguration();
-
- HttpChannelOverMux httpChannel = new HttpChannelOverMux(//
- connector,configuration,endPoint,transport,input);
-
- HttpMethod method = HttpMethod.fromString(request.getMethod());
- HttpVersion version = HttpVersion.fromString(request.getHttpVersion());
- httpChannel.startRequest(method,request.getMethod(),BufferUtil.toBuffer(request.getRequestURI().toASCIIString()),version);
-
- for (String headerName : request.getHeaders().keySet())
- {
- HttpHeader header = HttpHeader.CACHE.getBest(headerName.getBytes(),0,headerName.length());
- for (String value : request.getHeaders().get(headerName))
- {
- httpChannel.parsedHeader(new HttpField(header,value));
- }
- }
-
- httpChannel.headerComplete();
- httpChannel.messageComplete();
- httpChannel.run(); // calls into server for appropriate resource
-
- // TODO: what's in request handshake is not enough to process the request.
- // like a partial http request. (consider this a AddChannelRequest failure)
- throw new MuxException("Not a valid request");
- }
-}
diff --git a/jetty-websocket/websocket-server/src/main/java/org/eclipse/jetty/websocket/server/mux/MuxServerExtension.java b/jetty-websocket/websocket-server/src/main/java/org/eclipse/jetty/websocket/server/mux/MuxServerExtension.java
deleted file mode 100644
index 1aa510d..0000000
--- a/jetty-websocket/websocket-server/src/main/java/org/eclipse/jetty/websocket/server/mux/MuxServerExtension.java
+++ /dev/null
@@ -1,31 +0,0 @@
-//
-// ========================================================================
-// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
-// ------------------------------------------------------------------------
-// All rights reserved. This program and the accompanying materials
-// are made available under the terms of the Eclipse Public License v1.0
-// and Apache License v2.0 which accompanies this distribution.
-//
-// The Eclipse Public License is available at
-// http://www.eclipse.org/legal/epl-v10.html
-//
-// The Apache License v2.0 is available at
-// http://www.opensource.org/licenses/apache2.0.php
-//
-// You may elect to redistribute this code under either of these licenses.
-// ========================================================================
-//
-
-package org.eclipse.jetty.websocket.server.mux;
-
-import org.eclipse.jetty.websocket.common.extensions.mux.AbstractMuxExtension;
-import org.eclipse.jetty.websocket.common.extensions.mux.Muxer;
-
-public class MuxServerExtension extends AbstractMuxExtension
-{
- @Override
- public void configureMuxer(Muxer muxer)
- {
- muxer.setAddServer(new MuxAddHandler());
- }
-}
diff --git a/jetty-websocket/websocket-server/src/main/java/org/eclipse/jetty/websocket/server/mux/package-info.java b/jetty-websocket/websocket-server/src/main/java/org/eclipse/jetty/websocket/server/mux/package-info.java
deleted file mode 100644
index 3436788..0000000
--- a/jetty-websocket/websocket-server/src/main/java/org/eclipse/jetty/websocket/server/mux/package-info.java
+++ /dev/null
@@ -1,23 +0,0 @@
-//
-// ========================================================================
-// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
-// ------------------------------------------------------------------------
-// All rights reserved. This program and the accompanying materials
-// are made available under the terms of the Eclipse Public License v1.0
-// and Apache License v2.0 which accompanies this distribution.
-//
-// The Eclipse Public License is available at
-// http://www.eclipse.org/legal/epl-v10.html
-//
-// The Apache License v2.0 is available at
-// http://www.opensource.org/licenses/apache2.0.php
-//
-// You may elect to redistribute this code under either of these licenses.
-// ========================================================================
-//
-
-/**
- * Jetty WebSocket Server : MUX Extension [<em>Unstable Early Draft</em>]
- */
-package org.eclipse.jetty.websocket.server.mux;
-
diff --git a/jetty-websocket/websocket-server/src/main/java/org/eclipse/jetty/websocket/server/pathmap/PathMappings.java b/jetty-websocket/websocket-server/src/main/java/org/eclipse/jetty/websocket/server/pathmap/PathMappings.java
new file mode 100644
index 0000000..31785ad
--- /dev/null
+++ b/jetty-websocket/websocket-server/src/main/java/org/eclipse/jetty/websocket/server/pathmap/PathMappings.java
@@ -0,0 +1,189 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.server.pathmap;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+
+import org.eclipse.jetty.util.annotation.ManagedAttribute;
+import org.eclipse.jetty.util.annotation.ManagedObject;
+import org.eclipse.jetty.util.component.ContainerLifeCycle;
+import org.eclipse.jetty.util.component.Dumpable;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+import org.eclipse.jetty.websocket.server.pathmap.PathMappings.MappedResource;
+
+/**
+ * Path Mappings of PathSpec to Resource.
+ * <p>
+ * Sorted into search order upon entry into the Set
+ *
+ * @param <E>
+ */
+@ManagedObject("Path Mappings")
+public class PathMappings<E> implements Iterable<MappedResource<E>>, Dumpable
+{
+ @ManagedObject("Mapped Resource")
+ public static class MappedResource<E> implements Comparable<MappedResource<E>>
+ {
+ private final PathSpec pathSpec;
+ private final E resource;
+
+ public MappedResource(PathSpec pathSpec, E resource)
+ {
+ this.pathSpec = pathSpec;
+ this.resource = resource;
+ }
+
+ /**
+ * Comparison is based solely on the pathSpec
+ */
+ @Override
+ public int compareTo(MappedResource<E> other)
+ {
+ return this.pathSpec.compareTo(other.pathSpec);
+ }
+
+ @Override
+ public boolean equals(Object obj)
+ {
+ if (this == obj)
+ {
+ return true;
+ }
+ if (obj == null)
+ {
+ return false;
+ }
+ if (getClass() != obj.getClass())
+ {
+ return false;
+ }
+ MappedResource<?> other = (MappedResource<?>)obj;
+ if (pathSpec == null)
+ {
+ if (other.pathSpec != null)
+ {
+ return false;
+ }
+ }
+ else if (!pathSpec.equals(other.pathSpec))
+ {
+ return false;
+ }
+ return true;
+ }
+
+ @ManagedAttribute(value = "path spec", readonly = true)
+ public PathSpec getPathSpec()
+ {
+ return pathSpec;
+ }
+
+ @ManagedAttribute(value = "resource", readonly = true)
+ public E getResource()
+ {
+ return resource;
+ }
+
+ @Override
+ public int hashCode()
+ {
+ final int prime = 31;
+ int result = 1;
+ result = (prime * result) + ((pathSpec == null)?0:pathSpec.hashCode());
+ return result;
+ }
+
+ @Override
+ public String toString()
+ {
+ return String.format("MappedResource[pathSpec=%s,resource=%s]",pathSpec,resource);
+ }
+ }
+
+ private static final Logger LOG = Log.getLogger(PathMappings.class);
+ private List<MappedResource<E>> mappings = new ArrayList<MappedResource<E>>();
+ private MappedResource<E> defaultResource = null;
+
+ @Override
+ public String dump()
+ {
+ return ContainerLifeCycle.dump(this);
+ }
+
+ @Override
+ public void dump(Appendable out, String indent) throws IOException
+ {
+ ContainerLifeCycle.dump(out,indent,mappings);
+ }
+
+ @ManagedAttribute(value = "mappings", readonly = true)
+ public List<MappedResource<E>> getMappings()
+ {
+ return mappings;
+ }
+
+ public void reset()
+ {
+ mappings.clear();
+ }
+
+ public MappedResource<E> getMatch(String path)
+ {
+ int len = mappings.size();
+ for (int i = 0; i < len; i++)
+ {
+ MappedResource<E> mr = mappings.get(i);
+ if (mr.getPathSpec().matches(path))
+ {
+ return mr;
+ }
+ }
+ return defaultResource;
+ }
+
+ @Override
+ public Iterator<MappedResource<E>> iterator()
+ {
+ return mappings.iterator();
+ }
+
+ public void put(PathSpec pathSpec, E resource)
+ {
+ MappedResource<E> entry = new MappedResource<>(pathSpec,resource);
+ if (pathSpec.group == PathSpecGroup.DEFAULT)
+ {
+ defaultResource = entry;
+ }
+ // TODO: warning on replacement of existing mapping?
+ mappings.add(entry);
+ LOG.debug("Added {} to {}",entry,this);
+ Collections.sort(mappings);
+ }
+
+ @Override
+ public String toString()
+ {
+ return String.format("%s[size=%d]",this.getClass().getSimpleName(),mappings.size());
+ }
+}
diff --git a/jetty-websocket/websocket-server/src/main/java/org/eclipse/jetty/websocket/server/pathmap/PathSpec.java b/jetty-websocket/websocket-server/src/main/java/org/eclipse/jetty/websocket/server/pathmap/PathSpec.java
new file mode 100644
index 0000000..945b1aa
--- /dev/null
+++ b/jetty-websocket/websocket-server/src/main/java/org/eclipse/jetty/websocket/server/pathmap/PathSpec.java
@@ -0,0 +1,167 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.server.pathmap;
+
+/**
+ * The base PathSpec, what all other path specs are based on
+ */
+public abstract class PathSpec implements Comparable<PathSpec>
+{
+ protected String pathSpec;
+ protected PathSpecGroup group;
+ protected int pathDepth;
+ protected int specLength;
+
+ @Override
+ public int compareTo(PathSpec other)
+ {
+ // Grouping (increasing)
+ int diff = this.group.ordinal() - other.group.ordinal();
+ if (diff != 0)
+ {
+ return diff;
+ }
+
+ // Spec Length (decreasing)
+ diff = other.specLength - this.specLength;
+ if (diff != 0)
+ {
+ return diff;
+ }
+
+ // Path Spec Name (alphabetical)
+ return this.pathSpec.compareTo(other.pathSpec);
+ }
+
+ @Override
+ public boolean equals(Object obj)
+ {
+ if (this == obj)
+ {
+ return true;
+ }
+ if (obj == null)
+ {
+ return false;
+ }
+ if (getClass() != obj.getClass())
+ {
+ return false;
+ }
+ PathSpec other = (PathSpec)obj;
+ if (pathSpec == null)
+ {
+ if (other.pathSpec != null)
+ {
+ return false;
+ }
+ }
+ else if (!pathSpec.equals(other.pathSpec))
+ {
+ return false;
+ }
+ return true;
+ }
+
+ public PathSpecGroup getGroup()
+ {
+ return group;
+ }
+
+ /**
+ * Get the number of path elements that this path spec declares.
+ * <p>
+ * This is used to determine longest match logic.
+ *
+ * @return the depth of the path segments that this spec declares
+ */
+ public int getPathDepth()
+ {
+ return pathDepth;
+ }
+
+ /**
+ * Return the portion of the path that is after the path spec.
+ *
+ * @param path
+ * the path to match against
+ * @return the path info portion of the string
+ */
+ public abstract String getPathInfo(String path);
+
+ /**
+ * Return the portion of the path that matches a path spec.
+ *
+ * @param path
+ * the path to match against
+ * @return the match, or null if no match at all
+ */
+ public abstract String getPathMatch(String path);
+
+ /**
+ * The as-provided path spec.
+ *
+ * @return the as-provided path spec
+ */
+ public String getPathSpec()
+ {
+ return pathSpec;
+ }
+
+ /**
+ * Get the relative path.
+ *
+ * @param base
+ * the base the path is relative to
+ * @param path
+ * the additional path
+ * @return the base plus path with pathSpec portion removed
+ */
+ public abstract String getRelativePath(String base, String path);
+
+ @Override
+ public int hashCode()
+ {
+ final int prime = 31;
+ int result = 1;
+ result = (prime * result) + ((pathSpec == null)?0:pathSpec.hashCode());
+ return result;
+ }
+
+ /**
+ * Test to see if the provided path matches this path spec
+ *
+ * @param path
+ * the path to test
+ * @return true if the path matches this path spec, false otherwise
+ */
+ public abstract boolean matches(String path);
+
+ @Override
+ public String toString()
+ {
+ StringBuilder str = new StringBuilder();
+ str.append(this.getClass().getSimpleName()).append("[\"");
+ str.append(pathSpec);
+ str.append("\",pathDepth=").append(pathDepth);
+ str.append(",group=").append(group);
+ str.append("]");
+ return str.toString();
+ }
+}
diff --git a/jetty-websocket/websocket-server/src/main/java/org/eclipse/jetty/websocket/server/pathmap/PathSpecGroup.java b/jetty-websocket/websocket-server/src/main/java/org/eclipse/jetty/websocket/server/pathmap/PathSpecGroup.java
new file mode 100644
index 0000000..2ea700e
--- /dev/null
+++ b/jetty-websocket/websocket-server/src/main/java/org/eclipse/jetty/websocket/server/pathmap/PathSpecGroup.java
@@ -0,0 +1,82 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.server.pathmap;
+
+/**
+ * Types of path spec groups.
+ * <p>
+ * This is used to facilitate proper pathspec search order.
+ * <p>
+ * Search Order: {@link PathSpecGroup#ordinal()} [increasin], {@link PathSpec#specLength} [decreasing], {@link PathSpec#pathSpec} [natural sort order]
+ */
+public enum PathSpecGroup
+{
+ // NOTE: Order of enums determines order of Groups.
+
+ /**
+ * For exactly defined path specs, no glob.
+ */
+ EXACT,
+ /**
+ * For path specs that have a hardcoded prefix and suffix with wildcard glob in the middle.
+ *
+ * <pre>
+ * "^/downloads/[^/]*.zip$" - regex spec
+ * "/a/{var}/c" - websocket spec
+ * </pre>
+ *
+ * Note: there is no known servlet spec variant of this kind of path spec
+ */
+ MIDDLE_GLOB,
+ /**
+ * For path specs that have a hardcoded prefix and a trailing wildcard glob.
+ * <p>
+ *
+ * <pre>
+ * "/downloads/*" - servlet spec
+ * "/api/*" - servlet spec
+ * "^/rest/.*$" - regex spec
+ * "/bookings/{guest-id}" - websocket spec
+ * "/rewards/{vip-level}" - websocket spec
+ * </pre>
+ */
+ PREFIX_GLOB,
+ /**
+ * For path specs that have a wildcard glob with a hardcoded suffix
+ *
+ * <pre>
+ * "*.do" - servlet spec
+ * "*.css" - servlet spec
+ * "^.*\.zip$" - regex spec
+ * </pre>
+ *
+ * Note: there is no known websocket spec variant of this kind of path spec
+ */
+ SUFFIX_GLOB,
+ /**
+ * The default spec for accessing the Root and/or Default behavior.
+ *
+ * <pre>
+ * "/" - servlet spec (Default Servlet)
+ * "/" - websocket spec (Root Context)
+ * "^/$" - regex spec (Root Context)
+ * </pre>
+ */
+ DEFAULT;
+}
diff --git a/jetty-websocket/websocket-server/src/main/java/org/eclipse/jetty/websocket/server/pathmap/RegexPathSpec.java b/jetty-websocket/websocket-server/src/main/java/org/eclipse/jetty/websocket/server/pathmap/RegexPathSpec.java
new file mode 100644
index 0000000..4b29126
--- /dev/null
+++ b/jetty-websocket/websocket-server/src/main/java/org/eclipse/jetty/websocket/server/pathmap/RegexPathSpec.java
@@ -0,0 +1,166 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.server.pathmap;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class RegexPathSpec extends PathSpec
+{
+ protected Pattern pattern;
+
+ protected RegexPathSpec()
+ {
+ super();
+ }
+
+ public RegexPathSpec(String regex)
+ {
+ super.pathSpec = regex;
+ boolean inGrouping = false;
+ this.pathDepth = 0;
+ this.specLength = pathSpec.length();
+ // build up a simple signature we can use to identify the grouping
+ StringBuilder signature = new StringBuilder();
+ for (char c : pathSpec.toCharArray())
+ {
+ switch (c)
+ {
+ case '[':
+ inGrouping = true;
+ break;
+ case ']':
+ inGrouping = false;
+ signature.append('g'); // glob
+ break;
+ case '*':
+ signature.append('g'); // glob
+ break;
+ case '/':
+ if (!inGrouping)
+ {
+ this.pathDepth++;
+ }
+ break;
+ default:
+ if (!inGrouping)
+ {
+ if (Character.isLetterOrDigit(c))
+ {
+ signature.append('l'); // literal (exact)
+ }
+ }
+ break;
+ }
+ }
+ this.pattern = Pattern.compile(pathSpec);
+
+ // Figure out the grouping based on the signature
+ String sig = signature.toString();
+
+ if (Pattern.matches("^l*$",sig))
+ {
+ this.group = PathSpecGroup.EXACT;
+ }
+ else if (Pattern.matches("^l*g+",sig))
+ {
+ this.group = PathSpecGroup.PREFIX_GLOB;
+ }
+ else if (Pattern.matches("^g+l+$",sig))
+ {
+ this.group = PathSpecGroup.SUFFIX_GLOB;
+ }
+ else
+ {
+ this.group = PathSpecGroup.MIDDLE_GLOB;
+ }
+ }
+
+ public Matcher getMatcher(String path)
+ {
+ return this.pattern.matcher(path);
+ }
+
+ @Override
+ public String getPathInfo(String path)
+ {
+ // Path Info only valid for PREFIX_GLOB types
+ if (group == PathSpecGroup.PREFIX_GLOB)
+ {
+ Matcher matcher = getMatcher(path);
+ if (matcher.matches())
+ {
+ if (matcher.groupCount() >= 1)
+ {
+ String pathInfo = matcher.group(1);
+ if ("".equals(pathInfo))
+ {
+ return "/";
+ }
+ else
+ {
+ return pathInfo;
+ }
+ }
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public String getPathMatch(String path)
+ {
+ Matcher matcher = getMatcher(path);
+ if (matcher.matches())
+ {
+ if (matcher.groupCount() >= 1)
+ {
+ int idx = matcher.start(1);
+ if (idx > 0)
+ {
+ if (path.charAt(idx - 1) == '/')
+ {
+ idx--;
+ }
+ return path.substring(0,idx);
+ }
+ }
+ return path;
+ }
+ return null;
+ }
+
+ public Pattern getPattern()
+ {
+ return this.pattern;
+ }
+
+ @Override
+ public String getRelativePath(String base, String path)
+ {
+ // TODO Auto-generated method stub
+ return null;
+ }
+
+ @Override
+ public boolean matches(String path)
+ {
+ return getMatcher(path).matches();
+ }
+}
diff --git a/jetty-websocket/websocket-server/src/main/java/org/eclipse/jetty/websocket/server/pathmap/ServletPathSpec.java b/jetty-websocket/websocket-server/src/main/java/org/eclipse/jetty/websocket/server/pathmap/ServletPathSpec.java
new file mode 100644
index 0000000..a6feb8f
--- /dev/null
+++ b/jetty-websocket/websocket-server/src/main/java/org/eclipse/jetty/websocket/server/pathmap/ServletPathSpec.java
@@ -0,0 +1,291 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.server.pathmap;
+
+import org.eclipse.jetty.util.URIUtil;
+
+public class ServletPathSpec extends PathSpec
+{
+ public static final String PATH_SPEC_SEPARATORS = ":,";
+
+ /**
+ * Get multi-path spec splits.
+ *
+ * @param servletPathSpec
+ * the path spec that might contain multiple declared path specs
+ * @return the individual path specs found.
+ */
+ public static ServletPathSpec[] getMultiPathSpecs(String servletPathSpec)
+ {
+ String pathSpecs[] = servletPathSpec.split(PATH_SPEC_SEPARATORS);
+ int len = pathSpecs.length;
+ ServletPathSpec sps[] = new ServletPathSpec[len];
+ for (int i = 0; i < len; i++)
+ {
+ sps[i] = new ServletPathSpec(pathSpecs[i]);
+ }
+ return sps;
+ }
+
+ public ServletPathSpec(String servletPathSpec)
+ {
+ super();
+ assertValidServletPathSpec(servletPathSpec);
+
+ // The Path Spec for Default Servlet
+ if ((servletPathSpec == null) || (servletPathSpec.length() == 0) || "/".equals(servletPathSpec))
+ {
+ super.pathSpec = "/";
+ super.pathDepth = -1; // force this to be last in sort order
+ this.specLength = 1;
+ this.group = PathSpecGroup.DEFAULT;
+ return;
+ }
+
+ this.specLength = servletPathSpec.length();
+ super.pathDepth = 0;
+ char lastChar = servletPathSpec.charAt(specLength - 1);
+ // prefix based
+ if ((servletPathSpec.charAt(0) == '/') && (specLength > 1) && (lastChar == '*'))
+ {
+ this.group = PathSpecGroup.PREFIX_GLOB;
+ }
+ // suffix based
+ else if (servletPathSpec.charAt(0) == '*')
+ {
+ this.group = PathSpecGroup.SUFFIX_GLOB;
+ }
+ else
+ {
+ this.group = PathSpecGroup.EXACT;
+ }
+
+ for (int i = 0; i < specLength; i++)
+ {
+ int cp = servletPathSpec.codePointAt(i);
+ if (cp < 128)
+ {
+ char c = (char)cp;
+ switch (c)
+ {
+ case '/':
+ super.pathDepth++;
+ break;
+ }
+ }
+ }
+
+ super.pathSpec = servletPathSpec;
+ }
+
+ private void assertValidServletPathSpec(String servletPathSpec)
+ {
+ if ((servletPathSpec == null) || servletPathSpec.equals(""))
+ {
+ return; // empty path spec
+ }
+
+ // Ensure we don't have path spec separators here in our single path spec.
+ for (char c : PATH_SPEC_SEPARATORS.toCharArray())
+ {
+ if (servletPathSpec.indexOf(c) >= 0)
+ {
+ throw new IllegalArgumentException("Servlet Spec 12.2 violation: encountered Path Spec Separator [" + PATH_SPEC_SEPARATORS
+ + "] within specified path spec. did you forget to split this path spec up?");
+ }
+ }
+
+ int len = servletPathSpec.length();
+ // path spec must either start with '/' or '*.'
+ if (servletPathSpec.charAt(0) == '/')
+ {
+ // Prefix Based
+ if (len == 1)
+ {
+ return; // simple '/' path spec
+ }
+ int idx = servletPathSpec.indexOf('*');
+ if (idx < 0)
+ {
+ return; // no hit on glob '*'
+ }
+ // only allowed to have '*' at the end of the path spec
+ if (idx != (len - 1))
+ {
+ throw new IllegalArgumentException("Servlet Spec 12.2 violation: glob '*' can only exist at end of prefix based matches");
+ }
+ }
+ else if (servletPathSpec.startsWith("*."))
+ {
+ // Suffix Based
+ int idx = servletPathSpec.indexOf('/');
+ // cannot have path separator
+ if (idx >= 0)
+ {
+ throw new IllegalArgumentException("Servlet Spec 12.2 violation: suffix based path spec cannot have path separators");
+ }
+
+ idx = servletPathSpec.indexOf('*',2);
+ // only allowed to have 1 glob '*', at the start of the path spec
+ if (idx >= 1)
+ {
+ throw new IllegalArgumentException("Servlet Spec 12.2 violation: suffix based path spec cannot have multiple glob '*'");
+ }
+ }
+ else
+ {
+ throw new IllegalArgumentException("Servlet Spec 12.2 violation: path spec must start with \"/\" or \"*.\"");
+ }
+ }
+
+ @Override
+ public String getPathInfo(String path)
+ {
+ // Path Info only valid for PREFIX_GLOB types
+ if (group == PathSpecGroup.PREFIX_GLOB)
+ {
+ if (path.length() == (specLength - 2))
+ {
+ return null;
+ }
+ return path.substring(specLength - 2);
+ }
+
+ return null;
+ }
+
+ @Override
+ public String getPathMatch(String path)
+ {
+ switch (group)
+ {
+ case EXACT:
+ if (pathSpec.equals(path))
+ {
+ return path;
+ }
+ else
+ {
+ return null;
+ }
+ case PREFIX_GLOB:
+ if (isWildcardMatch(path))
+ {
+ return path.substring(0,specLength - 2);
+ }
+ else
+ {
+ return null;
+ }
+ case SUFFIX_GLOB:
+ if (path.regionMatches(path.length() - (specLength - 1),pathSpec,1,specLength - 1))
+ {
+ return path;
+ }
+ else
+ {
+ return null;
+ }
+ case DEFAULT:
+ return path;
+ default:
+ return null;
+ }
+ }
+
+ @Override
+ public String getRelativePath(String base, String path)
+ {
+ String info = getPathInfo(path);
+ if (info == null)
+ {
+ info = path;
+ }
+
+ if (info.startsWith("./"))
+ {
+ info = info.substring(2);
+ }
+ if (base.endsWith(URIUtil.SLASH))
+ {
+ if (info.startsWith(URIUtil.SLASH))
+ {
+ path = base + info.substring(1);
+ }
+ else
+ {
+ path = base + info;
+ }
+ }
+ else if (info.startsWith(URIUtil.SLASH))
+ {
+ path = base + info;
+ }
+ else
+ {
+ path = base + URIUtil.SLASH + info;
+ }
+ return path;
+ }
+
+ private boolean isExactMatch(String path)
+ {
+ if (group == PathSpecGroup.EXACT)
+ {
+ if (pathSpec.equals(path))
+ {
+ return true;
+ }
+ return (path.charAt(path.length() - 1) == '/') && (path.equals(pathSpec + '/'));
+ }
+ return false;
+ }
+
+ private boolean isWildcardMatch(String path)
+ {
+ // For a spec of "/foo/*" match "/foo" , "/foo/..." but not "/foobar"
+ int cpl = specLength - 2;
+ if ((group == PathSpecGroup.PREFIX_GLOB) && (path.regionMatches(0,pathSpec,0,cpl)))
+ {
+ if ((path.length() == cpl) || ('/' == path.charAt(cpl)))
+ {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public boolean matches(String path)
+ {
+ switch (group)
+ {
+ case EXACT:
+ return isExactMatch(path);
+ case PREFIX_GLOB:
+ return isWildcardMatch(path);
+ case SUFFIX_GLOB:
+ return path.regionMatches((path.length() - specLength) + 1,pathSpec,1,specLength - 1);
+ case DEFAULT:
+ return true;
+ default:
+ return false;
+ }
+ }
+}
diff --git a/jetty-websocket/websocket-server/src/test/java/org/eclipse/jetty/websocket/server/AnnotatedMaxMessageSizeTest.java b/jetty-websocket/websocket-server/src/test/java/org/eclipse/jetty/websocket/server/AnnotatedMaxMessageSizeTest.java
index 2db4a48..ef44827 100644
--- a/jetty-websocket/websocket-server/src/test/java/org/eclipse/jetty/websocket/server/AnnotatedMaxMessageSizeTest.java
+++ b/jetty-websocket/websocket-server/src/test/java/org/eclipse/jetty/websocket/server/AnnotatedMaxMessageSizeTest.java
@@ -26,13 +26,10 @@
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
-import org.eclipse.jetty.websocket.api.UpgradeRequest;
-import org.eclipse.jetty.websocket.api.UpgradeResponse;
import org.eclipse.jetty.websocket.common.WebSocketFrame;
import org.eclipse.jetty.websocket.server.blockhead.BlockheadClient;
import org.eclipse.jetty.websocket.server.examples.echo.BigEchoSocket;
import org.eclipse.jetty.websocket.server.helper.IncomingFramesCapture;
-import org.eclipse.jetty.websocket.servlet.WebSocketCreator;
import org.eclipse.jetty.websocket.servlet.WebSocketServletFactory;
import org.junit.AfterClass;
import org.junit.Assert;
@@ -57,14 +54,7 @@
@Override
public void configure(WebSocketServletFactory factory)
{
- factory.setCreator(new WebSocketCreator()
- {
- @Override
- public Object createWebSocket(UpgradeRequest req, UpgradeResponse resp)
- {
- return new BigEchoSocket();
- }
- });
+ factory.register(BigEchoSocket.class);
}
};
diff --git a/jetty-websocket/websocket-server/src/test/java/org/eclipse/jetty/websocket/server/FirefoxTest.java b/jetty-websocket/websocket-server/src/test/java/org/eclipse/jetty/websocket/server/FirefoxTest.java
new file mode 100644
index 0000000..fc20291
--- /dev/null
+++ b/jetty-websocket/websocket-server/src/test/java/org/eclipse/jetty/websocket/server/FirefoxTest.java
@@ -0,0 +1,77 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.server;
+
+import static org.hamcrest.Matchers.*;
+
+import java.util.concurrent.TimeUnit;
+
+import org.eclipse.jetty.websocket.common.WebSocketFrame;
+import org.eclipse.jetty.websocket.server.blockhead.BlockheadClient;
+import org.eclipse.jetty.websocket.server.examples.MyEchoServlet;
+import org.eclipse.jetty.websocket.server.helper.IncomingFramesCapture;
+import org.junit.AfterClass;
+import org.junit.Assert;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+public class FirefoxTest
+{
+ private static SimpleServletServer server;
+
+ @BeforeClass
+ public static void startServer() throws Exception
+ {
+ server = new SimpleServletServer(new MyEchoServlet());
+ server.start();
+ }
+
+ @AfterClass
+ public static void stopServer()
+ {
+ server.stop();
+ }
+
+ @Test
+ public void testConnectionKeepAlive() throws Exception
+ {
+ BlockheadClient client = new BlockheadClient(server.getServerUri());
+ try
+ {
+ // Odd Connection Header value seen in Firefox
+ client.setConnectionValue("keep-alive, Upgrade");
+ client.connect();
+ client.sendStandardRequest();
+ client.expectUpgradeResponse();
+
+ // Generate text frame
+ String msg = "this is an echo ... cho ... ho ... o";
+ client.write(WebSocketFrame.text(msg));
+
+ // Read frame (hopefully text frame)
+ IncomingFramesCapture capture = client.readFrames(1,TimeUnit.MILLISECONDS,500);
+ WebSocketFrame tf = capture.getFrames().poll();
+ Assert.assertThat("Text Frame.status code",tf.getPayloadAsUTF8(),is(msg));
+ }
+ finally
+ {
+ client.close();
+ }
+ }
+}
diff --git a/jetty-websocket/websocket-server/src/test/java/org/eclipse/jetty/websocket/server/IdleTimeoutTest.java b/jetty-websocket/websocket-server/src/test/java/org/eclipse/jetty/websocket/server/IdleTimeoutTest.java
index 97e34c6..3909933 100644
--- a/jetty-websocket/websocket-server/src/test/java/org/eclipse/jetty/websocket/server/IdleTimeoutTest.java
+++ b/jetty-websocket/websocket-server/src/test/java/org/eclipse/jetty/websocket/server/IdleTimeoutTest.java
@@ -20,7 +20,6 @@
import static org.hamcrest.Matchers.*;
-import java.io.IOException;
import java.util.concurrent.TimeUnit;
import org.eclipse.jetty.websocket.api.StatusCode;
diff --git a/jetty-websocket/websocket-server/src/test/java/org/eclipse/jetty/websocket/server/RequestHeadersTest.java b/jetty-websocket/websocket-server/src/test/java/org/eclipse/jetty/websocket/server/RequestHeadersTest.java
index b28d79f..d6f051b 100644
--- a/jetty-websocket/websocket-server/src/test/java/org/eclipse/jetty/websocket/server/RequestHeadersTest.java
+++ b/jetty-websocket/websocket-server/src/test/java/org/eclipse/jetty/websocket/server/RequestHeadersTest.java
@@ -28,6 +28,8 @@
import org.eclipse.jetty.websocket.api.UpgradeResponse;
import org.eclipse.jetty.websocket.server.blockhead.BlockheadClient;
import org.eclipse.jetty.websocket.server.helper.EchoSocket;
+import org.eclipse.jetty.websocket.servlet.ServletUpgradeRequest;
+import org.eclipse.jetty.websocket.servlet.ServletUpgradeResponse;
import org.eclipse.jetty.websocket.servlet.WebSocketCreator;
import org.eclipse.jetty.websocket.servlet.WebSocketServlet;
import org.eclipse.jetty.websocket.servlet.WebSocketServletFactory;
@@ -45,7 +47,7 @@
private EchoSocket echoSocket = new EchoSocket();
@Override
- public Object createWebSocket(UpgradeRequest req, UpgradeResponse resp)
+ public Object createWebSocket(ServletUpgradeRequest req, ServletUpgradeResponse resp)
{
this.lastRequest = req;
this.lastResponse = resp;
diff --git a/jetty-websocket/websocket-server/src/test/java/org/eclipse/jetty/websocket/server/WebSocketCloseTest.java b/jetty-websocket/websocket-server/src/test/java/org/eclipse/jetty/websocket/server/WebSocketCloseTest.java
index aafdea8..17fac30 100644
--- a/jetty-websocket/websocket-server/src/test/java/org/eclipse/jetty/websocket/server/WebSocketCloseTest.java
+++ b/jetty-websocket/websocket-server/src/test/java/org/eclipse/jetty/websocket/server/WebSocketCloseTest.java
@@ -20,7 +20,6 @@
import static org.hamcrest.Matchers.*;
-import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CountDownLatch;
@@ -31,16 +30,16 @@
import org.eclipse.jetty.util.log.StacklessLogging;
import org.eclipse.jetty.websocket.api.Session;
import org.eclipse.jetty.websocket.api.StatusCode;
-import org.eclipse.jetty.websocket.api.UpgradeRequest;
-import org.eclipse.jetty.websocket.api.UpgradeResponse;
import org.eclipse.jetty.websocket.api.WebSocketAdapter;
import org.eclipse.jetty.websocket.common.CloseInfo;
import org.eclipse.jetty.websocket.common.OpCode;
import org.eclipse.jetty.websocket.common.WebSocketFrame;
-import org.eclipse.jetty.websocket.common.events.EventDriver;
+import org.eclipse.jetty.websocket.common.events.AbstractEventDriver;
import org.eclipse.jetty.websocket.server.blockhead.BlockheadClient;
import org.eclipse.jetty.websocket.server.helper.IncomingFramesCapture;
import org.eclipse.jetty.websocket.server.helper.RFCSocket;
+import org.eclipse.jetty.websocket.servlet.ServletUpgradeRequest;
+import org.eclipse.jetty.websocket.servlet.ServletUpgradeResponse;
import org.eclipse.jetty.websocket.servlet.WebSocketCreator;
import org.eclipse.jetty.websocket.servlet.WebSocketServlet;
import org.eclipse.jetty.websocket.servlet.WebSocketServletFactory;
@@ -75,7 +74,6 @@
{
errors.add(cause);
}
-
}
@SuppressWarnings("serial")
@@ -88,7 +86,7 @@
}
@Override
- public Object createWebSocket(UpgradeRequest req, UpgradeResponse resp)
+ public Object createWebSocket(ServletUpgradeRequest req, ServletUpgradeResponse resp)
{
if (req.hasSubProtocol("fastclose"))
{
@@ -116,14 +114,7 @@
public void onWebSocketConnect(Session sess)
{
LOG.debug("onWebSocketConnect({})",sess);
- try
- {
- sess.close();
- }
- catch (IOException e)
- {
- e.printStackTrace(System.err);
- }
+ sess.close();
}
}
@@ -209,7 +200,7 @@
client.setTimeout(TimeUnit.SECONDS,1);
try
{
- try (StacklessLogging scope = new StacklessLogging(EventDriver.class))
+ try (StacklessLogging scope = new StacklessLogging(AbstractEventDriver.class))
{
client.connect();
client.sendStandardRequest();
diff --git a/jetty-websocket/websocket-server/src/test/java/org/eclipse/jetty/websocket/server/WebSocketOverSSLTest.java b/jetty-websocket/websocket-server/src/test/java/org/eclipse/jetty/websocket/server/WebSocketOverSSLTest.java
index 3502924..9a326d9 100644
--- a/jetty-websocket/websocket-server/src/test/java/org/eclipse/jetty/websocket/server/WebSocketOverSSLTest.java
+++ b/jetty-websocket/websocket-server/src/test/java/org/eclipse/jetty/websocket/server/WebSocketOverSSLTest.java
@@ -26,8 +26,6 @@
import org.eclipse.jetty.toolchain.test.EventQueue;
import org.eclipse.jetty.toolchain.test.TestTracker;
-import org.eclipse.jetty.util.log.Log;
-import org.eclipse.jetty.util.log.Logger;
import org.eclipse.jetty.websocket.api.Session;
import org.eclipse.jetty.websocket.client.WebSocketClient;
import org.eclipse.jetty.websocket.server.helper.CaptureSocket;
diff --git a/jetty-websocket/websocket-server/src/test/java/org/eclipse/jetty/websocket/server/WebSocketServletRFCTest.java b/jetty-websocket/websocket-server/src/test/java/org/eclipse/jetty/websocket/server/WebSocketServletRFCTest.java
index 215f491..2682efd 100644
--- a/jetty-websocket/websocket-server/src/test/java/org/eclipse/jetty/websocket/server/WebSocketServletRFCTest.java
+++ b/jetty-websocket/websocket-server/src/test/java/org/eclipse/jetty/websocket/server/WebSocketServletRFCTest.java
@@ -20,7 +20,6 @@
import static org.hamcrest.Matchers.*;
-import java.net.SocketException;
import java.nio.ByteBuffer;
import java.util.Arrays;
import java.util.concurrent.TimeUnit;
@@ -45,7 +44,6 @@
import org.junit.AfterClass;
import org.junit.Assert;
import org.junit.BeforeClass;
-import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -199,6 +197,9 @@
@Test
public void testInternalError() throws Exception
{
+ // Disable Long Stacks from EventDriver (we know this test will throw an exception)
+ enableStacks(EventDriver.class,false);
+
BlockheadClient client = new BlockheadClient(server.getServerUri());
try
{
@@ -220,6 +221,8 @@
}
finally
{
+ // Reenable Long Stacks from EventDriver
+ enableStacks(EventDriver.class,true);
client.close();
}
}
@@ -267,90 +270,6 @@
}
@Test
- @Ignore("Should be moved to Fuzzer")
- public void testMaxBinarySize() throws Exception
- {
- BlockheadClient client = new BlockheadClient(server.getServerUri());
- client.setProtocols("other");
- try
- {
- client.connect();
- client.sendStandardRequest();
- client.expectUpgradeResponse();
-
- // Choose a size for a single frame larger than the
- // server side policy
- int dataSize = 1024 * 100;
- byte buf[] = new byte[dataSize];
- Arrays.fill(buf,(byte)0x44);
-
- WebSocketFrame bin = WebSocketFrame.binary(buf).setFin(true);
- ByteBuffer bb = generator.generate(bin);
- try
- {
- client.writeRaw(bb);
- Assert.fail("Write should have failed due to terminated connection");
- }
- catch (SocketException e)
- {
- Assert.assertThat("Exception",e.getMessage(),containsString("Broken pipe"));
- }
-
- IncomingFramesCapture capture = client.readFrames(1,TimeUnit.SECONDS,1);
- WebSocketFrame frame = capture.getFrames().poll();
- Assert.assertThat("frames[0].opcode",frame.getOpCode(),is(OpCode.CLOSE));
- CloseInfo close = new CloseInfo(frame);
- Assert.assertThat("Close Status Code",close.getStatusCode(),is(StatusCode.MESSAGE_TOO_LARGE));
- }
- finally
- {
- client.close();
- }
- }
-
- @Test
- @Ignore("Should be moved to Fuzzer")
- public void testMaxTextSize() throws Exception
- {
- BlockheadClient client = new BlockheadClient(server.getServerUri());
- client.setProtocols("other");
- try
- {
- client.connect();
- client.sendStandardRequest();
- client.expectUpgradeResponse();
-
- // Choose a size for a single frame larger than the
- // server side policy
- int dataSize = 1024 * 100;
- byte buf[] = new byte[dataSize];
- Arrays.fill(buf,(byte)'z');
-
- WebSocketFrame text = WebSocketFrame.text().setPayload(buf).setFin(true);
- ByteBuffer bb = generator.generate(text);
- try
- {
- client.writeRaw(bb);
- Assert.fail("Write should have failed due to terminated connection");
- }
- catch (SocketException e)
- {
- Assert.assertThat("Exception",e.getMessage(),containsString("Broken pipe"));
- }
-
- IncomingFramesCapture capture = client.readFrames(1,TimeUnit.SECONDS,1);
- WebSocketFrame frame = capture.getFrames().poll();
- Assert.assertThat("frames[0].opcode",frame.getOpCode(),is(OpCode.CLOSE));
- CloseInfo close = new CloseInfo(frame);
- Assert.assertThat("Close Status Code",close.getStatusCode(),is(StatusCode.MESSAGE_TOO_LARGE));
- }
- finally
- {
- client.close();
- }
- }
-
- @Test
public void testTextNotUTF8() throws Exception
{
// Disable Long Stacks from Parser (we know this test will throw an exception)
diff --git a/jetty-websocket/websocket-server/src/test/java/org/eclipse/jetty/websocket/server/ab/ABServlet.java b/jetty-websocket/websocket-server/src/test/java/org/eclipse/jetty/websocket/server/ab/ABServlet.java
index 72f79f5..4c606d0 100644
--- a/jetty-websocket/websocket-server/src/test/java/org/eclipse/jetty/websocket/server/ab/ABServlet.java
+++ b/jetty-websocket/websocket-server/src/test/java/org/eclipse/jetty/websocket/server/ab/ABServlet.java
@@ -36,7 +36,8 @@
// Test cases 9.x uses BIG frame sizes, let policy handle them.
int bigFrameSize = 20 * MBYTE;
- factory.getPolicy().setMaxMessageSize(bigFrameSize);
+ factory.getPolicy().setMaxTextMessageSize(bigFrameSize);
+ factory.getPolicy().setMaxBinaryMessageSize(bigFrameSize);
factory.register(ABSocket.class);
}
diff --git a/jetty-websocket/websocket-server/src/test/java/org/eclipse/jetty/websocket/server/ab/Fuzzer.java b/jetty-websocket/websocket-server/src/test/java/org/eclipse/jetty/websocket/server/ab/Fuzzer.java
index 82b2ce9..e01ede7 100644
--- a/jetty-websocket/websocket-server/src/test/java/org/eclipse/jetty/websocket/server/ab/Fuzzer.java
+++ b/jetty-websocket/websocket-server/src/test/java/org/eclipse/jetty/websocket/server/ab/Fuzzer.java
@@ -92,7 +92,8 @@
int bigMessageSize = 20 * MBYTE;
- policy.setMaxMessageSize(bigMessageSize);
+ policy.setMaxTextMessageSize(bigMessageSize);
+ policy.setMaxBinaryMessageSize(bigMessageSize);
policy.setIdleTimeout(5000);
this.client = new BlockheadClient(policy,testcase.getServer().getServerUri());
diff --git a/jetty-websocket/websocket-server/src/test/java/org/eclipse/jetty/websocket/server/blockhead/BlockheadClient.java b/jetty-websocket/websocket-server/src/test/java/org/eclipse/jetty/websocket/server/blockhead/BlockheadClient.java
index 8573c93..74234bd 100644
--- a/jetty-websocket/websocket-server/src/test/java/org/eclipse/jetty/websocket/server/blockhead/BlockheadClient.java
+++ b/jetty-websocket/websocket-server/src/test/java/org/eclipse/jetty/websocket/server/blockhead/BlockheadClient.java
@@ -49,7 +49,6 @@
import org.eclipse.jetty.util.StringUtil;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;
-import org.eclipse.jetty.websocket.api.WebSocketException;
import org.eclipse.jetty.websocket.api.WebSocketPolicy;
import org.eclipse.jetty.websocket.api.WriteCallback;
import org.eclipse.jetty.websocket.api.extensions.ExtensionConfig;
@@ -116,6 +115,7 @@
private IOState ioState;
private CountDownLatch disconnectedLatch = new CountDownLatch(1);
private ByteBuffer remainingBuffer;
+ private String connectionValue = "Upgrade";
public BlockheadClient(URI destWebsocketURI) throws URISyntaxException
{
@@ -233,6 +233,36 @@
}
}
+ public void expectServerDisconnect()
+ {
+ if (eof)
+ {
+ return;
+ }
+
+ try
+ {
+ int len = in.read();
+ if (len == (-1))
+ {
+ // we are disconnected
+ eof = true;
+ return;
+ }
+
+ Assert.assertThat("Expecting no data and proper socket disconnect (issued from server)",len,is(-1));
+ }
+ catch (SocketTimeoutException e)
+ {
+ LOG.warn(e);
+ Assert.fail("Expected a server initiated disconnect, instead the read timed out");
+ }
+ catch (IOException e)
+ {
+ // acceptable path
+ }
+ }
+
public HttpResponse expectUpgradeResponse() throws IOException
{
HttpResponse response = readResponseHeader();
@@ -294,6 +324,11 @@
out.flush();
}
+ public String getConnectionValue()
+ {
+ return connectionValue;
+ }
+
private List<ExtensionConfig> getExtensionConfigs(HttpResponse response)
{
List<ExtensionConfig> configs = new ArrayList<>();
@@ -375,7 +410,7 @@
* Errors received (after extensions)
*/
@Override
- public void incomingError(WebSocketException e)
+ public void incomingError(Throwable e)
{
incomingFrames.incomingError(e);
}
@@ -465,36 +500,6 @@
}
}
- public void expectServerDisconnect()
- {
- if (eof)
- {
- return;
- }
-
- try
- {
- int len = in.read();
- if (len == (-1))
- {
- // we are disconnected
- eof = true;
- return;
- }
-
- Assert.assertThat("Expecting no data and proper socket disconnect (issued from server)",len,is(-1));
- }
- catch (SocketTimeoutException e)
- {
- LOG.warn(e);
- Assert.fail("Expected a server initiated disconnect, instead the read timed out");
- }
- catch (IOException e)
- {
- // acceptable path
- }
- }
-
public int read(ByteBuffer buf) throws IOException
{
if (eof)
@@ -607,7 +612,7 @@
req.append("GET ").append(getRequestPath()).append(" HTTP/1.1\r\n");
req.append("Host: ").append(getRequestHost()).append("\r\n");
req.append("Upgrade: websocket\r\n");
- req.append("Connection: Upgrade\r\n");
+ req.append("Connection: ").append(connectionValue).append("\r\n");
for (String header : headers)
{
req.append(header);
@@ -628,6 +633,11 @@
writeRaw(req.toString());
}
+ public void setConnectionValue(String connectionValue)
+ {
+ this.connectionValue = connectionValue;
+ }
+
public void setDebug(boolean flag)
{
this.debug = flag;
diff --git a/jetty-websocket/websocket-server/src/test/java/org/eclipse/jetty/websocket/server/blockhead/HttpResponse.java b/jetty-websocket/websocket-server/src/test/java/org/eclipse/jetty/websocket/server/blockhead/HttpResponse.java
index 59d44eb..c18366a 100644
--- a/jetty-websocket/websocket-server/src/test/java/org/eclipse/jetty/websocket/server/blockhead/HttpResponse.java
+++ b/jetty-websocket/websocket-server/src/test/java/org/eclipse/jetty/websocket/server/blockhead/HttpResponse.java
@@ -22,6 +22,7 @@
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
+import java.util.TreeMap;
import org.eclipse.jetty.websocket.common.io.http.HttpResponseHeaderParseListener;
@@ -29,13 +30,13 @@
{
private int statusCode;
private String statusReason;
- private Map<String, String> headers = new HashMap<>();
+ private Map<String, String> headers = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
private ByteBuffer remainingBuffer;
@Override
public void addHeader(String name, String value)
{
- headers.put(name.toLowerCase(Locale.ENGLISH),value);
+ headers.put(name,value);
}
public String getExtensionsHeader()
@@ -45,7 +46,7 @@
public String getHeader(String name)
{
- return headers.get(name.toLowerCase(Locale.ENGLISH));
+ return headers.get(name);
}
public ByteBuffer getRemainingBuffer()
diff --git a/jetty-websocket/websocket-server/src/test/java/org/eclipse/jetty/websocket/server/browser/BrowserDebugTool.java b/jetty-websocket/websocket-server/src/test/java/org/eclipse/jetty/websocket/server/browser/BrowserDebugTool.java
index d618681..579ad42 100644
--- a/jetty-websocket/websocket-server/src/test/java/org/eclipse/jetty/websocket/server/browser/BrowserDebugTool.java
+++ b/jetty-websocket/websocket-server/src/test/java/org/eclipse/jetty/websocket/server/browser/BrowserDebugTool.java
@@ -23,11 +23,11 @@
import org.eclipse.jetty.server.handler.ResourceHandler;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;
-import org.eclipse.jetty.websocket.api.UpgradeRequest;
-import org.eclipse.jetty.websocket.api.UpgradeResponse;
import org.eclipse.jetty.websocket.common.extensions.compress.FrameCompressionExtension;
-import org.eclipse.jetty.websocket.common.extensions.compress.MessageCompressionExtension;
+import org.eclipse.jetty.websocket.common.extensions.compress.MessageDeflateCompressionExtension;
import org.eclipse.jetty.websocket.server.WebSocketHandler;
+import org.eclipse.jetty.websocket.servlet.ServletUpgradeRequest;
+import org.eclipse.jetty.websocket.servlet.ServletUpgradeResponse;
import org.eclipse.jetty.websocket.servlet.WebSocketCreator;
import org.eclipse.jetty.websocket.servlet.WebSocketServletFactory;
@@ -69,7 +69,7 @@
private Server server;
@Override
- public Object createWebSocket(UpgradeRequest req, UpgradeResponse resp)
+ public Object createWebSocket(ServletUpgradeRequest req, ServletUpgradeResponse resp)
{
LOG.debug("Creating BrowserSocket");
@@ -112,7 +112,7 @@
// Setup some extensions we want to test against
factory.getExtensionFactory().register("x-webkit-deflate-frame",FrameCompressionExtension.class);
- factory.getExtensionFactory().register("permessage-compress",MessageCompressionExtension.class);
+ factory.getExtensionFactory().register("permessage-compress",MessageDeflateCompressionExtension.class);
// Setup the desired Socket to use for all incoming upgrade requests
factory.setCreator(BrowserDebugTool.this);
diff --git a/jetty-websocket/websocket-server/src/test/java/org/eclipse/jetty/websocket/server/examples/MyCustomCreationServlet.java b/jetty-websocket/websocket-server/src/test/java/org/eclipse/jetty/websocket/server/examples/MyCustomCreationServlet.java
index 5a2d1e8..9a78bba 100644
--- a/jetty-websocket/websocket-server/src/test/java/org/eclipse/jetty/websocket/server/examples/MyCustomCreationServlet.java
+++ b/jetty-websocket/websocket-server/src/test/java/org/eclipse/jetty/websocket/server/examples/MyCustomCreationServlet.java
@@ -20,9 +20,9 @@
import java.io.IOException;
-import org.eclipse.jetty.websocket.api.UpgradeRequest;
-import org.eclipse.jetty.websocket.api.UpgradeResponse;
import org.eclipse.jetty.websocket.server.examples.echo.BigEchoSocket;
+import org.eclipse.jetty.websocket.servlet.ServletUpgradeRequest;
+import org.eclipse.jetty.websocket.servlet.ServletUpgradeResponse;
import org.eclipse.jetty.websocket.servlet.WebSocketCreator;
import org.eclipse.jetty.websocket.servlet.WebSocketServlet;
import org.eclipse.jetty.websocket.servlet.WebSocketServletFactory;
@@ -33,7 +33,7 @@
public static class MyCustomCreator implements WebSocketCreator
{
@Override
- public Object createWebSocket(UpgradeRequest req, UpgradeResponse resp)
+ public Object createWebSocket(ServletUpgradeRequest req, ServletUpgradeResponse resp)
{
String query = req.getQueryString();
diff --git a/jetty-websocket/websocket-server/src/test/java/org/eclipse/jetty/websocket/server/examples/echo/BigEchoSocket.java b/jetty-websocket/websocket-server/src/test/java/org/eclipse/jetty/websocket/server/examples/echo/BigEchoSocket.java
index da5aa8b..3c8aec6 100644
--- a/jetty-websocket/websocket-server/src/test/java/org/eclipse/jetty/websocket/server/examples/echo/BigEchoSocket.java
+++ b/jetty-websocket/websocket-server/src/test/java/org/eclipse/jetty/websocket/server/examples/echo/BigEchoSocket.java
@@ -29,7 +29,7 @@
/**
* Example Socket for echoing back Big data using the Annotation techniques along with stateless techniques.
*/
-@WebSocket(maxMessageSize = 64 * 1024)
+@WebSocket(maxTextMessageSize = 64 * 1024, maxBinaryMessageSize = 64 * 1024)
public class BigEchoSocket
{
private static final Logger LOG = Log.getLogger(BigEchoSocket.class);
diff --git a/jetty-websocket/websocket-server/src/test/java/org/eclipse/jetty/websocket/server/examples/echo/EchoCreator.java b/jetty-websocket/websocket-server/src/test/java/org/eclipse/jetty/websocket/server/examples/echo/EchoCreator.java
index 8b21bf2..c477517 100644
--- a/jetty-websocket/websocket-server/src/test/java/org/eclipse/jetty/websocket/server/examples/echo/EchoCreator.java
+++ b/jetty-websocket/websocket-server/src/test/java/org/eclipse/jetty/websocket/server/examples/echo/EchoCreator.java
@@ -18,8 +18,8 @@
package org.eclipse.jetty.websocket.server.examples.echo;
-import org.eclipse.jetty.websocket.api.UpgradeRequest;
-import org.eclipse.jetty.websocket.api.UpgradeResponse;
+import org.eclipse.jetty.websocket.servlet.ServletUpgradeRequest;
+import org.eclipse.jetty.websocket.servlet.ServletUpgradeResponse;
import org.eclipse.jetty.websocket.servlet.WebSocketCreator;
/**
@@ -32,7 +32,7 @@
private LogSocket logSocket = new LogSocket();
@Override
- public Object createWebSocket(UpgradeRequest req, UpgradeResponse resp)
+ public Object createWebSocket(ServletUpgradeRequest req, ServletUpgradeResponse resp)
{
for (String protocol : req.getSubProtocols())
{
diff --git a/jetty-websocket/websocket-server/src/test/java/org/eclipse/jetty/websocket/server/helper/CaptureSocket.java b/jetty-websocket/websocket-server/src/test/java/org/eclipse/jetty/websocket/server/helper/CaptureSocket.java
index a1b597a..b983ba9 100644
--- a/jetty-websocket/websocket-server/src/test/java/org/eclipse/jetty/websocket/server/helper/CaptureSocket.java
+++ b/jetty-websocket/websocket-server/src/test/java/org/eclipse/jetty/websocket/server/helper/CaptureSocket.java
@@ -18,8 +18,6 @@
package org.eclipse.jetty.websocket.server.helper;
-import java.io.IOException;
-import java.sql.Connection;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
@@ -44,14 +42,7 @@
public void close()
{
- try
- {
- getSession().close();
- }
- catch (IOException ignore)
- {
- /* ignore */
- }
+ getSession().close();
}
@Override
diff --git a/jetty-websocket/websocket-server/src/test/java/org/eclipse/jetty/websocket/server/helper/EchoServlet.java b/jetty-websocket/websocket-server/src/test/java/org/eclipse/jetty/websocket/server/helper/EchoServlet.java
index 7cbbd2e..877d57b 100644
--- a/jetty-websocket/websocket-server/src/test/java/org/eclipse/jetty/websocket/server/helper/EchoServlet.java
+++ b/jetty-websocket/websocket-server/src/test/java/org/eclipse/jetty/websocket/server/helper/EchoServlet.java
@@ -19,7 +19,7 @@
package org.eclipse.jetty.websocket.server.helper;
import org.eclipse.jetty.websocket.common.extensions.compress.FrameCompressionExtension;
-import org.eclipse.jetty.websocket.common.extensions.compress.MessageCompressionExtension;
+import org.eclipse.jetty.websocket.common.extensions.compress.MessageDeflateCompressionExtension;
import org.eclipse.jetty.websocket.servlet.WebSocketServlet;
import org.eclipse.jetty.websocket.servlet.WebSocketServletFactory;
@@ -34,7 +34,7 @@
{
// Setup some extensions we want to test against
factory.getExtensionFactory().register("x-webkit-deflate-frame",FrameCompressionExtension.class);
- factory.getExtensionFactory().register("permessage-compress",MessageCompressionExtension.class);
+ factory.getExtensionFactory().register("permessage-compress",MessageDeflateCompressionExtension.class);
// Setup the desired Socket to use for all incoming upgrade requests
factory.register(EchoSocket.class);
diff --git a/jetty-websocket/websocket-server/src/test/java/org/eclipse/jetty/websocket/server/helper/IncomingFramesCapture.java b/jetty-websocket/websocket-server/src/test/java/org/eclipse/jetty/websocket/server/helper/IncomingFramesCapture.java
index 82a4723..a501d34 100644
--- a/jetty-websocket/websocket-server/src/test/java/org/eclipse/jetty/websocket/server/helper/IncomingFramesCapture.java
+++ b/jetty-websocket/websocket-server/src/test/java/org/eclipse/jetty/websocket/server/helper/IncomingFramesCapture.java
@@ -37,7 +37,7 @@
{
private static final Logger LOG = Log.getLogger(IncomingFramesCapture.class);
private EventQueue<WebSocketFrame> frames = new EventQueue<>();
- private EventQueue<WebSocketException> errors = new EventQueue<>();
+ private EventQueue<Throwable> errors = new EventQueue<>();
public void assertErrorCount(int expectedCount)
{
@@ -86,14 +86,14 @@
for (Frame frame : frames)
{
System.err.printf("[%3d] %s%n",i++,frame);
- System.err.printf(" %s%n",BufferUtil.toDetailString(frame.getPayload()));
+ System.err.printf(" payload: %s%n",BufferUtil.toDetailString(frame.getPayload()));
}
}
- public int getErrorCount(Class<? extends WebSocketException> errorType)
+ public int getErrorCount(Class<? extends Throwable> errorType)
{
int count = 0;
- for (WebSocketException error : errors)
+ for (Throwable error : errors)
{
if (errorType.isInstance(error))
{
@@ -103,7 +103,7 @@
return count;
}
- public Queue<WebSocketException> getErrors()
+ public Queue<Throwable> getErrors()
{
return errors;
}
@@ -127,7 +127,7 @@
}
@Override
- public void incomingError(WebSocketException e)
+ public void incomingError(Throwable e)
{
LOG.debug(e);
errors.add(e);
diff --git a/jetty-websocket/websocket-server/src/test/java/org/eclipse/jetty/websocket/server/helper/SafariD00.java b/jetty-websocket/websocket-server/src/test/java/org/eclipse/jetty/websocket/server/helper/SafariD00.java
index d720634..e5357e9 100644
--- a/jetty-websocket/websocket-server/src/test/java/org/eclipse/jetty/websocket/server/helper/SafariD00.java
+++ b/jetty-websocket/websocket-server/src/test/java/org/eclipse/jetty/websocket/server/helper/SafariD00.java
@@ -18,6 +18,8 @@
package org.eclipse.jetty.websocket.server.helper;
+import static org.hamcrest.Matchers.*;
+
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
@@ -34,8 +36,6 @@
import org.eclipse.jetty.util.TypeUtil;
import org.junit.Assert;
-import static org.hamcrest.Matchers.is;
-
public class SafariD00
{
private URI uri;
@@ -113,6 +113,7 @@
{
line = br.readLine();
// System.out.printf("RESP: %s%n",line);
+ Assert.assertThat(line, notNullValue());
if (line.length() == 0)
{
foundEnd = true;
diff --git a/jetty-websocket/websocket-server/src/test/java/org/eclipse/jetty/websocket/server/helper/SessionSocket.java b/jetty-websocket/websocket-server/src/test/java/org/eclipse/jetty/websocket/server/helper/SessionSocket.java
index 9c00b8a..2e431dd 100644
--- a/jetty-websocket/websocket-server/src/test/java/org/eclipse/jetty/websocket/server/helper/SessionSocket.java
+++ b/jetty-websocket/websocket-server/src/test/java/org/eclipse/jetty/websocket/server/helper/SessionSocket.java
@@ -18,6 +18,7 @@
package org.eclipse.jetty.websocket.server.helper;
+import java.util.List;
import java.util.Map;
import org.eclipse.jetty.util.log.Log;
@@ -52,11 +53,11 @@
{
if (message.startsWith("getParameterMap"))
{
- Map<String, String[]> parameterMap = session.getUpgradeRequest().getParameterMap();
+ Map<String, List<String>> parameterMap = session.getUpgradeRequest().getParameterMap();
int idx = message.indexOf('|');
String key = message.substring(idx + 1);
- String values[] = parameterMap.get(key);
+ List<String> values = parameterMap.get(key);
if (values == null)
{
diff --git a/jetty-websocket/websocket-server/src/test/java/org/eclipse/jetty/websocket/server/pathmap/PathMappingsBenchmarkTest.java b/jetty-websocket/websocket-server/src/test/java/org/eclipse/jetty/websocket/server/pathmap/PathMappingsBenchmarkTest.java
new file mode 100644
index 0000000..081b7ff
--- /dev/null
+++ b/jetty-websocket/websocket-server/src/test/java/org/eclipse/jetty/websocket/server/pathmap/PathMappingsBenchmarkTest.java
@@ -0,0 +1,226 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.server.pathmap;
+
+import java.util.concurrent.CyclicBarrier;
+import java.util.concurrent.TimeUnit;
+
+import org.eclipse.jetty.http.PathMap;
+import org.eclipse.jetty.toolchain.test.AdvancedRunner;
+import org.eclipse.jetty.toolchain.test.annotation.Stress;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+import org.eclipse.jetty.websocket.server.pathmap.PathMappings;
+import org.eclipse.jetty.websocket.server.pathmap.ServletPathSpec;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AdvancedRunner.class)
+public class PathMappingsBenchmarkTest
+{
+ public static abstract class AbstractPathMapThread extends Thread
+ {
+ private int iterations;
+ private CyclicBarrier barrier;
+ private long success;
+ private long error;
+
+ public AbstractPathMapThread(int iterations, CyclicBarrier barrier)
+ {
+ this.iterations = iterations;
+ this.barrier = barrier;
+ }
+
+ public abstract String getMatchedResource(String path);
+
+ @Override
+ public void run()
+ {
+ int llen = LOOKUPS.length;
+ String path;
+ String expectedResource;
+ String matchedResource;
+ await(barrier);
+ for (int iter = 0; iter < iterations; iter++)
+ {
+ for (int li = 0; li < llen; li++)
+ {
+ path = LOOKUPS[li][0];
+ expectedResource = LOOKUPS[li][1];
+ matchedResource = getMatchedResource(path);
+ if (matchedResource.equals(expectedResource))
+ {
+ success++;
+ }
+ else
+ {
+ error++;
+ }
+ }
+ }
+ await(barrier);
+ }
+ }
+
+ public static class PathMapMatchThread extends AbstractPathMapThread
+ {
+ private PathMap<String> pathmap;
+
+ public PathMapMatchThread(PathMap<String> pathmap, int iters, CyclicBarrier barrier)
+ {
+ super(iters,barrier);
+ this.pathmap = pathmap;
+ }
+
+ @Override
+ public String getMatchedResource(String path)
+ {
+ return pathmap.getMatch(path).getValue();
+ }
+ }
+
+ public static class PathMatchThread extends AbstractPathMapThread
+ {
+ private PathMappings<String> pathmap;
+
+ public PathMatchThread(PathMappings<String> pathmap, int iters, CyclicBarrier barrier)
+ {
+ super(iters,barrier);
+ this.pathmap = pathmap;
+ }
+
+ @Override
+ public String getMatchedResource(String path)
+ {
+ return pathmap.getMatch(path).getResource();
+ }
+ }
+
+ private static final Logger LOG = Log.getLogger(PathMappingsBenchmarkTest.class);
+ private static final String[][] LOOKUPS;
+ private int runs = 20;
+ private int threads = 200;
+ private int iters = 10000;
+
+ static
+ {
+ LOOKUPS = new String[][]
+ {
+ // @formatter:off
+ { "/abs/path", "path" },
+ { "/abs/path/longer","longpath" },
+ { "/abs/path/foo","default" },
+ { "/main.css","default" },
+ { "/downloads/script.gz","gzipped" },
+ { "/downloads/distribution.tar.gz","tarball" },
+ { "/downloads/readme.txt","default" },
+ { "/downloads/logs.tgz","default" },
+ { "/animal/horse/mustang","animals" },
+ { "/animal/bird/eagle/bald","birds" },
+ { "/animal/fish/shark/hammerhead","fishes" },
+ { "/animal/insect/ladybug","animals" },
+ // @formatter:on
+ };
+ }
+
+ private static void await(CyclicBarrier barrier)
+ {
+ try
+ {
+ barrier.await();
+ }
+ catch (Exception x)
+ {
+ throw new RuntimeException(x);
+ }
+ }
+
+ @Stress("High CPU")
+ @Test
+ public void testServletPathMap()
+ {
+ // Setup (old) PathMap
+
+ PathMap<String> p = new PathMap<>();
+
+ p.put("/abs/path","path");
+ p.put("/abs/path/longer","longpath");
+ p.put("/animal/bird/*","birds");
+ p.put("/animal/fish/*","fishes");
+ p.put("/animal/*","animals");
+ p.put("*.tar.gz","tarball");
+ p.put("*.gz","gzipped");
+ p.put("/","default");
+
+ final CyclicBarrier barrier = new CyclicBarrier(threads + 1);
+
+ for (int r = 0; r < runs; r++)
+ {
+ for (int t = 0; t < threads; t++)
+ {
+ PathMapMatchThread thread = new PathMapMatchThread(p,iters,barrier);
+ thread.start();
+ }
+ await(barrier);
+ long begin = System.nanoTime();
+ await(barrier);
+ long end = System.nanoTime();
+ long elapsed = TimeUnit.NANOSECONDS.toMillis(end - begin);
+ int totalMatches = threads * iters * LOOKUPS.length;
+ LOG.info("jetty-http/PathMap (Servlet only) threads:{}/iters:{}/total-matches:{} => {} ms",threads,iters,totalMatches,elapsed);
+ }
+ }
+
+ @Stress("High CPU")
+ @Test
+ public void testServletPathMappings()
+ {
+ // Setup (new) PathMappings
+
+ PathMappings<String> p = new PathMappings<>();
+
+ p.put(new ServletPathSpec("/abs/path"),"path");
+ p.put(new ServletPathSpec("/abs/path/longer"),"longpath");
+ p.put(new ServletPathSpec("/animal/bird/*"),"birds");
+ p.put(new ServletPathSpec("/animal/fish/*"),"fishes");
+ p.put(new ServletPathSpec("/animal/*"),"animals");
+ p.put(new ServletPathSpec("*.tar.gz"),"tarball");
+ p.put(new ServletPathSpec("*.gz"),"gzipped");
+ p.put(new ServletPathSpec("/"),"default");
+
+ final CyclicBarrier barrier = new CyclicBarrier(threads + 1);
+
+ for (int r = 0; r < runs; r++)
+ {
+ for (int t = 0; t < threads; t++)
+ {
+ PathMatchThread thread = new PathMatchThread(p,iters,barrier);
+ thread.start();
+ }
+ await(barrier);
+ long begin = System.nanoTime();
+ await(barrier);
+ long end = System.nanoTime();
+ long elapsed = TimeUnit.NANOSECONDS.toMillis(end - begin);
+ int totalMatches = threads * iters * LOOKUPS.length;
+ LOG.info("jetty-websocket/PathMappings (Servlet only) threads:{}/iters:{}/total-matches:{} => {} ms",threads,iters,totalMatches,elapsed);
+
+ }
+ }
+}
diff --git a/jetty-websocket/websocket-server/src/test/java/org/eclipse/jetty/websocket/server/pathmap/PathMappingsTest.java b/jetty-websocket/websocket-server/src/test/java/org/eclipse/jetty/websocket/server/pathmap/PathMappingsTest.java
new file mode 100644
index 0000000..807b168
--- /dev/null
+++ b/jetty-websocket/websocket-server/src/test/java/org/eclipse/jetty/websocket/server/pathmap/PathMappingsTest.java
@@ -0,0 +1,119 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.server.pathmap;
+
+import static org.hamcrest.Matchers.*;
+
+import org.eclipse.jetty.websocket.server.pathmap.PathMappings.MappedResource;
+import org.junit.Assert;
+import org.junit.Test;
+
+public class PathMappingsTest
+{
+ private void assertMatch(PathMappings<String> pathmap, String path, String expectedValue)
+ {
+ String msg = String.format(".getMatch(\"%s\")",path);
+ MappedResource<String> match = pathmap.getMatch(path);
+ Assert.assertThat(msg,match,notNullValue());
+ String actualMatch = match.getResource();
+ Assert.assertEquals(msg,expectedValue,actualMatch);
+ }
+
+ public void dumpMappings(PathMappings<String> p)
+ {
+ for (MappedResource<String> res : p)
+ {
+ System.out.printf(" %s%n",res);
+ }
+ }
+
+ /**
+ * Test the match order rules with a mixed Servlet and WebSocket path specs
+ * <p>
+ * <ul>
+ * <li>Exact match</li>
+ * <li>Longest prefix match</li>
+ * <li>Longest suffix match</li>
+ * </ul>
+ */
+ @Test
+ public void testMixedMatchOrder()
+ {
+ PathMappings<String> p = new PathMappings<>();
+
+ p.put(new ServletPathSpec("/"),"default");
+ p.put(new ServletPathSpec("/animal/bird/*"),"birds");
+ p.put(new ServletPathSpec("/animal/fish/*"),"fishes");
+ p.put(new ServletPathSpec("/animal/*"),"animals");
+ p.put(new RegexPathSpec("^/animal/.*/chat$"),"animalChat");
+ p.put(new RegexPathSpec("^/animal/.*/cam$"),"animalCam");
+ p.put(new RegexPathSpec("^/entrance/cam$"),"entranceCam");
+
+ // dumpMappings(p);
+
+ assertMatch(p,"/animal/bird/eagle","birds");
+ assertMatch(p,"/animal/fish/bass/sea","fishes");
+ assertMatch(p,"/animal/peccary/javalina/evolution","animals");
+ assertMatch(p,"/","default");
+ assertMatch(p,"/animal/bird/eagle/chat","animalChat");
+ assertMatch(p,"/animal/bird/penguin/chat","animalChat");
+ assertMatch(p,"/animal/fish/trout/cam","animalCam");
+ assertMatch(p,"/entrance/cam","entranceCam");
+ }
+
+ /**
+ * Test the match order rules imposed by the Servlet API.
+ * <p>
+ * <ul>
+ * <li>Exact match</li>
+ * <li>Longest prefix match</li>
+ * <li>Longest suffix match</li>
+ * <li>default</li>
+ * </ul>
+ */
+ @Test
+ public void testServletMatchOrder()
+ {
+ PathMappings<String> p = new PathMappings<>();
+
+ p.put(new ServletPathSpec("/abs/path"),"path");
+ p.put(new ServletPathSpec("/abs/path/longer"),"longpath");
+ p.put(new ServletPathSpec("/animal/bird/*"),"birds");
+ p.put(new ServletPathSpec("/animal/fish/*"),"fishes");
+ p.put(new ServletPathSpec("/animal/*"),"animals");
+ p.put(new ServletPathSpec("*.tar.gz"),"tarball");
+ p.put(new ServletPathSpec("*.gz"),"gzipped");
+ p.put(new ServletPathSpec("/"),"default");
+
+ // dumpMappings(p);
+
+ assertMatch(p,"/abs/path","path");
+ assertMatch(p,"/abs/path/longer","longpath");
+ assertMatch(p,"/abs/path/foo","default");
+ assertMatch(p,"/main.css","default");
+ assertMatch(p,"/downloads/script.gz","gzipped");
+ assertMatch(p,"/downloads/distribution.tar.gz","tarball");
+ assertMatch(p,"/downloads/readme.txt","default");
+ assertMatch(p,"/downloads/logs.tgz","default");
+ assertMatch(p,"/animal/horse/mustang","animals");
+ assertMatch(p,"/animal/bird/eagle/bald","birds");
+ assertMatch(p,"/animal/fish/shark/hammerhead","fishes");
+ assertMatch(p,"/animal/insect/ladybug","animals");
+ }
+}
diff --git a/jetty-websocket/websocket-server/src/test/java/org/eclipse/jetty/websocket/server/pathmap/RegexPathSpecTest.java b/jetty-websocket/websocket-server/src/test/java/org/eclipse/jetty/websocket/server/pathmap/RegexPathSpecTest.java
new file mode 100644
index 0000000..45c684f
--- /dev/null
+++ b/jetty-websocket/websocket-server/src/test/java/org/eclipse/jetty/websocket/server/pathmap/RegexPathSpecTest.java
@@ -0,0 +1,134 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.server.pathmap;
+
+import static org.hamcrest.Matchers.*;
+import static org.junit.Assert.*;
+
+import org.junit.Test;
+
+public class RegexPathSpecTest
+{
+ public static void assertMatches(PathSpec spec, String path)
+ {
+ String msg = String.format("Spec(\"%s\").matches(\"%s\")",spec.getPathSpec(),path);
+ assertThat(msg,spec.matches(path),is(true));
+ }
+
+ public static void assertNotMatches(PathSpec spec, String path)
+ {
+ String msg = String.format("!Spec(\"%s\").matches(\"%s\")",spec.getPathSpec(),path);
+ assertThat(msg,spec.matches(path),is(false));
+ }
+
+ @Test
+ public void testExactSpec()
+ {
+ RegexPathSpec spec = new RegexPathSpec("^/a$");
+ assertEquals("Spec.pathSpec","^/a$",spec.getPathSpec());
+ assertEquals("Spec.pattern","^/a$",spec.getPattern().pattern());
+ assertEquals("Spec.pathDepth",1,spec.getPathDepth());
+ assertEquals("Spec.group",PathSpecGroup.EXACT,spec.group);
+
+ assertMatches(spec,"/a");
+
+ assertNotMatches(spec,"/aa");
+ assertNotMatches(spec,"/a/");
+ }
+
+ @Test
+ public void testMiddleSpec()
+ {
+ RegexPathSpec spec = new RegexPathSpec("^/rest/([^/]*)/list$");
+ assertEquals("Spec.pathSpec","^/rest/([^/]*)/list$",spec.getPathSpec());
+ assertEquals("Spec.pattern","^/rest/([^/]*)/list$",spec.getPattern().pattern());
+ assertEquals("Spec.pathDepth",3,spec.getPathDepth());
+ assertEquals("Spec.group",PathSpecGroup.MIDDLE_GLOB,spec.group);
+
+ assertMatches(spec,"/rest/api/list");
+ assertMatches(spec,"/rest/1.0/list");
+ assertMatches(spec,"/rest/2.0/list");
+ assertMatches(spec,"/rest/accounts/list");
+
+ assertNotMatches(spec,"/a");
+ assertNotMatches(spec,"/aa");
+ assertNotMatches(spec,"/aa/bb");
+ assertNotMatches(spec,"/rest/admin/delete");
+ assertNotMatches(spec,"/rest/list");
+ }
+
+ @Test
+ public void testMiddleSpecNoGrouping()
+ {
+ RegexPathSpec spec = new RegexPathSpec("^/rest/[^/]+/list$");
+ assertEquals("Spec.pathSpec","^/rest/[^/]+/list$",spec.getPathSpec());
+ assertEquals("Spec.pattern","^/rest/[^/]+/list$",spec.getPattern().pattern());
+ assertEquals("Spec.pathDepth",3,spec.getPathDepth());
+ assertEquals("Spec.group",PathSpecGroup.MIDDLE_GLOB,spec.group);
+
+ assertMatches(spec,"/rest/api/list");
+ assertMatches(spec,"/rest/1.0/list");
+ assertMatches(spec,"/rest/2.0/list");
+ assertMatches(spec,"/rest/accounts/list");
+
+ assertNotMatches(spec,"/a");
+ assertNotMatches(spec,"/aa");
+ assertNotMatches(spec,"/aa/bb");
+ assertNotMatches(spec,"/rest/admin/delete");
+ assertNotMatches(spec,"/rest/list");
+ }
+
+ @Test
+ public void testPrefixSpec()
+ {
+ RegexPathSpec spec = new RegexPathSpec("^/a/(.*)$");
+ assertEquals("Spec.pathSpec","^/a/(.*)$",spec.getPathSpec());
+ assertEquals("Spec.pattern","^/a/(.*)$",spec.getPattern().pattern());
+ assertEquals("Spec.pathDepth",2,spec.getPathDepth());
+ assertEquals("Spec.group",PathSpecGroup.PREFIX_GLOB,spec.group);
+
+ assertMatches(spec,"/a/");
+ assertMatches(spec,"/a/b");
+ assertMatches(spec,"/a/b/c/d/e");
+
+ assertNotMatches(spec,"/a");
+ assertNotMatches(spec,"/aa");
+ assertNotMatches(spec,"/aa/bb");
+ }
+
+ @Test
+ public void testSuffixSpec()
+ {
+ RegexPathSpec spec = new RegexPathSpec("^(.*).do$");
+ assertEquals("Spec.pathSpec","^(.*).do$",spec.getPathSpec());
+ assertEquals("Spec.pattern","^(.*).do$",spec.getPattern().pattern());
+ assertEquals("Spec.pathDepth",0,spec.getPathDepth());
+ assertEquals("Spec.group",PathSpecGroup.SUFFIX_GLOB,spec.group);
+
+ assertMatches(spec,"/a.do");
+ assertMatches(spec,"/a/b/c.do");
+ assertMatches(spec,"/abcde.do");
+ assertMatches(spec,"/abc/efg.do");
+
+ assertNotMatches(spec,"/a");
+ assertNotMatches(spec,"/aa");
+ assertNotMatches(spec,"/aa/bb");
+ assertNotMatches(spec,"/aa/bb.do/more");
+ }
+}
diff --git a/jetty-websocket/websocket-server/src/test/java/org/eclipse/jetty/websocket/server/pathmap/ServletPathSpecTest.java b/jetty-websocket/websocket-server/src/test/java/org/eclipse/jetty/websocket/server/pathmap/ServletPathSpecTest.java
new file mode 100644
index 0000000..d68dcb6
--- /dev/null
+++ b/jetty-websocket/websocket-server/src/test/java/org/eclipse/jetty/websocket/server/pathmap/ServletPathSpecTest.java
@@ -0,0 +1,187 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.server.pathmap;
+
+import static org.hamcrest.Matchers.*;
+import static org.junit.Assert.*;
+
+import org.eclipse.jetty.websocket.server.pathmap.ServletPathSpec;
+import org.junit.Test;
+
+public class ServletPathSpecTest
+{
+ private void assertBadServletPathSpec(String pathSpec)
+ {
+ try
+ {
+ new ServletPathSpec(pathSpec);
+ fail("Expected IllegalArgumentException for a bad servlet pathspec on: " + pathSpec);
+ }
+ catch (IllegalArgumentException e)
+ {
+ // expected path
+ System.out.println(e);
+ }
+ }
+
+ private void assertMatches(ServletPathSpec spec, String path)
+ {
+ String msg = String.format("Spec(\"%s\").matches(\"%s\")",spec.getPathSpec(),path);
+ assertThat(msg,spec.matches(path),is(true));
+ }
+
+ private void assertNotMatches(ServletPathSpec spec, String path)
+ {
+ String msg = String.format("!Spec(\"%s\").matches(\"%s\")",spec.getPathSpec(),path);
+ assertThat(msg,spec.matches(path),is(false));
+ }
+
+ @Test
+ public void testBadServletPathSpecA()
+ {
+ assertBadServletPathSpec("foo");
+ }
+
+ @Test
+ public void testBadServletPathSpecB()
+ {
+ assertBadServletPathSpec("/foo/*.do");
+ }
+
+ @Test
+ public void testBadServletPathSpecC()
+ {
+ assertBadServletPathSpec("foo/*.do");
+ }
+
+ @Test
+ public void testBadServletPathSpecD()
+ {
+ assertBadServletPathSpec("foo/*.*do");
+ }
+
+ @Test
+ public void testBadServletPathSpecE()
+ {
+ assertBadServletPathSpec("*do");
+ }
+
+ @Test
+ public void testDefaultPathSpec()
+ {
+ ServletPathSpec spec = new ServletPathSpec("/");
+ assertEquals("Spec.pathSpec","/",spec.getPathSpec());
+ assertEquals("Spec.pathDepth",-1,spec.getPathDepth());
+ }
+
+ @Test
+ public void testEmptyPathSpec()
+ {
+ ServletPathSpec spec = new ServletPathSpec("");
+ assertEquals("Spec.pathSpec","/",spec.getPathSpec());
+ assertEquals("Spec.pathDepth",-1,spec.getPathDepth());
+ }
+
+ @Test
+ public void testExactPathSpec()
+ {
+ ServletPathSpec spec = new ServletPathSpec("/abs/path");
+ assertEquals("Spec.pathSpec","/abs/path",spec.getPathSpec());
+ assertEquals("Spec.pathDepth",2,spec.getPathDepth());
+
+ assertMatches(spec,"/abs/path");
+ assertMatches(spec,"/abs/path/");
+
+ assertNotMatches(spec,"/abs/path/more");
+ assertNotMatches(spec,"/foo");
+ assertNotMatches(spec,"/foo/abs/path");
+ assertNotMatches(spec,"/foo/abs/path/");
+ }
+
+ @Test
+ public void testGetPathInfo()
+ {
+ assertEquals("pathInfo exact",null,new ServletPathSpec("/Foo/bar").getPathInfo("/Foo/bar"));
+ assertEquals("pathInfo prefix","/bar",new ServletPathSpec("/Foo/*").getPathInfo("/Foo/bar"));
+ assertEquals("pathInfo prefix","/*",new ServletPathSpec("/Foo/*").getPathInfo("/Foo/*"));
+ assertEquals("pathInfo prefix","/",new ServletPathSpec("/Foo/*").getPathInfo("/Foo/"));
+ assertEquals("pathInfo prefix",null,new ServletPathSpec("/Foo/*").getPathInfo("/Foo"));
+ assertEquals("pathInfo suffix",null,new ServletPathSpec("*.ext").getPathInfo("/Foo/bar.ext"));
+ assertEquals("pathInfo default",null,new ServletPathSpec("/").getPathInfo("/Foo/bar.ext"));
+
+ assertEquals("pathInfo default","/xxx/zzz",new ServletPathSpec("/*").getPathInfo("/xxx/zzz"));
+ }
+
+ @Test
+ public void testNullPathSpec()
+ {
+ ServletPathSpec spec = new ServletPathSpec(null);
+ assertEquals("Spec.pathSpec","/",spec.getPathSpec());
+ assertEquals("Spec.pathDepth",-1,spec.getPathDepth());
+ }
+
+ @Test
+ public void testPathMatch()
+ {
+ assertEquals("pathMatch exact","/Foo/bar",new ServletPathSpec("/Foo/bar").getPathMatch("/Foo/bar"));
+ assertEquals("pathMatch prefix","/Foo",new ServletPathSpec("/Foo/*").getPathMatch("/Foo/bar"));
+ assertEquals("pathMatch prefix","/Foo",new ServletPathSpec("/Foo/*").getPathMatch("/Foo/"));
+ assertEquals("pathMatch prefix","/Foo",new ServletPathSpec("/Foo/*").getPathMatch("/Foo"));
+ assertEquals("pathMatch suffix","/Foo/bar.ext",new ServletPathSpec("*.ext").getPathMatch("/Foo/bar.ext"));
+ assertEquals("pathMatch default","/Foo/bar.ext",new ServletPathSpec("/").getPathMatch("/Foo/bar.ext"));
+
+ assertEquals("pathMatch default","",new ServletPathSpec("/*").getPathMatch("/xxx/zzz"));
+ }
+
+ @Test
+ public void testPrefixPathSpec()
+ {
+ ServletPathSpec spec = new ServletPathSpec("/downloads/*");
+ assertEquals("Spec.pathSpec","/downloads/*",spec.getPathSpec());
+ assertEquals("Spec.pathDepth",2,spec.getPathDepth());
+
+ assertMatches(spec,"/downloads/logo.jpg");
+ assertMatches(spec,"/downloads/distribution.tar.gz");
+ assertMatches(spec,"/downloads/distribution.tgz");
+ assertMatches(spec,"/downloads/distribution.zip");
+
+ assertMatches(spec,"/downloads");
+
+ assertEquals("Spec.pathInfo","/",spec.getPathInfo("/downloads/"));
+ assertEquals("Spec.pathInfo","/distribution.zip",spec.getPathInfo("/downloads/distribution.zip"));
+ assertEquals("Spec.pathInfo","/dist/9.0/distribution.tar.gz",spec.getPathInfo("/downloads/dist/9.0/distribution.tar.gz"));
+ }
+
+ @Test
+ public void testSuffixPathSpec()
+ {
+ ServletPathSpec spec = new ServletPathSpec("*.gz");
+ assertEquals("Spec.pathSpec","*.gz",spec.getPathSpec());
+ assertEquals("Spec.pathDepth",0,spec.getPathDepth());
+
+ assertMatches(spec,"/downloads/distribution.tar.gz");
+ assertMatches(spec,"/downloads/jetty.log.gz");
+
+ assertNotMatches(spec,"/downloads/distribution.zip");
+ assertNotMatches(spec,"/downloads/distribution.tgz");
+ assertNotMatches(spec,"/abs/path");
+
+ assertEquals("Spec.pathInfo",null,spec.getPathInfo("/downloads/distribution.tar.gz"));
+ }
+}
diff --git a/jetty-websocket/websocket-servlet/pom.xml b/jetty-websocket/websocket-servlet/pom.xml
index a8e1d7e..3ccae46 100644
--- a/jetty-websocket/websocket-servlet/pom.xml
+++ b/jetty-websocket/websocket-servlet/pom.xml
@@ -3,7 +3,7 @@
<parent>
<groupId>org.eclipse.jetty.websocket</groupId>
<artifactId>websocket-parent</artifactId>
- <version>9.0.5-SNAPSHOT</version>
+ <version>9.1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
@@ -21,10 +21,17 @@
<version>${project.version}</version>
</dependency>
<dependency>
+ <groupId>javax.servlet</groupId>
+ <artifactId>javax.servlet-api</artifactId>
+ </dependency>
+
+<!--
+ <dependency>
<groupId>org.eclipse.jetty.orbit</groupId>
<artifactId>javax.servlet</artifactId>
<scope>provided</scope>
</dependency>
+-->
<dependency>
<groupId>org.eclipse.jetty.toolchain</groupId>
<artifactId>jetty-test-helper</artifactId>
diff --git a/jetty-websocket/websocket-servlet/src/main/java/org/eclipse/jetty/websocket/servlet/ServletUpgradeRequest.java b/jetty-websocket/websocket-servlet/src/main/java/org/eclipse/jetty/websocket/servlet/ServletUpgradeRequest.java
index fd739e0..52c685c 100644
--- a/jetty-websocket/websocket-servlet/src/main/java/org/eclipse/jetty/websocket/servlet/ServletUpgradeRequest.java
+++ b/jetty-websocket/websocket-servlet/src/main/java/org/eclipse/jetty/websocket/servlet/ServletUpgradeRequest.java
@@ -57,7 +57,15 @@
setHttpVersion(request.getProtocol());
// Copy parameters
- super.setParameterMap(request.getParameterMap());
+ Map<String, List<String>> pmap = new HashMap<>();
+ if (request.getParameterMap() != null)
+ {
+ for (Map.Entry<String, String[]> entry : request.getParameterMap().entrySet())
+ {
+ pmap.put(entry.getKey(),Arrays.asList(entry.getValue()));
+ }
+ }
+ super.setParameterMap(pmap);
// Copy Cookies
Cookie rcookies[] = request.getCookies();
@@ -78,12 +86,7 @@
while (headerNames.hasMoreElements())
{
String name = headerNames.nextElement();
- Enumeration<String> valuesEnum = request.getHeaders(name);
- List<String> values = new ArrayList<>();
- while (valuesEnum.hasMoreElements())
- {
- values.add(valuesEnum.nextElement());
- }
+ List<String> values = Collections.list(request.getHeaders(name));
setHeader(name,values);
}
@@ -266,4 +269,18 @@
this.req.setAttribute(name,o);
}
+ public Object getServletAttribute(String name)
+ {
+ return req.getAttribute(name);
+ }
+
+ public boolean isUserInRole(String role)
+ {
+ return req.isUserInRole(role);
+ }
+
+ public String getRequestPath()
+ {
+ return req.getServletPath();
+ }
}
diff --git a/jetty-websocket/websocket-servlet/src/main/java/org/eclipse/jetty/websocket/servlet/WebSocketCreator.java b/jetty-websocket/websocket-servlet/src/main/java/org/eclipse/jetty/websocket/servlet/WebSocketCreator.java
index aa3d162..85b0cf3 100644
--- a/jetty-websocket/websocket-servlet/src/main/java/org/eclipse/jetty/websocket/servlet/WebSocketCreator.java
+++ b/jetty-websocket/websocket-servlet/src/main/java/org/eclipse/jetty/websocket/servlet/WebSocketCreator.java
@@ -18,8 +18,6 @@
package org.eclipse.jetty.websocket.servlet;
-import org.eclipse.jetty.websocket.api.UpgradeRequest;
-import org.eclipse.jetty.websocket.api.UpgradeResponse;
import org.eclipse.jetty.websocket.api.extensions.Extension;
/**
@@ -35,11 +33,6 @@
{
/**
* Create a websocket from the incoming request.
- * <p>
- * Note: if you have Servlet specific information you need to access from the UpgradeRequest, cast the {@link UpgradeRequest} to
- * {@link ServletUpgradeRequest} for this extra information.
- * <p>
- * Future versions of this interface will change to use the Servlet specific Upgrade Request and Response parameters.
*
* @param req
* the request details
@@ -47,5 +40,5 @@
* the response details
* @return a websocket object to use, or null if no websocket should be created from this request.
*/
- Object createWebSocket(UpgradeRequest req, UpgradeResponse resp);
+ Object createWebSocket(ServletUpgradeRequest req, ServletUpgradeResponse resp);
}
diff --git a/jetty-websocket/websocket-servlet/src/main/java/org/eclipse/jetty/websocket/servlet/WebSocketServlet.java b/jetty-websocket/websocket-servlet/src/main/java/org/eclipse/jetty/websocket/servlet/WebSocketServlet.java
index 609a812..ee9308c 100644
--- a/jetty-websocket/websocket-servlet/src/main/java/org/eclipse/jetty/websocket/servlet/WebSocketServlet.java
+++ b/jetty-websocket/websocket-servlet/src/main/java/org/eclipse/jetty/websocket/servlet/WebSocketServlet.java
@@ -19,8 +19,6 @@
package org.eclipse.jetty.websocket.servlet;
import java.io.IOException;
-import java.util.Iterator;
-import java.util.ServiceLoader;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
@@ -71,8 +69,11 @@
* <dt>maxIdleTime</dt>
* <dd>set the time in ms that a websocket may be idle before closing<br>
*
- * <dt>maxMessagesSize</dt>
- * <dd>set the size in bytes that a websocket may be accept before closing<br>
+ * <dt>maxTextMessageSize</dt>
+ * <dd>set the size in UTF-8 bytes that a websocket may be accept as a Text Message before closing<br>
+ *
+ * <dt>maxBinaryMessageSize</dt>
+ * <dd>set the size in bytes that a websocket may be accept as a Binary Message before closing<br>
*
* <dt>inputBufferSize</dt>
* <dd>set the size in bytes of the buffer used to read raw bytes from the network layer<br>
@@ -107,10 +108,16 @@
policy.setIdleTimeout(Long.parseLong(max));
}
- max = getInitParameter("maxMessageSize");
+ max = getInitParameter("maxTextMessageSize");
if (max != null)
{
- policy.setMaxMessageSize(Long.parseLong(max));
+ policy.setMaxTextMessageSize(Integer.parseInt(max));
+ }
+
+ max = getInitParameter("maxBinaryMessageSize");
+ if (max != null)
+ {
+ policy.setMaxBinaryMessageSize(Integer.parseInt(max));
}
max = getInitParameter("inputBufferSize");
@@ -119,24 +126,7 @@
policy.setInputBufferSize(Integer.parseInt(max));
}
- WebSocketServletFactory baseFactory;
- Iterator<WebSocketServletFactory> factories = ServiceLoader.load(WebSocketServletFactory.class).iterator();
-
- if (factories.hasNext())
- {
- baseFactory = factories.next();
- }
- else
- {
- // Load the default class if ServiceLoader mechanism isn't valid in this environment. (such as OSGi)
- ClassLoader loader = Thread.currentThread().getContextClassLoader();
- @SuppressWarnings("unchecked")
- Class<WebSocketServletFactory> wssf = (Class<WebSocketServletFactory>)loader
- .loadClass("org.eclipse.jetty.websocket.server.WebSocketServerFactory");
- baseFactory = wssf.newInstance();
- }
-
- factory = baseFactory.createFactory(policy);
+ factory = WebSocketServletFactory.Loader.create(policy);
configure(factory);
diff --git a/jetty-websocket/websocket-servlet/src/main/java/org/eclipse/jetty/websocket/servlet/WebSocketServletFactory.java b/jetty-websocket/websocket-servlet/src/main/java/org/eclipse/jetty/websocket/servlet/WebSocketServletFactory.java
index 34fbf4d..1f02d69 100644
--- a/jetty-websocket/websocket-servlet/src/main/java/org/eclipse/jetty/websocket/servlet/WebSocketServletFactory.java
+++ b/jetty-websocket/websocket-servlet/src/main/java/org/eclipse/jetty/websocket/servlet/WebSocketServletFactory.java
@@ -19,6 +19,8 @@
package org.eclipse.jetty.websocket.servlet;
import java.io.IOException;
+import java.util.Iterator;
+import java.util.ServiceLoader;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@@ -32,8 +34,47 @@
*/
public interface WebSocketServletFactory
{
+ public static class Loader
+ {
+ private static WebSocketServletFactory INSTANCE;
+
+ public static WebSocketServletFactory create(WebSocketPolicy policy) throws ClassNotFoundException, InstantiationException, IllegalAccessException
+ {
+ return load(policy).createFactory(policy);
+ }
+
+ public static WebSocketServletFactory load(WebSocketPolicy policy) throws ClassNotFoundException, InstantiationException, IllegalAccessException
+ {
+ if (INSTANCE != null)
+ {
+ return INSTANCE;
+ }
+ WebSocketServletFactory baseFactory;
+ Iterator<WebSocketServletFactory> factories = ServiceLoader.load(WebSocketServletFactory.class).iterator();
+
+ if (factories.hasNext())
+ {
+ baseFactory = factories.next();
+ }
+ else
+ {
+ // Load the default class if ServiceLoader mechanism isn't valid in this environment. (such as OSGi)
+ ClassLoader loader = Thread.currentThread().getContextClassLoader();
+ @SuppressWarnings("unchecked")
+ Class<WebSocketServletFactory> wssf = (Class<WebSocketServletFactory>)loader
+ .loadClass("org.eclipse.jetty.websocket.server.WebSocketServerFactory");
+ baseFactory = wssf.newInstance();
+ }
+
+ INSTANCE = baseFactory;
+ return INSTANCE;
+ }
+ }
+
public boolean acceptWebSocket(HttpServletRequest request, HttpServletResponse response) throws IOException;
+ public boolean acceptWebSocket(WebSocketCreator creator, HttpServletRequest request, HttpServletResponse response) throws IOException;
+
public void cleanup();
public WebSocketServletFactory createFactory(WebSocketPolicy policy);
diff --git a/jetty-websocket/websocket-servlet/src/test/java/examples/MyAdvancedEchoCreator.java b/jetty-websocket/websocket-servlet/src/test/java/examples/MyAdvancedEchoCreator.java
index 98789a1..18db717 100644
--- a/jetty-websocket/websocket-servlet/src/test/java/examples/MyAdvancedEchoCreator.java
+++ b/jetty-websocket/websocket-servlet/src/test/java/examples/MyAdvancedEchoCreator.java
@@ -18,8 +18,8 @@
package examples;
-import org.eclipse.jetty.websocket.api.UpgradeRequest;
-import org.eclipse.jetty.websocket.api.UpgradeResponse;
+import org.eclipse.jetty.websocket.servlet.ServletUpgradeRequest;
+import org.eclipse.jetty.websocket.servlet.ServletUpgradeResponse;
import org.eclipse.jetty.websocket.servlet.WebSocketCreator;
public class MyAdvancedEchoCreator implements WebSocketCreator
@@ -35,7 +35,7 @@
}
@Override
- public Object createWebSocket(UpgradeRequest req, UpgradeResponse resp)
+ public Object createWebSocket(ServletUpgradeRequest req, ServletUpgradeResponse resp)
{
for (String subprotocol : req.getSubProtocols())
{
diff --git a/jetty-xml/pom.xml b/jetty-xml/pom.xml
index 873e272..7814361 100644
--- a/jetty-xml/pom.xml
+++ b/jetty-xml/pom.xml
@@ -2,7 +2,7 @@
<parent>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-project</artifactId>
- <version>9.0.5-SNAPSHOT</version>
+ <version>9.1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>jetty-xml</artifactId>
diff --git a/pom.xml b/pom.xml
index a8b9d44..43202ca 100644
--- a/pom.xml
+++ b/pom.xml
@@ -6,13 +6,12 @@
<version>20</version>
</parent>
<artifactId>jetty-project</artifactId>
- <version>9.0.5-SNAPSHOT</version>
+ <version>9.1.0-SNAPSHOT</version>
<name>Jetty :: Project</name>
<url>http://www.eclipse.org/jetty</url>
<packaging>pom</packaging>
<properties>
<jetty.url>http://www.eclipse.org/jetty</jetty.url>
- <orbit-servlet-api-version>3.0.0.v201112011016</orbit-servlet-api-version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<build-support-version>1.1</build-support-version>
<slf4j-version>1.6.1</slf4j-version>
@@ -138,37 +137,6 @@
</configuration>
</execution>
<execution>
- <id>enforce-orbit-deps</id>
- <goals>
- <goal>enforce</goal>
- </goals>
- <configuration>
- <rules>
- <!-- Banned Dependencies (should use Orbit based versions now) -->
- <bannedDependencies>
- <excludes>
- <exclude>javax.servlet</exclude>
- <exclude>javax.servlet.jsp</exclude>
- <exclude>org.apache.geronimo.specs</exclude>
- <exclude>javax.mail</exclude>
- <exclude>javax.activation</exclude>
- </excludes>
- <!-- allowed combinations -->
- <includes>
- <include>org.apache.geronimo.specs:geronimo-atinject_1.0_spec:jar:*</include>
- <include>javax.net.websocket:*:*:*</include>
- <include>javax.websocket:*:*:*</include>
- <include>javax.servlet:*:*:*:provided</include>
- <include>javax.servlet.jsp:*:*:*:provided</include>
- </includes>
- <searchTransitive>true</searchTransitive>
- <message>This dependency is banned, use the ORBIT provided dependency instead.</message>
- </bannedDependencies>
- </rules>
- <fail>true</fail>
- </configuration>
- </execution>
- <execution>
<id>ban-junit-dep.jar</id>
<goals>
<goal>enforce</goal>
@@ -460,16 +428,15 @@
</modules>
<dependencyManagement>
<dependencies>
- <!-- Orbit Deps -->
<dependency>
- <groupId>org.eclipse.jetty.orbit</groupId>
- <artifactId>javax.servlet</artifactId>
- <version>3.0.0.v201112011016</version>
+ <groupId>javax.servlet</groupId>
+ <artifactId>javax.servlet-api</artifactId>
+ <version>3.1.0</version>
</dependency>
<dependency>
- <groupId>org.eclipse.jetty.orbit</groupId>
- <artifactId>javax.annotation</artifactId>
- <version>1.1.0.v201108011116</version>
+ <groupId>javax.annotation</groupId>
+ <artifactId>javax.annotation-api</artifactId>
+ <version>1.2</version>
</dependency>
<dependency>
<groupId>org.eclipse.jetty.orbit</groupId>
@@ -478,61 +445,104 @@
</dependency>
<dependency>
<groupId>org.eclipse.jetty.orbit</groupId>
+ <artifactId>javax.security.auth.message</artifactId>
+ <version>1.0.0.v201108011116</version>
+ </dependency>
+
+ <!-- JSP Deps -->
+ <dependency>
+ <groupId>org.eclipse.jetty.toolchain</groupId>
+ <artifactId>jetty-schemas</artifactId>
+ <version>3.1.RC0</version>
+ </dependency>
+
+ <dependency>
+ <groupId>javax.servlet.jsp</groupId>
+ <artifactId>javax.servlet.jsp-api</artifactId>
+ <version>2.3.1</version>
+ </dependency>
+
+ <dependency>
+ <groupId>org.glassfish.web</groupId>
+ <artifactId>javax.servlet.jsp</artifactId>
+ <version>2.3.2</version>
+ </dependency>
+
+
+ <dependency>
+ <groupId>javax.el</groupId>
+ <artifactId>javax.el-api</artifactId>
+ <version>3.0.0</version>
+ </dependency>
+
+ <dependency>
+ <groupId>org.glassfish</groupId>
+ <artifactId>javax.el</artifactId>
+ <version>3.0.0</version>
+ </dependency>
+
+ <dependency>
+ <groupId>org.eclipse.jetty.orbit</groupId>
+ <artifactId>org.eclipse.jdt.core</artifactId>
+ <version>3.8.2.v20130121</version>
+ <exclusions>
+ <exclusion>
+ <groupId>org.eclipse.jetty.orbit</groupId>
+ <artifactId>javax.servlet</artifactId>
+ </exclusion>
+ </exclusions>
+ </dependency>
+
+ <!-- JSTL Impl -->
+ <dependency>
+ <groupId>org.eclipse.jetty.orbit</groupId>
+ <artifactId>org.apache.taglibs.standard.glassfish</artifactId>
+ <version>1.2.0.v201112081803</version>
+ <exclusions>
+ <exclusion>
+ <groupId>org.eclipse.jetty.orbit</groupId>
+ <artifactId>javax.servlet</artifactId>
+ </exclusion>
+ </exclusions>
+ </dependency>
+
+ <dependency>
+ <groupId>org.eclipse.jetty.orbit</groupId>
+ <artifactId>javax.servlet.jsp.jstl</artifactId>
+ <version>1.2.0.v201105211821</version>
+ <exclusions>
+ <exclusion>
+ <groupId>org.eclipse.jetty.orbit</groupId>
+ <artifactId>javax.servlet</artifactId>
+ </exclusion>
+ <exclusion>
+ <groupId>org.eclipse.jetty.orbit</groupId>
+ <artifactId>javax.servlet.jsp</artifactId>
+ </exclusion>
+ </exclusions>
+ </dependency>
+
+
+ <dependency>
+ <groupId>org.eclipse.jetty.orbit</groupId>
<artifactId>javax.activation</artifactId>
<version>1.1.0.v201105071233</version>
+ <scope>provided</scope>
</dependency>
+
<dependency>
<groupId>org.eclipse.jetty.orbit</groupId>
<artifactId>javax.mail.glassfish</artifactId>
<version>1.4.1.v201005082020</version>
</dependency>
+
<dependency>
- <groupId>org.eclipse.jetty.orbit</groupId>
- <artifactId>javax.transaction</artifactId>
- <version>1.1.1.v201105210645</version>
+ <groupId>javax.transaction</groupId>
+ <artifactId>javax.transaction-api</artifactId>
+ <version>1.2</version>
+ <scope>provided</scope>
</dependency>
- <dependency>
- <groupId>org.eclipse.jetty.orbit</groupId>
- <artifactId>javax.security.auth.message</artifactId>
- <version>1.0.0.v201108011116</version>
- </dependency>
- <!--
- <dependency>
- <groupId>org.eclipse.jetty.orbit</groupId>
- <artifactId>javax.servlet.jsp</artifactId>
- <version>2.1.0.v201105211820</version>
- </dependency>
- <dependency>
- <groupId>org.eclipse.jetty.orbit</groupId>
- <artifactId>javax.servlet.jsp.jstl</artifactId>
- <version>1.2.0.v201105211821</version>
- </dependency>
- <dependency>
- <groupId>org.eclipse.jetty.orbit</groupId>
- <artifactId>javax.el</artifactId>
- <version>2.1.0.v201105211819</version>
- </dependency>
- <dependency>
- <groupId>org.eclipse.jetty.orbit</groupId>
- <artifactId>com.sun.el</artifactId>
- <version>1.0.0.v201105211818</version>
- </dependency>
- <dependency>
- <groupId>org.eclipse.jetty.orbit</groupId>
- <artifactId>org.apache.jasper.glassfish</artifactId>
- <version>2.1.0.v201110031002</version>
- </dependency>
- <dependency>
- <groupId>org.eclipse.jetty.orbit</groupId>
- <artifactId>org.apache.taglibs.standard.glassfish</artifactId>
- <version>1.2.0.v201112081803</version>
- </dependency>
- <dependency>
- <groupId>org.eclipse.jetty.orbit</groupId>
- <artifactId>org.eclipse.jdt.core</artifactId>
- <version>3.7.1</version>
- </dependency>
- -->
+
<!-- Old Deps -->
<dependency>
diff --git a/tests/pom.xml b/tests/pom.xml
index ecf5c4e..c71bd23 100644
--- a/tests/pom.xml
+++ b/tests/pom.xml
@@ -21,7 +21,7 @@
<parent>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-project</artifactId>
- <version>9.0.5-SNAPSHOT</version>
+ <version>9.1.0-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<groupId>org.eclipse.jetty.tests</groupId>
diff --git a/tests/test-continuation/pom.xml b/tests/test-continuation/pom.xml
index 36dfbf8..d956ab9 100644
--- a/tests/test-continuation/pom.xml
+++ b/tests/test-continuation/pom.xml
@@ -20,7 +20,7 @@
<parent>
<groupId>org.eclipse.jetty.tests</groupId>
<artifactId>tests-parent</artifactId>
- <version>9.0.5-SNAPSHOT</version>
+ <version>9.1.0-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
diff --git a/tests/test-continuation/src/test/java/org/eclipse/jetty/continuation/ContinuationBase.java b/tests/test-continuation/src/test/java/org/eclipse/jetty/continuation/ContinuationBase.java
index e93f0ba..51e26b1 100644
--- a/tests/test-continuation/src/test/java/org/eclipse/jetty/continuation/ContinuationBase.java
+++ b/tests/test-continuation/src/test/java/org/eclipse/jetty/continuation/ContinuationBase.java
@@ -502,7 +502,6 @@
public void onTimeout(Continuation continuation)
{
((HttpServletResponse)continuation.getServletResponse()).addHeader("history","onTimeout");
- // continuation.resume();
}
};
diff --git a/tests/test-loginservice/pom.xml b/tests/test-loginservice/pom.xml
index cc47295..4cc8782 100644
--- a/tests/test-loginservice/pom.xml
+++ b/tests/test-loginservice/pom.xml
@@ -21,7 +21,7 @@
<parent>
<groupId>org.eclipse.jetty.tests</groupId>
<artifactId>tests-parent</artifactId>
- <version>9.0.5-SNAPSHOT</version>
+ <version>9.1.0-SNAPSHOT</version>
</parent>
<artifactId>test-loginservice</artifactId>
<name>Jetty Tests :: Login Service</name>
diff --git a/tests/test-loginservice/src/test/java/org/eclipse/jetty/JdbcLoginServiceTest.java b/tests/test-loginservice/src/test/java/org/eclipse/jetty/JdbcLoginServiceTest.java
index 794ebcc..5bc4146 100644
--- a/tests/test-loginservice/src/test/java/org/eclipse/jetty/JdbcLoginServiceTest.java
+++ b/tests/test-loginservice/src/test/java/org/eclipse/jetty/JdbcLoginServiceTest.java
@@ -155,7 +155,6 @@
security.setConstraintMappings(Collections.singletonList(mapping), knownRoles);
security.setAuthenticator(new BasicAuthenticator());
security.setLoginService(loginService);
- security.setStrict(false);
ServletContextHandler root = new ServletContextHandler();
root.setContextPath("/");
diff --git a/tests/test-sessions/pom.xml b/tests/test-sessions/pom.xml
index aedc181..a499af0 100644
--- a/tests/test-sessions/pom.xml
+++ b/tests/test-sessions/pom.xml
@@ -21,7 +21,7 @@
<parent>
<groupId>org.eclipse.jetty.tests</groupId>
<artifactId>tests-parent</artifactId>
- <version>9.0.5-SNAPSHOT</version>
+ <version>9.1.0-SNAPSHOT</version>
</parent>
<artifactId>test-sessions-parent</artifactId>
<name>Jetty Tests :: Sessions :: Parent</name>
diff --git a/tests/test-sessions/test-hash-sessions/pom.xml b/tests/test-sessions/test-hash-sessions/pom.xml
index e0fce94..c71dcc3 100644
--- a/tests/test-sessions/test-hash-sessions/pom.xml
+++ b/tests/test-sessions/test-hash-sessions/pom.xml
@@ -21,7 +21,7 @@
<parent>
<groupId>org.eclipse.jetty.tests</groupId>
<artifactId>test-sessions-parent</artifactId>
- <version>9.0.5-SNAPSHOT</version>
+ <version>9.1.0-SNAPSHOT</version>
</parent>
<artifactId>test-hash-sessions</artifactId>
<name>Jetty Tests :: Sessions :: Hash</name>
diff --git a/tests/test-sessions/test-jdbc-sessions/pom.xml b/tests/test-sessions/test-jdbc-sessions/pom.xml
index 0513c4e..9dab39c 100644
--- a/tests/test-sessions/test-jdbc-sessions/pom.xml
+++ b/tests/test-sessions/test-jdbc-sessions/pom.xml
@@ -21,7 +21,7 @@
<parent>
<groupId>org.eclipse.jetty.tests</groupId>
<artifactId>test-sessions-parent</artifactId>
- <version>9.0.5-SNAPSHOT</version>
+ <version>9.1.0-SNAPSHOT</version>
</parent>
<artifactId>test-jdbc-sessions</artifactId>
<name>Jetty Tests :: Sessions :: JDBC</name>
diff --git a/tests/test-sessions/test-mongodb-sessions/src/test/java/org/eclipse/jetty/nosql/mongodb/ServerCrossContextSessionTest.java b/tests/test-sessions/test-mongodb-sessions/src/test/java/org/eclipse/jetty/nosql/mongodb/ServerCrossContextSessionTest.java
index 4d9bd4e..ffa089f 100644
--- a/tests/test-sessions/test-mongodb-sessions/src/test/java/org/eclipse/jetty/nosql/mongodb/ServerCrossContextSessionTest.java
+++ b/tests/test-sessions/test-mongodb-sessions/src/test/java/org/eclipse/jetty/nosql/mongodb/ServerCrossContextSessionTest.java
@@ -23,6 +23,7 @@
import org.junit.Ignore;
import org.junit.Test;
+
public class ServerCrossContextSessionTest extends AbstractServerCrossContextSessionTest
{
public AbstractTestServer createServer(int port)
diff --git a/tests/test-sessions/test-sessions-common/pom.xml b/tests/test-sessions/test-sessions-common/pom.xml
index f0d4a03..d5aa814 100644
--- a/tests/test-sessions/test-sessions-common/pom.xml
+++ b/tests/test-sessions/test-sessions-common/pom.xml
@@ -21,7 +21,7 @@
<parent>
<groupId>org.eclipse.jetty.tests</groupId>
<artifactId>test-sessions-parent</artifactId>
- <version>9.0.5-SNAPSHOT</version>
+ <version>9.1.0-SNAPSHOT</version>
</parent>
<artifactId>test-sessions-common</artifactId>
<name>Jetty Tests :: Sessions :: Common</name>
diff --git a/tests/test-sessions/test-sessions-common/src/main/java/org/eclipse/jetty/server/session/AbstractProxySerializationTest.java b/tests/test-sessions/test-sessions-common/src/main/java/org/eclipse/jetty/server/session/AbstractProxySerializationTest.java
index e7fe83f..45ed2a3 100644
--- a/tests/test-sessions/test-sessions-common/src/main/java/org/eclipse/jetty/server/session/AbstractProxySerializationTest.java
+++ b/tests/test-sessions/test-sessions-common/src/main/java/org/eclipse/jetty/server/session/AbstractProxySerializationTest.java
@@ -119,10 +119,6 @@
//stop the context to be sure the sesssion will be passivated
context.stop();
- //after a stop some of the volatile info is lost, so reinstate it
- context.setClassLoader(loader);
- context.addServlet("TestServlet", servletMapping);
-
//restart the context
context.start();
diff --git a/tests/test-sessions/test-sessions-common/src/main/java/org/eclipse/jetty/server/session/AbstractSessionRenewTest.java b/tests/test-sessions/test-sessions-common/src/main/java/org/eclipse/jetty/server/session/AbstractSessionRenewTest.java
index 070ddf1..34cae2b 100644
--- a/tests/test-sessions/test-sessions-common/src/main/java/org/eclipse/jetty/server/session/AbstractSessionRenewTest.java
+++ b/tests/test-sessions/test-sessions-common/src/main/java/org/eclipse/jetty/server/session/AbstractSessionRenewTest.java
@@ -32,11 +32,14 @@
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
+import javax.servlet.http.HttpSessionEvent;
+import javax.servlet.http.HttpSessionIdListener;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.api.ContentResponse;
import org.eclipse.jetty.client.api.Request;
import org.eclipse.jetty.servlet.ServletContextHandler;
+import org.eclipse.jetty.webapp.WebAppContext;
public abstract class AbstractSessionRenewTest
@@ -49,8 +52,11 @@
String servletMapping = "/server";
int scavengePeriod = 3;
AbstractTestServer server = createServer(0, 1, scavengePeriod);
- ServletContextHandler context = server.addContext(contextPath);
+ WebAppContext context = server.addWebAppContext(".", contextPath);
context.addServlet(TestServlet.class, servletMapping);
+ TestHttpSessionIdListener testListener = new TestHttpSessionIdListener();
+ context.addEventListener(testListener);
+
HttpClient client = new HttpClient();
@@ -67,6 +73,7 @@
String sessionCookie = response.getHeaders().getStringField("Set-Cookie");
assertTrue(sessionCookie != null);
+ assertFalse(testListener.isCalled());
//make a request to change the sessionid
Request request = client.newRequest("http://localhost:" + port + contextPath + servletMapping + "?action=renew");
@@ -76,6 +83,7 @@
String renewSessionCookie = renewResponse.getHeaders().getStringField("Set-Cookie");
assertNotNull(renewSessionCookie);
assertNotSame(sessionCookie, renewSessionCookie);
+ assertTrue(testListener.isCalled());
}
finally
{
@@ -84,9 +92,30 @@
}
}
+
+
+ public static class TestHttpSessionIdListener implements HttpSessionIdListener
+ {
+ boolean called = false;
+
+ @Override
+ public void sessionIdChanged(HttpSessionEvent event, String oldSessionId)
+ {
+ assertNotNull(event.getSession());
+ assertNotSame(oldSessionId, event.getSession().getId());
+ called = true;
+ }
+
+ public boolean isCalled()
+ {
+ return called;
+ }
+ }
+
public static class TestServlet extends HttpServlet
{
+
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException
{
diff --git a/tests/test-webapps/pom.xml b/tests/test-webapps/pom.xml
index fc232de..74a820e 100644
--- a/tests/test-webapps/pom.xml
+++ b/tests/test-webapps/pom.xml
@@ -21,7 +21,7 @@
<parent>
<groupId>org.eclipse.jetty.tests</groupId>
<artifactId>tests-parent</artifactId>
- <version>9.0.5-SNAPSHOT</version>
+ <version>9.1.0-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<artifactId>test-webapps-parent</artifactId>
diff --git a/tests/test-webapps/test-jaas-webapp/pom.xml b/tests/test-webapps/test-jaas-webapp/pom.xml
index 1836e48..77213b9 100644
--- a/tests/test-webapps/test-jaas-webapp/pom.xml
+++ b/tests/test-webapps/test-jaas-webapp/pom.xml
@@ -4,7 +4,7 @@
<parent>
<groupId>org.eclipse.jetty.tests</groupId>
<artifactId>test-webapps-parent</artifactId>
- <version>9.0.5-SNAPSHOT</version>
+ <version>9.1.0-SNAPSHOT</version>
</parent>
<artifactId>test-jaas-webapp</artifactId>
<name>Jetty Tests :: WebApp :: JAAS</name>
diff --git a/tests/test-webapps/test-jetty-webapp/pom.xml b/tests/test-webapps/test-jetty-webapp/pom.xml
index 477a5f7..ff24123 100644
--- a/tests/test-webapps/test-jetty-webapp/pom.xml
+++ b/tests/test-webapps/test-jetty-webapp/pom.xml
@@ -20,7 +20,7 @@
<parent>
<groupId>org.eclipse.jetty.tests</groupId>
<artifactId>test-webapps-parent</artifactId>
- <version>9.0.5-SNAPSHOT</version>
+ <version>9.1.0-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
@@ -98,7 +98,7 @@
<configuration>
<instructions>
<Bundle-SymbolicName>org.eclipse.jetty.test-jetty-webapp</Bundle-SymbolicName>
- <Import-Package>javax.servlet.jsp.*;version="2.2.0",javax.servlet.*;version="[2.6,3.0)",org.eclipse.jetty.*;version="9.0",*</Import-Package>
+ <Import-Package>javax.servlet.jsp.*;version="[2.2.0, 3.0)",javax.servlet.*;version="[2.6,3.2)",org.eclipse.jetty.*;version="9.1",*</Import-Package>
<Export-Package>!com.acme*</Export-Package>
<!-- the test webapp is configured via a jetty xml file
in order to add the security handler. -->
@@ -193,13 +193,8 @@
<version>${project.version}</version>
</dependency>
<dependency>
- <groupId>org.eclipse.jetty</groupId>
- <artifactId>jetty-continuation</artifactId>
- <version>${project.version}</version>
- </dependency>
- <dependency>
- <groupId>org.eclipse.jetty.orbit</groupId>
- <artifactId>javax.servlet</artifactId>
+ <groupId>javax.servlet</groupId>
+ <artifactId>javax.servlet-api</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
diff --git a/tests/test-webapps/test-jetty-webapp/src/main/java/com/acme/WebSocketChatServlet.java b/tests/test-webapps/test-jetty-webapp/src/main/java/com/acme/WebSocketChatServlet.java
index 0367146..ca8f24d 100644
--- a/tests/test-webapps/test-jetty-webapp/src/main/java/com/acme/WebSocketChatServlet.java
+++ b/tests/test-webapps/test-jetty-webapp/src/main/java/com/acme/WebSocketChatServlet.java
@@ -48,12 +48,12 @@
import org.eclipse.jetty.websocket.api.RemoteEndpoint;
import org.eclipse.jetty.websocket.api.Session;
-import org.eclipse.jetty.websocket.api.UpgradeRequest;
-import org.eclipse.jetty.websocket.api.UpgradeResponse;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketClose;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketConnect;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage;
import org.eclipse.jetty.websocket.api.annotations.WebSocket;
+import org.eclipse.jetty.websocket.servlet.ServletUpgradeRequest;
+import org.eclipse.jetty.websocket.servlet.ServletUpgradeResponse;
import org.eclipse.jetty.websocket.servlet.WebSocketCreator;
import org.eclipse.jetty.websocket.servlet.WebSocketServlet;
import org.eclipse.jetty.websocket.servlet.WebSocketServletFactory;
@@ -71,7 +71,7 @@
}
@Override
- public Object createWebSocket(UpgradeRequest req, UpgradeResponse resp)
+ public Object createWebSocket(ServletUpgradeRequest req, ServletUpgradeResponse resp)
{
return new ChatWebSocket();
}
@@ -106,14 +106,7 @@
{
if (data.contains("disconnect"))
{
- try
- {
- session.close();
- }
- catch (IOException ignore)
- {
- // ignore
- }
+ session.close();
return;
}
diff --git a/tests/test-webapps/test-jetty-webapp/src/main/webapp/auth.html b/tests/test-webapps/test-jetty-webapp/src/main/webapp/auth.html
index 4b67966..3b55b76 100644
--- a/tests/test-webapps/test-jetty-webapp/src/main/webapp/auth.html
+++ b/tests/test-webapps/test-jetty-webapp/src/main/webapp/auth.html
@@ -15,7 +15,7 @@
<li><a href="auth2">auth2/index.html</a> - Authenticated (tests FormAuthenticator.setAlwaysSaveUri()) </li>
<li><a href="dump/auth/noaccess/info">dump/auth/noaccess/*</a> - Forbidden</li>
<li><a href="dump/auth/relax/info">dump/auth/relax/*</a> - Allowed</li>
-<li><a href="dump/auth/info">dump/auth/*</a> - Authenticated any user</li>
+<li><a href="dump/auth/info">dump/auth/*</a> - Authenticated any user with any role</li>
<li><a href="dump/auth/admin/info">dump/auth/admin/*</a> - Authenticated admin role (<a href="session/?Action=Invalidate">click</a> to invalidate session)</li>
<li><a href="dump/auth/ssl/info">dump/auth/ssl/*</a> - Confidential</li>
<li><a href="rego/info">rego/info/*</a> - Authenticated admin role from programmatic security (<a href="session/?Action=Invalidate">click</a> to invalidate session)</li>
diff --git a/tests/test-webapps/test-jndi-webapp/pom.xml b/tests/test-webapps/test-jndi-webapp/pom.xml
index 2f5c2f0..f33a6d4 100644
--- a/tests/test-webapps/test-jndi-webapp/pom.xml
+++ b/tests/test-webapps/test-jndi-webapp/pom.xml
@@ -4,7 +4,7 @@
<parent>
<groupId>org.eclipse.jetty.tests</groupId>
<artifactId>test-webapps-parent</artifactId>
- <version>9.0.5-SNAPSHOT</version>
+ <version>9.1.0-SNAPSHOT</version>
</parent>
<artifactId>test-jndi-webapp</artifactId>
<name>Jetty Tests :: WebApp :: JNDI</name>
@@ -19,7 +19,7 @@
<skip>true</skip>
</configuration>
</plugin>
- <plugin>
+ <plugin>
<artifactId>maven-antrun-plugin</artifactId>
<executions>
<execution>
@@ -61,36 +61,21 @@
<overWrite>true</overWrite>
<outputDirectory>${project.build.directory}/lib/jndi</outputDirectory>
</artifactItem>
+ <artifactItem>
+ <groupId>org.eclipse.jetty.orbit</groupId>
+ <artifactId>javax.mail.glassfish</artifactId>
+ <version>1.4.1.v201005082020</version>
+ <type>jar</type>
+ <includes>**</includes>
+ <overWrite>true</overWrite>
+ <outputDirectory>${project.build.directory}/lib/jndi</outputDirectory>
+ </artifactItem>
</artifactItems>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
- <artifactId>maven-resources-plugin</artifactId>
- <executions>
- <execution>
- <id>copy-transaction-properties</id>
- <phase>process-resources</phase>
- <goals>
- <goal>copy-resources</goal>
- </goals>
- <configuration>
- <outputDirectory>${project.build.directory}/resources</outputDirectory>
- <resources>
- <resource>
- <directory>src/main/config/resources</directory>
- <includes>
- <include>**/transactions.properties</include>
- </includes>
- <filtering>true</filtering>
- </resource>
- </resources>
- </configuration>
- </execution>
- </executions>
- </plugin>
- <plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<version>2.2-beta-3</version>
@@ -134,27 +119,31 @@
<artifactId>test-mock-resources</artifactId>
<version>${project.version}</version>
</dependency>
+ <dependency>
+ <groupId>org.eclipse.jetty.orbit</groupId>
+ <artifactId>javax.mail.glassfish</artifactId>
+ <version>1.4.1.v201005082020</version>
+ </dependency>
</dependencies>
</plugin>
</plugins>
</build>
<dependencies>
<dependency>
- <groupId>org.eclipse.jetty.orbit</groupId>
- <artifactId>javax.transaction</artifactId>
- <version>1.1.1.v201105210645</version>
+ <groupId>javax.transaction</groupId>
+ <artifactId>javax.transaction-api</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
- <groupId>org.eclipse.jetty.orbit</groupId>
- <artifactId>javax.servlet</artifactId>
+ <groupId>javax.servlet</groupId>
+ <artifactId>javax.servlet-api</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.eclipse.jetty.orbit</groupId>
<artifactId>javax.mail.glassfish</artifactId>
- <scope>provided</scope>
<version>1.4.1.v201005082020</version>
+ <scope>provided</scope>
</dependency>
</dependencies>
</project>
diff --git a/tests/test-webapps/test-mock-resources/pom.xml b/tests/test-webapps/test-mock-resources/pom.xml
index f07097d..dba37fb 100644
--- a/tests/test-webapps/test-mock-resources/pom.xml
+++ b/tests/test-webapps/test-mock-resources/pom.xml
@@ -3,7 +3,7 @@
<parent>
<groupId>org.eclipse.jetty.tests</groupId>
<artifactId>test-webapps-parent</artifactId>
- <version>9.0.5-SNAPSHOT</version>
+ <version>9.1.0-SNAPSHOT</version>
</parent>
<name>Jetty Tests :: WebApp :: Mock Resources</name>
<artifactId>test-mock-resources</artifactId>
@@ -22,14 +22,26 @@
</build>
<dependencies>
<dependency>
- <groupId>org.eclipse.jetty.orbit</groupId>
- <artifactId>javax.transaction</artifactId>
- <version>1.1.1.v201105210645</version>
+ <groupId>javax.transaction</groupId>
+ <artifactId>javax.transaction-api</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
- <groupId>org.eclipse.jetty.orbit</groupId>
- <artifactId>javax.servlet</artifactId>
+ <groupId>javax.servlet</groupId>
+ <artifactId>javax.servlet-api</artifactId>
</dependency>
+<!--
+ <dependency>
+ <groupId>javax.mail</groupId>
+ <artifactId>javax.mail-api</artifactId>
+ </dependency>
+-->
+ <dependency>
+ <groupId>org.eclipse.jetty.orbit</groupId>
+ <artifactId>javax.mail.glassfish</artifactId>
+ <version>1.4.1.v201005082020</version>
+ <scope>provided</scope>
+ </dependency>
+
</dependencies>
</project>
diff --git a/tests/test-webapps/test-mock-resources/src/main/java/com/acme/MockTransport.java b/tests/test-webapps/test-mock-resources/src/main/java/com/acme/MockTransport.java
new file mode 100644
index 0000000..adb86da
--- /dev/null
+++ b/tests/test-webapps/test-mock-resources/src/main/java/com/acme/MockTransport.java
@@ -0,0 +1,55 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+
+package com.acme;
+
+import javax.mail.Address;
+import javax.mail.Message;
+import javax.mail.MessagingException;
+import javax.mail.Session;
+import javax.mail.Transport;
+import javax.mail.URLName;
+
+/**
+ * MockTransport
+ *
+ *
+ */
+public class MockTransport extends Transport
+{
+
+ /**
+ * @param session
+ * @param urlname
+ */
+ public MockTransport(Session session, URLName urlname)
+ {
+ super(session, urlname);
+ }
+
+ /**
+ * @see javax.mail.Transport#sendMessage(javax.mail.Message, javax.mail.Address[])
+ */
+ @Override
+ public void sendMessage(Message arg0, Address[] arg1) throws MessagingException
+ {
+ System.err.println ("Sending message");
+ }
+
+}
diff --git a/tests/test-webapps/test-mock-resources/src/main/resources/META-INF/javaxmail.providers b/tests/test-webapps/test-mock-resources/src/main/resources/META-INF/javaxmail.providers
new file mode 100644
index 0000000..5ab3340
--- /dev/null
+++ b/tests/test-webapps/test-mock-resources/src/main/resources/META-INF/javaxmail.providers
@@ -0,0 +1 @@
+ protocol=smtp; type=transport; class=com.acme.MockTransport; vendor=Acme Tests;
diff --git a/tests/test-webapps/test-proxy-webapp/pom.xml b/tests/test-webapps/test-proxy-webapp/pom.xml
index 2b71367..4e3251c 100644
--- a/tests/test-webapps/test-proxy-webapp/pom.xml
+++ b/tests/test-webapps/test-proxy-webapp/pom.xml
@@ -20,7 +20,7 @@
<parent>
<groupId>org.eclipse.jetty.tests</groupId>
<artifactId>test-webapps-parent</artifactId>
- <version>9.0.5-SNAPSHOT</version>
+ <version>9.1.0-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
@@ -47,10 +47,18 @@
<version>${project.version}</version>
</dependency>
<dependency>
+ <groupId>javax.servlet</groupId>
+ <artifactId>javax.servlet-api</artifactId>
+ <scope>provided</scope>
+ </dependency>
+
+<!--
+ <dependency>
<groupId>org.eclipse.jetty.orbit</groupId>
<artifactId>javax.servlet</artifactId>
<scope>provided</scope>
</dependency>
+-->
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-webapp</artifactId>
diff --git a/tests/test-webapps/test-servlet-spec/pom.xml b/tests/test-webapps/test-servlet-spec/pom.xml
index 031f2fc..74f7261 100644
--- a/tests/test-webapps/test-servlet-spec/pom.xml
+++ b/tests/test-webapps/test-servlet-spec/pom.xml
@@ -4,7 +4,7 @@
<parent>
<groupId>org.eclipse.jetty.tests</groupId>
<artifactId>test-webapps-parent</artifactId>
- <version>9.0.5-SNAPSHOT</version>
+ <version>9.1.0-SNAPSHOT</version>
</parent>
<artifactId>test-servlet-spec-parent</artifactId>
<name>Jetty Tests :: Spec Test WebApp :: Parent</name>
diff --git a/tests/test-webapps/test-servlet-spec/test-container-initializer/pom.xml b/tests/test-webapps/test-servlet-spec/test-container-initializer/pom.xml
index 54da2e8..4e8fc78 100644
--- a/tests/test-webapps/test-servlet-spec/test-container-initializer/pom.xml
+++ b/tests/test-webapps/test-servlet-spec/test-container-initializer/pom.xml
@@ -3,7 +3,7 @@
<parent>
<groupId>org.eclipse.jetty.tests</groupId>
<artifactId>test-servlet-spec-parent</artifactId>
- <version>9.0.5-SNAPSHOT</version>
+ <version>9.1.0-SNAPSHOT</version>
</parent>
<artifactId>test-container-initializer</artifactId>
<packaging>jar</packaging>
@@ -22,9 +22,17 @@
</build>
<dependencies>
<dependency>
+ <groupId>javax.servlet</groupId>
+ <artifactId>javax.servlet-api</artifactId>
+ <scope>provided</scope>
+ </dependency>
+
+<!--
+ <dependency>
<groupId>org.eclipse.jetty.orbit</groupId>
<artifactId>javax.servlet</artifactId>
<scope>provided</scope>
</dependency>
+-->
</dependencies>
</project>
diff --git a/tests/test-webapps/test-servlet-spec/test-spec-webapp/pom.xml b/tests/test-webapps/test-servlet-spec/test-spec-webapp/pom.xml
index 44344dd..bd27bb8 100644
--- a/tests/test-webapps/test-servlet-spec/test-spec-webapp/pom.xml
+++ b/tests/test-webapps/test-servlet-spec/test-spec-webapp/pom.xml
@@ -4,7 +4,7 @@
<parent>
<groupId>org.eclipse.jetty.tests</groupId>
<artifactId>test-servlet-spec-parent</artifactId>
- <version>9.0.5-SNAPSHOT</version>
+ <version>9.1.0-SNAPSHOT</version>
</parent>
<name>Jetty Tests :: Webapps :: Spec Webapp</name>
<artifactId>test-spec-webapp</artifactId>
@@ -136,9 +136,9 @@
<scope>provided</scope>
</dependency>
<dependency>
- <groupId>org.eclipse.jetty.orbit</groupId>
- <artifactId>javax.servlet</artifactId>
- <scope>provided</scope>
+ <groupId>javax.servlet</groupId>
+ <artifactId>javax.servlet-api</artifactId>
+ <scope>provided</scope>
</dependency>
<dependency>
<groupId>org.eclipse.jetty.orbit</groupId>
diff --git a/tests/test-webapps/test-servlet-spec/test-spec-webapp/src/main/java/com/acme/AnnotationTest.java b/tests/test-webapps/test-servlet-spec/test-spec-webapp/src/main/java/com/acme/AnnotationTest.java
index b90f356..6d06c15 100644
--- a/tests/test-webapps/test-servlet-spec/test-spec-webapp/src/main/java/com/acme/AnnotationTest.java
+++ b/tests/test-webapps/test-servlet-spec/test-spec-webapp/src/main/java/com/acme/AnnotationTest.java
@@ -197,18 +197,19 @@
out.println("<pre>");
out.println("initParams={@WebInitParam(name=\"fromAnnotation\", value=\"xyz\")}");
out.println("</pre>");
- out.println("<br/><b>Result: "+("xyz".equals(config.getInitParameter("fromAnnotation"))? "<span class=\"pass\">PASS": "<span class=\"fail\">FAIL")+"</span>");
+ out.println("<p><b>Result: "+("xyz".equals(config.getInitParameter("fromAnnotation"))? "<span class=\"pass\">PASS": "<span class=\"fail\">FAIL")+"</span></p>");
out.println("<h2>Init Params from web-fragment</h2>");
out.println("<pre>");
out.println("extra1=123, extra2=345");
out.println("</pre>");
boolean fragInitParamResult = "123".equals(config.getInitParameter("extra1")) && "345".equals(config.getInitParameter("extra2"));
- out.println("<br/><b>Result: "+(fragInitParamResult? "<span class=\"pass\">PASS": "<span class=\"fail\">FAIL")+"</span>");
+ out.println("<p><b>Result: "+(fragInitParamResult? "<span class=\"pass\">PASS": "<span class=\"fail\">FAIL")+"</span></p>");
__HandlesTypes = Arrays.asList( "javax.servlet.GenericServlet",
"javax.servlet.http.HttpServlet",
+ "com.acme.AsyncListenerServlet",
"com.acme.AnnotationTest",
"com.acme.RoleAnnotationTest",
"com.acme.MultiPartTest",
@@ -220,7 +221,7 @@
out.println("<pre>");
out.println("@HandlesTypes({javax.servlet.Servlet.class, Foo.class})");
out.println("</pre>");
- out.print("<br/><b>Result: ");
+ out.print("<p><b>Result: ");
List<Class> classes = (List<Class>)config.getServletContext().getAttribute("com.acme.Foo");
List<String> classNames = new ArrayList<String>();
if (classes != null)
@@ -240,19 +241,28 @@
}
else
out.print("<br/><span class=\"fail\">FAIL</span> (No such attribute com.acme.Foo)");
- out.println("</b>");
+ out.println("</b></p>");
out.println("<h2>Complete Servlet Registration</h2>");
Boolean complete = (Boolean)config.getServletContext().getAttribute("com.acme.AnnotationTest.complete");
- out.println("<br/><b>Result: "+(complete.booleanValue()?"<span class=\"pass\">PASS":"<span class=\"fail\">FAIL")+"</span></b>");
+ out.println("<p><b>Result: "+(complete.booleanValue()?"<span class=\"pass\">PASS":"<span class=\"fail\">FAIL")+"</span></b></p>");
out.println("<h2>ServletContextListener Programmatic Registration from ServletContainerInitializer</h2>");
Boolean programmaticListener = (Boolean)config.getServletContext().getAttribute("com.acme.AnnotationTest.listenerTest");
- out.println("<br/><b>Result: "+(programmaticListener.booleanValue()?"<span class=\"pass\">PASS":"<span class=\"fail\">FAIL")+"</span></b>");
+ out.println("<p><b>Result: "+(programmaticListener.booleanValue()?"<span class=\"pass\">PASS":"<span class=\"fail\">FAIL")+"</span></b></p>");
out.println("<h2>ServletContextListener Programmatic Registration Prevented from ServletContextListener</h2>");
Boolean programmaticListenerPrevention = (Boolean)config.getServletContext().getAttribute("com.acme.AnnotationTest.listenerRegoTest");
- out.println("<br/><b>Result: "+(programmaticListenerPrevention.booleanValue()?"<span class=\"pass\">PASS":"<span class=\"fail\">FAIL")+"</span></b>");
+ out.println("<p><b>Result: "+(programmaticListenerPrevention.booleanValue()?"<span class=\"pass\">PASS":"<span class=\"fail\">FAIL")+"</span></b></p>");
+
+ out.println("<h2>ServletContextListener Registration Prevented from ServletContextListener</h2>");
+ Boolean webListenerPrevention = (Boolean)config.getServletContext().getAttribute("com.acme.AnnotationTest.sclFromSclRegoTest");
+ out.println("<p><b>Result: "+(webListenerPrevention.booleanValue()?"<span class=\"pass\">PASS":"<span class=\"fail\">FAIL")+"</span></b></p>");
+
+ out.println("<h2>Invalid Type for Listener Detection</h2>");
+ Boolean badListener = (Boolean)config.getServletContext().getAttribute("com.acme.AnnotationTest.invalidListenerRegoTest");
+ out.println("<p><b>Result: "+(badListener.booleanValue()?"<span class=\"pass\">PASS":"<span class=\"fail\">FAIL")+"</span></b></p>");
+
out.println("<h2>@PostConstruct Callback</h2>");
out.println("<pre>");
@@ -260,7 +270,7 @@
out.println("private void myPostConstructMethod ()");
out.println("{}");
out.println("</pre>");
- out.println("<br/><b>Result: "+postConstructResult+"</b>");
+ out.println("<p><b>Result: "+postConstructResult+"</b></p>");
out.println("<h2>@Resource Injection for DataSource</h2>");
@@ -271,8 +281,8 @@
out.println("myDS=ds;");
out.println("}");
out.println("</pre>");
- out.println("<br/><b>Result: "+dsResult+"</b>");
- out.println("<br/><b>JNDI Lookup Result: "+dsLookupResult+"</b>");
+ out.println("<p><b>Result: "+dsResult+"</b>");
+ out.println("<br/><b>JNDI Lookup Result: "+dsLookupResult+"</b></p>");
out.println("<h2>@Resource Injection for env-entry </h2>");
@@ -282,19 +292,20 @@
out.println("@Resource(name=\"minAmount\")");
out.println("private Double minAmount;");
out.println("</pre>");
- out.println("<br/><b>Result: "+envResult+": "+(maxAmount.compareTo(new Double(55))==0?" <span class=\"pass\">PASS":" <span class=\"fail\">FAIL")+"</span></b>");
+ out.println("<p><b>Result: "+envResult+": "+(maxAmount.compareTo(new Double(55))==0?" <span class=\"pass\">PASS":" <span class=\"fail\">FAIL")+"</span></b>");
out.println("<br/><b>JNDI Lookup Result: "+envLookupResult+"</b>");
out.println("<br/><b>Result: "+envResult2+": "+(minAmount.compareTo(new Double("0.99"))==0?" <span class=\"pass\">PASS":" <span class=\"fail\">FAIL")+"</span></b>");
out.println("<br/><b>JNDI Lookup Result: "+envLookupResult2+"</b>");
out.println("<br/><b>Result: "+envResult3+": "+(avgAmount.compareTo(new Double("1.25"))==0?" <span class=\"pass\">PASS":" <span class=\"fail\">FAIL")+"</span></b>");
- out.println("<br/><b>JNDI Lookup Result: "+envLookupResult3+"</b>");
+ out.println("<br/><b>JNDI Lookup Result: "+envLookupResult3+"</b></p>");
+
out.println("<h2>@Resource Injection for UserTransaction </h2>");
out.println("<pre>");
out.println("@Resource(mappedName=\"UserTransaction\")");
out.println("private UserTransaction myUserTransaction;");
out.println("</pre>");
- out.println("<br/><b>Result: "+txResult+"</b>");
- out.println("<br/><b>JNDI Lookup Result: "+txLookupResult+"</b>");
+ out.println("<p><b>Result: "+txResult+"</b>");
+ out.println("<br/><b>JNDI Lookup Result: "+txLookupResult+"</b></p>");
out.println("</body>");
out.println("</html>");
diff --git a/tests/test-webapps/test-servlet-spec/test-spec-webapp/src/main/java/com/acme/AsyncListenerServlet.java b/tests/test-webapps/test-servlet-spec/test-spec-webapp/src/main/java/com/acme/AsyncListenerServlet.java
new file mode 100644
index 0000000..b828610
--- /dev/null
+++ b/tests/test-webapps/test-servlet-spec/test-spec-webapp/src/main/java/com/acme/AsyncListenerServlet.java
@@ -0,0 +1,127 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package com.acme;
+
+import java.io.IOException;
+import java.io.PrintWriter;
+
+import javax.annotation.PostConstruct;
+import javax.annotation.Resource;
+import javax.servlet.AsyncContext;
+import javax.servlet.AsyncEvent;
+import javax.servlet.AsyncListener;
+import javax.servlet.ServletException;
+import javax.servlet.annotation.WebServlet;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+@WebServlet(urlPatterns="/asy/*", asyncSupported=true)
+public class AsyncListenerServlet extends HttpServlet
+{
+ public static class MyAsyncListener implements AsyncListener
+ {
+ @Resource(mappedName="maxAmount")
+ private Double maxAmount;
+
+ boolean postConstructCalled = false;
+ boolean resourceInjected = false;
+
+ @PostConstruct
+ public void postConstruct()
+ {
+ postConstructCalled = true;
+ resourceInjected = (maxAmount != null);
+ }
+
+ public boolean isPostConstructCalled()
+ {
+ return postConstructCalled;
+ }
+
+ public boolean isResourceInjected()
+ {
+ return resourceInjected;
+ }
+
+ @Override
+ public void onComplete(AsyncEvent event) throws IOException
+ {
+ // TODO Auto-generated method stub
+
+ }
+
+ @Override
+ public void onTimeout(AsyncEvent event) throws IOException
+ {
+ // TODO Auto-generated method stub
+
+ }
+
+ @Override
+ public void onError(AsyncEvent event) throws IOException
+ {
+ // TODO Auto-generated method stub
+
+ }
+
+ @Override
+ public void onStartAsync(AsyncEvent event) throws IOException
+ {
+ // TODO Auto-generated method stub
+
+ }
+ }
+
+
+ protected void doPost(HttpServletRequest req, HttpServletResponse resp)
+ throws ServletException, IOException
+ {
+ AsyncContext asyncContext = req.startAsync();
+ MyAsyncListener listener = asyncContext.createListener(MyAsyncListener.class);
+
+ PrintWriter writer = resp.getWriter();
+ writer.println( "<html>");
+ writer.println("<HEAD><link rel=\"stylesheet\" type=\"text/css\" href=\"../stylesheet.css\"/></HEAD>");
+ writer.println( "<body>");
+ writer.println("<h1>AsyncListener</h2>");
+ writer.println("<pre>");
+ writer.println("<h2>@PostConstruct Callback</h2>");
+ writer.println("<pre>");
+ writer.println("@PostConstruct");
+ writer.println("private void postConstruct ()");
+ writer.println("{}");
+ writer.println("</pre>");
+ writer.println("<br/><b>Result: "+(listener.isPostConstructCalled()?"<span class=\"pass\">PASS</span>":"<span class=\"fail\">FAIL</span>")+"</b>");
+
+ writer.println("<h2>@Resource Injection for env-entry </h2>");
+ writer.println("<pre>");
+ writer.println("@Resource(mappedName=\"maxAmount\")");
+ writer.println("private Double maxAmount;");
+ writer.println("</pre>");
+ writer.println("<br/><b>Result: "+(listener.isResourceInjected()?" <span class=\"pass\">PASS</span>":" <span class=\"FAIL\">FAIL</span>")+"</b>");
+
+ writer.println( "</body>");
+ writer.println( "</html>");
+ writer.flush();
+ writer.close();
+
+ asyncContext.complete();
+ }
+}
diff --git a/tests/test-webapps/test-servlet-spec/test-spec-webapp/src/main/java/com/acme/TestListener.java b/tests/test-webapps/test-servlet-spec/test-spec-webapp/src/main/java/com/acme/TestListener.java
index 7d7592d..8892fcb 100644
--- a/tests/test-webapps/test-servlet-spec/test-spec-webapp/src/main/java/com/acme/TestListener.java
+++ b/tests/test-webapps/test-servlet-spec/test-spec-webapp/src/main/java/com/acme/TestListener.java
@@ -17,6 +17,8 @@
//
package com.acme;
+import java.util.EventListener;
+
import javax.annotation.Resource;
import javax.servlet.ServletContextAttributeEvent;
import javax.servlet.ServletContextAttributeListener;
@@ -38,6 +40,26 @@
@WebListener
public class TestListener implements HttpSessionListener, HttpSessionAttributeListener, HttpSessionActivationListener, ServletContextListener, ServletContextAttributeListener, ServletRequestListener, ServletRequestAttributeListener
{
+ public class NaughtyServletContextListener implements ServletContextListener
+ {
+
+ @Override
+ public void contextInitialized(ServletContextEvent sce)
+ {
+ throw new IllegalStateException("Should not call NaughtServletContextListener.contextInitialized");
+ }
+
+ @Override
+ public void contextDestroyed(ServletContextEvent sce)
+ {
+ throw new IllegalStateException("Should not call NaughtServletContextListener.contextDestroyed");
+ }
+ }
+
+ public class InvalidListener implements EventListener
+ {
+
+ }
@Resource(mappedName="maxAmount")
private Double maxAmount;
@@ -69,7 +91,36 @@
public void contextInitialized(ServletContextEvent sce)
{
- //System.err.println("contextInitialized, maxAmount injected as "+maxAmount);
+ //Can't add a ServletContextListener from a ServletContextListener even if it is declared in web.xml
+ try
+ {
+ sce.getServletContext().addListener(new NaughtyServletContextListener());
+ sce.getServletContext().setAttribute("com.acme.AnnotationTest.sclFromSclRegoTest", Boolean.FALSE);
+ }
+ catch (IllegalArgumentException e)
+ {
+ sce.getServletContext().setAttribute("com.acme.AnnotationTest.sclFromSclRegoTest", Boolean.TRUE);
+ }
+ catch (Exception e)
+ {
+ sce.getServletContext().setAttribute("com.acme.AnnotationTest.sclFromSclRegoTest", Boolean.FALSE);
+ }
+
+
+ //Can't add an EventListener not part of the specified list for addListener()
+ try
+ {
+ sce.getServletContext().addListener(new InvalidListener());
+ sce.getServletContext().setAttribute("com.acme.AnnotationTest.invalidListenerRegoTest", Boolean.FALSE);
+ }
+ catch (IllegalArgumentException e)
+ {
+ sce.getServletContext().setAttribute("com.acme.AnnotationTest.invalidListenerRegoTest", Boolean.TRUE);
+ }
+ catch (Exception e)
+ {
+ sce.getServletContext().setAttribute("com.acme.AnnotationTest.invalidListenerRegoTest", Boolean.FALSE);
+ }
}
public void contextDestroyed(ServletContextEvent sce)
diff --git a/tests/test-webapps/test-servlet-spec/test-spec-webapp/src/main/webapp/index.html b/tests/test-webapps/test-servlet-spec/test-spec-webapp/src/main/webapp/index.html
index 854082f..405b05f 100644
--- a/tests/test-webapps/test-servlet-spec/test-spec-webapp/src/main/webapp/index.html
+++ b/tests/test-webapps/test-servlet-spec/test-spec-webapp/src/main/webapp/index.html
@@ -54,6 +54,12 @@
<input TYPE="submit" VALUE="Test Upload">
</form>
+<h3>AsyncListener Resource Injection</h3>
+<p>Click the following link to test that javax.servlet.AsyncListeners are injectable</p>
+<form action="asy/xx" method="post">
+ <button type="submit">Test AsyncListener</button>
+</form>
+
<center>
<hr/>
<a href="http://www.eclipse.org/jetty"><img style="border:0" src="images/small_powered_by.gif"/></a>
diff --git a/tests/test-webapps/test-servlet-spec/test-spec-webapp/src/main/webapp/stylesheet.css b/tests/test-webapps/test-servlet-spec/test-spec-webapp/src/main/webapp/stylesheet.css
index 4ecc2cb..f90463d 100644
--- a/tests/test-webapps/test-servlet-spec/test-spec-webapp/src/main/webapp/stylesheet.css
+++ b/tests/test-webapps/test-servlet-spec/test-spec-webapp/src/main/webapp/stylesheet.css
@@ -1,6 +1,6 @@
body {color: #2E2E2E; font-family:sans-serif; font-size:90%;}
h1 {font-variant: small-caps; font-size:130%; letter-spacing: 0.1em;}
-h2 {font-variant: small-caps; font-size:100%; letter-spacing: 0.1em;}
+h2 {font-variant: small-caps; font-size:100%; letter-spacing: 0.1em; margin-top:2em;}
h3 {font-size:100%; letter-spacing: 0.1em;}
span.pass { color: green; }
diff --git a/tests/test-webapps/test-servlet-spec/test-spec-webapp/src/main/webapp/stylesheet.css~ b/tests/test-webapps/test-servlet-spec/test-spec-webapp/src/main/webapp/stylesheet.css~
new file mode 100644
index 0000000..def6847
--- /dev/null
+++ b/tests/test-webapps/test-servlet-spec/test-spec-webapp/src/main/webapp/stylesheet.css~
@@ -0,0 +1,7 @@
+body {color: #2E2E2E; font-family:sans-serif; font-size:90%;}
+h1 {font-variant: small-caps; font-size:130%; letter-spacing: 0.1em;}
+h2 {font-variant: small-caps; font-size:100%; letter-spacing: 0.1em; margin-top:2em}
+h3 {font-size:100%; letter-spacing: 0.1em;}
+
+span.pass { color: green; }
+span.fail { color:red; }
diff --git a/tests/test-webapps/test-servlet-spec/test-web-fragment/pom.xml b/tests/test-webapps/test-servlet-spec/test-web-fragment/pom.xml
index abef592..66ede09 100644
--- a/tests/test-webapps/test-servlet-spec/test-web-fragment/pom.xml
+++ b/tests/test-webapps/test-servlet-spec/test-web-fragment/pom.xml
@@ -3,7 +3,7 @@
<parent>
<groupId>org.eclipse.jetty.tests</groupId>
<artifactId>test-servlet-spec-parent</artifactId>
- <version>9.0.5-SNAPSHOT</version>
+ <version>9.1.0-SNAPSHOT</version>
</parent>
<name>Jetty Tests :: WebApp :: Servlet Spec :: Fragment Jar</name>
<groupId>org.eclipse.jetty.tests</groupId>
@@ -22,9 +22,16 @@
</plugins>
</build>
<dependencies>
+ <dependency>
+ <groupId>javax.servlet</groupId>
+ <artifactId>javax.servlet-api</artifactId>
+ </dependency>
+
+<!--
<dependency>
<groupId>org.eclipse.jetty.orbit</groupId>
<artifactId>javax.servlet</artifactId>
</dependency>
+-->
</dependencies>
</project>
diff --git a/tests/test-webapps/test-webapp-rfc2616/pom.xml b/tests/test-webapps/test-webapp-rfc2616/pom.xml
index 433b97a..f458ac0 100644
--- a/tests/test-webapps/test-webapp-rfc2616/pom.xml
+++ b/tests/test-webapps/test-webapp-rfc2616/pom.xml
@@ -21,7 +21,7 @@
<parent>
<groupId>org.eclipse.jetty.tests</groupId>
<artifactId>test-webapps-parent</artifactId>
- <version>9.0.5-SNAPSHOT</version>
+ <version>9.1.0-SNAPSHOT</version>
</parent>
<artifactId>test-webapp-rfc2616</artifactId>
<name>Jetty Tests :: WebApp :: RFC2616</name>
@@ -62,8 +62,8 @@
<version>${project.version}</version>
</dependency>
<dependency>
- <groupId>org.eclipse.jetty.orbit</groupId>
- <artifactId>javax.servlet</artifactId>
+ <groupId>javax.servlet</groupId>
+ <artifactId>javax.servlet-api</artifactId>
<scope>provided</scope>
</dependency>
</dependencies>