diff --git a/.gitignore b/.gitignore index 7966828f435..ff5ed7e2d15 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ .classpath .project .settings +.gitignore # maven target/ diff --git a/examples/async-rest/async-rest-jar/pom.xml b/examples/async-rest/async-rest-jar/pom.xml index 6156b0c4b42..03f8e5450a5 100644 --- a/examples/async-rest/async-rest-jar/pom.xml +++ b/examples/async-rest/async-rest-jar/pom.xml @@ -24,7 +24,6 @@ javax.servlet javax.servlet-api - 3.1-b08 provided diff --git a/examples/async-rest/async-rest-jar/src/main/java/org/eclipse/jetty/example/asyncrest/AbstractRestServlet.java b/examples/async-rest/async-rest-jar/src/main/java/org/eclipse/jetty/example/asyncrest/AbstractRestServlet.java index 8778d986678..9a549fe1406 100644 --- a/examples/async-rest/async-rest-jar/src/main/java/org/eclipse/jetty/example/asyncrest/AbstractRestServlet.java +++ b/examples/async-rest/async-rest-jar/src/main/java/org/eclipse/jetty/example/asyncrest/AbstractRestServlet.java @@ -66,6 +66,14 @@ public class AbstractRestServlet extends HttpServlet else _appid = servletConfig.getInitParameter(APPID_PARAM); } + + + public static String sanitize(String s) + { + if (s==null) + return null; + return s.replace("<","?").replace("&","?").replace("\n","?"); + } protected String restURL(String item) { diff --git a/examples/async-rest/async-rest-jar/src/main/java/org/eclipse/jetty/example/asyncrest/AsyncRestServlet.java b/examples/async-rest/async-rest-jar/src/main/java/org/eclipse/jetty/example/asyncrest/AsyncRestServlet.java index 421eb6e5433..c5868d2c50b 100644 --- a/examples/async-rest/async-rest-jar/src/main/java/org/eclipse/jetty/example/asyncrest/AsyncRestServlet.java +++ b/examples/async-rest/async-rest-jar/src/main/java/org/eclipse/jetty/example/asyncrest/AsyncRestServlet.java @@ -97,7 +97,7 @@ public class AsyncRestServlet extends AbstractRestServlet async.setTimeout(30000); // extract keywords to search for - String[] keywords=request.getParameter(ITEMS_PARAM).split(","); + String[] keywords=sanitize(request.getParameter(ITEMS_PARAM)).split(","); final AtomicInteger outstanding=new AtomicInteger(keywords.length); // Send request each keyword @@ -146,7 +146,7 @@ public class AsyncRestServlet extends AbstractRestServlet long generate=now-start; long thread=initial+generate; - out.print("Asynchronous: "+request.getParameter(ITEMS_PARAM)+"
"); + out.print("Asynchronous: "+sanitize(request.getParameter(ITEMS_PARAM))+"
"); out.print("Total Time: "+ms(total)+"ms
"); out.print("Thread held (red): "+ms(thread)+"ms (" + ms(initial) + " initial + " + ms(generate) + " generate )
"); @@ -162,7 +162,7 @@ public class AsyncRestServlet extends AbstractRestServlet out.println(""); out.close(); } - + private abstract class AsyncRestRequest extends Response.Listener.Adapter { final Utf8StringBuilder _content = new Utf8StringBuilder(); diff --git a/examples/async-rest/async-rest-jar/src/main/java/org/eclipse/jetty/example/asyncrest/SerialRestServlet.java b/examples/async-rest/async-rest-jar/src/main/java/org/eclipse/jetty/example/asyncrest/SerialRestServlet.java index 340b85c05b7..2a167bdd34d 100644 --- a/examples/async-rest/async-rest-jar/src/main/java/org/eclipse/jetty/example/asyncrest/SerialRestServlet.java +++ b/examples/async-rest/async-rest-jar/src/main/java/org/eclipse/jetty/example/asyncrest/SerialRestServlet.java @@ -45,7 +45,7 @@ public class SerialRestServlet extends AbstractRestServlet long start = System.nanoTime(); - String[] keywords=request.getParameter(ITEMS_PARAM).split(","); + String[] keywords=sanitize(request.getParameter(ITEMS_PARAM)).split(","); Queue> results = new LinkedList>(); // make all requests serially @@ -78,7 +78,7 @@ public class SerialRestServlet extends AbstractRestServlet long now = System.nanoTime(); long total=now-start; - out.print("Blocking: "+request.getParameter(ITEMS_PARAM)+"
"); + out.print("Blocking: "+sanitize(request.getParameter(ITEMS_PARAM))+"
"); out.print("Total Time: "+ms(total)+"ms
"); out.print("Thread held (red): "+ms(total)+"ms
"); diff --git a/examples/async-rest/async-rest-webapp/pom.xml b/examples/async-rest/async-rest-webapp/pom.xml index fc5523bc861..dfa2a49f8e5 100644 --- a/examples/async-rest/async-rest-webapp/pom.xml +++ b/examples/async-rest/async-rest-webapp/pom.xml @@ -27,7 +27,6 @@ javax.servlet javax.servlet-api - 3.1-b08 provided 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 87459b3fe33..541ac475ba2 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 @@ -438,18 +438,29 @@ public class AnnotationConfiguration extends AbstractConfiguration createServletContainerInitializerAnnotationHandlers(context, getNonExcludedInitializers(context)); if (!_discoverableAnnotationHandlers.isEmpty() || _classInheritanceHandler != null || !_containerInitializerAnnotationHandlers.isEmpty()) - scanForAnnotations(context); + scanForAnnotations(context); + + // Resolve container initializers + List initializers = + (List)context.getAttribute(AnnotationConfiguration.CONTAINER_INITIALIZERS); + if (initializers != null && initializers.size()>0) + { + Map> map = ( Map>) context.getAttribute(AnnotationConfiguration.CLASS_INHERITANCE_MAP); + if (map == null) + throw new IllegalStateException ("No class hierarchy"); + for (ContainerInitializer i : initializers) + i.resolveClasses(context,map); + } } - /** * @see org.eclipse.jetty.webapp.AbstractConfiguration#postConfigure(org.eclipse.jetty.webapp.WebAppContext) */ @Override public void postConfigure(WebAppContext context) throws Exception { - ConcurrentHashMap> classMap = (ConcurrentHashMap>)context.getAttribute(CLASS_INHERITANCE_MAP); + ConcurrentHashMap> classMap = (ClassInheritanceMap)context.getAttribute(CLASS_INHERITANCE_MAP); List initializers = (List)context.getAttribute(CONTAINER_INITIALIZERS); context.removeAttribute(CLASS_INHERITANCE_MAP); @@ -655,7 +666,7 @@ public class AnnotationConfiguration extends AbstractConfiguration if (annotation != null) { //There is a HandlesTypes annotation on the on the ServletContainerInitializer - Class[] classes = annotation.value(); + Class[] classes = annotation.value(); if (classes != null) { initializer = new ContainerInitializer(service, classes); @@ -665,12 +676,12 @@ public class AnnotationConfiguration extends AbstractConfiguration if (context.getAttribute(CLASS_INHERITANCE_MAP) == null) { //MultiMap map = new MultiMap<>(); - ConcurrentHashMap> map = new ConcurrentHashMap>(); + ConcurrentHashMap> map = new ClassInheritanceMap(); context.setAttribute(CLASS_INHERITANCE_MAP, map); _classInheritanceHandler = new ClassInheritanceHandler(map); } - for (Class c: classes) + for (Class c: classes) { //The value of one of the HandlesTypes classes is actually an Annotation itself so //register a handler for it @@ -1044,4 +1055,16 @@ public class AnnotationConfiguration extends AbstractConfiguration { return (d!=null && d.getMetaDataComplete() == MetaDataComplete.True); } + + public static class ClassInheritanceMap extends ConcurrentHashMap> + { + + @Override + public String toString() + { + return String.format("ClassInheritanceMap@%x{size=%d}",hashCode(),size()); + } + } } + + diff --git a/jetty-annotations/src/main/java/org/eclipse/jetty/annotations/MultiPartConfigAnnotationHandler.java b/jetty-annotations/src/main/java/org/eclipse/jetty/annotations/MultiPartConfigAnnotationHandler.java index eb49af7c6cd..5e789126e97 100644 --- a/jetty-annotations/src/main/java/org/eclipse/jetty/annotations/MultiPartConfigAnnotationHandler.java +++ b/jetty-annotations/src/main/java/org/eclipse/jetty/annotations/MultiPartConfigAnnotationHandler.java @@ -70,7 +70,7 @@ public class MultiPartConfigAnnotationHandler extends AbstractIntrospectableAnno //let the annotation override it if (d == null) { - metaData.setOrigin(holder.getName()+".servlet.multipart-config"); + metaData.setOrigin(holder.getName()+".servlet.multipart-config",multi,clazz); holder.getRegistration().setMultipartConfig(new MultipartConfigElement(multi)); } } 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 8c637f69a4e..b37a4dab52f 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 @@ -180,7 +180,7 @@ public class ResourceAnnotationHandler extends AbstractIntrospectableAnnotationH injections.add(injection); //TODO - an @Resource is equivalent to a resource-ref, resource-env-ref, message-destination - metaData.setOrigin("resource-ref."+name+".injection"); + metaData.setOrigin("resource-ref."+name+".injection",resource,clazz); } else if (!Util.isEnvEntryType(type)) { @@ -334,7 +334,7 @@ public class ResourceAnnotationHandler extends AbstractIntrospectableAnnotationH injection.setMappingName(mappedName); injections.add(injection); //TODO - an @Resource is equivalent to a resource-ref, resource-env-ref, message-destination - metaData.setOrigin("resource-ref."+name+".injection"); + metaData.setOrigin("resource-ref."+name+".injection",resource,clazz); } else if (!Util.isEnvEntryType(paramType)) { diff --git a/jetty-annotations/src/main/java/org/eclipse/jetty/annotations/RunAsAnnotationHandler.java b/jetty-annotations/src/main/java/org/eclipse/jetty/annotations/RunAsAnnotationHandler.java index 37589b8dd1c..a2db0a7f5d3 100644 --- a/jetty-annotations/src/main/java/org/eclipse/jetty/annotations/RunAsAnnotationHandler.java +++ b/jetty-annotations/src/main/java/org/eclipse/jetty/annotations/RunAsAnnotationHandler.java @@ -64,7 +64,7 @@ public class RunAsAnnotationHandler extends AbstractIntrospectableAnnotationHand //let the annotation override it if (d == null) { - metaData.setOrigin(holder.getName()+".servlet.run-as"); + metaData.setOrigin(holder.getName()+".servlet.run-as",runAs,clazz); org.eclipse.jetty.plus.annotation.RunAs ra = new org.eclipse.jetty.plus.annotation.RunAs(); ra.setTargetClassName(clazz.getCanonicalName()); ra.setRoleName(role); diff --git a/jetty-annotations/src/main/java/org/eclipse/jetty/annotations/ServletContainerInitializersStarter.java b/jetty-annotations/src/main/java/org/eclipse/jetty/annotations/ServletContainerInitializersStarter.java index f38b3792e50..2ebaa9623a1 100644 --- a/jetty-annotations/src/main/java/org/eclipse/jetty/annotations/ServletContainerInitializersStarter.java +++ b/jetty-annotations/src/main/java/org/eclipse/jetty/annotations/ServletContainerInitializersStarter.java @@ -59,14 +59,9 @@ public class ServletContainerInitializersStarter extends AbstractLifeCycle imple List initializers = (List)_context.getAttribute(AnnotationConfiguration.CONTAINER_INITIALIZERS); if (initializers == null) return; - - ConcurrentHashMap> map = ( ConcurrentHashMap>)_context.getAttribute(AnnotationConfiguration.CLASS_INHERITANCE_MAP); for (ContainerInitializer i : initializers) { - configureHandlesTypes(_context, i, map); - - //instantiate ServletContainerInitializers, call doStart try { if (LOG.isDebugEnabled()) @@ -80,82 +75,6 @@ public class ServletContainerInitializersStarter extends AbstractLifeCycle imple } } } - - - private void configureHandlesTypes (WebAppContext context, ContainerInitializer initializer, ConcurrentHashMap> classMap) - { - doHandlesTypesAnnotations(context, initializer, classMap); - doHandlesTypesClasses(context, initializer, classMap); - } - - private void doHandlesTypesAnnotations(WebAppContext context, ContainerInitializer initializer, ConcurrentHashMap> classMap) - { - if (initializer == null) - return; - if (context == null) - throw new IllegalArgumentException("WebAppContext null"); - - //We have already found the classes that directly have an annotation that was in the HandlesTypes - //annotation of the ServletContainerInitializer. For each of those classes, walk the inheritance - //hierarchy to find classes that extend or implement them. - Set annotatedClassNames = initializer.getAnnotatedTypeNames(); - if (annotatedClassNames != null && !annotatedClassNames.isEmpty()) - { - if (classMap == null) - throw new IllegalStateException ("No class hierarchy"); - - for (String name : annotatedClassNames) - { - //add the class that has the annotation - initializer.addApplicableTypeName(name); - - //find and add the classes that inherit the annotation - addInheritedTypes(classMap, initializer, (ConcurrentHashSet)classMap.get(name)); - } - } - } - - - - private void doHandlesTypesClasses (WebAppContext context, ContainerInitializer initializer, ConcurrentHashMap> classMap) - { - if (initializer == null) - return; - if (context == null) - throw new IllegalArgumentException("WebAppContext null"); - - //Now we need to look at the HandlesTypes classes that were not annotations. We need to - //find all classes that extend or implement them. - if (initializer.getInterestedTypes() != null) - { - if (classMap == null) - throw new IllegalStateException ("No class hierarchy"); - - for (Class c : initializer.getInterestedTypes()) - { - if (!c.isAnnotation()) - { - //find and add the classes that implement or extend the class. - //but not including the class itself - addInheritedTypes(classMap, initializer, (ConcurrentHashSet)classMap.get(c.getName())); - } - } - } - } - private void addInheritedTypes (ConcurrentHashMap> classMap, ContainerInitializer initializer, ConcurrentHashSet names) - { - if (names == null || names.isEmpty()) - return; - - for (String s : names) - { - //add the name of the class - initializer.addApplicableTypeName(s); - - //walk the hierarchy and find all types that extend or implement the class - addInheritedTypes(classMap, initializer, (ConcurrentHashSet)classMap.get(s)); - } - } } 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 8c198a63ae2..743826793a4 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 @@ -102,7 +102,7 @@ public class ServletSecurityAnnotationHandler extends AbstractIntrospectableAnno { for (String url : sm.getPathSpecs()) { - _context.getMetaData().setOrigin("constraint.url."+url, Origin.Annotation); + _context.getMetaData().setOrigin("constraint.url."+url,servletSecurity,clazz); constraintMappings.addAll(ConstraintSecurityHandler.createConstraintsWithMappingsForPath(clazz.getName(), url, securityElement)); } } diff --git a/jetty-annotations/src/main/java/org/eclipse/jetty/annotations/WebFilterAnnotation.java b/jetty-annotations/src/main/java/org/eclipse/jetty/annotations/WebFilterAnnotation.java index 25dd8ee3f7a..9b312d27516 100644 --- a/jetty-annotations/src/main/java/org/eclipse/jetty/annotations/WebFilterAnnotation.java +++ b/jetty-annotations/src/main/java/org/eclipse/jetty/annotations/WebFilterAnnotation.java @@ -104,15 +104,15 @@ public class WebFilterAnnotation extends DiscoveredAnnotation holder.setName(name); holder.setHeldClass(clazz); - metaData.setOrigin(name+".filter.filter-class"); + metaData.setOrigin(name+".filter.filter-class",filterAnnotation,clazz); holder.setDisplayName(filterAnnotation.displayName()); - metaData.setOrigin(name+".filter.display-name"); + metaData.setOrigin(name+".filter.display-name",filterAnnotation,clazz); for (WebInitParam ip: filterAnnotation.initParams()) { holder.setInitParameter(ip.name(), ip.value()); - metaData.setOrigin(name+".filter.init-param."+ip.name()); + metaData.setOrigin(name+".filter.init-param."+ip.name(),ip,clazz); } FilterMapping mapping = new FilterMapping(); @@ -120,12 +120,12 @@ public class WebFilterAnnotation extends DiscoveredAnnotation if (urlPatterns.length > 0) { - ArrayList paths = new ArrayList(); + ArrayList paths = new ArrayList(); for (String s:urlPatterns) { paths.add(Util.normalizePattern(s)); } - mapping.setPathSpecs((String[])paths.toArray(new String[paths.size()])); + mapping.setPathSpecs(paths.toArray(new String[paths.size()])); } if (filterAnnotation.servletNames().length > 0) @@ -135,7 +135,7 @@ public class WebFilterAnnotation extends DiscoveredAnnotation { names.add(s); } - mapping.setServletNames((String[])names.toArray(new String[names.size()])); + mapping.setServletNames(names.toArray(new String[names.size()])); } EnumSet dispatcherSet = EnumSet.noneOf(DispatcherType.class); @@ -144,10 +144,10 @@ public class WebFilterAnnotation extends DiscoveredAnnotation dispatcherSet.add(d); } mapping.setDispatcherTypes(dispatcherSet); - metaData.setOrigin(name+".filter.mappings"); + metaData.setOrigin(name+".filter.mappings",filterAnnotation,clazz); holder.setAsyncSupported(filterAnnotation.asyncSupported()); - metaData.setOrigin(name+".filter.async-supported"); + metaData.setOrigin(name+".filter.async-supported",filterAnnotation,clazz); _context.getServletHandler().addFilter(holder); _context.getServletHandler().addFilterMapping(mapping); @@ -165,7 +165,7 @@ public class WebFilterAnnotation extends DiscoveredAnnotation if (metaData.getOrigin(name+".filter.init-param."+ip.name())==Origin.NotSet) { holder.setInitParameter(ip.name(), ip.value()); - metaData.setOrigin(name+".filter.init-param."+ip.name()); + metaData.setOrigin(name+".filter.init-param."+ip.name(),ip,clazz); } } @@ -191,12 +191,12 @@ public class WebFilterAnnotation extends DiscoveredAnnotation if (urlPatterns.length > 0) { - ArrayList paths = new ArrayList(); + ArrayList paths = new ArrayList(); for (String s:urlPatterns) { paths.add(Util.normalizePattern(s)); } - mapping.setPathSpecs((String[])paths.toArray(new String[paths.size()])); + mapping.setPathSpecs(paths.toArray(new String[paths.size()])); } if (filterAnnotation.servletNames().length > 0) { @@ -205,7 +205,7 @@ public class WebFilterAnnotation extends DiscoveredAnnotation { names.add(s); } - mapping.setServletNames((String[])names.toArray(new String[names.size()])); + mapping.setServletNames(names.toArray(new String[names.size()])); } EnumSet dispatcherSet = EnumSet.noneOf(DispatcherType.class); @@ -215,7 +215,7 @@ public class WebFilterAnnotation extends DiscoveredAnnotation } mapping.setDispatcherTypes(dispatcherSet); _context.getServletHandler().addFilterMapping(mapping); - metaData.setOrigin(name+".filter.mappings"); + metaData.setOrigin(name+".filter.mappings",filterAnnotation,clazz); } } } diff --git a/jetty-annotations/src/main/java/org/eclipse/jetty/annotations/WebServletAnnotation.java b/jetty-annotations/src/main/java/org/eclipse/jetty/annotations/WebServletAnnotation.java index 71d24369ba7..b259419a03a 100644 --- a/jetty-annotations/src/main/java/org/eclipse/jetty/annotations/WebServletAnnotation.java +++ b/jetty-annotations/src/main/java/org/eclipse/jetty/annotations/WebServletAnnotation.java @@ -20,6 +20,7 @@ package org.eclipse.jetty.annotations; import java.util.ArrayList; +import javax.servlet.Servlet; import javax.servlet.annotation.WebInitParam; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; @@ -62,7 +63,7 @@ public class WebServletAnnotation extends DiscoveredAnnotation public void apply() { //TODO check this algorithm with new rules for applying descriptors and annotations in order - Class clazz = getTargetClass(); + Class clazz = (Class)getTargetClass(); if (clazz == null) { @@ -127,22 +128,22 @@ public class WebServletAnnotation extends DiscoveredAnnotation //or another annotation (which would be impossible). holder = _context.getServletHandler().newServletHolder(Holder.Source.ANNOTATION); holder.setHeldClass(clazz); - metaData.setOrigin(servletName+".servlet.servlet-class"); + metaData.setOrigin(servletName+".servlet.servlet-class",annotation,clazz); holder.setName(servletName); holder.setDisplayName(annotation.displayName()); - metaData.setOrigin(servletName+".servlet.display-name"); + metaData.setOrigin(servletName+".servlet.display-name",annotation,clazz); holder.setInitOrder(annotation.loadOnStartup()); - metaData.setOrigin(servletName+".servlet.load-on-startup"); + metaData.setOrigin(servletName+".servlet.load-on-startup",annotation,clazz); holder.setAsyncSupported(annotation.asyncSupported()); - metaData.setOrigin(servletName+".servlet.async-supported"); + metaData.setOrigin(servletName+".servlet.async-supported",annotation,clazz); for (WebInitParam ip:annotation.initParams()) { holder.setInitParameter(ip.name(), ip.value()); - metaData.setOrigin(servletName+".servlet.init-param."+ip.name()); + metaData.setOrigin(servletName+".servlet.init-param."+ip.name(),ip,clazz); } _context.getServletHandler().addServlet(holder); @@ -150,7 +151,7 @@ public class WebServletAnnotation extends DiscoveredAnnotation mapping.setServletName(holder.getName()); mapping.setPathSpecs( LazyList.toStringArray(urlPatternList)); _context.getServletHandler().addServletMapping(mapping); - metaData.setOrigin(servletName+".servlet.mappings"); + metaData.setOrigin(servletName+".servlet.mappings",annotation,clazz); } else { @@ -170,7 +171,7 @@ public class WebServletAnnotation extends DiscoveredAnnotation if (metaData.getOrigin(servletName+".servlet.init-param."+ip.name())==Origin.NotSet) { holder.setInitParameter(ip.name(), ip.value()); - metaData.setOrigin(servletName+".servlet.init-param."+ip.name()); + metaData.setOrigin(servletName+".servlet.init-param."+ip.name(),ip,clazz); } } 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 3cd1dad7c28..afe214f5297 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 @@ -206,10 +206,22 @@ public class HttpRequest implements Request @Override public Request param(String name, String value) + { + return param(name, value, false); + } + + private Request param(String name, String value, boolean fromQuery) { params.add(name, value); - this.query = buildQuery(); - this.uri = null; + if (!fromQuery) + { + // If we have an existing query string, preserve it and append the new parameter. + if (query != null) + query += "&" + name + "=" + urlEncode(value); + else + query = buildQuery(); + uri = null; + } return this; } @@ -640,6 +652,9 @@ public class HttpRequest implements Request private String urlEncode(String value) { + if (value == null) + return ""; + String encoding = "UTF-8"; try { @@ -663,7 +678,7 @@ public class HttpRequest implements Request String name = parts[0]; if (name.trim().length() == 0) continue; - param(name, parts.length < 2 ? "" : urlDecode(parts[1])); + param(name, parts.length < 2 ? "" : urlDecode(parts[1]), true); } } } diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/RequestNotifier.java b/jetty-client/src/main/java/org/eclipse/jetty/client/RequestNotifier.java index 34aa933f96f..0c42fd3954b 100644 --- a/jetty-client/src/main/java/org/eclipse/jetty/client/RequestNotifier.java +++ b/jetty-client/src/main/java/org/eclipse/jetty/client/RequestNotifier.java @@ -36,7 +36,6 @@ public class RequestNotifier this.client = client; } - @SuppressWarnings("ForLoopReplaceableByForEach") public void notifyQueued(Request request) { // Optimized to avoid allocations of iterator instances @@ -67,7 +66,6 @@ public class RequestNotifier } } - @SuppressWarnings("ForLoopReplaceableByForEach") public void notifyBegin(Request request) { // Optimized to avoid allocations of iterator instances @@ -98,7 +96,6 @@ public class RequestNotifier } } - @SuppressWarnings("ForLoopReplaceableByForEach") public void notifyHeaders(Request request) { // Optimized to avoid allocations of iterator instances @@ -129,7 +126,6 @@ public class RequestNotifier } } - @SuppressWarnings("ForLoopReplaceableByForEach") public void notifyCommit(Request request) { // Optimized to avoid allocations of iterator instances @@ -160,7 +156,6 @@ public class RequestNotifier } } - @SuppressWarnings("ForLoopReplaceableByForEach") public void notifyContent(Request request, ByteBuffer content) { // Slice the buffer to avoid that listeners peek into data they should not look at. @@ -203,7 +198,6 @@ public class RequestNotifier } } - @SuppressWarnings("ForLoopReplaceableByForEach") public void notifySuccess(Request request) { // Optimized to avoid allocations of iterator instances @@ -234,7 +228,6 @@ public class RequestNotifier } } - @SuppressWarnings("ForLoopReplaceableByForEach") public void notifyFailure(Request request, Throwable failure) { // Optimized to avoid allocations of iterator instances diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/ResponseNotifier.java b/jetty-client/src/main/java/org/eclipse/jetty/client/ResponseNotifier.java index 1caf716a495..5a9ec8e9ba9 100644 --- a/jetty-client/src/main/java/org/eclipse/jetty/client/ResponseNotifier.java +++ b/jetty-client/src/main/java/org/eclipse/jetty/client/ResponseNotifier.java @@ -40,7 +40,6 @@ public class ResponseNotifier this.client = client; } - @SuppressWarnings("ForLoopReplaceableByForEach") public void notifyBegin(List listeners, Response response) { // Optimized to avoid allocations of iterator instances @@ -64,7 +63,6 @@ public class ResponseNotifier } } - @SuppressWarnings("ForLoopReplaceableByForEach") public boolean notifyHeader(List listeners, Response response, HttpField field) { boolean result = true; @@ -91,7 +89,6 @@ public class ResponseNotifier } } - @SuppressWarnings("ForLoopReplaceableByForEach") public void notifyHeaders(List listeners, Response response) { // Optimized to avoid allocations of iterator instances @@ -115,7 +112,6 @@ public class ResponseNotifier } } - @SuppressWarnings("ForLoopReplaceableByForEach") public void notifyContent(List listeners, Response response, ByteBuffer buffer) { // Slice the buffer to avoid that listeners peek into data they should not look at. @@ -148,7 +144,6 @@ public class ResponseNotifier } } - @SuppressWarnings("ForLoopReplaceableByForEach") public void notifySuccess(List listeners, Response response) { // Optimized to avoid allocations of iterator instances @@ -172,7 +167,6 @@ public class ResponseNotifier } } - @SuppressWarnings("ForLoopReplaceableByForEach") public void notifyFailure(List listeners, Response response, Throwable failure) { // Optimized to avoid allocations of iterator instances @@ -196,7 +190,6 @@ public class ResponseNotifier } } - @SuppressWarnings("ForLoopReplaceableByForEach") public void notifyComplete(List listeners, Result result) { // Optimized to avoid allocations of iterator instances 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 f7ccdfcf63b..1b7e3ee42ca 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 @@ -277,6 +277,7 @@ public class HttpClientTest extends AbstractHttpClientServerTest { final String paramName = "a"; final String paramValue = "\u20AC"; + final String encodedParamValue = URLEncoder.encode(paramValue, "UTF-8"); start(new AbstractHandler() { @Override @@ -293,7 +294,7 @@ public class HttpClientTest extends AbstractHttpClientServerTest } }); - URI uri = URI.create(scheme + "://localhost:" + connector.getLocalPort() + "/path?" + paramName + "=" + paramValue); + URI uri = URI.create(scheme + "://localhost:" + connector.getLocalPort() + "/path?" + paramName + "=" + encodedParamValue); ContentResponse response = client.newRequest(uri) .method(HttpMethod.PUT) .timeout(5, TimeUnit.SECONDS) diff --git a/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientURITest.java b/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientURITest.java index 78381b44979..0ba36728d92 100644 --- a/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientURITest.java +++ b/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientURITest.java @@ -21,7 +21,6 @@ package org.eclipse.jetty.client; import java.io.IOException; import java.net.URLEncoder; import java.util.concurrent.TimeUnit; - import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @@ -339,4 +338,97 @@ public class HttpClientURITest extends AbstractHttpClientServerTest Assert.assertEquals(HttpStatus.OK_200, response.getStatus()); } + + @Test + public void testRawQueryIsPreservedInURI() throws Exception + { + final String name = "a"; + final String rawValue = "Hello%20World"; + final String rawQuery = name + "=" + rawValue; + final String value = "Hello World"; + 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); + Assert.assertEquals(rawQuery, request.getQueryString()); + Assert.assertEquals(value, request.getParameter(name)); + } + }); + + String uri = scheme + "://localhost:" + connector.getLocalPort() + "/path?" + rawQuery; + Request request = client.newRequest(uri) + .timeout(5, TimeUnit.SECONDS); + Assert.assertEquals(rawQuery, request.getQuery()); + + ContentResponse response = request.send(); + + Assert.assertEquals(HttpStatus.OK_200, response.getStatus()); + } + + @Test + public void testRawQueryIsPreservedInPath() throws Exception + { + final String name = "a"; + final String rawValue = "Hello%20World"; + final String rawQuery = name + "=" + rawValue; + final String value = "Hello World"; + 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); + Assert.assertEquals(rawQuery, request.getQueryString()); + Assert.assertEquals(value, request.getParameter(name)); + } + }); + + Request request = client.newRequest("localhost", connector.getLocalPort()) + .scheme(scheme) + .path("/path?" + rawQuery) + .timeout(5, TimeUnit.SECONDS); + Assert.assertEquals(rawQuery, request.getQuery()); + + ContentResponse response = request.send(); + + Assert.assertEquals(HttpStatus.OK_200, response.getStatus()); + } + + @Test + public void testRawQueryIsPreservedWithParam() throws Exception + { + final String name1 = "a"; + final String name2 = "b"; + final String rawValue1 = "Hello%20World"; + final String rawQuery1 = name1 + "=" + rawValue1; + final String value1 = "Hello World"; + final String value2 = "alfa omega"; + final String encodedQuery2 = name2 + "=" + URLEncoder.encode(value2, "UTF-8"); + final String query = rawQuery1 + "&" + encodedQuery2; + 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); + Assert.assertEquals(query, request.getQueryString()); + Assert.assertEquals(value1, request.getParameter(name1)); + Assert.assertEquals(value2, request.getParameter(name2)); + } + }); + + Request request = client.newRequest("localhost", connector.getLocalPort()) + .scheme(scheme) + .path("/path?" + rawQuery1) + .param(name2, value2) + .timeout(5, TimeUnit.SECONDS); + Assert.assertEquals(query, request.getQuery()); + + ContentResponse response = request.send(); + + Assert.assertEquals(HttpStatus.OK_200, response.getStatus()); + } + } 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 cc6359148a2..8443f67de43 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 @@ -442,6 +442,13 @@ public class HttpParser break; else if (ch<0) throw new BadMessage(); + + // count this white space as a header byte to avoid DOS + if (_maxHeaderBytes>0 && ++_headerBytes>_maxHeaderBytes) + { + LOG.warn("padding is too large >"+_maxHeaderBytes); + throw new BadMessage(HttpStatus.BAD_REQUEST_400); + } } return false; } diff --git a/jetty-io/src/main/java/org/eclipse/jetty/io/ByteArrayEndPoint.java b/jetty-io/src/main/java/org/eclipse/jetty/io/ByteArrayEndPoint.java index 770a116d443..4b8c527c2f6 100644 --- a/jetty-io/src/main/java/org/eclipse/jetty/io/ByteArrayEndPoint.java +++ b/jetty-io/src/main/java/org/eclipse/jetty/io/ByteArrayEndPoint.java @@ -311,7 +311,7 @@ public class ByteArrayEndPoint extends AbstractEndPoint shutdownInput(); if (_ishut) return -1; - int filled=BufferUtil.flipPutFlip(_in,buffer); + int filled=BufferUtil.append(buffer,_in); if (filled>0) notIdle(); return filled; @@ -342,12 +342,12 @@ public class ByteArrayEndPoint extends AbstractEndPoint if (b.remaining()>BufferUtil.space(_out)) { ByteBuffer n = BufferUtil.allocate(_out.capacity()+b.remaining()*2); - BufferUtil.flipPutFlip(_out,n); + BufferUtil.append(n,_out); _out=n; } } - if (BufferUtil.flipPutFlip(b,_out)>0) + if (BufferUtil.append(_out,b)>0) idle=false; if (BufferUtil.hasContent(b)) diff --git a/jetty-io/src/main/java/org/eclipse/jetty/io/ChannelEndPoint.java b/jetty-io/src/main/java/org/eclipse/jetty/io/ChannelEndPoint.java index ef1c716258b..c65ca0ccfc8 100644 --- a/jetty-io/src/main/java/org/eclipse/jetty/io/ChannelEndPoint.java +++ b/jetty-io/src/main/java/org/eclipse/jetty/io/ChannelEndPoint.java @@ -181,7 +181,8 @@ public class ChannelEndPoint extends AbstractEndPoint } } } - LOG.debug("flushed {} {}", flushed, this); + if (LOG.isDebugEnabled()) + LOG.debug("flushed {} {}", flushed, this); } catch (IOException e) { 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 8cfc0e2ea83..5c7606f1280 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 @@ -469,7 +469,7 @@ public class SslConnection extends AbstractConnection { // Do we already have some decrypted data? if (BufferUtil.hasContent(_decryptedInput)) - return BufferUtil.flipPutFlip(_decryptedInput, buffer); + return BufferUtil.append(buffer,_decryptedInput); // We will need a network buffer if (_encryptedInput == null) @@ -574,7 +574,7 @@ public class SslConnection extends AbstractConnection { if (app_in == buffer) return unwrapResult.bytesProduced(); - return BufferUtil.flipPutFlip(_decryptedInput, buffer); + return BufferUtil.append(buffer,_decryptedInput); } switch (handshakeStatus) diff --git a/jetty-io/src/test/java/org/eclipse/jetty/io/SelectChannelEndPointTest.java b/jetty-io/src/test/java/org/eclipse/jetty/io/SelectChannelEndPointTest.java index 90b8208e0e3..bdeb332bfbc 100644 --- a/jetty-io/src/test/java/org/eclipse/jetty/io/SelectChannelEndPointTest.java +++ b/jetty-io/src/test/java/org/eclipse/jetty/io/SelectChannelEndPointTest.java @@ -162,7 +162,7 @@ public class SelectChannelEndPointTest } // Copy to the out buffer - if (BufferUtil.hasContent(_in) && BufferUtil.flipPutFlip(_in, _out) > 0) + if (BufferUtil.hasContent(_in) && BufferUtil.append(_out, _in) > 0) progress = true; // Blocking writes diff --git a/jetty-jndi/src/main/java/org/eclipse/jetty/jndi/NamingContext.java b/jetty-jndi/src/main/java/org/eclipse/jetty/jndi/NamingContext.java index 68154e3624f..1120520aad7 100644 --- a/jetty-jndi/src/main/java/org/eclipse/jetty/jndi/NamingContext.java +++ b/jetty-jndi/src/main/java/org/eclipse/jetty/jndi/NamingContext.java @@ -55,17 +55,18 @@ import org.eclipse.jetty.util.log.Logger; * *

Notes

*

All Names are expected to be Compound, not Composite. - * - * */ +@SuppressWarnings("unchecked") public class NamingContext implements Context, Cloneable, Dumpable { private final static Logger __log=NamingUtil.__log; private final static List __empty = Collections.emptyList(); - public static final String LOCK_PROPERTY = "org.eclipse.jndi.lock"; - public static final String UNLOCK_PROPERTY = "org.eclipse.jndi.unlock"; + public static final String DEEP_BINDING = "org.eclipse.jetty.jndi.deepBinding"; + public static final String LOCK_PROPERTY = "org.eclipse.jetty.jndi.lock"; + public static final String UNLOCK_PROPERTY = "org.eclipse.jetty.jndi.unlock"; protected final Hashtable _env = new Hashtable(); + private boolean _supportDeepBinding = false; protected Map _bindings = new HashMap(); protected NamingContext _parent = null; @@ -110,15 +111,33 @@ public class NamingContext implements Context, Cloneable, Dumpable NameParser parser) { if (env != null) + { _env.putAll(env); + // look for deep binding support in _env + Object support = _env.get(DEEP_BINDING); + if (support == null) + _supportDeepBinding = false; + else + _supportDeepBinding = support != null?Boolean.parseBoolean(support.toString()):false; + } + else + { + // no env? likely this is a root context (java or local) that + // was created without an _env. Look for a system property. + String value = System.getProperty(DEEP_BINDING,"false"); + _supportDeepBinding = Boolean.parseBoolean(value); + // put what we discovered into the _env for later sub-contexts + // to utilize. + _env.put(DEEP_BINDING,_supportDeepBinding); + } _name = name; _parent = parent; _parser = parser; + if(__log.isDebugEnabled()) + __log.debug("supportDeepBinding={}",_supportDeepBinding); } - - /*------------------------------------------------*/ /** * Clone this NamingContext @@ -170,10 +189,13 @@ public class NamingContext implements Context, Cloneable, Dumpable } - public void setEnv (Hashtable env) + public final void setEnv (Hashtable env) { _env.clear(); + if(env == null) + return; _env.putAll(env); + _supportDeepBinding = _env.containsKey(DEEP_BINDING); } @@ -240,8 +262,19 @@ public class NamingContext implements Context, Cloneable, Dumpable { Binding binding = getBinding (firstComponent); - if (binding == null) - throw new NameNotFoundException (firstComponent+ " is not bound"); + if (binding == null) { + if (_supportDeepBinding) + { + Name subname = _parser.parse(firstComponent); + Context subctx = new NamingContext((Hashtable)_env.clone(),firstComponent,this,_parser); + addBinding(subname,subctx); + binding = getBinding(subname); + } + else + { + throw new NameNotFoundException(firstComponent + " is not bound"); + } + } ctx = binding.getObject(); @@ -1248,8 +1281,15 @@ public class NamingContext implements Context, Cloneable, Dumpable if (binding!=null) { - if (_bindings.containsKey(key)) + if (_bindings.containsKey(key)) { + if(_supportDeepBinding) { + // quietly return (no exception) + // this is jndi spec breaking, but is added to support broken + // jndi users like openejb. + return; + } throw new NameAlreadyBoundException(name.toString()); + } _bindings.put(key,binding); } } @@ -1407,4 +1447,28 @@ public class NamingContext implements Context, Cloneable, Dumpable { return _listeners.remove(listener); } + + @Override + public String toString() + { + StringBuilder buf = new StringBuilder(); + buf.append(this.getClass().getName()).append('@').append(Integer.toHexString(this.hashCode())); + buf.append("[name=").append(this._name); + buf.append(",parent="); + if (this._parent != null) + { + buf.append(this._parent.getClass().getName()).append('@').append(Integer.toHexString(this._parent.hashCode())); + } + buf.append(",bindings"); + if (this._bindings == null) + { + buf.append("="); + } + else + { + buf.append(".size=").append(this._bindings.size()); + } + buf.append(']'); + return buf.toString(); + } } diff --git a/jetty-jndi/src/main/java/org/eclipse/jetty/jndi/java/javaRootURLContext.java b/jetty-jndi/src/main/java/org/eclipse/jetty/jndi/java/javaRootURLContext.java index 62942470c44..3892dd409a0 100644 --- a/jetty-jndi/src/main/java/org/eclipse/jetty/jndi/java/javaRootURLContext.java +++ b/jetty-jndi/src/main/java/org/eclipse/jetty/jndi/java/javaRootURLContext.java @@ -46,14 +46,6 @@ import org.eclipse.jetty.util.log.Logger; *

Usage

*
  */
-/*
-* 
-* -* @see -* -* -* @version 1.0 -*/ public class javaRootURLContext implements Context { private static Logger __log = NamingUtil.__log; @@ -81,7 +73,7 @@ public class javaRootURLContext implements Context ContextFactory.class.getName(), (String)null); - //bind special object factory at comp + // bind special object factory at comp __nameRoot.bind ("comp", ref); } catch (Exception e) @@ -103,7 +95,6 @@ public class javaRootURLContext implements Context _env = env; } - public Object lookup(Name name) throws NamingException { diff --git a/jetty-maven-plugin/src/main/java/org/eclipse/jetty/maven/plugin/JettyStopMojo.java b/jetty-maven-plugin/src/main/java/org/eclipse/jetty/maven/plugin/JettyStopMojo.java index 69362e7e4b2..80c530cf2fc 100644 --- a/jetty-maven-plugin/src/main/java/org/eclipse/jetty/maven/plugin/JettyStopMojo.java +++ b/jetty-maven-plugin/src/main/java/org/eclipse/jetty/maven/plugin/JettyStopMojo.java @@ -85,7 +85,7 @@ public class JettyStopMojo extends AbstractMojo s.setSoTimeout(stopWait * 1000); s.getInputStream(); - System.err.printf("Waiting %d seconds for jetty to stop%n",stopWait); + getLog().info("Waiting "+stopWait+" seconds for jetty to stop"); LineNumberReader lin = new LineNumberReader(new InputStreamReader(s.getInputStream())); String response; boolean stopped = false; @@ -94,7 +94,7 @@ public class JettyStopMojo extends AbstractMojo if ("Stopped".equals(response)) { stopped = true; - System.err.println("Server reports itself as Stopped"); + getLog().info("Server reports itself as stopped"); } } } diff --git a/jetty-nosql/src/main/java/org/eclipse/jetty/nosql/mongodb/MongoSessionManager.java b/jetty-nosql/src/main/java/org/eclipse/jetty/nosql/mongodb/MongoSessionManager.java index 867233add2a..d8556da87ac 100644 --- a/jetty-nosql/src/main/java/org/eclipse/jetty/nosql/mongodb/MongoSessionManager.java +++ b/jetty-nosql/src/main/java/org/eclipse/jetty/nosql/mongodb/MongoSessionManager.java @@ -365,13 +365,16 @@ public class MongoSessionManager extends NoSqlSessionManager // followed by bindings and then activation. session.willPassivate(); try - { - session.clearAttributes(); - - DBObject attrs = (DBObject)getNestedValue(o,getContextKey()); - - if (attrs != null) + { + DBObject attrs = (DBObject)getNestedValue(o,getContextKey()); + //if disk version now has no attributes, get rid of them + if (attrs == null || attrs.keySet().size() == 0) { + session.clearAttributes(); + } + else + { + //iterate over the names of the attributes on the disk version, updating the value for (String name : attrs.keySet()) { //skip special metadata field which is not one of the session attributes @@ -381,23 +384,25 @@ public class MongoSessionManager extends NoSqlSessionManager String attr = decodeName(name); Object value = decodeValue(attrs.get(name)); - if (attrs.keySet().contains(name)) - { + //session does not already contain this attribute, so bind it + if (session.getAttribute(attr) == null) + { session.doPutOrRemove(attr,value); session.bindValue(attr,value); } - else + else //session already contains this attribute, update its value { session.doPutOrRemove(attr,value); } + } // cleanup, remove values from session, that don't exist in data anymore: - for (String name : session.getNames()) + for (String str : session.getNames()) { - if (!attrs.keySet().contains(name)) + if (!attrs.keySet().contains(str)) { - session.doPutOrRemove(name,null); - session.unbindValue(name,session.getAttribute(name)); + session.doPutOrRemove(str,null); + session.unbindValue(str,session.getAttribute(str)); } } } 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 bff329e155f..66b4ddccc0a 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 @@ -18,15 +18,22 @@ package org.eclipse.jetty.plus.annotation; +import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.HashSet; +import java.util.List; +import java.util.Map; import java.util.Set; import java.util.concurrent.TimeUnit; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import javax.servlet.ServletContainerInitializer; import org.eclipse.jetty.util.ConcurrentHashSet; import org.eclipse.jetty.util.Loader; +import org.eclipse.jetty.util.StringUtil; import org.eclipse.jetty.util.log.Log; import org.eclipse.jetty.util.log.Logger; import org.eclipse.jetty.webapp.WebAppContext; @@ -36,17 +43,41 @@ public class ContainerInitializer private static final Logger LOG = Log.getLogger(ContainerInitializer.class); final protected ServletContainerInitializer _target; - final protected Class[] _interestedTypes; - protected Set _applicableTypeNames = new ConcurrentHashSet(); - protected Set _annotatedTypeNames = new ConcurrentHashSet(); + final protected Class[] _interestedTypes; + final protected Set _applicableTypeNames = new ConcurrentHashSet(); + final protected Set _annotatedTypeNames = new ConcurrentHashSet(); - public ContainerInitializer (ServletContainerInitializer target, Class[] classes) + public ContainerInitializer (ServletContainerInitializer target, Class[] classes) { _target = target; _interestedTypes = classes; } - + + public ContainerInitializer (ClassLoader loader, String toString) + { + Matcher m = Pattern.compile("ContainerInitializer\\{(.*),interested=(.*),applicable=(.*),annotated=(.*)\\}").matcher(toString); + if (!m.matches()) + throw new IllegalArgumentException(toString); + + try + { + _target = (ServletContainerInitializer)loader.loadClass(m.group(1)).newInstance(); + String[] interested = StringUtil.arrayFromString(m.group(2)); + _interestedTypes = new Class[interested.length]; + for (int i=0;i interested = Collections.emptyList(); + if (_interestedTypes != null) + { + interested = new ArrayList<>(_interestedTypes.length); + for (Class c : _interestedTypes) + interested.add(c.getName()); + } + + return String.format("ContainerInitializer{%s,interested=%s,applicable=%s,annotated=%s}",_target.getClass().getName(),interested,_applicableTypeNames,_annotatedTypeNames); + } + + public void resolveClasses(WebAppContext context, Map> classMap) + { + //We have already found the classes that directly have an annotation that was in the HandlesTypes + //annotation of the ServletContainerInitializer. For each of those classes, walk the inheritance + //hierarchy to find classes that extend or implement them. + Set annotatedClassNames = getAnnotatedTypeNames(); + if (annotatedClassNames != null && !annotatedClassNames.isEmpty()) + { + for (String name : annotatedClassNames) + { + //add the class that has the annotation + addApplicableTypeName(name); + + //find and add the classes that inherit the annotation + addInheritedTypes(classMap, (Set)classMap.get(name)); + } + } + + + //Now we need to look at the HandlesTypes classes that were not annotations. We need to + //find all classes that extend or implement them. + if (getInterestedTypes() != null) + { + for (Class c : getInterestedTypes()) + { + if (!c.isAnnotation()) + { + //find and add the classes that implement or extend the class. + //but not including the class itself + addInheritedTypes(classMap, (Set)classMap.get(c.getName())); + } + } + } + } + + private void addInheritedTypes(Map> classMap,Set names) + { + if (names == null || names.isEmpty()) + return; + + for (String s : names) + { + //add the name of the class + addApplicableTypeName(s); + + //walk the hierarchy and find all types that extend or implement the class + addInheritedTypes(classMap, (Set)classMap.get(s)); + } + } } diff --git a/jetty-plus/src/main/java/org/eclipse/jetty/plus/annotation/LifeCycleCallbackCollection.java b/jetty-plus/src/main/java/org/eclipse/jetty/plus/annotation/LifeCycleCallbackCollection.java index 2ebd8a49de4..27b88fe100f 100644 --- a/jetty-plus/src/main/java/org/eclipse/jetty/plus/annotation/LifeCycleCallbackCollection.java +++ b/jetty-plus/src/main/java/org/eclipse/jetty/plus/annotation/LifeCycleCallbackCollection.java @@ -19,6 +19,8 @@ package org.eclipse.jetty.plus.annotation; import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -136,4 +138,51 @@ public class LifeCycleCallbackCollection for (int i=0;i> getPostConstructCallbackMap() + { + return Collections.unmodifiableMap(postConstructCallbacksMap); + } + + /** + * Generate a read-only view of the pre-destroy callbacks + * @return + */ + public Map> getPreDestroyCallbackMap() + { + return Collections.unmodifiableMap(preDestroyCallbacksMap); + } + + /** + * Amalgamate all post-construct callbacks and return a read only list + * @return + */ + public Collection getPostConstructCallbacks() + { + List list = new ArrayList(); + for (String s:postConstructCallbacksMap.keySet()) + { + list.addAll(postConstructCallbacksMap.get(s)); + } + return Collections.unmodifiableCollection(list); + } + + /** + * Amalgamate all pre-destroy callbacks and return a read only list + * @return + */ + public Collection getPreDestroyCallbacks() + { + List list = new ArrayList(); + for (String s:preDestroyCallbacksMap.keySet()) + { + list.addAll(preDestroyCallbacksMap.get(s)); + } + return Collections.unmodifiableCollection(list); + } + } diff --git a/jetty-plus/src/main/java/org/eclipse/jetty/plus/webapp/PlusConfiguration.java b/jetty-plus/src/main/java/org/eclipse/jetty/plus/webapp/PlusConfiguration.java index 64e61cbe8c1..e0e2be17c87 100644 --- a/jetty-plus/src/main/java/org/eclipse/jetty/plus/webapp/PlusConfiguration.java +++ b/jetty-plus/src/main/java/org/eclipse/jetty/plus/webapp/PlusConfiguration.java @@ -109,7 +109,7 @@ public class PlusConfiguration extends AbstractConfiguration _key = new Integer(random.nextInt()); Context context = new InitialContext(); Context compCtx = (Context)context.lookup("java:comp"); - compCtx.addToEnvironment("org.eclipse.jndi.lock", _key); + compCtx.addToEnvironment("org.eclipse.jetty.jndi.lock", _key); } finally { @@ -129,7 +129,7 @@ public class PlusConfiguration extends AbstractConfiguration { Context context = new InitialContext(); Context compCtx = (Context)context.lookup("java:comp"); - compCtx.addToEnvironment("org.eclipse.jndi.unlock", _key); + compCtx.addToEnvironment("org.eclipse.jetty.jndi.unlock", _key); } finally { 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 99a94235d12..711391d83ee 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 @@ -163,7 +163,7 @@ public class HttpOutput extends ServletOutputStream implements Runnable } } - /* Called to indicated that the output is already closed and the state needs to be updated to match */ + /* Called to indicated that the output is already closed (write with last==true performed) and the state needs to be updated to match */ void closed() { loop: while(true) @@ -894,19 +894,25 @@ public class HttpOutput extends ServletOutputStream implements Runnable // all content written, but if we have not yet signal completion, we // need to do so - if (_complete) + if (_complete && !_completed) { - if (!_completed) - { - _completed=true; - write(BufferUtil.EMPTY_BUFFER, _complete, this); - return Action.SCHEDULED; - } - closed(); + _completed=true; + write(BufferUtil.EMPTY_BUFFER, _complete, this); + return Action.SCHEDULED; } return Action.SUCCEEDED; } + + @Override + protected void completed() + { + super.completed(); + if (_complete) + closed(); + } + + } 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 a7f46d340e8..7d92c5615ab 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 @@ -70,6 +70,8 @@ public class Response implements HttpServletResponse private static final Logger LOG = Log.getLogger(Response.class); private static final String __COOKIE_DELIM="\",;\\ \t"; private final static String __01Jan1970_COOKIE = DateGenerator.formatCookieDate(0).trim(); + private final static int __MIN_BUFFER_SIZE = 1; + // Cookie building buffer. Reduce garbage for cookie using applications private static final ThreadLocal __cookieBuilder = new ThreadLocal() @@ -1197,6 +1199,8 @@ public class Response implements HttpServletResponse { if (isCommitted() || getContentCount() > 0) throw new IllegalStateException("Committed or content written"); + if (size <= 0) + size = __MIN_BUFFER_SIZE; _out.setBufferSize(size); } diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/Server.java b/jetty-server/src/main/java/org/eclipse/jetty/server/Server.java index 3aa3d62f321..09747622e05 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/Server.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/Server.java @@ -19,6 +19,7 @@ package org.eclipse.jetty.server; import java.io.IOException; +import java.lang.management.ManagementFactory; import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.URI; @@ -345,6 +346,8 @@ public class Server extends HandlerWrapper implements Attributes dumpStdErr(); mex.ifExceptionThrow(); + + LOG.info(String.format("Started @%dms",ManagementFactory.getRuntimeMXBean().getUptime())); } @Override 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 3330ddabee7..1f4e0e007ba 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 @@ -1604,6 +1604,19 @@ public class ContextHandler extends ScopedHandler implements Attributes, Gracefu encoding = _localeEncodingMap.get(locale.getLanguage()); return encoding; } + + /* ------------------------------------------------------------ */ + /** + * Get all of the locale encodings + * + * @return a map of all the locale encodings: key is name of the locale and value is the char encoding + */ + public Map getLocaleEncodings() + { + if (_localeEncodingMap == null) + return null; + return Collections.unmodifiableMap(_localeEncodingMap); + } /* ------------------------------------------------------------ */ /* 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 ce32d6f3f80..f76df6864bf 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 @@ -880,107 +880,7 @@ public abstract class AbstractSessionManager extends AbstractLifeCycle implement /* ------------------------------------------------------------ */ private SessionCookieConfig _cookieConfig = - new SessionCookieConfig() - { - @Override - public String getComment() - { - return _sessionComment; - } - - @Override - public String getDomain() - { - return _sessionDomain; - } - - @Override - public int getMaxAge() - { - return _maxCookieAge; - } - - @Override - public String getName() - { - return _sessionCookie; - } - - @Override - public String getPath() - { - return _sessionPath; - } - - @Override - public boolean isHttpOnly() - { - return _httpOnly; - } - - @Override - public boolean isSecure() - { - return _secureCookies; - } - - @Override - public void setComment(String comment) - { - if (_context != null && _context.getContextHandler().isAvailable()) - throw new IllegalStateException("CookieConfig cannot be set after ServletContext is started"); - _sessionComment = comment; - } - - @Override - public void setDomain(String domain) - { - if (_context != null && _context.getContextHandler().isAvailable()) - throw new IllegalStateException("CookieConfig cannot be set after ServletContext is started"); - _sessionDomain=domain; - } - - @Override - public void setHttpOnly(boolean httpOnly) - { - if (_context != null && _context.getContextHandler().isAvailable()) - throw new IllegalStateException("CookieConfig cannot be set after ServletContext is started"); - _httpOnly=httpOnly; - } - - @Override - public void setMaxAge(int maxAge) - { - if (_context != null && _context.getContextHandler().isAvailable()) - throw new IllegalStateException("CookieConfig cannot be set after ServletContext is started"); - _maxCookieAge=maxAge; - } - - @Override - public void setName(String name) - { - if (_context != null && _context.getContextHandler().isAvailable()) - throw new IllegalStateException("CookieConfig cannot be set after ServletContext is started"); - _sessionCookie=name; - } - - @Override - public void setPath(String path) - { - if (_context != null && _context.getContextHandler().isAvailable()) - throw new IllegalStateException("CookieConfig cannot be set after ServletContext is started"); - _sessionPath=path; - } - - @Override - public void setSecure(boolean secure) - { - if (_context != null && _context.getContextHandler().isAvailable()) - throw new IllegalStateException("CookieConfig cannot be set after ServletContext is started"); - _secureCookies=secure; - } - - }; + new CookieConfig(); /* ------------------------------------------------------------ */ @@ -1055,6 +955,112 @@ public abstract class AbstractSessionManager extends AbstractLifeCycle implement } + /** + * CookieConfig + * + * Implementation of the javax.servlet.SessionCookieConfig. + */ + public final class CookieConfig implements SessionCookieConfig + { + @Override + public String getComment() + { + return _sessionComment; + } + + @Override + public String getDomain() + { + return _sessionDomain; + } + + @Override + public int getMaxAge() + { + return _maxCookieAge; + } + + @Override + public String getName() + { + return _sessionCookie; + } + + @Override + public String getPath() + { + return _sessionPath; + } + + @Override + public boolean isHttpOnly() + { + return _httpOnly; + } + + @Override + public boolean isSecure() + { + return _secureCookies; + } + + @Override + public void setComment(String comment) + { + if (_context != null && _context.getContextHandler().isAvailable()) + throw new IllegalStateException("CookieConfig cannot be set after ServletContext is started"); + _sessionComment = comment; + } + + @Override + public void setDomain(String domain) + { + if (_context != null && _context.getContextHandler().isAvailable()) + throw new IllegalStateException("CookieConfig cannot be set after ServletContext is started"); + _sessionDomain=domain; + } + + @Override + public void setHttpOnly(boolean httpOnly) + { + if (_context != null && _context.getContextHandler().isAvailable()) + throw new IllegalStateException("CookieConfig cannot be set after ServletContext is started"); + _httpOnly=httpOnly; + } + + @Override + public void setMaxAge(int maxAge) + { + if (_context != null && _context.getContextHandler().isAvailable()) + throw new IllegalStateException("CookieConfig cannot be set after ServletContext is started"); + _maxCookieAge=maxAge; + } + + @Override + public void setName(String name) + { + if (_context != null && _context.getContextHandler().isAvailable()) + throw new IllegalStateException("CookieConfig cannot be set after ServletContext is started"); + _sessionCookie=name; + } + + @Override + public void setPath(String path) + { + if (_context != null && _context.getContextHandler().isAvailable()) + throw new IllegalStateException("CookieConfig cannot be set after ServletContext is started"); + _sessionPath=path; + } + + @Override + public void setSecure(boolean secure) + { + if (_context != null && _context.getContextHandler().isAvailable()) + throw new IllegalStateException("CookieConfig cannot be set after ServletContext is started"); + _secureCookies=secure; + } + } + /* ------------------------------------------------------------ */ /* ------------------------------------------------------------ */ /* ------------------------------------------------------------ */ diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/session/JDBCSessionIdManager.java b/jetty-server/src/main/java/org/eclipse/jetty/server/session/JDBCSessionIdManager.java index 3075493eb1c..a93b65ce5d6 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/session/JDBCSessionIdManager.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/session/JDBCSessionIdManager.java @@ -1411,7 +1411,7 @@ public class JDBCSessionIdManager extends AbstractSessionIdManager String[] ids = expiredIds.toArray(new String[expiredIds.size()]); try (Connection con = getConnection()) { - con.setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE); + con.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED); con.setAutoCommit(false); int start = 0; 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 4102b474582..cf07d414683 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 @@ -28,6 +28,7 @@ import java.io.InputStream; import java.nio.ByteBuffer; import java.nio.channels.ReadableByteChannel; import java.util.Arrays; +import java.util.concurrent.atomic.AtomicInteger; import javax.servlet.AsyncContext; import javax.servlet.ServletException; @@ -36,7 +37,6 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.junit.Assert; - import org.eclipse.jetty.server.handler.AbstractHandler; import org.eclipse.jetty.util.BufferUtil; import org.eclipse.jetty.util.resource.Resource; @@ -546,6 +546,23 @@ public class HttpOutputTest assertThat(response,Matchers.not(containsString("Content-Length"))); assertThat(response,containsString("400\tThis is a big file")); } + + @Test + public void testAsyncWriteBufferLargeHEAD() throws Exception + { + final Resource big = Resource.newClassPathResource("simple/big.txt"); + _handler._writeLengthIfKnown=false; + _handler._content=BufferUtil.toBuffer(big,false); + _handler._byteBuffer=BufferUtil.allocate(8192); + _handler._async=true; + + int start=_handler._owp.get(); + String response=_connector.getResponses("HEAD / HTTP/1.0\nHost: localhost:80\n\n"); + assertThat(_handler._owp.get()-start,Matchers.greaterThan(0)); + assertThat(response,containsString("HTTP/1.1 200 OK")); + assertThat(response,Matchers.not(containsString("Content-Length"))); + assertThat(response,Matchers.not(containsString("400\tThis is a big file"))); + } @Test public void testAsyncWriteSimpleKnown() throws Exception @@ -562,9 +579,28 @@ public class HttpOutputTest assertThat(response,containsString("Content-Length: 11")); assertThat(response,containsString("simple text")); } + + @Test + public void testAsyncWriteSimpleKnownHEAD() throws Exception + { + final Resource big = Resource.newClassPathResource("simple/simple.txt"); + + _handler._async=true; + _handler._writeLengthIfKnown=true; + _handler._content=BufferUtil.toBuffer(big,false); + _handler._arrayBuffer=new byte[4000]; + + int start=_handler._owp.get(); + String response=_connector.getResponses("HEAD / HTTP/1.0\nHost: localhost:80\n\n"); + assertThat(_handler._owp.get()-start,Matchers.equalTo(1)); + assertThat(response,containsString("HTTP/1.1 200 OK")); + assertThat(response,containsString("Content-Length: 11")); + assertThat(response,Matchers.not(containsString("simple text"))); + } static class ContentHandler extends AbstractHandler { + AtomicInteger _owp = new AtomicInteger(); boolean _writeLengthIfKnown=true; boolean _async; ByteBuffer _byteBuffer; @@ -609,6 +645,8 @@ public class HttpOutputTest @Override public void onWritePossible() throws IOException { + _owp.incrementAndGet(); + while (out.isReady()) { Assert.assertTrue(out.isReady()); @@ -666,6 +704,8 @@ public class HttpOutputTest @Override public void onWritePossible() throws IOException { + _owp.incrementAndGet(); + while (out.isReady()) { Assert.assertTrue(out.isReady()); @@ -693,7 +733,6 @@ public class HttpOutputTest return; } - while(BufferUtil.hasContent(_content)) { BufferUtil.clearToFill(_byteBuffer); @@ -714,10 +753,7 @@ public class HttpOutputTest _content=null; return; } - - } - } } diff --git a/jetty-servlet/src/main/java/org/eclipse/jetty/servlet/FilterMapping.java b/jetty-servlet/src/main/java/org/eclipse/jetty/servlet/FilterMapping.java index 4e09dc438ea..1d5a9a42054 100644 --- a/jetty-servlet/src/main/java/org/eclipse/jetty/servlet/FilterMapping.java +++ b/jetty-servlet/src/main/java/org/eclipse/jetty/servlet/FilterMapping.java @@ -129,6 +129,18 @@ public class FilterMapping implements Dumpable return (_dispatches&type)!=0; } + /* ------------------------------------------------------------ */ + public boolean appliesTo(DispatcherType t) + { + return appliesTo(dispatch(t)); + } + + /* ------------------------------------------------------------ */ + public boolean isDefaultDispatches() + { + return _dispatches==0; + } + /* ------------------------------------------------------------ */ /** * @return Returns the filterName. diff --git a/jetty-servlet/src/main/java/org/eclipse/jetty/servlet/ServletHolder.java b/jetty-servlet/src/main/java/org/eclipse/jetty/servlet/ServletHolder.java index dbe5ed10e78..370979e9db3 100644 --- a/jetty-servlet/src/main/java/org/eclipse/jetty/servlet/ServletHolder.java +++ b/jetty-servlet/src/main/java/org/eclipse/jetty/servlet/ServletHolder.java @@ -242,12 +242,6 @@ public class ServletHolder extends Holder implements UserIdentity.Scope return (link==null)?name:link; } - /* ------------------------------------------------------------ */ - public Map getRoleMap() - { - return _roleMap == null? NO_MAPPED_ROLES : _roleMap; - } - /* ------------------------------------------------------------ */ /** * @return Returns the forcedPath. diff --git a/jetty-servlets/src/main/java/org/eclipse/jetty/servlets/gzip/GzipHandler.java b/jetty-servlets/src/main/java/org/eclipse/jetty/servlets/gzip/GzipHandler.java index 893b54c8731..44d34d1f055 100644 --- a/jetty-servlets/src/main/java/org/eclipse/jetty/servlets/gzip/GzipHandler.java +++ b/jetty-servlets/src/main/java/org/eclipse/jetty/servlets/gzip/GzipHandler.java @@ -36,6 +36,7 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.eclipse.jetty.http.HttpMethod; +import org.eclipse.jetty.http.MimeTypes; import org.eclipse.jetty.server.Request; import org.eclipse.jetty.server.handler.HandlerWrapper; import org.eclipse.jetty.util.log.Log; @@ -243,6 +244,25 @@ public class GzipHandler extends HandlerWrapper { _minGzipSize = minGzipSize; } + + /* ------------------------------------------------------------ */ + @Override + protected void doStart() throws Exception + { + if (_mimeTypes.size()==0) + { + for (String type:MimeTypes.getKnownMimeTypes()) + { + if (type.startsWith("image/")|| + type.startsWith("audio/")|| + type.startsWith("video/")) + _mimeTypes.add(type); + _mimeTypes.add("application/compress"); + _mimeTypes.add("application/zip"); + _mimeTypes.add("application/gzip"); + } + } + } /* ------------------------------------------------------------ */ /** 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 index d178437af66..0de7a209abd 100644 --- 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 @@ -188,6 +188,7 @@ public class HttpClientTest extends AbstractHttpClientServerTest { final String paramName = "a"; final String paramValue = "\u20AC"; + final String encodedParamValue = URLEncoder.encode(paramValue, "UTF-8"); start(new AbstractHandler() { @Override @@ -204,7 +205,7 @@ public class HttpClientTest extends AbstractHttpClientServerTest } }); - URI uri = URI.create(scheme + "://localhost:" + connector.getLocalPort() + "/path?" + paramName + "=" + paramValue); + URI uri = URI.create(scheme + "://localhost:" + connector.getLocalPort() + "/path?" + paramName + "=" + encodedParamValue); ContentResponse response = client.newRequest(uri) .method(HttpMethod.PUT) .timeout(5, TimeUnit.SECONDS) 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 24936b26757..b5e2dcc2439 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 @@ -339,23 +339,20 @@ public class BufferUtil * @param from Buffer to take bytes from in flush mode * @param to Buffer to put bytes to in flush mode. The buffer is flipToFill before the put and flipToFlush after. * @return number of bytes moved + * @deprecated use {@link #append(ByteBuffer, ByteBuffer)} */ public static int flipPutFlip(ByteBuffer from, ByteBuffer to) { - int pos = flipToFill(to); - try - { - return put(from, to); - } - finally - { - flipToFlush(to, pos); - } + return append(to,from); } /* ------------------------------------------------------------ */ /** Append bytes to a buffer. - * + * @param to Buffer is flush mode + * @param b bytes to append + * @param off offset into byte + * @param len length to append + * @throws BufferOverflowException */ public static void append(ByteBuffer to, byte[] b, int off, int len) throws BufferOverflowException { @@ -372,6 +369,8 @@ public class BufferUtil /* ------------------------------------------------------------ */ /** Appends a byte to a buffer + * @param to Buffer is flush mode + * @param b byte to append */ public static void append(ByteBuffer to, byte b) { @@ -386,9 +385,31 @@ public class BufferUtil } } + /* ------------------------------------------------------------ */ + /** Appends a byte to a buffer + * @param to Buffer is flush mode + * @param b bytes to append + */ + public static int append(ByteBuffer to, ByteBuffer b) + { + int pos = flipToFill(to); + try + { + return put(b, to); + } + finally + { + flipToFlush(to, pos); + } + } + /* ------------------------------------------------------------ */ /** * Like append, but does not throw {@link BufferOverflowException} + * @param to Buffer is flush mode + * @param b bytes to fill + * @param off offset into byte + * @param len length to fill */ public static int fill(ByteBuffer to, byte[] b, int off, int len) { diff --git a/jetty-util/src/main/java/org/eclipse/jetty/util/StringUtil.java b/jetty-util/src/main/java/org/eclipse/jetty/util/StringUtil.java index a02bfe631a7..55868ad5edd 100644 --- a/jetty-util/src/main/java/org/eclipse/jetty/util/StringUtil.java +++ b/jetty-util/src/main/java/org/eclipse/jetty/util/StringUtil.java @@ -719,4 +719,17 @@ public class StringUtil return str.substring(0,maxSize); } + public static String[] arrayFromString(String s) + { + if (s==null) + return new String[]{}; + + if (!s.startsWith("[") || !s.endsWith("]")) + throw new IllegalArgumentException(); + if (s.length()==2) + return new String[]{}; + + return s.substring(1,s.length()-1).split(" *, *"); + } + } diff --git a/jetty-util/src/main/java/org/eclipse/jetty/util/TypeUtil.java b/jetty-util/src/main/java/org/eclipse/jetty/util/TypeUtil.java index 12af538491d..b96a520c110 100644 --- a/jetty-util/src/main/java/org/eclipse/jetty/util/TypeUtil.java +++ b/jetty-util/src/main/java/org/eclipse/jetty/util/TypeUtil.java @@ -616,4 +616,32 @@ public class TypeUtil } throw new NoSuchMethodException(""); } + + /* ------------------------------------------------------------ */ + /** + * @param o Object to test for true + * @return True if passed object is not null and is either a Boolean with value true or evaluates to a string that evaluates to true. + */ + public static boolean isTrue(Object o) + { + if (o==null) + return false; + if (o instanceof Boolean) + return ((Boolean)o).booleanValue(); + return Boolean.parseBoolean(o.toString()); + } + + /* ------------------------------------------------------------ */ + /** + * @param o Object to test for false + * @return True if passed object is not null and is either a Boolean with value false or evaluates to a string that evaluates to false. + */ + public static boolean isFalse(Object o) + { + if (o==null) + return false; + if (o instanceof Boolean) + return !((Boolean)o).booleanValue(); + return "false".equalsIgnoreCase(o.toString()); + } } diff --git a/jetty-util/src/main/java/org/eclipse/jetty/util/component/AbstractLifeCycle.java b/jetty-util/src/main/java/org/eclipse/jetty/util/component/AbstractLifeCycle.java index c9c6a623918..8f2d9dc7ab0 100644 --- a/jetty-util/src/main/java/org/eclipse/jetty/util/component/AbstractLifeCycle.java +++ b/jetty-util/src/main/java/org/eclipse/jetty/util/component/AbstractLifeCycle.java @@ -18,6 +18,7 @@ package org.eclipse.jetty.util.component; +import java.lang.management.ManagementFactory; import java.util.concurrent.CopyOnWriteArrayList; import org.eclipse.jetty.util.annotation.ManagedAttribute; @@ -27,14 +28,12 @@ import org.eclipse.jetty.util.log.Logger; /** * Basic implementation of the life cycle interface for components. - * - * */ @ManagedObject("Abstract Implementation of LifeCycle") public abstract class AbstractLifeCycle implements LifeCycle { private static final Logger LOG = Log.getLogger(AbstractLifeCycle.class); - + public static final String STOPPED="STOPPED"; public static final String FAILED="FAILED"; public static final String STARTING="STARTING"; @@ -174,7 +173,9 @@ public abstract class AbstractLifeCycle implements LifeCycle private void setStarted() { _state = __STARTED; - LOG.debug(STARTED+" {}",this); + if (LOG.isDebugEnabled()) + + LOG.debug(STARTED+" @{}ms {}",ManagementFactory.getRuntimeMXBean().getUptime(),this); for (Listener listener : _listeners) listener.lifeCycleStarted(this); } diff --git a/jetty-util/src/main/java/org/eclipse/jetty/util/log/Log.java b/jetty-util/src/main/java/org/eclipse/jetty/util/log/Log.java index a9c37d26d9c..55966d7ab0e 100644 --- a/jetty-util/src/main/java/org/eclipse/jetty/util/log/Log.java +++ b/jetty-util/src/main/java/org/eclipse/jetty/util/log/Log.java @@ -20,6 +20,7 @@ package org.eclipse.jetty.util.log; import java.io.IOException; import java.io.InputStream; +import java.lang.management.ManagementFactory; import java.lang.reflect.Method; import java.net.URL; import java.security.AccessController; @@ -178,6 +179,9 @@ public class Log // Unable to load specified Logger implementation, default to standard logging. initStandardLogging(e); } + + if (LOG!=null) + LOG.info(String.format("Logging initialized @%dms",ManagementFactory.getRuntimeMXBean().getUptime())); return LOG != null; } 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 6d3cca1ef8c..28c003b8538 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 @@ -120,6 +120,12 @@ public class Constraint implements Cloneable, Serializable _name = name; } + /* ------------------------------------------------------------ */ + public String getName() + { + return _name; + } + /* ------------------------------------------------------------ */ public void setRoles(String[] roles) { diff --git a/jetty-util/src/test/java/org/eclipse/jetty/util/BufferUtilTest.java b/jetty-util/src/test/java/org/eclipse/jetty/util/BufferUtilTest.java index 4aa1de0f37c..042d6b28b41 100644 --- a/jetty-util/src/test/java/org/eclipse/jetty/util/BufferUtilTest.java +++ b/jetty-util/src/test/java/org/eclipse/jetty/util/BufferUtilTest.java @@ -141,14 +141,14 @@ public class BufferUtilTest ByteBuffer from=BufferUtil.toBuffer("12345"); BufferUtil.clear(to); - assertEquals(5,BufferUtil.flipPutFlip(from,to)); + assertEquals(5,BufferUtil.append(to,from)); assertTrue(BufferUtil.isEmpty(from)); assertEquals("12345",BufferUtil.toString(to)); from=BufferUtil.toBuffer("XX67890ZZ"); from.position(2); - assertEquals(5,BufferUtil.flipPutFlip(from,to)); + assertEquals(5,BufferUtil.append(to,from)); assertEquals(2,from.remaining()); assertEquals("1234567890",BufferUtil.toString(to)); } @@ -183,14 +183,14 @@ public class BufferUtilTest ByteBuffer from=BufferUtil.toBuffer("12345"); BufferUtil.clear(to); - assertEquals(5,BufferUtil.flipPutFlip(from,to)); + assertEquals(5,BufferUtil.append(to,from)); assertTrue(BufferUtil.isEmpty(from)); assertEquals("12345",BufferUtil.toString(to)); from=BufferUtil.toBuffer("XX67890ZZ"); from.position(2); - assertEquals(5,BufferUtil.flipPutFlip(from,to)); + assertEquals(5,BufferUtil.append(to,from)); assertEquals(2,from.remaining()); assertEquals("1234567890",BufferUtil.toString(to)); } diff --git a/jetty-util/src/test/java/org/eclipse/jetty/util/TypeUtilTest.java b/jetty-util/src/test/java/org/eclipse/jetty/util/TypeUtilTest.java index e7defaee75c..ba2121a06b4 100644 --- a/jetty-util/src/test/java/org/eclipse/jetty/util/TypeUtilTest.java +++ b/jetty-util/src/test/java/org/eclipse/jetty/util/TypeUtilTest.java @@ -18,8 +18,8 @@ package org.eclipse.jetty.util; -import junit.framework.Assert; +import org.junit.Assert; import org.junit.Test; public class TypeUtilTest @@ -90,4 +90,33 @@ public class TypeUtilTest Assert.assertEquals("123456789ABCDEF0",b.toString()); } + @Test + public void testIsTrue() throws Exception + { + Assert.assertTrue(TypeUtil.isTrue(Boolean.TRUE)); + Assert.assertTrue(TypeUtil.isTrue(true)); + Assert.assertTrue(TypeUtil.isTrue("true")); + Assert.assertTrue(TypeUtil.isTrue(new Object(){@Override public String toString(){return "true";}})); + + Assert.assertFalse(TypeUtil.isTrue(Boolean.FALSE)); + Assert.assertFalse(TypeUtil.isTrue(false)); + Assert.assertFalse(TypeUtil.isTrue("false")); + Assert.assertFalse(TypeUtil.isTrue("blargle")); + Assert.assertFalse(TypeUtil.isTrue(new Object(){@Override public String toString(){return "false";}})); + } + + @Test + public void testIsFalse() throws Exception + { + Assert.assertTrue(TypeUtil.isFalse(Boolean.FALSE)); + Assert.assertTrue(TypeUtil.isFalse(false)); + Assert.assertTrue(TypeUtil.isFalse("false")); + Assert.assertTrue(TypeUtil.isFalse(new Object(){@Override public String toString(){return "false";}})); + + Assert.assertFalse(TypeUtil.isFalse(Boolean.TRUE)); + Assert.assertFalse(TypeUtil.isFalse(true)); + Assert.assertFalse(TypeUtil.isFalse("true")); + Assert.assertFalse(TypeUtil.isFalse("blargle")); + Assert.assertFalse(TypeUtil.isFalse(new Object(){@Override public String toString(){return "true";}})); + } } diff --git a/jetty-webapp/src/main/java/org/eclipse/jetty/webapp/Descriptor.java b/jetty-webapp/src/main/java/org/eclipse/jetty/webapp/Descriptor.java index 9fe18a2b87b..413f68892a3 100644 --- a/jetty-webapp/src/main/java/org/eclipse/jetty/webapp/Descriptor.java +++ b/jetty-webapp/src/main/java/org/eclipse/jetty/webapp/Descriptor.java @@ -20,6 +20,7 @@ package org.eclipse.jetty.webapp; import java.net.URL; +import org.eclipse.jetty.util.log.Log; import org.eclipse.jetty.util.resource.Resource; import org.eclipse.jetty.xml.XmlParser; @@ -43,7 +44,8 @@ public abstract class Descriptor protected void redirect(XmlParser parser, String resource, URL source) { - if (source != null) parser.redirectEntity(resource, source); + if (source != null) + parser.redirectEntity(resource, source); } diff --git a/jetty-webapp/src/main/java/org/eclipse/jetty/webapp/MetaData.java b/jetty-webapp/src/main/java/org/eclipse/jetty/webapp/MetaData.java index 237f0685b8b..2d32f033e22 100644 --- a/jetty-webapp/src/main/java/org/eclipse/jetty/webapp/MetaData.java +++ b/jetty-webapp/src/main/java/org/eclipse/jetty/webapp/MetaData.java @@ -18,6 +18,7 @@ package org.eclipse.jetty.webapp; +import java.lang.annotation.Annotation; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; @@ -69,14 +70,27 @@ public class MetaData public static class OriginInfo { - protected String name; - protected Origin origin; - protected Descriptor descriptor; + private final String name; + private final Origin origin; + private final Descriptor descriptor; + private final Annotation annotation; + private final Class annotated; + public OriginInfo (String n, Annotation a,Class ac) + { + name=n; + origin=Origin.Annotation; + descriptor=null; + annotation=a; + annotated=ac; + } + public OriginInfo (String n, Descriptor d) { name = n; descriptor = d; + annotation=null; + annotated=null; if (d == null) throw new IllegalArgumentException("No descriptor"); if (d instanceof FragmentDescriptor) @@ -89,16 +103,13 @@ public class MetaData origin = Origin.WebXml; } - public OriginInfo (String n) + public OriginInfo(String n) { name = n; - origin = Origin.Annotation; - } - - public OriginInfo(String n, Origin o) - { - name = n; - origin = o; + origin = Origin.API; + annotation=null; + descriptor=null; + annotated=null; } public String getName() @@ -115,6 +126,15 @@ public class MetaData { return descriptor; } + + public String toString() + { + if (descriptor!=null) + return descriptor.toString(); + if (annotation!=null) + return "@"+annotation.annotationType().getSimpleName()+" on "+annotated.getName(); + return origin.toString(); + } } public MetaData () @@ -172,8 +192,6 @@ public class MetaData _webXmlRoot.parse(); _metaDataComplete=_webXmlRoot.getMetaDataComplete() == MetaDataComplete.True; - - if (_webXmlRoot.isOrdered()) { if (_ordering == null) @@ -526,6 +544,14 @@ public class MetaData return x.getOriginType(); } + public OriginInfo getOriginInfo (String name) + { + OriginInfo x = _origins.get(name); + if (x == null) + return null; + + return x; + } public Descriptor getOriginDescriptor (String name) { @@ -541,21 +567,21 @@ public class MetaData _origins.put(name, x); } - public void setOrigin (String name) + public void setOrigin (String name, Annotation annotation, Class annotated) { if (name == null) return; - OriginInfo x = new OriginInfo (name, Origin.Annotation); + OriginInfo x = new OriginInfo (name, annotation, annotated); _origins.put(name, x); } - public void setOrigin(String name, Origin origin) + public void setOriginAPI(String name) { if (name == null) return; - OriginInfo x = new OriginInfo (name, origin); + OriginInfo x = new OriginInfo (name); _origins.put(name, x); } @@ -604,4 +630,9 @@ public class MetaData { this.allowDuplicateFragmentNames = allowDuplicateFragmentNames; } + + public Map getOrigins() + { + return Collections.unmodifiableMap(_origins); + } } 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 f938cd3cf8b..15d94535121 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 @@ -54,6 +54,7 @@ import org.eclipse.jetty.util.log.Log; import org.eclipse.jetty.util.log.Logger; import org.eclipse.jetty.util.security.Constraint; import org.eclipse.jetty.xml.XmlParser; +import org.eclipse.jetty.xml.XmlParser.Node; /** * StandardDescriptorProcessor @@ -157,6 +158,8 @@ public class StandardDescriptorProcessor extends IterativeDescriptorProcessor } break; } + default: + LOG.warn(new Throwable()); // TODO throw ISE? } if (LOG.isDebugEnabled()) LOG.debug("ContextParam: " + name + "=" + value); @@ -244,18 +247,17 @@ public class StandardDescriptorProcessor extends IterativeDescriptorProcessor throw new IllegalStateException("Mismatching init-param "+pname+"="+pvalue+" in "+descriptor.getResource()); break; } + default: + LOG.warn(new Throwable()); // TODO throw ISE? } } String servlet_class = node.getString("servlet-class", false, true); - // Handle JSP - String jspServletClass=null; //Handle the default jsp servlet instance if (id != null && id.equals("jsp")) { - jspServletClass = servlet_class; try { Loader.loadClass(this.getClass(), servlet_class); @@ -272,7 +274,7 @@ public class StandardDescriptorProcessor extends IterativeDescriptorProcessor catch (ClassNotFoundException e) { LOG.info("NO JSP Support for {}, did not find {}", context.getContextPath(), servlet_class); - jspServletClass = servlet_class = "org.eclipse.jetty.servlet.NoJspServlet"; + servlet_class = "org.eclipse.jetty.servlet.NoJspServlet"; } } @@ -309,6 +311,8 @@ public class StandardDescriptorProcessor extends IterativeDescriptorProcessor throw new IllegalStateException("Conflicting servlet-class "+servlet_class+" in "+descriptor.getResource()); break; } + default: + LOG.warn(new Throwable()); // TODO throw ISE? } } @@ -374,10 +378,12 @@ public class StandardDescriptorProcessor extends IterativeDescriptorProcessor throw new IllegalStateException("Conflicting load-on-startup value in "+descriptor.getResource()); break; } + default: + LOG.warn(new Throwable()); // TODO throw ISE? } } - Iterator sRefsIter = node.iterator("security-role-ref"); + Iterator sRefsIter = node.iterator("security-role-ref"); while (sRefsIter.hasNext()) { XmlParser.Node securityRef = (XmlParser.Node) sRefsIter.next(); @@ -413,6 +419,8 @@ public class StandardDescriptorProcessor extends IterativeDescriptorProcessor throw new IllegalStateException("Conflicting role-link for role-name "+roleName+" for servlet "+servlet_name+" in "+descriptor.getResource()); break; } + default: + LOG.warn(new Throwable()); // TODO throw ISE? } } else @@ -457,6 +465,8 @@ public class StandardDescriptorProcessor extends IterativeDescriptorProcessor throw new IllegalStateException("Conflicting run-as role "+roleName+" for servlet "+servlet_name+" in "+descriptor.getResource()); break; } + default: + LOG.warn(new Throwable()); // TODO throw ISE? } } } @@ -493,6 +503,8 @@ public class StandardDescriptorProcessor extends IterativeDescriptorProcessor throw new IllegalStateException("Conflicting async-supported="+async+" for servlet "+servlet_name+" in "+descriptor.getResource()); break; } + default: + LOG.warn(new Throwable()); // TODO throw ISE? } } @@ -528,6 +540,8 @@ public class StandardDescriptorProcessor extends IterativeDescriptorProcessor throw new IllegalStateException("Conflicting value of servlet enabled for servlet "+servlet_name+" in "+descriptor.getResource()); break; } + default: + LOG.warn(new Throwable()); // TODO throw ISE? } } @@ -585,6 +599,8 @@ public class StandardDescriptorProcessor extends IterativeDescriptorProcessor throw new IllegalStateException("Conflicting multipart-config location for servlet "+servlet_name+" in "+descriptor.getResource()); break; } + default: + LOG.warn(new Throwable()); // TODO throw ISE? } } } @@ -632,6 +648,8 @@ public class StandardDescriptorProcessor extends IterativeDescriptorProcessor addServletMapping(servlet_name, node, context, descriptor); break; } + default: + LOG.warn(new Throwable()); // TODO throw ISE? } } @@ -653,7 +671,7 @@ public class StandardDescriptorProcessor extends IterativeDescriptorProcessor //Servlet Spec 3.0 // // this is additive across web-fragments - Iterator iter = node.iterator("tracking-mode"); + Iterator iter = node.iterator("tracking-mode"); if (iter.hasNext()) { Set modes = null; @@ -679,7 +697,9 @@ public class StandardDescriptorProcessor extends IterativeDescriptorProcessor modes = new HashSet(context.getSessionHandler().getSessionManager().getEffectiveSessionTrackingModes()); context.getMetaData().setOrigin("session.tracking-mode", descriptor); break; - } + } + default: + LOG.warn(new Throwable()); // TODO throw ISE? } while (iter.hasNext()) @@ -729,6 +749,8 @@ public class StandardDescriptorProcessor extends IterativeDescriptorProcessor throw new IllegalStateException("Conflicting cookie-config name "+name+" in "+descriptor.getResource()); break; } + default: + LOG.warn(new Throwable()); // TODO throw ISE? } } @@ -764,6 +786,8 @@ public class StandardDescriptorProcessor extends IterativeDescriptorProcessor throw new IllegalStateException("Conflicting cookie-config domain "+domain+" in "+descriptor.getResource()); break; } + default: + LOG.warn(new Throwable()); // TODO throw ISE? } } @@ -799,6 +823,8 @@ public class StandardDescriptorProcessor extends IterativeDescriptorProcessor throw new IllegalStateException("Conflicting cookie-config path "+path+" in "+descriptor.getResource()); break; } + default: + LOG.warn(new Throwable()); // TODO throw ISE? } } @@ -834,6 +860,8 @@ public class StandardDescriptorProcessor extends IterativeDescriptorProcessor throw new IllegalStateException("Conflicting cookie-config comment "+comment+" in "+descriptor.getResource()); break; } + default: + LOG.warn(new Throwable()); // TODO throw ISE? } } @@ -870,6 +898,8 @@ public class StandardDescriptorProcessor extends IterativeDescriptorProcessor throw new IllegalStateException("Conflicting cookie-config http-only "+httpOnly+" in "+descriptor.getResource()); break; } + default: + LOG.warn(new Throwable()); // TODO throw ISE? } } @@ -906,6 +936,8 @@ public class StandardDescriptorProcessor extends IterativeDescriptorProcessor throw new IllegalStateException("Conflicting cookie-config secure "+secure+" in "+descriptor.getResource()); break; } + default: + LOG.warn(new Throwable()); // TODO throw ISE? } } @@ -942,6 +974,8 @@ public class StandardDescriptorProcessor extends IterativeDescriptorProcessor throw new IllegalStateException("Conflicting cookie-config max-age "+maxAge+" in "+descriptor.getResource()); break; } + default: + LOG.warn(new Throwable()); // TODO throw ISE? } } } @@ -990,6 +1024,8 @@ public class StandardDescriptorProcessor extends IterativeDescriptorProcessor throw new IllegalStateException("Conflicting mime-type "+mimeType+" for extension "+extension+" in "+descriptor.getResource()); break; } + default: + LOG.warn(new Throwable()); // TODO throw ISE? } } } @@ -1038,6 +1074,8 @@ public class StandardDescriptorProcessor extends IterativeDescriptorProcessor addWelcomeFiles(context,node); break; } + default: + LOG.warn(new Throwable()); // TODO throw ISE? } } @@ -1085,6 +1123,8 @@ public class StandardDescriptorProcessor extends IterativeDescriptorProcessor throw new IllegalStateException("Conflicting loacle-encoding mapping for locale "+locale+" in "+descriptor.getResource()); break; } + default: + LOG.warn(new Throwable()); // TODO throw ISE? } } } @@ -1150,6 +1190,8 @@ public class StandardDescriptorProcessor extends IterativeDescriptorProcessor throw new IllegalStateException("Conflicting error-code or exception-type "+error+" in "+descriptor.getResource()); break; } + default: + LOG.warn(new Throwable()); // TODO throw ISE? } } @@ -1422,18 +1464,18 @@ public class StandardDescriptorProcessor extends IterativeDescriptorProcessor //remember origin so we can process ServletRegistration.Dynamic.setServletSecurityElement() correctly context.getMetaData().setOrigin("constraint.url."+url, descriptor); - Iterator iter3 = collection.iterator("http-method"); - Iterator iter4 = collection.iterator("http-method-omission"); + Iterator methods = collection.iterator("http-method"); + Iterator ommissions = collection.iterator("http-method-omission"); - if (iter3.hasNext()) + if (methods.hasNext()) { - if (iter4.hasNext()) + if (ommissions.hasNext()) throw new IllegalStateException ("web-resource-collection cannot contain both http-method and http-method-omission"); //configure all the http-method elements for each url - while (iter3.hasNext()) + while (methods.hasNext()) { - String method = ((XmlParser.Node) iter3.next()).toString(false, true); + String method = ((XmlParser.Node) methods.next()).toString(false, true); ConstraintMapping mapping = new ConstraintMapping(); mapping.setMethod(method); mapping.setPathSpec(url); @@ -1441,12 +1483,13 @@ public class StandardDescriptorProcessor extends IterativeDescriptorProcessor ((ConstraintAware)context.getSecurityHandler()).addConstraintMapping(mapping); } } - else if (iter4.hasNext()) + else if (ommissions.hasNext()) { //configure all the http-method-omission elements for each url - while (iter4.hasNext()) + // TODO use the array + while (ommissions.hasNext()) { - String method = ((XmlParser.Node)iter4.next()).toString(false, true); + String method = ((XmlParser.Node)ommissions.next()).toString(false, true); ConstraintMapping mapping = new ConstraintMapping(); mapping.setMethodOmissions(new String[]{method}); mapping.setPathSpec(url); @@ -1514,6 +1557,8 @@ public class StandardDescriptorProcessor extends IterativeDescriptorProcessor throw new IllegalStateException("Conflicting auth-method value in "+descriptor.getResource()); break; } + default: + LOG.warn(new Throwable()); // TODO throw ISE? } //handle realm-name merge @@ -1547,9 +1592,11 @@ public class StandardDescriptorProcessor extends IterativeDescriptorProcessor throw new IllegalStateException("Conflicting realm-name value in "+descriptor.getResource()); break; } + default: + LOG.warn(new Throwable()); // TODO throw ISE? } - if (Constraint.__FORM_AUTH.equals(context.getSecurityHandler().getAuthMethod())) + if (Constraint.__FORM_AUTH.equalsIgnoreCase(context.getSecurityHandler().getAuthMethod())) { XmlParser.Node formConfig = node.get("form-login-config"); if (formConfig != null) @@ -1592,6 +1639,8 @@ public class StandardDescriptorProcessor extends IterativeDescriptorProcessor throw new IllegalStateException("Conflicting form-login-page value in "+descriptor.getResource()); break; } + default: + LOG.warn(new Throwable()); // TODO throw ISE? } //handle form-error-page @@ -1623,6 +1672,8 @@ public class StandardDescriptorProcessor extends IterativeDescriptorProcessor throw new IllegalStateException("Conflicting form-error-page value in "+descriptor.getResource()); break; } + default: + LOG.warn(new Throwable()); // TODO throw ISE? } } else @@ -1696,6 +1747,8 @@ public class StandardDescriptorProcessor extends IterativeDescriptorProcessor throw new IllegalStateException("Conflicting filter-class for filter "+name+" in "+descriptor.getResource()); break; } + default: + LOG.warn(new Throwable()); // TODO throw ISE? } } @@ -1735,6 +1788,8 @@ public class StandardDescriptorProcessor extends IterativeDescriptorProcessor throw new IllegalStateException("Mismatching init-param "+pname+"="+pvalue+" in "+descriptor.getResource()); break; } + default: + LOG.warn(new Throwable()); // TODO throw ISE? } } @@ -1772,6 +1827,8 @@ public class StandardDescriptorProcessor extends IterativeDescriptorProcessor throw new IllegalStateException("Conflicting async-supported="+async+" for filter "+name+" in "+descriptor.getResource()); break; } + default: + LOG.warn(new Throwable()); // TODO throw ISE? } } } @@ -1814,6 +1871,8 @@ public class StandardDescriptorProcessor extends IterativeDescriptorProcessor addFilterMapping(filter_name, node, context, descriptor); break; } + default: + LOG.warn(new Throwable()); // TODO throw ISE? } } 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 eff5a74194d..2a54dcc90e1 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 @@ -1386,7 +1386,7 @@ public class WebAppContext extends ServletContextHandler implements WebAppClassL for (ConstraintMapping m:mappings) ((ConstraintAware)getSecurityHandler()).addConstraintMapping(m); ((ConstraintAware)getSecurityHandler()).checkPathsWithUncoveredHttpMethods(); - getMetaData().setOrigin("constraint.url."+pathSpec, Origin.API); + getMetaData().setOriginAPI("constraint.url."+pathSpec); break; } case WebXml: diff --git a/jetty-webapp/src/main/java/org/eclipse/jetty/webapp/WebDescriptor.java b/jetty-webapp/src/main/java/org/eclipse/jetty/webapp/WebDescriptor.java index 2e69d043e08..cef576a7716 100644 --- a/jetty-webapp/src/main/java/org/eclipse/jetty/webapp/WebDescriptor.java +++ b/jetty-webapp/src/main/java/org/eclipse/jetty/webapp/WebDescriptor.java @@ -30,6 +30,7 @@ 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.xml.XmlParser; +import org.xml.sax.InputSource; @@ -63,108 +64,130 @@ public class WebDescriptor extends Descriptor _parser = _parserSingleton; } - public XmlParser newParser() throws ClassNotFoundException { - XmlParser xmlParser=new XmlParser(); - //set up cache of DTDs and schemas locally - URL dtd22=Loader.getResource(Servlet.class,"javax/servlet/resources/web-app_2_2.dtd"); - URL dtd23=Loader.getResource(Servlet.class,"javax/servlet/resources/web-app_2_3.dtd"); - URL j2ee14xsd=Loader.getResource(Servlet.class,"javax/servlet/resources/j2ee_1_4.xsd"); - URL javaee5=Loader.getResource(Servlet.class,"javax/servlet/resources/javaee_5.xsd"); - URL javaee6=Loader.getResource(Servlet.class,"javax/servlet/resources/javaee_6.xsd"); - URL javaee7=Loader.getResource(Servlet.class,"javax/servlet/resources/javaee_7.xsd"); - - URL webapp24xsd=Loader.getResource(Servlet.class,"javax/servlet/resources/web-app_2_4.xsd"); - URL webapp25xsd=Loader.getResource(Servlet.class,"javax/servlet/resources/web-app_2_5.xsd"); - URL webapp30xsd=Loader.getResource(Servlet.class,"javax/servlet/resources/web-app_3_0.xsd"); - URL webcommon30xsd=Loader.getResource(Servlet.class,"javax/servlet/resources/web-common_3_0.xsd"); - URL webfragment30xsd=Loader.getResource(Servlet.class,"javax/servlet/resources/web-fragment_3_0.xsd"); - URL webapp31xsd=Loader.getResource(Servlet.class, "javax/servlet/resources/web-app_3_1.xsd"); - URL webcommon31xsd=Loader.getResource(Servlet.class, "javax/servlet/resources/web-common_3_1.xsd"); - URL webfragment31xsd=Loader.getResource(Servlet.class, "javax/servlet/resources/web-fragment_3_1.xsd"); - URL schemadtd=Loader.getResource(Servlet.class,"javax/servlet/resources/XMLSchema.dtd"); - URL xmlxsd=Loader.getResource(Servlet.class,"javax/servlet/resources/xml.xsd"); - URL webservice11xsd=Loader.getResource(Servlet.class,"javax/servlet/resources/j2ee_web_services_client_1_1.xsd"); - URL webservice12xsd=Loader.getResource(Servlet.class,"javax/servlet/resources/javaee_web_services_client_1_2.xsd"); - URL webservice13xsd=Loader.getResource(Servlet.class,"javax/servlet/resources/javaee_web_services_client_1_3.xsd"); - URL webservice14xsd=Loader.getResource(Servlet.class,"javax/servlet/resources/javaee_web_services_client_1_4.xsd"); - URL datatypesdtd=Loader.getResource(Servlet.class,"javax/servlet/resources/datatypes.dtd"); - - URL jsp20xsd = null; - URL jsp21xsd = null; - URL jsp22xsd = null; - URL jsp23xsd = null; - - try + XmlParser xmlParser=new XmlParser() { - //try both javax/servlet/resources and javax/servlet/jsp/resources to load - jsp20xsd = Loader.getResource(Servlet.class, "javax/servlet/resources/jsp_2_0.xsd"); - jsp21xsd = Loader.getResource(Servlet.class, "javax/servlet/resources/jsp_2_1.xsd"); - jsp22xsd = Loader.getResource(Servlet.class, "javax/servlet/resources/jsp_2_2.xsd"); - jsp23xsd = Loader.getResource(Servlet.class, "javax/servlet/resources/jsp_2_3.xsd"); - } - catch (Exception e) - { - LOG.ignore(e); - } - finally - { - if (jsp20xsd == null) jsp20xsd = Loader.getResource(Servlet.class, "javax/servlet/jsp/resources/jsp_2_0.xsd"); - if (jsp21xsd == null) jsp21xsd = Loader.getResource(Servlet.class, "javax/servlet/jsp/resources/jsp_2_1.xsd"); - if (jsp22xsd == null) jsp22xsd = Loader.getResource(Servlet.class, "javax/servlet/jsp/resources/jsp_2_2.xsd"); - if (jsp23xsd == null) jsp23xsd = Loader.getResource(Servlet.class, "javax/servlet/jsp/resources/jsp_2_3.xsd"); - } + boolean mapped=false; + + @Override + protected InputSource resolveEntity(String pid, String sid) + { + if (!mapped) + { + mapResources(); + mapped=true; + } + InputSource is = super.resolveEntity(pid,sid); + return is; + } + + void mapResources() + { + //set up cache of DTDs and schemas locally + URL dtd22=Loader.getResource(Servlet.class,"javax/servlet/resources/web-app_2_2.dtd"); + URL dtd23=Loader.getResource(Servlet.class,"javax/servlet/resources/web-app_2_3.dtd"); + URL j2ee14xsd=Loader.getResource(Servlet.class,"javax/servlet/resources/j2ee_1_4.xsd"); + URL javaee5=Loader.getResource(Servlet.class,"javax/servlet/resources/javaee_5.xsd"); + URL javaee6=Loader.getResource(Servlet.class,"javax/servlet/resources/javaee_6.xsd"); + URL javaee7=Loader.getResource(Servlet.class,"javax/servlet/resources/javaee_7.xsd"); - redirect(xmlParser,"web-app_2_2.dtd",dtd22); - redirect(xmlParser,"-//Sun Microsystems, Inc.//DTD Web Application 2.2//EN",dtd22); - redirect(xmlParser,"web.dtd",dtd23); - redirect(xmlParser,"web-app_2_3.dtd",dtd23); - redirect(xmlParser,"-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN",dtd23); - redirect(xmlParser,"XMLSchema.dtd",schemadtd); - redirect(xmlParser,"http://www.w3.org/2001/XMLSchema.dtd",schemadtd); - redirect(xmlParser,"-//W3C//DTD XMLSCHEMA 200102//EN",schemadtd); - redirect(xmlParser,"jsp_2_0.xsd",jsp20xsd); - redirect(xmlParser,"http://java.sun.com/xml/ns/j2ee/jsp_2_0.xsd",jsp20xsd); - redirect(xmlParser,"http://java.sun.com/xml/ns/javaee/jsp_2_1.xsd",jsp21xsd); - redirect(xmlParser,"jsp_2_2.xsd",jsp22xsd); - redirect(xmlParser,"http://java.sun.com/xml/ns/javaee/jsp_2_2.xsd",jsp22xsd); - redirect(xmlParser,"jsp_2_3.xsd",jsp23xsd); - redirect(xmlParser,"http://xmlns.jcp.org/xml/ns/javaee/jsp_2_3.xsd",jsp23xsd); - redirect(xmlParser,"j2ee_1_4.xsd",j2ee14xsd); - redirect(xmlParser,"http://java.sun.com/xml/ns/j2ee/j2ee_1_4.xsd",j2ee14xsd); - redirect(xmlParser, "http://java.sun.com/xml/ns/javaee/javaee_5.xsd",javaee5); - redirect(xmlParser, "http://java.sun.com/xml/ns/javaee/javaee_6.xsd",javaee6); - redirect(xmlParser, "http://xmlns.jcp.org/xml/ns/javaee/javaee_7.xsd",javaee7); - redirect(xmlParser,"web-app_2_4.xsd",webapp24xsd); - redirect(xmlParser,"http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd",webapp24xsd); - redirect(xmlParser,"web-app_2_5.xsd",webapp25xsd); - redirect(xmlParser,"http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd",webapp25xsd); - redirect(xmlParser,"web-app_3_0.xsd",webapp30xsd); - redirect(xmlParser,"http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd",webapp30xsd); - redirect(xmlParser,"web-common_3_0.xsd",webcommon30xsd); - redirect(xmlParser,"http://java.sun.com/xml/ns/javaee/web-common_3_0.xsd",webcommon30xsd); - redirect(xmlParser,"web-fragment_3_0.xsd",webfragment30xsd); - redirect(xmlParser,"http://java.sun.com/xml/ns/javaee/web-fragment_3_0.xsd",webfragment30xsd); - redirect(xmlParser,"web-app_3_1.xsd",webapp31xsd); - redirect(xmlParser,"http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd",webapp31xsd); - redirect(xmlParser,"web-common_3_1.xsd",webcommon30xsd); - redirect(xmlParser,"http://xmlns.jcp.org/xml/ns/javaee/web-common_3_1.xsd",webcommon31xsd); - redirect(xmlParser,"web-fragment_3_1.xsd",webfragment30xsd); - redirect(xmlParser,"http://xmlns.jcp.org/xml/ns/javaee/web-fragment_3_1.xsd",webfragment31xsd); - redirect(xmlParser,"xml.xsd",xmlxsd); - redirect(xmlParser,"http://www.w3.org/2001/xml.xsd",xmlxsd); - redirect(xmlParser,"datatypes.dtd",datatypesdtd); - redirect(xmlParser,"http://www.w3.org/2001/datatypes.dtd",datatypesdtd); - redirect(xmlParser,"j2ee_web_services_client_1_1.xsd",webservice11xsd); - redirect(xmlParser,"http://www.ibm.com/webservices/xsd/j2ee_web_services_client_1_1.xsd",webservice11xsd); - redirect(xmlParser,"javaee_web_services_client_1_2.xsd",webservice12xsd); - redirect(xmlParser,"http://www.ibm.com/webservices/xsd/javaee_web_services_client_1_2.xsd",webservice12xsd); - redirect(xmlParser,"javaee_web_services_client_1_3.xsd",webservice13xsd); - redirect(xmlParser,"http://java.sun.com/xml/ns/javaee/javaee_web_services_client_1_3.xsd",webservice13xsd); - redirect(xmlParser,"javaee_web_services_client_1_4.xsd",webservice14xsd); - redirect(xmlParser,"http://xmlns.jcp.org/xml/ns/javaee/javaee_web_services_client_1_4.xsd",webservice14xsd); + URL webapp24xsd=Loader.getResource(Servlet.class,"javax/servlet/resources/web-app_2_4.xsd"); + URL webapp25xsd=Loader.getResource(Servlet.class,"javax/servlet/resources/web-app_2_5.xsd"); + URL webapp30xsd=Loader.getResource(Servlet.class,"javax/servlet/resources/web-app_3_0.xsd"); + URL webapp31xsd=Loader.getResource(Servlet.class,"javax/servlet/resources/web-app_3_1.xsd"); + + URL webcommon30xsd=Loader.getResource(Servlet.class,"javax/servlet/resources/web-common_3_0.xsd"); + URL webcommon31xsd=Loader.getResource(Servlet.class,"javax/servlet/resources/web-common_3_1.xsd"); + + URL webfragment30xsd=Loader.getResource(Servlet.class,"javax/servlet/resources/web-fragment_3_0.xsd"); + URL webfragment31xsd=Loader.getResource(Servlet.class,"javax/servlet/resources/web-fragment_3_1.xsd"); + + URL schemadtd=Loader.getResource(Servlet.class,"javax/servlet/resources/XMLSchema.dtd"); + URL xmlxsd=Loader.getResource(Servlet.class,"javax/servlet/resources/xml.xsd"); + URL webservice11xsd=Loader.getResource(Servlet.class,"javax/servlet/resources/j2ee_web_services_client_1_1.xsd"); + URL webservice12xsd=Loader.getResource(Servlet.class,"javax/servlet/resources/javaee_web_services_client_1_2.xsd"); + URL webservice13xsd=Loader.getResource(Servlet.class,"javax/servlet/resources/javaee_web_services_client_1_3.xsd"); + URL webservice14xsd=Loader.getResource(Servlet.class,"javax/servlet/resources/javaee_web_services_client_1_4.xsd"); + URL datatypesdtd=Loader.getResource(Servlet.class,"javax/servlet/resources/datatypes.dtd"); + + URL jsp20xsd = null; + URL jsp21xsd = null; + URL jsp22xsd = null; + URL jsp23xsd = null; + + try + { + //try both javax/servlet/resources and javax/servlet/jsp/resources to load + jsp20xsd = Loader.getResource(Servlet.class, "javax/servlet/resources/jsp_2_0.xsd"); + jsp21xsd = Loader.getResource(Servlet.class, "javax/servlet/resources/jsp_2_1.xsd"); + jsp22xsd = Loader.getResource(Servlet.class, "javax/servlet/resources/jsp_2_2.xsd"); + jsp23xsd = Loader.getResource(Servlet.class, "javax/servlet/resources/jsp_2_3.xsd"); + } + catch (Exception e) + { + LOG.ignore(e); + } + finally + { + if (jsp20xsd == null) jsp20xsd = Loader.getResource(Servlet.class, "javax/servlet/jsp/resources/jsp_2_0.xsd"); + if (jsp21xsd == null) jsp21xsd = Loader.getResource(Servlet.class, "javax/servlet/jsp/resources/jsp_2_1.xsd"); + if (jsp22xsd == null) jsp22xsd = Loader.getResource(Servlet.class, "javax/servlet/jsp/resources/jsp_2_2.xsd"); + if (jsp23xsd == null) jsp23xsd = Loader.getResource(Servlet.class, "javax/servlet/jsp/resources/jsp_2_3.xsd"); + } + + redirect(this,"web-app_2_2.dtd",dtd22); + redirect(this,"-//Sun Microsystems, Inc.//DTD Web Application 2.2//EN",dtd22); + redirect(this,"web.dtd",dtd23); + redirect(this,"web-app_2_3.dtd",dtd23); + redirect(this,"-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN",dtd23); + redirect(this,"XMLSchema.dtd",schemadtd); + redirect(this,"http://www.w3.org/2001/XMLSchema.dtd",schemadtd); + redirect(this,"-//W3C//DTD XMLSCHEMA 200102//EN",schemadtd); + redirect(this,"jsp_2_0.xsd",jsp20xsd); + redirect(this,"http://java.sun.com/xml/ns/j2ee/jsp_2_0.xsd",jsp20xsd); + redirect(this,"http://java.sun.com/xml/ns/javaee/jsp_2_1.xsd",jsp21xsd); + redirect(this,"jsp_2_2.xsd",jsp22xsd); + redirect(this,"http://java.sun.com/xml/ns/javaee/jsp_2_2.xsd",jsp22xsd); + redirect(this,"jsp_2_3.xsd",jsp23xsd); + redirect(this,"http://xmlns.jcp.org/xml/ns/javaee/jsp_2_3.xsd",jsp23xsd); + redirect(this,"j2ee_1_4.xsd",j2ee14xsd); + redirect(this,"http://java.sun.com/xml/ns/j2ee/j2ee_1_4.xsd",j2ee14xsd); + redirect(this, "http://java.sun.com/xml/ns/javaee/javaee_5.xsd",javaee5); + redirect(this, "http://java.sun.com/xml/ns/javaee/javaee_6.xsd",javaee6); + redirect(this, "http://xmlns.jcp.org/xml/ns/javaee/javaee_7.xsd",javaee7); + redirect(this,"web-app_2_4.xsd",webapp24xsd); + redirect(this,"http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd",webapp24xsd); + redirect(this,"web-app_2_5.xsd",webapp25xsd); + redirect(this,"http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd",webapp25xsd); + redirect(this,"web-app_3_0.xsd",webapp30xsd); + redirect(this,"http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd",webapp30xsd); + redirect(this,"web-common_3_0.xsd",webcommon30xsd); + redirect(this,"http://java.sun.com/xml/ns/javaee/web-common_3_0.xsd",webcommon30xsd); + redirect(this,"web-fragment_3_0.xsd",webfragment30xsd); + redirect(this,"http://java.sun.com/xml/ns/javaee/web-fragment_3_0.xsd",webfragment30xsd); + redirect(this,"web-app_3_1.xsd",webapp31xsd); + redirect(this,"http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd",webapp31xsd); + + redirect(this,"web-common_3_1.xsd",webcommon30xsd); + redirect(this,"http://xmlns.jcp.org/xml/ns/javaee/web-common_3_1.xsd",webcommon31xsd); + redirect(this,"web-fragment_3_1.xsd",webfragment30xsd); + redirect(this,"http://xmlns.jcp.org/xml/ns/javaee/web-fragment_3_1.xsd",webfragment31xsd); + redirect(this,"xml.xsd",xmlxsd); + redirect(this,"http://www.w3.org/2001/xml.xsd",xmlxsd); + redirect(this,"datatypes.dtd",datatypesdtd); + redirect(this,"http://www.w3.org/2001/datatypes.dtd",datatypesdtd); + redirect(this,"j2ee_web_services_client_1_1.xsd",webservice11xsd); + redirect(this,"http://www.ibm.com/webservices/xsd/j2ee_web_services_client_1_1.xsd",webservice11xsd); + redirect(this,"javaee_web_services_client_1_2.xsd",webservice12xsd); + redirect(this,"http://www.ibm.com/webservices/xsd/javaee_web_services_client_1_2.xsd",webservice12xsd); + redirect(this,"javaee_web_services_client_1_3.xsd",webservice13xsd); + redirect(this,"http://java.sun.com/xml/ns/javaee/javaee_web_services_client_1_3.xsd",webservice13xsd); + redirect(this,"javaee_web_services_client_1_4.xsd",webservice14xsd); + redirect(this,"http://xmlns.jcp.org/xml/ns/javaee/javaee_web_services_client_1_4.xsd",webservice14xsd); + } + }; return xmlParser; } 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 index a1660158dd4..d35cda2e5c9 100644 --- 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 @@ -21,7 +21,6 @@ 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; @@ -30,6 +29,7 @@ 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.api.BatchMode; import org.eclipse.jetty.websocket.common.WebSocketRemoteEndpoint; import org.eclipse.jetty.websocket.common.io.FutureWriteCallback; import org.eclipse.jetty.websocket.common.message.MessageOutputStream; @@ -80,24 +80,31 @@ public abstract class AbstractJsrRemote implements RemoteEndpoint @Override public void flushBatch() throws IOException { - // TODO Auto-generated method stub + jettyRemote.flush(); } @Override public boolean getBatchingAllowed() { - // TODO Auto-generated method stub - return false; + return jettyRemote.getBatchMode() == BatchMode.ON; + } + + @Override + public void setBatchingAllowed(boolean allowed) throws IOException + { + if (jettyRemote.getBatchMode() == BatchMode.ON && !allowed) + jettyRemote.flush(); + jettyRemote.setBatchMode(allowed ? BatchMode.ON : BatchMode.OFF); } @SuppressWarnings( - { "rawtypes", "unchecked" }) + {"rawtypes", "unchecked"}) public Future sendObjectViaFuture(Object data) { assertMessageNotNull(data); if (LOG.isDebugEnabled()) { - LOG.debug("sendObject({})",data); + LOG.debug("sendObject({})", data); } Encoder encoder = encoders.getEncoderFor(data.getClass()); @@ -108,15 +115,15 @@ public abstract class AbstractJsrRemote implements RemoteEndpoint if (encoder instanceof Encoder.Text) { - Encoder.Text etxt = (Encoder.Text)encoder; + Encoder.Text text = (Encoder.Text)encoder; try { - String msg = etxt.encode(data); + String msg = text.encode(data); return jettyRemote.sendStringByFuture(msg); } catch (EncodeException e) { - return new EncodeFailedFuture(data,etxt,Encoder.Text.class,e); + return new EncodeFailedFuture(data, text, Encoder.Text.class, e); } } else if (encoder instanceof Encoder.TextStream) @@ -126,12 +133,12 @@ public abstract class AbstractJsrRemote implements RemoteEndpoint try (MessageWriter writer = new MessageWriter(session)) { writer.setCallback(callback); - etxt.encode(data,writer); + etxt.encode(data, writer); return callback; } catch (EncodeException | IOException e) { - return new EncodeFailedFuture(data,etxt,Encoder.Text.class,e); + return new EncodeFailedFuture(data, etxt, Encoder.Text.class, e); } } else if (encoder instanceof Encoder.Binary) @@ -144,7 +151,7 @@ public abstract class AbstractJsrRemote implements RemoteEndpoint } catch (EncodeException e) { - return new EncodeFailedFuture(data,ebin,Encoder.Binary.class,e); + return new EncodeFailedFuture(data, ebin, Encoder.Binary.class, e); } } else if (encoder instanceof Encoder.BinaryStream) @@ -154,12 +161,12 @@ public abstract class AbstractJsrRemote implements RemoteEndpoint try (MessageOutputStream out = new MessageOutputStream(session)) { out.setCallback(callback); - ebin.encode(data,out); + ebin.encode(data, out); return callback; } catch (EncodeException | IOException e) { - return new EncodeFailedFuture(data,ebin,Encoder.Binary.class,e); + return new EncodeFailedFuture(data, ebin, Encoder.Binary.class, e); } } @@ -171,7 +178,7 @@ public abstract class AbstractJsrRemote implements RemoteEndpoint { if (LOG.isDebugEnabled()) { - LOG.debug("sendPing({})",BufferUtil.toDetailString(data)); + LOG.debug("sendPing({})", BufferUtil.toDetailString(data)); } jettyRemote.sendPing(data); } @@ -181,14 +188,8 @@ public abstract class AbstractJsrRemote implements RemoteEndpoint { if (LOG.isDebugEnabled()) { - LOG.debug("sendPong({})",BufferUtil.toDetailString(data)); + 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/ClientContainer.java b/jetty-websocket/javax-websocket-client-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/ClientContainer.java index 4e847b5067e..aa395eb5762 100644 --- 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 @@ -99,7 +99,7 @@ public class ClientContainer extends ContainerLifeCycle implements WebSocketCont client = new WebSocketClient(executor); client.setEventDriverFactory(new JsrEventDriverFactory(client.getPolicy())); - client.setSessionFactory(new JsrSessionFactory(this,this)); + client.setSessionFactory(new JsrSessionFactory(this,this,client)); addBean(client); ShutdownThread.register(this); 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 index 02faf20a48c..b9a24f23316 100644 --- 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 @@ -29,7 +29,6 @@ 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; @@ -41,6 +40,7 @@ import javax.websocket.WebSocketContainer; import org.eclipse.jetty.util.log.Log; import org.eclipse.jetty.util.log.Logger; +import org.eclipse.jetty.websocket.api.BatchMode; import org.eclipse.jetty.websocket.api.extensions.ExtensionConfig; import org.eclipse.jetty.websocket.common.LogicalConnection; import org.eclipse.jetty.websocket.common.SessionListener; @@ -73,9 +73,9 @@ public class JsrSession extends WebSocketSession implements javax.websocket.Sess private JsrAsyncRemote asyncRemote; private JsrBasicRemote basicRemote; - public JsrSession(URI requestURI, EventDriver websocket, LogicalConnection connection, ClientContainer container, String id, SessionListener[] sessionListeners) + public JsrSession(URI requestURI, EventDriver websocket, LogicalConnection connection, ClientContainer container, String id, SessionListener... sessionListeners) { - super(requestURI,websocket,connection,sessionListeners); + super(requestURI, websocket, connection, sessionListeners); if (!(websocket instanceof AbstractJsrEventDriver)) { throw new IllegalArgumentException("Cannot use, not a JSR WebSocket: " + websocket); @@ -90,13 +90,12 @@ public class JsrSession extends WebSocketSession implements javax.websocket.Sess 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"); + Objects.requireNonNull(handler, "MessageHandler cannot be null"); synchronized (wrappers) { @@ -374,4 +373,11 @@ public class JsrSession extends WebSocketSession implements javax.websocket.Sess messageHandlerSet.add(wrapper.getHandler()); } } + + @Override + public BatchMode getBatchMode() + { + // JSR 356 specification mandates default batch mode to be off. + return BatchMode.OFF; + } } 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 index 7a1d66d7a79..a550c20b8b4 100644 --- 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 @@ -23,7 +23,6 @@ import java.io.InputStream; import java.io.Reader; import java.nio.ByteBuffer; import java.util.Map; - import javax.websocket.CloseReason; import javax.websocket.DecodeException; @@ -103,7 +102,7 @@ public class JsrAnnotatedEventDriver extends AbstractJsrEventDriver implements E if (activeMessage == null) { LOG.debug("Binary Message InputStream"); - final MessageInputStream stream = new MessageInputStream(session.getConnection()); + final MessageInputStream stream = new MessageInputStream(); activeMessage = stream; // Always dispatch streaming read to another thread. @@ -311,7 +310,7 @@ public class JsrAnnotatedEventDriver extends AbstractJsrEventDriver implements E { LOG.debug("Text Message Writer"); - final MessageReader stream = new MessageReader(new MessageInputStream(session.getConnection())); + final MessageReader stream = new MessageReader(new MessageInputStream()); activeMessage = stream; // Always dispatch streaming read to another thread. 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 index 29536c81a50..b977147fb91 100644 --- 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 @@ -23,7 +23,6 @@ import java.io.InputStream; import java.io.Reader; import java.nio.ByteBuffer; import java.util.Map; - import javax.websocket.CloseReason; import javax.websocket.Endpoint; import javax.websocket.MessageHandler; @@ -86,7 +85,7 @@ public class JsrEndpointEventDriver extends AbstractJsrEventDriver implements Ev } else if (wrapper.wantsStreams()) { - final MessageInputStream stream = new MessageInputStream(session.getConnection()); + final MessageInputStream stream = new MessageInputStream(); activeMessage = stream; dispatch(new Runnable() { @@ -181,7 +180,7 @@ public class JsrEndpointEventDriver extends AbstractJsrEventDriver implements Ev } else if (wrapper.wantsStreams()) { - final MessageReader stream = new MessageReader(new MessageInputStream(session.getConnection())); + final MessageReader stream = new MessageReader(new MessageInputStream()); activeMessage = stream; dispatch(new Runnable() 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 index 1bb8effb566..bc6484e37a6 100644 --- 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 @@ -18,9 +18,14 @@ package org.eclipse.jetty.websocket.jsr356; +import java.io.IOException; + +import org.eclipse.jetty.io.RuntimeIOException; 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.BatchMode; +import org.eclipse.jetty.websocket.api.RemoteEndpoint; import org.eclipse.jetty.websocket.api.WebSocketAdapter; /** @@ -33,7 +38,17 @@ public class JettyEchoSocket extends WebSocketAdapter @Override public void onWebSocketBinary(byte[] payload, int offset, int len) { - getRemote().sendBytes(BufferUtil.toBuffer(payload,offset,len),null); + try + { + RemoteEndpoint remote = getRemote(); + remote.sendBytes(BufferUtil.toBuffer(payload, offset, len), null); + if (remote.getBatchMode() == BatchMode.ON) + remote.flush(); + } + catch (IOException x) + { + throw new RuntimeIOException(x); + } } @Override @@ -45,6 +60,16 @@ public class JettyEchoSocket extends WebSocketAdapter @Override public void onWebSocketText(String message) { - getRemote().sendString(message,null); + try + { + RemoteEndpoint remote = getRemote(); + remote.sendString(message, null); + if (remote.getBatchMode() == BatchMode.ON) + remote.flush(); + } + catch (IOException x) + { + throw new RuntimeIOException(x); + } } } 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 index 9d4c5d7e40d..1de304bd229 100644 --- 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 @@ -18,17 +18,13 @@ package org.eclipse.jetty.websocket.jsr356; -import static org.hamcrest.Matchers.instanceOf; - 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.SessionListener; import org.eclipse.jetty.websocket.common.events.EventDriver; import org.eclipse.jetty.websocket.jsr356.client.EmptyClientEndpointConfig; import org.eclipse.jetty.websocket.jsr356.client.SimpleEndpointMetadata; @@ -44,6 +40,8 @@ import org.junit.Assert; import org.junit.Before; import org.junit.Test; +import static org.hamcrest.Matchers.instanceOf; + public class JsrSessionTest { private ClientContainer container; @@ -65,7 +63,7 @@ public class JsrSessionTest EventDriver driver = new JsrEndpointEventDriver(policy,ei); DummyConnection connection = new DummyConnection(); - session = new JsrSession(requestURI,driver,connection,container,id,new SessionListener[0]); + session = new JsrSession(requestURI,driver,connection,container,id); } @Test 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 index 27dae9f3544..8adc1b26ea1 100644 --- 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 @@ -22,6 +22,7 @@ import java.net.InetSocketAddress; import java.util.concurrent.Executor; import org.eclipse.jetty.io.ByteBufferPool; +import org.eclipse.jetty.websocket.api.BatchMode; import org.eclipse.jetty.websocket.api.SuspendToken; import org.eclipse.jetty.websocket.api.WebSocketPolicy; import org.eclipse.jetty.websocket.api.WriteCallback; @@ -123,7 +124,7 @@ public class DummyConnection implements LogicalConnection } @Override - public void outgoingFrame(Frame frame, WriteCallback callback) + public void outgoingFrame(Frame frame, WriteCallback callback, BatchMode batchMode) { } diff --git a/jetty-websocket/javax-websocket-server-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/server/deploy/WebSocketServerContainerInitializer.java b/jetty-websocket/javax-websocket-server-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/server/deploy/WebSocketServerContainerInitializer.java index 7cb10b325bd..73b01c885d3 100644 --- a/jetty-websocket/javax-websocket-server-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/server/deploy/WebSocketServerContainerInitializer.java +++ b/jetty-websocket/javax-websocket-server-impl/src/main/java/org/eclipse/jetty/websocket/jsr356/server/deploy/WebSocketServerContainerInitializer.java @@ -33,6 +33,7 @@ import javax.websocket.server.ServerEndpointConfig; import org.eclipse.jetty.server.handler.ContextHandler; import org.eclipse.jetty.servlet.ServletContextHandler; +import org.eclipse.jetty.util.TypeUtil; import org.eclipse.jetty.util.log.Log; import org.eclipse.jetty.util.log.Logger; import org.eclipse.jetty.websocket.jsr356.server.ServerContainer; @@ -45,16 +46,6 @@ public class WebSocketServerContainerInitializer implements ServletContainerInit public static final String ENABLE_KEY = "org.eclipse.jetty.websocket.jsr356"; private static final Logger LOG = Log.getLogger(WebSocketServerContainerInitializer.class); - public static boolean isJSR356EnabledOnContext(ServletContext context) - { - Object enable = context.getAttribute(ENABLE_KEY); - if (enable instanceof Boolean) - { - return ((Boolean)enable).booleanValue(); - } - - return true; - } public static ServerContainer configureContext(ServletContextHandler context) { @@ -77,9 +68,22 @@ public class WebSocketServerContainerInitializer implements ServletContainerInit @Override public void onStartup(Set> c, ServletContext context) throws ServletException { - if (!isJSR356EnabledOnContext(context)) + Object enable = context.getAttribute(ENABLE_KEY); + + // Disable if explicitly disabled + if (TypeUtil.isFalse(enable)) { - LOG.info("JSR-356 support disabled via attribute on context {} - {}",context.getContextPath(),context); + if (c.isEmpty()) + LOG.debug("JSR-356 support disabled via attribute on context {} - {}",context.getContextPath(),context); + else + LOG.warn("JSR-356 support disabled via attribute on context {} - {}",context.getContextPath(),context); + return; + } + + // Disabled if not explicitly enabled and there are no discovered annotations or interfaces + if (!TypeUtil.isTrue(enable) && c.isEmpty()) + { + LOG.debug("No JSR-356 annotations or interfaces discovered. JSR-356 support disabled",context.getContextPath(),context); return; } @@ -87,12 +91,12 @@ public class WebSocketServerContainerInitializer implements ServletContainerInit if (handler == null) { - throw new ServletException("Not running on Jetty, JSR support disabled"); + throw new ServletException("Not running on Jetty, JSR-356 support disabled"); } if (!(handler instanceof ServletContextHandler)) { - throw new ServletException("Not running in Jetty ServletContextHandler, JSR support disabled"); + throw new ServletException("Not running in Jetty ServletContextHandler, JSR-356 support disabled"); } ServletContextHandler jettyContext = (ServletContextHandler)handler; @@ -126,7 +130,7 @@ public class WebSocketServerContainerInitializer implements ServletContainerInit LOG.debug("Found ServerApplicationConfig: {}",clazz); try { - ServerApplicationConfig config = (ServerApplicationConfig)clazz.newInstance(); + ServerApplicationConfig config = clazz.newInstance(); Set seconfigs = config.getEndpointConfigs(discoveredExtendedEndpoints); if (seconfigs != null) diff --git a/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/BinaryStreamTest.java b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/BinaryStreamTest.java new file mode 100644 index 00000000000..1dc6b02863f --- /dev/null +++ b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/BinaryStreamTest.java @@ -0,0 +1,177 @@ +// +// ======================================================================== +// Copyright (c) 1995-2014 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.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.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.URI; +import java.util.Random; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import javax.websocket.ClientEndpoint; +import javax.websocket.ContainerProvider; +import javax.websocket.OnMessage; +import javax.websocket.Session; +import javax.websocket.WebSocketContainer; +import javax.websocket.server.ServerEndpoint; +import javax.websocket.server.ServerEndpointConfig; + +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.ServerConnector; +import org.eclipse.jetty.servlet.ServletContextHandler; +import org.eclipse.jetty.websocket.jsr356.server.deploy.WebSocketServerContainerInitializer; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +public class BinaryStreamTest +{ + private static final String PATH = "/echo"; + + private Server server; + private ServerConnector connector; + private WebSocketContainer wsClient; + + @Before + public void prepare() throws Exception + { + server = new Server(); + connector = new ServerConnector(server); + server.addConnector(connector); + + ServletContextHandler context = new ServletContextHandler(server, "/", true, false); + ServerContainer container = WebSocketServerContainerInitializer.configureContext(context); + ServerEndpointConfig config = ServerEndpointConfig.Builder.create(ServerBinaryStreamer.class, PATH).build(); + container.addEndpoint(config); + + server.start(); + + wsClient = ContainerProvider.getWebSocketContainer(); + server.addBean(wsClient, true); + } + + @After + public void dispose() throws Exception + { + server.stop(); + } + + @Test + public void testEchoWithMediumMessage() throws Exception + { + testEcho(1024); + } + + @Test + public void testLargestMessage() throws Exception + { + testEcho(wsClient.getDefaultMaxBinaryMessageBufferSize()); + } + + private void testEcho(int size) throws Exception + { + byte[] data = randomBytes(size); + URI uri = URI.create("ws://localhost:" + connector.getLocalPort() + PATH); + ClientBinaryStreamer client = new ClientBinaryStreamer(); + Session session = wsClient.connectToServer(client, uri); + + try (OutputStream output = session.getBasicRemote().getSendStream()) + { + output.write(data); + } + + Assert.assertTrue(client.await(5, TimeUnit.SECONDS)); + Assert.assertArrayEquals(data, client.getEcho()); + } + + @Test + public void testMoreThanLargestMessageOneByteAtATime() throws Exception + { + int size = wsClient.getDefaultMaxBinaryMessageBufferSize() + 16; + byte[] data = randomBytes(size); + URI uri = URI.create("ws://localhost:" + connector.getLocalPort() + PATH); + ClientBinaryStreamer client = new ClientBinaryStreamer(); + Session session = wsClient.connectToServer(client, uri); + + try (OutputStream output = session.getBasicRemote().getSendStream()) + { + for (int i = 0; i < size; ++i) + output.write(data[i]); + } + + Assert.assertTrue(client.await(5, TimeUnit.SECONDS)); + Assert.assertArrayEquals(data, client.getEcho()); + } + + private byte[] randomBytes(int size) + { + byte[] data = new byte[size]; + new Random().nextBytes(data); + return data; + } + + @ClientEndpoint + public static class ClientBinaryStreamer + { + private final CountDownLatch latch = new CountDownLatch(1); + private final ByteArrayOutputStream output = new ByteArrayOutputStream(); + + @OnMessage + public void echoed(InputStream input) throws IOException + { + while (true) + { + int read = input.read(); + if (read < 0) + break; + output.write(read); + } + latch.countDown(); + } + + public byte[] getEcho() + { + return output.toByteArray(); + } + + public boolean await(long timeout, TimeUnit unit) throws InterruptedException + { + return latch.await(timeout, unit); + } + } + + @ServerEndpoint(PATH) + public static class ServerBinaryStreamer + { + @OnMessage + public void echo(Session session, InputStream input) throws IOException + { + byte[] buffer = new byte[128]; + try (OutputStream output = session.getBasicRemote().getSendStream()) + { + int read; + while ((read = input.read(buffer)) >= 0) + output.write(buffer, 0, read); + } + } + } +} diff --git a/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/DummyConnection.java b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/DummyConnection.java index c2c3e6e2a60..7111ed7ef20 100644 --- a/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/DummyConnection.java +++ b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/DummyConnection.java @@ -24,6 +24,7 @@ import java.util.concurrent.Executor; 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.BatchMode; import org.eclipse.jetty.websocket.api.SuspendToken; import org.eclipse.jetty.websocket.api.WebSocketPolicy; import org.eclipse.jetty.websocket.api.WriteCallback; @@ -126,7 +127,7 @@ public class DummyConnection implements LogicalConnection } @Override - public void outgoingFrame(Frame frame, WriteCallback callback) + public void outgoingFrame(Frame frame, WriteCallback callback, BatchMode batchMode) { callback.writeSuccess(); } diff --git a/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/ExtensionStackProcessingTest.java b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/ExtensionStackProcessingTest.java new file mode 100644 index 00000000000..d2fbcd7c853 --- /dev/null +++ b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/ExtensionStackProcessingTest.java @@ -0,0 +1,171 @@ +// +// ======================================================================== +// Copyright (c) 1995-2014 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.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.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import javax.websocket.ClientEndpointConfig; +import javax.websocket.ContainerProvider; +import javax.websocket.Endpoint; +import javax.websocket.EndpointConfig; +import javax.websocket.Extension; +import javax.websocket.MessageHandler; +import javax.websocket.SendHandler; +import javax.websocket.SendResult; +import javax.websocket.Session; +import javax.websocket.WebSocketContainer; +import javax.websocket.server.ServerEndpointConfig; + +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.ServerConnector; +import org.eclipse.jetty.servlet.ServletContextHandler; +import org.eclipse.jetty.websocket.api.extensions.OutgoingFrames; +import org.eclipse.jetty.websocket.client.io.WebSocketClientConnection; +import org.eclipse.jetty.websocket.common.extensions.ExtensionStack; +import org.eclipse.jetty.websocket.common.extensions.compress.DeflateFrameExtension; +import org.eclipse.jetty.websocket.jsr356.JsrExtension; +import org.eclipse.jetty.websocket.jsr356.JsrSession; +import org.eclipse.jetty.websocket.jsr356.server.deploy.WebSocketServerContainerInitializer; +import org.eclipse.jetty.websocket.jsr356.server.samples.echo.BasicEchoEndpoint; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +public class ExtensionStackProcessingTest +{ + private Server server; + private ServerConnector connector; + private WebSocketContainer client; + + @Before + public void prepare() throws Exception + { + server = new Server(); + connector = new ServerConnector(server); + server.addConnector(connector); + + ServletContextHandler context = new ServletContextHandler(server, "/", true, false); + ServerContainer container = WebSocketServerContainerInitializer.configureContext(context); + ServerEndpointConfig config = ServerEndpointConfig.Builder.create(BasicEchoEndpoint.class, "/").build(); + container.addEndpoint(config); + + server.start(); + + client = ContainerProvider.getWebSocketContainer(); + server.addBean(client, true); + } + + @After + public void dispose() throws Exception + { + server.stop(); + } + + @Test + public void testDeflateFrameExtension() throws Exception + { + ClientEndpointConfig config = ClientEndpointConfig.Builder.create() + .extensions(Arrays.asList(new JsrExtension("deflate-frame"))) + .build(); + + final String content = "deflate_me"; + final CountDownLatch messageLatch = new CountDownLatch(1); + URI uri = URI.create("ws://localhost:" + connector.getLocalPort()); + Session session = client.connectToServer(new EndpointAdapter() + { + @Override + public void onMessage(String message) + { + Assert.assertEquals(content, message); + messageLatch.countDown(); + } + }, config, uri); + + // Make sure everything is wired properly. + OutgoingFrames firstOut = ((JsrSession)session).getOutgoingHandler(); + Assert.assertTrue(firstOut instanceof ExtensionStack); + ExtensionStack extensionStack = (ExtensionStack)firstOut; + Assert.assertTrue(extensionStack.isRunning()); + OutgoingFrames secondOut = extensionStack.getNextOutgoing(); + Assert.assertTrue(secondOut instanceof DeflateFrameExtension); + DeflateFrameExtension deflateExtension = (DeflateFrameExtension)secondOut; + Assert.assertTrue(deflateExtension.isRunning()); + OutgoingFrames thirdOut = deflateExtension.getNextOutgoing(); + Assert.assertTrue(thirdOut instanceof WebSocketClientConnection); + + final CountDownLatch latch = new CountDownLatch(1); + session.getAsyncRemote().sendText(content, new SendHandler() + { + @Override + public void onResult(SendResult result) + { + latch.countDown(); + } + }); + + Assert.assertTrue(latch.await(5, TimeUnit.SECONDS)); + Assert.assertTrue(messageLatch.await(5, TimeUnit.SECONDS)); + } + + @Test + public void testPerMessageDeflateExtension() throws Exception + { + ClientEndpointConfig config = ClientEndpointConfig.Builder.create() + .extensions(Arrays.asList(new JsrExtension("permessage-deflate"))) + .build(); + + final String content = "deflate_me"; + final CountDownLatch messageLatch = new CountDownLatch(1); + URI uri = URI.create("ws://localhost:" + connector.getLocalPort()); + Session session = client.connectToServer(new EndpointAdapter() + { + @Override + public void onMessage(String message) + { + Assert.assertEquals(content, message); + messageLatch.countDown(); + } + }, config, uri); + + final CountDownLatch latch = new CountDownLatch(1); + session.getAsyncRemote().sendText(content, new SendHandler() + { + @Override + public void onResult(SendResult result) + { + latch.countDown(); + } + }); + + Assert.assertTrue(latch.await(5, TimeUnit.SECONDS)); + Assert.assertTrue(messageLatch.await(5, TimeUnit.SECONDS)); + } + + private static abstract class EndpointAdapter extends Endpoint implements MessageHandler.Whole + { + @Override + public void onOpen(Session session, EndpointConfig config) + { + session.addMessageHandler(this); + } + } +} 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 index 726ed67931b..c7bb1e03a37 100644 --- 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 @@ -18,6 +18,7 @@ package org.eclipse.jetty.websocket.jsr356.server; +import java.io.IOException; import java.util.Queue; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; @@ -25,6 +26,7 @@ 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.BatchMode; import org.eclipse.jetty.websocket.api.RemoteEndpoint; import org.eclipse.jetty.websocket.api.Session; import org.eclipse.jetty.websocket.api.annotations.OnWebSocketClose; @@ -74,10 +76,10 @@ public class JettyEchoSocket } @OnWebSocketMessage - public void onMessage(String msg) + public void onMessage(String msg) throws IOException { incomingMessages.add(msg); - remote.sendString(msg,null); + sendMessage(msg); } @OnWebSocketConnect @@ -88,8 +90,10 @@ public class JettyEchoSocket this.remote = session.getRemote(); } - public void sendMessage(String msg) + public void sendMessage(String msg) throws IOException { remote.sendStringByFuture(msg); + if (remote.getBatchMode() == BatchMode.ON) + remote.flush(); } } diff --git a/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/JsrBatchModeTest.java b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/JsrBatchModeTest.java new file mode 100644 index 00000000000..69586dd8a9e --- /dev/null +++ b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/JsrBatchModeTest.java @@ -0,0 +1,181 @@ +// +// ======================================================================== +// Copyright (c) 1995-2014 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.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.concurrent.CountDownLatch; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import javax.websocket.ClientEndpointConfig; +import javax.websocket.ContainerProvider; +import javax.websocket.Endpoint; +import javax.websocket.EndpointConfig; +import javax.websocket.MessageHandler; +import javax.websocket.RemoteEndpoint; +import javax.websocket.Session; +import javax.websocket.WebSocketContainer; +import javax.websocket.server.ServerEndpointConfig; + +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.ServerConnector; +import org.eclipse.jetty.servlet.ServletContextHandler; +import org.eclipse.jetty.websocket.jsr356.server.deploy.WebSocketServerContainerInitializer; +import org.eclipse.jetty.websocket.jsr356.server.samples.echo.BasicEchoEndpoint; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +public class JsrBatchModeTest +{ + private Server server; + private ServerConnector connector; + private WebSocketContainer client; + + @Before + public void prepare() throws Exception + { + server = new Server(); + connector = new ServerConnector(server); + server.addConnector(connector); + + ServletContextHandler context = new ServletContextHandler(server, "/", true, false); + ServerContainer container = WebSocketServerContainerInitializer.configureContext(context); + ServerEndpointConfig config = ServerEndpointConfig.Builder.create(BasicEchoEndpoint.class, "/").build(); + container.addEndpoint(config); + + server.start(); + + client = ContainerProvider.getWebSocketContainer(); + server.addBean(client, true); + } + + @After + public void dispose() throws Exception + { + server.stop(); + } + + @Test + public void testBatchModeOn() throws Exception + { + ClientEndpointConfig config = ClientEndpointConfig.Builder.create().build(); + + URI uri = URI.create("ws://localhost:" + connector.getLocalPort()); + + final CountDownLatch latch = new CountDownLatch(1); + EndpointAdapter endpoint = new EndpointAdapter() + { + @Override + public void onMessage(String message) + { + latch.countDown(); + } + }; + + try (Session session = client.connectToServer(endpoint, config, uri)) + { + RemoteEndpoint.Async remote = session.getAsyncRemote(); + remote.setBatchingAllowed(true); + + Future future = remote.sendText("batch_mode_on"); + // The write is aggregated and therefore completes immediately. + future.get(1, TimeUnit.MICROSECONDS); + + // Did not flush explicitly, so the message should not be back yet. + Assert.assertFalse(latch.await(1, TimeUnit.SECONDS)); + + // Explicitly flush. + remote.flushBatch(); + + // Wait for the echo. + Assert.assertTrue(latch.await(5, TimeUnit.SECONDS)); + } + } + + @Test + public void testBatchModeOff() throws Exception + { + ClientEndpointConfig config = ClientEndpointConfig.Builder.create().build(); + + URI uri = URI.create("ws://localhost:" + connector.getLocalPort()); + + final CountDownLatch latch = new CountDownLatch(1); + EndpointAdapter endpoint = new EndpointAdapter() + { + @Override + public void onMessage(String message) + { + latch.countDown(); + } + }; + + try (Session session = client.connectToServer(endpoint, config, uri)) + { + RemoteEndpoint.Async remote = session.getAsyncRemote(); + remote.setBatchingAllowed(false); + + Future future = remote.sendText("batch_mode_off"); + // The write is immediate. + future.get(1, TimeUnit.SECONDS); + + // Wait for the echo. + Assert.assertTrue(latch.await(5, TimeUnit.SECONDS)); + } + } + + @Test + public void testBatchModeAuto() throws Exception + { + ClientEndpointConfig config = ClientEndpointConfig.Builder.create().build(); + + URI uri = URI.create("ws://localhost:" + connector.getLocalPort()); + + final CountDownLatch latch = new CountDownLatch(1); + EndpointAdapter endpoint = new EndpointAdapter() + { + @Override + public void onMessage(String message) + { + latch.countDown(); + } + }; + + try (Session session = client.connectToServer(endpoint, config, uri)) + { + RemoteEndpoint.Async remote = session.getAsyncRemote(); + + Future future = remote.sendText("batch_mode_auto"); + // The write is immediate, as per the specification. + future.get(1, TimeUnit.SECONDS); + + // Wait for the echo. + Assert.assertTrue(latch.await(5, TimeUnit.SECONDS)); + } + } + + private static abstract class EndpointAdapter extends Endpoint implements MessageHandler.Whole + { + @Override + public void onOpen(Session session, EndpointConfig config) + { + session.addMessageHandler(this); + } + } +} diff --git a/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/MemoryUsageTest.java b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/MemoryUsageTest.java new file mode 100644 index 00000000000..29c5fee44c9 --- /dev/null +++ b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/MemoryUsageTest.java @@ -0,0 +1,130 @@ +// +// ======================================================================== +// Copyright (c) 1995-2014 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.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.lang.management.ManagementFactory; +import java.lang.management.MemoryMXBean; +import java.lang.management.MemoryUsage; +import java.net.URI; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import javax.websocket.ContainerProvider; +import javax.websocket.Endpoint; +import javax.websocket.EndpointConfig; +import javax.websocket.MessageHandler; +import javax.websocket.Session; +import javax.websocket.WebSocketContainer; +import javax.websocket.server.ServerEndpointConfig; + +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.ServerConnector; +import org.eclipse.jetty.servlet.ServletContextHandler; +import org.eclipse.jetty.websocket.jsr356.server.deploy.WebSocketServerContainerInitializer; +import org.eclipse.jetty.websocket.jsr356.server.samples.echo.BasicEchoEndpoint; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +public class MemoryUsageTest +{ + private Server server; + private ServerConnector connector; + private WebSocketContainer client; + + @Before + public void prepare() throws Exception + { + server = new Server(); + connector = new ServerConnector(server); + server.addConnector(connector); + + ServletContextHandler context = new ServletContextHandler(server, "/", true, false); + ServerContainer container = WebSocketServerContainerInitializer.configureContext(context); + ServerEndpointConfig config = ServerEndpointConfig.Builder.create(BasicEchoEndpoint.class, "/").build(); + container.addEndpoint(config); + + server.start(); + + client = ContainerProvider.getWebSocketContainer(); + server.addBean(client, true); + } + + @After + public void dispose() throws Exception + { + server.stop(); + } + + @Test + public void testMemoryUsage() throws Exception + { + int sessionCount = 1000; + Session[] sessions = new Session[sessionCount]; + + MemoryMXBean memoryMXBean = ManagementFactory.getMemoryMXBean(); + + System.gc(); + MemoryUsage heapBefore = memoryMXBean.getHeapMemoryUsage(); + MemoryUsage nonHeapBefore = memoryMXBean.getNonHeapMemoryUsage(); + + URI uri = URI.create("ws://localhost:" + connector.getLocalPort()); + final CountDownLatch latch = new CountDownLatch(sessionCount); + for (int i = 0; i < sessionCount; ++i) + { + sessions[i] = client.connectToServer(new EndpointAdapter() + { + @Override + public void onMessage(String message) + { + latch.countDown(); + } + }, uri); + } + for (int i = 0; i < sessionCount; ++i) + { + sessions[i].getBasicRemote().sendText("OK"); + } + latch.await(5 * sessionCount, TimeUnit.MILLISECONDS); + + System.gc(); + MemoryUsage heapAfter = memoryMXBean.getHeapMemoryUsage(); + MemoryUsage nonHeapAfter = memoryMXBean.getNonHeapMemoryUsage(); + + long heapUsed = heapAfter.getUsed() - heapBefore.getUsed(); + long nonHeapUsed = nonHeapAfter.getUsed() - nonHeapBefore.getUsed(); + +// System.out.println("heapUsed = " + heapUsed); +// System.out.println("nonHeapUsed = " + nonHeapUsed); +// new CountDownLatch(1).await(); + + // Assume no more than 25 KiB per session pair (client and server). + long expected = 25 * 1024 * sessionCount; + Assert.assertTrue(heapUsed < expected); + } + + private static abstract class EndpointAdapter extends Endpoint implements MessageHandler.Whole + { + @Override + public void onOpen(Session session, EndpointConfig config) + { + session.addMessageHandler(this); + } + } +} diff --git a/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/OnPartialTest.java b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/OnPartialTest.java index cf997370d57..990cf71b17b 100644 --- a/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/OnPartialTest.java +++ b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/OnPartialTest.java @@ -18,17 +18,13 @@ package org.eclipse.jetty.websocket.jsr356.server; -import static org.hamcrest.Matchers.*; - import java.net.URI; import java.util.ArrayList; import java.util.List; - import javax.websocket.server.ServerEndpoint; import javax.websocket.server.ServerEndpointConfig; import org.eclipse.jetty.websocket.api.WebSocketPolicy; -import org.eclipse.jetty.websocket.common.SessionListener; import org.eclipse.jetty.websocket.common.WebSocketFrame; import org.eclipse.jetty.websocket.common.events.EventDriver; import org.eclipse.jetty.websocket.common.events.EventDriverFactory; @@ -45,6 +41,9 @@ import org.junit.Rule; import org.junit.Test; import org.junit.rules.TestName; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; + public class OnPartialTest { @Rule @@ -80,7 +79,7 @@ public class OnPartialTest DummyConnection connection = new DummyConnection(); ClientContainer container = new ClientContainer(); @SuppressWarnings("resource") - JsrSession session = new JsrSession(requestURI,driver,connection,container,id,new SessionListener[0]); + JsrSession session = new JsrSession(requestURI,driver,connection,container,id); session.setPolicy(policy); session.open(); return driver; diff --git a/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/TextStreamTest.java b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/TextStreamTest.java new file mode 100644 index 00000000000..dd66d135bf3 --- /dev/null +++ b/jetty-websocket/javax-websocket-server-impl/src/test/java/org/eclipse/jetty/websocket/jsr356/server/TextStreamTest.java @@ -0,0 +1,179 @@ +// +// ======================================================================== +// Copyright (c) 1995-2014 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.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.Reader; +import java.io.Writer; +import java.net.URI; +import java.util.Random; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import javax.websocket.ClientEndpoint; +import javax.websocket.ContainerProvider; +import javax.websocket.OnMessage; +import javax.websocket.Session; +import javax.websocket.WebSocketContainer; +import javax.websocket.server.ServerEndpoint; +import javax.websocket.server.ServerEndpointConfig; + +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.ServerConnector; +import org.eclipse.jetty.servlet.ServletContextHandler; +import org.eclipse.jetty.websocket.jsr356.server.deploy.WebSocketServerContainerInitializer; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +public class TextStreamTest +{ + private static final String PATH = "/echo"; + private static final String CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; + + private Server server; + private ServerConnector connector; + private WebSocketContainer wsClient; + + @Before + public void prepare() throws Exception + { + server = new Server(); + connector = new ServerConnector(server); + server.addConnector(connector); + + ServletContextHandler context = new ServletContextHandler(server, "/", true, false); + ServerContainer container = WebSocketServerContainerInitializer.configureContext(context); + ServerEndpointConfig config = ServerEndpointConfig.Builder.create(ServerTextStreamer.class, PATH).build(); + container.addEndpoint(config); + + server.start(); + + wsClient = ContainerProvider.getWebSocketContainer(); + server.addBean(wsClient, true); + } + + @After + public void dispose() throws Exception + { + server.stop(); + } + + @Test + public void testEchoWithMediumMessage() throws Exception + { + testEcho(1024); + } + + @Test + public void testLargestMessage() throws Exception + { + testEcho(wsClient.getDefaultMaxBinaryMessageBufferSize()); + } + + private void testEcho(int size) throws Exception + { + char[] data = randomChars(size); + URI uri = URI.create("ws://localhost:" + connector.getLocalPort() + PATH); + ClientTextStreamer client = new ClientTextStreamer(); + Session session = wsClient.connectToServer(client, uri); + + try (Writer output = session.getBasicRemote().getSendWriter()) + { + output.write(data); + } + + Assert.assertTrue(client.await(5, TimeUnit.SECONDS)); + Assert.assertArrayEquals(data, client.getEcho()); + } + + @Test + public void testMoreThanLargestMessageOneByteAtATime() throws Exception + { + int size = wsClient.getDefaultMaxBinaryMessageBufferSize() + 16; + char[] data = randomChars(size); + URI uri = URI.create("ws://localhost:" + connector.getLocalPort() + PATH); + ClientTextStreamer client = new ClientTextStreamer(); + Session session = wsClient.connectToServer(client, uri); + + try (Writer output = session.getBasicRemote().getSendWriter()) + { + for (int i = 0; i < size; ++i) + output.write(data[i]); + } + + Assert.assertTrue(client.await(5, TimeUnit.SECONDS)); + Assert.assertArrayEquals(data, client.getEcho()); + } + + private char[] randomChars(int size) + { + char[] data = new char[size]; + Random random = new Random(); + for (int i = 0; i < data.length; ++i) + data[i] = CHARS.charAt(random.nextInt(CHARS.length())); + return data; + } + + @ClientEndpoint + public static class ClientTextStreamer + { + private final CountDownLatch latch = new CountDownLatch(1); + private final StringBuilder output = new StringBuilder(); + + @OnMessage + public void echoed(Reader input) throws IOException + { + while (true) + { + int read = input.read(); + if (read < 0) + break; + output.append((char)read); + } + latch.countDown(); + } + + public char[] getEcho() + { + return output.toString().toCharArray(); + } + + public boolean await(long timeout, TimeUnit unit) throws InterruptedException + { + return latch.await(timeout, unit); + } + } + + @ServerEndpoint(PATH) + public static class ServerTextStreamer + { + @OnMessage + public void echo(Session session, Reader input) throws IOException + { + char[] buffer = new char[128]; + try (Writer output = session.getBasicRemote().getSendWriter()) + { + int read; + while ((read = input.read(buffer)) >= 0) + output.write(buffer, 0, read); + } + } + } +} diff --git a/jetty-websocket/websocket-api/src/main/java/org/eclipse/jetty/websocket/api/BatchMode.java b/jetty-websocket/websocket-api/src/main/java/org/eclipse/jetty/websocket/api/BatchMode.java new file mode 100644 index 00000000000..a3f7aab753b --- /dev/null +++ b/jetty-websocket/websocket-api/src/main/java/org/eclipse/jetty/websocket/api/BatchMode.java @@ -0,0 +1,50 @@ +// +// ======================================================================== +// Copyright (c) 1995-2014 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.websocket.api; + +import org.eclipse.jetty.websocket.api.extensions.Frame; +import org.eclipse.jetty.websocket.api.extensions.OutgoingFrames; + +/** + * The possible batch modes when invoking {@link OutgoingFrames#outgoingFrame(Frame, WriteCallback, BatchMode)}. + */ +public enum BatchMode +{ + /** + * Implementers are free to decide whether to send or not frames + * to the network layer. + */ + AUTO, + + /** + * Implementers must batch frames. + */ + ON, + + /** + * Implementers must send frames to the network layer. + */ + OFF; + + public static BatchMode max(BatchMode one, BatchMode two) + { + // Return the BatchMode that has the higher priority, where AUTO < ON < OFF. + return one.ordinal() < two.ordinal() ? two : one; + } +} 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 909e151721d..c1a880a1533 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 @@ -121,4 +121,17 @@ public interface RemoteEndpoint * callback to notify of success or failure of the write operation */ void sendString(String text, WriteCallback callback); + + /** + * @return the batch mode with which messages are sent. + * @see #flush() + */ + BatchMode getBatchMode(); + + /** + * Flushes messages that may have been batched by the implementation. + * @throws IOException if the flush fails + * @see #getBatchMode() + */ + void flush() throws IOException; } 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 c76dd5419ac..81059efbc39 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 @@ -34,16 +34,16 @@ import org.eclipse.jetty.websocket.api.util.QuoteUtil; public class UpgradeRequest { private URI requestURI; - private List subProtocols = new ArrayList<>(); - private List extensions = new ArrayList<>(); - private List cookies = new ArrayList<>(); + private List subProtocols = new ArrayList<>(1); + private List extensions = new ArrayList<>(1); + private List cookies = new ArrayList<>(1); private Map> headers = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); - private Map> parameters = new HashMap<>(); + private Map> parameters = new HashMap<>(1); private Object session; private String httpVersion; private String method; private String host; - private boolean secure = false; + private boolean secure; protected UpgradeRequest() { @@ -57,16 +57,12 @@ public class UpgradeRequest public UpgradeRequest(URI requestURI) { - this(); setRequestURI(requestURI); } public void addExtensions(ExtensionConfig... configs) { - for (ExtensionConfig config : configs) - { - extensions.add(config); - } + Collections.addAll(extensions, configs); } public void addExtensions(String... configs) @@ -357,10 +353,7 @@ public class UpgradeRequest */ public void setSubProtocols(String... protocols) { - this.subProtocols.clear(); - for (String protocol : protocols) - { - this.subProtocols.add(protocol); - } + subProtocols.clear(); + Collections.addAll(subProtocols, protocols); } } diff --git a/jetty-websocket/websocket-api/src/main/java/org/eclipse/jetty/websocket/api/extensions/Frame.java b/jetty-websocket/websocket-api/src/main/java/org/eclipse/jetty/websocket/api/extensions/Frame.java index 06a2b9dee36..ad3dd8da348 100644 --- a/jetty-websocket/websocket-api/src/main/java/org/eclipse/jetty/websocket/api/extensions/Frame.java +++ b/jetty-websocket/websocket-api/src/main/java/org/eclipse/jetty/websocket/api/extensions/Frame.java @@ -68,6 +68,11 @@ public interface Frame return (opcode == TEXT.getOpCode()) | (opcode == BINARY.getOpCode()); } + public boolean isContinuation() + { + return opcode == CONTINUATION.getOpCode(); + } + @Override public String toString() { 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 03e73d8b788..451e57b3a94 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 @@ -25,5 +25,14 @@ public interface IncomingFrames { public void incomingError(Throwable t); + /** + * Process the incoming frame. + *

+ * Note: if you need to hang onto any information from the frame, be sure + * to copy it, as the information contained in the Frame will be released + * and/or reused by the implementation. + * + * @param frame the frame to process + */ public void incomingFrame(Frame frame); } diff --git a/jetty-websocket/websocket-api/src/main/java/org/eclipse/jetty/websocket/api/extensions/OutgoingFrames.java b/jetty-websocket/websocket-api/src/main/java/org/eclipse/jetty/websocket/api/extensions/OutgoingFrames.java index d0f1fe869b7..9a3aed27da3 100644 --- a/jetty-websocket/websocket-api/src/main/java/org/eclipse/jetty/websocket/api/extensions/OutgoingFrames.java +++ b/jetty-websocket/websocket-api/src/main/java/org/eclipse/jetty/websocket/api/extensions/OutgoingFrames.java @@ -18,24 +18,27 @@ package org.eclipse.jetty.websocket.api.extensions; +import org.eclipse.jetty.websocket.api.BatchMode; import org.eclipse.jetty.websocket.api.WriteCallback; /** - * Interface for dealing with frames outgoing to the network (eventually) + * Interface for dealing with frames outgoing to (eventually) the network layer. */ public interface OutgoingFrames { /** - * A frame, and optional callback, intended for the network. - *

- * Note: the frame can undergo many transformations in the various layers and extensions present in the implementation. - *

- * If you are implementing a mutation, you are obliged to handle the incoming WriteCallback appropriately. - * - * @param frame - * the frame to eventually write to the network. - * @param callback - * the optional callback to use for success/failure of the network write operation. Can be null. + * A frame, and optional callback, intended for the network layer. + *

+ * Note: the frame can undergo many transformations in the various + * layers and extensions present in the implementation. + *

+ * If you are implementing a mutation, you are obliged to handle + * the incoming WriteCallback appropriately. + * + * @param frame the frame to eventually write to the network layer. + * @param callback the callback to notify when the frame is written. + * @param batchMode the batch mode requested by the sender. */ - void outgoingFrame(Frame frame, WriteCallback callback); + void outgoingFrame(Frame frame, WriteCallback callback, BatchMode batchMode); + } 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 8af85198edd..387874a52ec 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 @@ -23,11 +23,10 @@ import java.net.CookieStore; import java.net.SocketAddress; import java.net.URI; import java.util.ArrayList; -import java.util.Collections; +import java.util.HashSet; import java.util.List; import java.util.Locale; import java.util.Set; -import java.util.concurrent.CopyOnWriteArraySet; import java.util.concurrent.Executor; import java.util.concurrent.Future; @@ -75,7 +74,6 @@ public class WebSocketClient extends ContainerLifeCycle implements SessionListen private boolean daemon = false; private EventDriverFactory eventDriverFactory; private SessionFactory sessionFactory; - private Set openSessions = new CopyOnWriteArraySet<>(); private ByteBufferPool bufferPool; private Executor executor; private Scheduler scheduler; @@ -374,7 +372,7 @@ public class WebSocketClient extends ContainerLifeCycle implements SessionListen public Set getOpenSessions() { - return Collections.unmodifiableSet(this.openSessions); + return new HashSet<>(getBeans(WebSocketSession.class)); } public WebSocketPolicy getPolicy() @@ -472,15 +470,14 @@ public class WebSocketClient extends ContainerLifeCycle implements SessionListen @Override public void onSessionClosed(WebSocketSession session) { - LOG.info("Session Closed: {}",session); - this.openSessions.remove(session); + LOG.debug("Session Closed: {}",session); + removeBean(session); } @Override public void onSessionOpened(WebSocketSession session) { - LOG.info("Session Opened: {}",session); - this.openSessions.add(session); + LOG.debug("Session Opened: {}",session); } public void setAsyncWriteTimeout(long ms) @@ -566,4 +563,11 @@ public class WebSocketClient extends ContainerLifeCycle implements SessionListen { this.sessionFactory = sessionFactory; } + + @Override + public void dump(Appendable out, String indent) throws IOException + { + dumpThis(out); + dump(out, indent, getOpenSessions()); + } } 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 ccd8aa1c99c..9bc359b1daf 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 @@ -259,6 +259,9 @@ public class UpgradeConnection extends AbstractConnection session.setOutgoingHandler(extensionStack); extensionStack.setNextOutgoing(connection); + session.addBean(extensionStack); + connectPromise.getClient().addBean(session); + // Now swap out the connection endp.setConnection(connection); connection.onOpen(); diff --git a/jetty-websocket/websocket-client/src/main/java/org/eclipse/jetty/websocket/client/io/WebSocketClientConnection.java b/jetty-websocket/websocket-client/src/main/java/org/eclipse/jetty/websocket/client/io/WebSocketClientConnection.java index 1acee95e41f..a362248a7f9 100644 --- a/jetty-websocket/websocket-client/src/main/java/org/eclipse/jetty/websocket/client/io/WebSocketClientConnection.java +++ b/jetty-websocket/websocket-client/src/main/java/org/eclipse/jetty/websocket/client/io/WebSocketClientConnection.java @@ -26,7 +26,7 @@ import java.util.concurrent.atomic.AtomicBoolean; import org.eclipse.jetty.io.EndPoint; import org.eclipse.jetty.util.log.Log; import org.eclipse.jetty.util.log.Logger; -import org.eclipse.jetty.websocket.api.ProtocolException; +import org.eclipse.jetty.websocket.api.BatchMode; import org.eclipse.jetty.websocket.api.WebSocketPolicy; import org.eclipse.jetty.websocket.api.WriteCallback; import org.eclipse.jetty.websocket.api.extensions.Frame; @@ -96,26 +96,16 @@ public class WebSocketClientConnection extends AbstractWebSocketConnection } /** - * Overrride to set masker + * Override to set the masker. */ @Override - public void outgoingFrame(Frame frame, WriteCallback callback) + public void outgoingFrame(Frame frame, WriteCallback callback, BatchMode batchMode) { if (frame instanceof WebSocketFrame) { - if (masker == null) - { - ProtocolException ex = new ProtocolException("Must set a Masker"); - LOG.warn(ex); - if (callback != null) - { - callback.writeFailed(ex); - } - return; - } masker.setMask((WebSocketFrame)frame); } - super.outgoingFrame(frame,callback); + super.outgoingFrame(frame,callback, batchMode); } @Override diff --git a/jetty-websocket/websocket-client/src/test/java/org/eclipse/jetty/websocket/client/ClientWriteThread.java b/jetty-websocket/websocket-client/src/test/java/org/eclipse/jetty/websocket/client/ClientWriteThread.java index 4b83a016695..3e484ec406a 100644 --- a/jetty-websocket/websocket-client/src/test/java/org/eclipse/jetty/websocket/client/ClientWriteThread.java +++ b/jetty-websocket/websocket-client/src/test/java/org/eclipse/jetty/websocket/client/ClientWriteThread.java @@ -18,14 +18,13 @@ package org.eclipse.jetty.websocket.client; -import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicInteger; import org.eclipse.jetty.util.log.Log; import org.eclipse.jetty.util.log.Logger; +import org.eclipse.jetty.websocket.api.BatchMode; import org.eclipse.jetty.websocket.api.RemoteEndpoint; import org.eclipse.jetty.websocket.api.Session; @@ -79,10 +78,13 @@ public class ClientWriteThread extends Thread TimeUnit.MILLISECONDS.sleep(slowness); } } + if (remote.getBatchMode() == BatchMode.ON) + remote.flush(); // block on write of last message - lastMessage.get(2,TimeUnit.MINUTES); // block on write + if (lastMessage != null) + lastMessage.get(2,TimeUnit.MINUTES); // block on write } - catch (InterruptedException | ExecutionException | TimeoutException e) + catch (Exception e) { LOG.warn(e); } diff --git a/jetty-websocket/websocket-client/src/test/java/org/eclipse/jetty/websocket/client/ServerReadThread.java b/jetty-websocket/websocket-client/src/test/java/org/eclipse/jetty/websocket/client/ServerReadThread.java index 0c4ca8fe458..a059583ac47 100644 --- a/jetty-websocket/websocket-client/src/test/java/org/eclipse/jetty/websocket/client/ServerReadThread.java +++ b/jetty-websocket/websocket-client/src/test/java/org/eclipse/jetty/websocket/client/ServerReadThread.java @@ -18,8 +18,6 @@ package org.eclipse.jetty.websocket.client; -import static org.hamcrest.Matchers.*; - import java.io.IOException; import java.nio.ByteBuffer; import java.util.Queue; @@ -37,6 +35,8 @@ import org.eclipse.jetty.websocket.common.WebSocketFrame; import org.eclipse.jetty.websocket.common.test.BlockheadServer.ServerConnection; import org.junit.Assert; +import static org.hamcrest.Matchers.is; + public class ServerReadThread extends Thread { private static final int BUFFER_SIZE = 8192; @@ -44,13 +44,13 @@ public class ServerReadThread extends Thread private final ServerConnection conn; private boolean active = true; private int slowness = -1; // disabled is default - private AtomicInteger frameCount = new AtomicInteger(); - private CountDownLatch expectedMessageCount; + private final AtomicInteger frameCount = new AtomicInteger(); + private final CountDownLatch expectedMessageCount; - public ServerReadThread(ServerConnection conn) + public ServerReadThread(ServerConnection conn, int expectedMessages) { this.conn = conn; - this.expectedMessageCount = new CountDownLatch(1); + this.expectedMessageCount = new CountDownLatch(expectedMessages); } public void cancel() @@ -75,14 +75,12 @@ public class ServerReadThread extends Thread ByteBuffer buf = bufferPool.acquire(BUFFER_SIZE,false); BufferUtil.clearToFill(buf); - int len = 0; - try { while (active) { BufferUtil.clearToFill(buf); - len = conn.read(buf); + int len = conn.read(buf); if (len > 0) { @@ -108,7 +106,7 @@ public class ServerReadThread extends Thread } if (slowness > 0) { - TimeUnit.MILLISECONDS.sleep(slowness); + TimeUnit.MILLISECONDS.sleep(getSlowness()); } } } @@ -122,11 +120,6 @@ public class ServerReadThread extends Thread } } - public void setExpectedMessageCount(int expectedMessageCount) - { - this.expectedMessageCount = new CountDownLatch(expectedMessageCount); - } - public void setSlowness(int slowness) { this.slowness = slowness; diff --git a/jetty-websocket/websocket-client/src/test/java/org/eclipse/jetty/websocket/client/SessionTest.java b/jetty-websocket/websocket-client/src/test/java/org/eclipse/jetty/websocket/client/SessionTest.java index 5e74e1171f4..d88236fe590 100644 --- a/jetty-websocket/websocket-client/src/test/java/org/eclipse/jetty/websocket/client/SessionTest.java +++ b/jetty-websocket/websocket-client/src/test/java/org/eclipse/jetty/websocket/client/SessionTest.java @@ -18,13 +18,13 @@ package org.eclipse.jetty.websocket.client; -import static org.hamcrest.Matchers.*; - import java.net.URI; import java.util.Set; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; +import org.eclipse.jetty.websocket.api.BatchMode; +import org.eclipse.jetty.websocket.api.RemoteEndpoint; import org.eclipse.jetty.websocket.api.Session; import org.eclipse.jetty.websocket.common.WebSocketSession; import org.eclipse.jetty.websocket.common.test.BlockheadServer; @@ -34,6 +34,9 @@ import org.junit.Assert; import org.junit.Before; import org.junit.Test; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; + public class SessionTest { private BlockheadServer server; @@ -81,7 +84,10 @@ public class SessionTest Assert.assertThat("client.connectionManager.sessions.size",client.getConnectionManager().getSessions().size(),is(1)); - cliSock.getSession().getRemote().sendStringByFuture("Hello World!"); + RemoteEndpoint remote = cliSock.getSession().getRemote(); + remote.sendStringByFuture("Hello World!"); + if (remote.getBatchMode() == BatchMode.ON) + remote.flush(); srvSock.echoMessage(1,TimeUnit.MILLISECONDS,500); // wait for response from server cliSock.waitForMessage(500,TimeUnit.MILLISECONDS); 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 4337d625dba..5589c831490 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 @@ -18,8 +18,6 @@ package org.eclipse.jetty.websocket.client; -import static org.hamcrest.Matchers.is; - import java.net.URI; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; @@ -36,6 +34,8 @@ import org.junit.Before; import org.junit.Rule; import org.junit.Test; +import static org.hamcrest.Matchers.is; + public class SlowClientTest { @Rule @@ -79,24 +79,23 @@ public class SlowClientTest client.getPolicy().setIdleTimeout(60000); URI wsUri = server.getWsUri(); - Future future = client.connect(tsocket,wsUri); + Future future = client.connect(tsocket, wsUri); ServerConnection sconnection = server.accept(); sconnection.setSoTimeout(60000); sconnection.upgrade(); // Confirm connected - future.get(500,TimeUnit.MILLISECONDS); - tsocket.waitForConnected(500,TimeUnit.MILLISECONDS); + future.get(500, TimeUnit.MILLISECONDS); + tsocket.waitForConnected(500, TimeUnit.MILLISECONDS); + + int messageCount = 10; // Setup server read thread - ServerReadThread reader = new ServerReadThread(sconnection); - reader.setExpectedMessageCount(Integer.MAX_VALUE); // keep reading till I tell you to stop + ServerReadThread reader = new ServerReadThread(sconnection, messageCount); reader.start(); // Have client write slowly. - int messageCount = 1000; - ClientWriteThread writer = new ClientWriteThread(tsocket.getSession()); writer.setMessageCount(messageCount); writer.setMessage("Hello"); @@ -104,13 +103,15 @@ public class SlowClientTest writer.start(); writer.join(); + reader.waitForExpectedMessageCount(1, TimeUnit.MINUTES); + // Verify receive - Assert.assertThat("Frame Receive Count",reader.getFrameCount(),is(messageCount)); + Assert.assertThat("Frame Receive Count", reader.getFrameCount(), is(messageCount)); // Close - tsocket.getSession().close(StatusCode.NORMAL,"Done"); + tsocket.getSession().close(StatusCode.NORMAL, "Done"); - Assert.assertTrue("Client Socket Closed",tsocket.closeLatch.await(3,TimeUnit.MINUTES)); + Assert.assertTrue("Client Socket Closed", tsocket.closeLatch.await(3, TimeUnit.MINUTES)); tsocket.assertCloseCode(StatusCode.NORMAL); reader.cancel(); // stop reading 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 ddc26b622fe..80720f329f3 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 @@ -18,8 +18,6 @@ package org.eclipse.jetty.websocket.client; -import static org.hamcrest.Matchers.is; - import java.net.URI; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; @@ -37,6 +35,8 @@ import org.junit.Before; import org.junit.Rule; import org.junit.Test; +import static org.hamcrest.Matchers.is; + public class SlowServerTest { @Rule @@ -91,11 +91,10 @@ public class SlowServerTest future.get(500,TimeUnit.MILLISECONDS); tsocket.waitForConnected(500,TimeUnit.MILLISECONDS); - int messageCount = 10; // TODO: increase to 1000 + int messageCount = 10; // Setup slow server read thread - ServerReadThread reader = new ServerReadThread(sconnection); - reader.setExpectedMessageCount(messageCount); + ServerReadThread reader = new ServerReadThread(sconnection, messageCount); reader.setSlowness(100); // slow it down reader.start(); 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 03856618fd0..62d1c7df0e4 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 @@ -18,11 +18,6 @@ package org.eclipse.jetty.websocket.client; -import static org.hamcrest.Matchers.greaterThan; -import static org.hamcrest.Matchers.is; -import static org.hamcrest.Matchers.notNullValue; -import static org.hamcrest.Matchers.nullValue; - import java.net.InetSocketAddress; import java.net.URI; import java.util.Arrays; @@ -33,6 +28,8 @@ import java.util.concurrent.TimeUnit; import org.eclipse.jetty.toolchain.test.AdvancedRunner; import org.eclipse.jetty.util.StringUtil; +import org.eclipse.jetty.websocket.api.BatchMode; +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.common.frames.TextFrame; @@ -45,6 +42,11 @@ import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import static org.hamcrest.Matchers.greaterThan; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.nullValue; + @RunWith(AdvancedRunner.class) public class WebSocketClientTest { @@ -118,7 +120,10 @@ public class WebSocketClientTest Assert.assertThat("client.connectionManager.sessions.size",client.getConnectionManager().getSessions().size(),is(1)); - cliSock.getSession().getRemote().sendStringByFuture("Hello World!"); + RemoteEndpoint remote = cliSock.getSession().getRemote(); + remote.sendStringByFuture("Hello World!"); + if (remote.getBatchMode() == BatchMode.ON) + remote.flush(); srvSock.echoMessage(1,TimeUnit.MILLISECONDS,500); // wait for response from server cliSock.waitForMessage(500,TimeUnit.MILLISECONDS); diff --git a/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/Generator.java b/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/Generator.java index ac62849ea05..076b78ab7a5 100644 --- a/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/Generator.java +++ b/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/Generator.java @@ -58,7 +58,7 @@ public class Generator /** * The overhead (maximum) for a framing header. Assuming a maximum sized payload with masking key. */ - public static final int OVERHEAD = 28; + public static final int MAX_HEADER_LENGTH = 28; private final WebSocketBehavior behavior; private final ByteBufferPool bufferPool; @@ -193,12 +193,18 @@ public class Generator public ByteBuffer generateHeaderBytes(Frame frame) { + ByteBuffer buffer = bufferPool.acquire(MAX_HEADER_LENGTH,true); + generateHeaderBytes(frame,buffer); + return buffer; + } + + public void generateHeaderBytes(Frame frame, ByteBuffer buffer) + { + int p=BufferUtil.flipToFill(buffer); + // we need a framing header assertFrameValid(frame); - - ByteBuffer buffer = bufferPool.acquire(OVERHEAD,true); - BufferUtil.clearToFill(buffer); - + /* * start the generation process */ @@ -284,7 +290,9 @@ public class Generator { byte[] mask = frame.getMask(); buffer.put(mask); - int maskInt = ByteBuffer.wrap(mask).getInt(); + int maskInt = 0; + for (byte maskByte : mask) + maskInt = (maskInt << 8) + (maskByte & 0xFF); // perform data masking here ByteBuffer payload = frame.getPayload(); @@ -311,8 +319,7 @@ public class Generator } } - BufferUtil.flipToFlush(buffer,0); - return buffer; + BufferUtil.flipToFlush(buffer,p); } /** 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 44ae1c9d8d3..0cf7e5f4b2c 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 @@ -97,10 +97,8 @@ public class Parser private void assertSanePayloadLength(long 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) { @@ -184,9 +182,7 @@ public class Parser protected void notifyFrame(final Frame f) { if (LOG.isDebugEnabled()) - { LOG.debug("{} Notify {}",policy.getBehavior(),getIncomingFramesHandler()); - } if (policy.getBehavior() == WebSocketBehavior.SERVER) { @@ -243,7 +239,7 @@ public class Parser incomingFramesHandler.incomingError(e); } - public synchronized void parse(ByteBuffer buffer) + public void parse(ByteBuffer buffer) { if (buffer.remaining() <= 0) { @@ -256,7 +252,8 @@ public class Parser // parse through all the frames in the buffer while (parseFrame(buffer)) { - LOG.debug("{} Parsed Frame: {}",policy.getBehavior(),frame); + if (LOG.isDebugEnabled()) + LOG.debug("{} Parsed Frame: {}",policy.getBehavior(),frame); notifyFrame(frame); if (frame.isDataFrame()) { @@ -301,7 +298,8 @@ public class Parser */ private boolean parseFrame(ByteBuffer buffer) { - LOG.debug("{} Parsing {} bytes",policy.getBehavior(),buffer.remaining()); + if (LOG.isDebugEnabled()) + LOG.debug("{} Parsing {} bytes",policy.getBehavior(),buffer.remaining()); while (buffer.hasRemaining()) { switch (state) @@ -320,14 +318,12 @@ public class Parser } if (LOG.isDebugEnabled()) - { LOG.debug("OpCode {}, fin={} rsv={}{}{}", OpCode.name(opcode), fin, (isRsv1InUse()?'1':'.'), (isRsv2InUse()?'1':'.'), (isRsv3InUse()?'1':'.')); - } // base framing flags switch(opcode) @@ -419,9 +415,7 @@ public class Parser else { if (LOG.isDebugEnabled()) - { LOG.debug("OpCode {}, fin={} rsv=000",OpCode.name(opcode),fin); - } } state = State.PAYLOAD_LEN; @@ -598,9 +592,7 @@ public class Parser buffer.position(buffer.position() + window.remaining()); if (LOG.isDebugEnabled()) - { LOG.debug("Window: {}",BufferUtil.toDetailString(window)); - } maskProcessor.process(window); 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 48ee92eb81b..b03ee02522f 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 @@ -28,6 +28,7 @@ import java.util.concurrent.atomic.AtomicInteger; 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.BatchMode; import org.eclipse.jetty.websocket.api.RemoteEndpoint; import org.eclipse.jetty.websocket.api.WriteCallback; import org.eclipse.jetty.websocket.api.extensions.OutgoingFrames; @@ -37,6 +38,7 @@ import org.eclipse.jetty.websocket.common.frames.DataFrame; import org.eclipse.jetty.websocket.common.frames.PingFrame; import org.eclipse.jetty.websocket.common.frames.PongFrame; import org.eclipse.jetty.websocket.common.frames.TextFrame; +import org.eclipse.jetty.websocket.common.io.FrameFlusher; import org.eclipse.jetty.websocket.common.io.FutureWriteCallback; /** @@ -44,23 +46,22 @@ import org.eclipse.jetty.websocket.common.io.FutureWriteCallback; */ public class WebSocketRemoteEndpoint implements RemoteEndpoint { - /** Message Type*/ - private enum MsgType + private enum MsgType { BLOCKING, ASYNC, STREAMING, PARTIAL_TEXT, PARTIAL_BINARY - }; - + } + private static final WriteCallback NOOP_CALLBACK = new WriteCallback() { @Override public void writeSuccess() { } - + @Override public void writeFailed(Throwable x) { @@ -68,21 +69,25 @@ public class WebSocketRemoteEndpoint implements RemoteEndpoint }; private static final Logger LOG = Log.getLogger(WebSocketRemoteEndpoint.class); - public final LogicalConnection connection; - public final OutgoingFrames outgoing; - /** JSR-356 blocking send behaviour message and Type sanity to support partial send properly */ - private final static int ASYNC_MASK = 0x0000FFFF; - private final static int BLOCK_MASK = 0x00010000; - private final static int STREAM_MASK = 0x00020000; - private final static int PARTIAL_TEXT_MASK= 0x00040000; - private final static int PARTIAL_BINARY_MASK= 0x00080000; - + private final static int ASYNC_MASK = 0x0000FFFF; + private final static int BLOCK_MASK = 0x00010000; + private final static int STREAM_MASK = 0x00020000; + private final static int PARTIAL_TEXT_MASK = 0x00040000; + private final static int PARTIAL_BINARY_MASK = 0x00080000; + + private final LogicalConnection connection; + private final OutgoingFrames outgoing; private final AtomicInteger msgState = new AtomicInteger(); - private final BlockingWriteCallback blocker = new BlockingWriteCallback(); + private volatile BatchMode batchMode; public WebSocketRemoteEndpoint(LogicalConnection connection, OutgoingFrames outgoing) + { + this(connection, outgoing, BatchMode.AUTO); + } + + public WebSocketRemoteEndpoint(LogicalConnection connection, OutgoingFrames outgoing, BatchMode batchMode) { if (connection == null) { @@ -90,11 +95,12 @@ public class WebSocketRemoteEndpoint implements RemoteEndpoint } this.connection = connection; this.outgoing = outgoing; + this.batchMode = batchMode; } private void blockingWrite(WebSocketFrame frame) throws IOException { - uncheckedSendFrame(frame,blocker); + uncheckedSendFrame(frame, blocker); blocker.block(); } @@ -106,107 +112,107 @@ public class WebSocketRemoteEndpoint implements RemoteEndpoint // Blocking -> Pending!! ; Async -> STREAMING ; Partial -> Pending!! ; Stream -> STREAMING // Blocking -> Pending!! ; Async -> Pending!! ; Partial -> PARTIAL_TEXT ; Stream -> Pending!! // Blocking -> Pending!! ; Async -> Pending!! ; Partial -> PARTIAL_BIN ; Stream -> Pending!! - - while(true) + + while (true) { int state = msgState.get(); - + switch (type) { case BLOCKING: - if ((state&(PARTIAL_BINARY_MASK+PARTIAL_TEXT_MASK))!=0) - throw new IllegalStateException(String.format("Partial message pending %x for %s",state,type)); - if ((state&BLOCK_MASK)!=0) - throw new IllegalStateException(String.format("Blocking message pending %x for %s",state,type)); - if (msgState.compareAndSet(state,state|BLOCK_MASK)) - return state==0; + if ((state & (PARTIAL_BINARY_MASK + PARTIAL_TEXT_MASK)) != 0) + throw new IllegalStateException(String.format("Partial message pending %x for %s", state, type)); + if ((state & BLOCK_MASK) != 0) + throw new IllegalStateException(String.format("Blocking message pending %x for %s", state, type)); + if (msgState.compareAndSet(state, state | BLOCK_MASK)) + return state == 0; break; - + case ASYNC: - if ((state&(PARTIAL_BINARY_MASK+PARTIAL_TEXT_MASK))!=0) - throw new IllegalStateException(String.format("Partial message pending %x for %s",state,type)); - if ((state&ASYNC_MASK)==ASYNC_MASK) - throw new IllegalStateException(String.format("Too many async sends: %x",state)); - if (msgState.compareAndSet(state,state+1)) - return state==0; + if ((state & (PARTIAL_BINARY_MASK + PARTIAL_TEXT_MASK)) != 0) + throw new IllegalStateException(String.format("Partial message pending %x for %s", state, type)); + if ((state & ASYNC_MASK) == ASYNC_MASK) + throw new IllegalStateException(String.format("Too many async sends: %x", state)); + if (msgState.compareAndSet(state, state + 1)) + return state == 0; break; - + case STREAMING: - if ((state&(PARTIAL_BINARY_MASK+PARTIAL_TEXT_MASK))!=0) - throw new IllegalStateException(String.format("Partial message pending %x for %s",state,type)); - if ((state&STREAM_MASK)!=0) - throw new IllegalStateException(String.format("Already streaming %x for %s",state,type)); - if (msgState.compareAndSet(state,state|STREAM_MASK)) - return state==0; + if ((state & (PARTIAL_BINARY_MASK + PARTIAL_TEXT_MASK)) != 0) + throw new IllegalStateException(String.format("Partial message pending %x for %s", state, type)); + if ((state & STREAM_MASK) != 0) + throw new IllegalStateException(String.format("Already streaming %x for %s", state, type)); + if (msgState.compareAndSet(state, state | STREAM_MASK)) + return state == 0; break; - + case PARTIAL_BINARY: - if (state==PARTIAL_BINARY_MASK) + if (state == PARTIAL_BINARY_MASK) return false; - if (state==0) + if (state == 0) { - if (msgState.compareAndSet(0,state|PARTIAL_BINARY_MASK)) + if (msgState.compareAndSet(0, state | PARTIAL_BINARY_MASK)) return true; } - throw new IllegalStateException(String.format("Cannot send %s in state %x",type,state)); - + throw new IllegalStateException(String.format("Cannot send %s in state %x", type, state)); + case PARTIAL_TEXT: - if (state==PARTIAL_TEXT_MASK) + if (state == PARTIAL_TEXT_MASK) return false; - if (state==0) + if (state == 0) { - if (msgState.compareAndSet(0,state|PARTIAL_TEXT_MASK)) + if (msgState.compareAndSet(0, state | PARTIAL_TEXT_MASK)) return true; } - throw new IllegalStateException(String.format("Cannot send %s in state %x",type,state)); + throw new IllegalStateException(String.format("Cannot send %s in state %x", type, state)); } } } private void unlockMsg(MsgType type) { - while(true) + while (true) { int state = msgState.get(); - + switch (type) { case BLOCKING: - if ((state&BLOCK_MASK)==0) - throw new IllegalStateException(String.format("Not Blocking in state %x",state)); - if (msgState.compareAndSet(state,state&~BLOCK_MASK)) + if ((state & BLOCK_MASK) == 0) + throw new IllegalStateException(String.format("Not Blocking in state %x", state)); + if (msgState.compareAndSet(state, state & ~BLOCK_MASK)) return; break; - + case ASYNC: - if ((state&ASYNC_MASK)==0) - throw new IllegalStateException(String.format("Not Async in %x",state)); - if (msgState.compareAndSet(state,state-1)) + if ((state & ASYNC_MASK) == 0) + throw new IllegalStateException(String.format("Not Async in %x", state)); + if (msgState.compareAndSet(state, state - 1)) return; break; - + case STREAMING: - if ((state&STREAM_MASK)==0) - throw new IllegalStateException(String.format("Not Streaming in state %x",state)); - if (msgState.compareAndSet(state,state&~STREAM_MASK)) + if ((state & STREAM_MASK) == 0) + throw new IllegalStateException(String.format("Not Streaming in state %x", state)); + if (msgState.compareAndSet(state, state & ~STREAM_MASK)) return; break; - + case PARTIAL_BINARY: - if (msgState.compareAndSet(PARTIAL_BINARY_MASK,0)) + if (msgState.compareAndSet(PARTIAL_BINARY_MASK, 0)) return; - throw new IllegalStateException(String.format("Not Partial Binary in state %x",state)); - + throw new IllegalStateException(String.format("Not Partial Binary in state %x", state)); + case PARTIAL_TEXT: - if (msgState.compareAndSet(PARTIAL_TEXT_MASK,0)) + if (msgState.compareAndSet(PARTIAL_TEXT_MASK, 0)) return; - throw new IllegalStateException(String.format("Not Partial Text in state %x",state)); - + throw new IllegalStateException(String.format("Not Partial Text in state %x", state)); + } } } - - + + public InetSocketAddress getInetSocketAddress() { return connection.getRemoteAddress(); @@ -214,15 +220,14 @@ public class WebSocketRemoteEndpoint implements RemoteEndpoint /** * Internal - * - * @param frame - * the frame to write + * + * @param frame the frame to write * @return the future for the network write of the frame */ private Future sendAsyncFrame(WebSocketFrame frame) { FutureWriteCallback future = new FutureWriteCallback(); - uncheckedSendFrame(frame,future); + uncheckedSendFrame(frame, future); return future; } @@ -238,7 +243,7 @@ public class WebSocketRemoteEndpoint implements RemoteEndpoint connection.getIOState().assertOutputOpen(); if (LOG.isDebugEnabled()) { - LOG.debug("sendBytes with {}",BufferUtil.toDetailString(data)); + LOG.debug("sendBytes with {}", BufferUtil.toDetailString(data)); } blockingWrite(new BinaryFrame().setPayload(data)); } @@ -256,7 +261,7 @@ public class WebSocketRemoteEndpoint implements RemoteEndpoint { if (LOG.isDebugEnabled()) { - LOG.debug("sendBytesByFuture with {}",BufferUtil.toDetailString(data)); + LOG.debug("sendBytesByFuture with {}", BufferUtil.toDetailString(data)); } return sendAsyncFrame(new BinaryFrame().setPayload(data)); } @@ -274,9 +279,9 @@ public class WebSocketRemoteEndpoint implements RemoteEndpoint { if (LOG.isDebugEnabled()) { - LOG.debug("sendBytes({}, {})",BufferUtil.toDetailString(data),callback); + LOG.debug("sendBytes({}, {})", BufferUtil.toDetailString(data), callback); } - uncheckedSendFrame(new BinaryFrame().setPayload(data),callback==null?NOOP_CALLBACK:callback); + uncheckedSendFrame(new BinaryFrame().setPayload(data), callback == null ? NOOP_CALLBACK : callback); } finally { @@ -284,17 +289,15 @@ public class WebSocketRemoteEndpoint implements RemoteEndpoint } } - /* ------------------------------------------------------------ */ - /** unchecked send - * @param frame - * @param callback - */ public void uncheckedSendFrame(WebSocketFrame frame, WriteCallback callback) { try { + BatchMode batchMode = BatchMode.OFF; + if (frame.isDataFrame()) + batchMode = getBatchMode(); connection.getIOState().assertOutputOpen(); - outgoing.outgoingFrame(frame,callback); + outgoing.outgoingFrame(frame, callback, batchMode); } catch (IOException e) { @@ -305,14 +308,14 @@ public class WebSocketRemoteEndpoint implements RemoteEndpoint @Override public void sendPartialBytes(ByteBuffer fragment, boolean isLast) throws IOException { - boolean first=lockMsg(MsgType.PARTIAL_BINARY); + boolean first = lockMsg(MsgType.PARTIAL_BINARY); try { if (LOG.isDebugEnabled()) { - LOG.debug("sendPartialBytes({}, {})",BufferUtil.toDetailString(fragment),isLast); + LOG.debug("sendPartialBytes({}, {})", BufferUtil.toDetailString(fragment), isLast); } - DataFrame frame = first?new BinaryFrame():new ContinuationFrame(); + DataFrame frame = first ? new BinaryFrame() : new ContinuationFrame(); frame.setPayload(fragment); frame.setFin(isLast); blockingWrite(frame); @@ -327,15 +330,15 @@ public class WebSocketRemoteEndpoint implements RemoteEndpoint @Override public void sendPartialString(String fragment, boolean isLast) throws IOException { - boolean first=lockMsg(MsgType.PARTIAL_TEXT); + boolean first = lockMsg(MsgType.PARTIAL_TEXT); try { if (LOG.isDebugEnabled()) { - LOG.debug("sendPartialString({}, {})",fragment,isLast); + LOG.debug("sendPartialString({}, {})", fragment, isLast); } - DataFrame frame = first?new TextFrame():new ContinuationFrame(); - frame.setPayload(BufferUtil.toBuffer(fragment,StandardCharsets.UTF_8)); + DataFrame frame = first ? new TextFrame() : new ContinuationFrame(); + frame.setPayload(BufferUtil.toBuffer(fragment, StandardCharsets.UTF_8)); frame.setFin(isLast); blockingWrite(frame); } @@ -351,7 +354,7 @@ public class WebSocketRemoteEndpoint implements RemoteEndpoint { if (LOG.isDebugEnabled()) { - LOG.debug("sendPing with {}",BufferUtil.toDetailString(applicationData)); + LOG.debug("sendPing with {}", BufferUtil.toDetailString(applicationData)); } sendAsyncFrame(new PingFrame().setPayload(applicationData)); } @@ -361,7 +364,7 @@ public class WebSocketRemoteEndpoint implements RemoteEndpoint { if (LOG.isDebugEnabled()) { - LOG.debug("sendPong with {}",BufferUtil.toDetailString(applicationData)); + LOG.debug("sendPong with {}", BufferUtil.toDetailString(applicationData)); } sendAsyncFrame(new PongFrame().setPayload(applicationData)); } @@ -375,7 +378,7 @@ public class WebSocketRemoteEndpoint implements RemoteEndpoint WebSocketFrame frame = new TextFrame().setPayload(text); if (LOG.isDebugEnabled()) { - LOG.debug("sendString with {}",BufferUtil.toDetailString(frame.getPayload())); + LOG.debug("sendString with {}", BufferUtil.toDetailString(frame.getPayload())); } blockingWrite(frame); } @@ -394,9 +397,9 @@ public class WebSocketRemoteEndpoint implements RemoteEndpoint TextFrame frame = new TextFrame().setPayload(text); if (LOG.isDebugEnabled()) { - LOG.debug("sendStringByFuture with {}",BufferUtil.toDetailString(frame.getPayload())); + LOG.debug("sendStringByFuture with {}", BufferUtil.toDetailString(frame.getPayload())); } - return sendAsyncFrame(frame); + return sendAsyncFrame(frame); } finally { @@ -413,13 +416,47 @@ public class WebSocketRemoteEndpoint implements RemoteEndpoint TextFrame frame = new TextFrame().setPayload(text); if (LOG.isDebugEnabled()) { - LOG.debug("sendString({},{})",BufferUtil.toDetailString(frame.getPayload()),callback); + LOG.debug("sendString({},{})", BufferUtil.toDetailString(frame.getPayload()), callback); } - uncheckedSendFrame(frame,callback==null?NOOP_CALLBACK:callback); + uncheckedSendFrame(frame, callback == null ? NOOP_CALLBACK : callback); } finally { unlockMsg(MsgType.ASYNC); } } + + @Override + public BatchMode getBatchMode() + { + return batchMode; + } + + // Only the JSR needs to have this method exposed. + // In the Jetty implementation the batching is set + // at the moment of opening the session. + public void setBatchMode(BatchMode batchMode) + { + this.batchMode = batchMode; + } + + public void flush() throws IOException + { + lockMsg(MsgType.ASYNC); + try + { + uncheckedSendFrame(FrameFlusher.FLUSH_FRAME, blocker); + blocker.block(); + } + finally + { + unlockMsg(MsgType.ASYNC); + } + } + + @Override + public String toString() + { + return String.format("%s@%x[batching=%b]", getClass().getSimpleName(), hashCode(), getBatchMode()); + } } 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 60fc1969a3d..e038e4d9109 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 @@ -33,6 +33,7 @@ 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.BatchMode; import org.eclipse.jetty.websocket.api.CloseStatus; import org.eclipse.jetty.websocket.api.RemoteEndpoint; import org.eclipse.jetty.websocket.api.Session; @@ -57,8 +58,8 @@ public class WebSocketSession extends ContainerLifeCycle implements Session, Inc private final URI requestURI; private final EventDriver websocket; private final LogicalConnection connection; - private final Executor executor; private final SessionListener[] sessionListeners; + private final Executor executor; private ExtensionFactory extensionFactory; private String protocolVersion; private Map parameterMap = new HashMap<>(); @@ -69,7 +70,7 @@ public class WebSocketSession extends ContainerLifeCycle implements Session, Inc private UpgradeRequest upgradeRequest; private UpgradeResponse upgradeResponse; - public WebSocketSession(URI requestURI, EventDriver websocket, LogicalConnection connection, SessionListener[] sessionListeners) + public WebSocketSession(URI requestURI, EventDriver websocket, LogicalConnection connection, SessionListener... sessionListeners) { if (requestURI == null) { @@ -83,27 +84,26 @@ public class WebSocketSession extends ContainerLifeCycle implements Session, Inc this.executor = connection.getExecutor(); this.outgoingHandler = connection; this.incomingHandler = websocket; - this.connection.getIOState().addListener(this); } @Override public void close() { - this.close(StatusCode.NORMAL,null); + this.close(StatusCode.NORMAL, null); } @Override public void close(CloseStatus closeStatus) { - this.close(closeStatus.getCode(),closeStatus.getPhrase()); + this.close(closeStatus.getCode(), closeStatus.getPhrase()); } @Override public void close(int statusCode, String reason) { - connection.close(statusCode,reason); - notifyClose(statusCode,reason); + connection.close(statusCode, reason); + notifyClose(statusCode, reason); } /** @@ -115,7 +115,7 @@ public class WebSocketSession extends ContainerLifeCycle implements Session, Inc connection.disconnect(); // notify of harsh disconnect - notifyClose(StatusCode.NO_CLOSE,"Harsh disconnect"); + notifyClose(StatusCode.NO_CLOSE, "Harsh disconnect"); } public void dispatch(Runnable runnable) @@ -126,25 +126,25 @@ public class WebSocketSession extends ContainerLifeCycle implements Session, Inc @Override public void dump(Appendable out, String indent) throws IOException { - super.dump(out,indent); + dumpThis(out); out.append(indent).append(" +- incomingHandler : "); if (incomingHandler instanceof Dumpable) { - ((Dumpable)incomingHandler).dump(out,indent + " "); + ((Dumpable)incomingHandler).dump(out, indent + " "); } else { - out.append(incomingHandler.toString()).append('\n'); + out.append(incomingHandler.toString()).append(System.lineSeparator()); } out.append(indent).append(" +- outgoingHandler : "); if (outgoingHandler instanceof Dumpable) { - ((Dumpable)outgoingHandler).dump(out,indent + " "); + ((Dumpable)outgoingHandler).dump(out, indent + " "); } else { - out.append(outgoingHandler.toString()).append('\n'); + out.append(outgoingHandler.toString()).append(System.lineSeparator()); } } @@ -273,7 +273,7 @@ public class WebSocketSession extends ContainerLifeCycle implements Session, Inc { final int prime = 31; int result = 1; - result = (prime * result) + ((connection == null)?0:connection.hashCode()); + result = (prime * result) + ((connection == null) ? 0 : connection.hashCode()); return result; } @@ -328,14 +328,14 @@ public class WebSocketSession extends ContainerLifeCycle implements Session, Inc public void notifyClose(int statusCode, String reason) { - websocket.onClose(new CloseInfo(statusCode,reason)); + websocket.onClose(new CloseInfo(statusCode, reason)); } public void notifyError(Throwable cause) { incomingError(cause); } - + @SuppressWarnings("incomplete-switch") @Override public void onConnectionStateChange(ConnectionState state) @@ -363,9 +363,9 @@ public class WebSocketSession extends ContainerLifeCycle implements Session, Inc if (ioState.wasAbnormalClose()) { CloseInfo close = ioState.getCloseInfo(); - LOG.debug("Detected abnormal close: {}",close); + LOG.debug("Detected abnormal close: {}", close); // notify local endpoint - notifyClose(close.getStatusCode(),close.getReason()); + notifyClose(close.getStatusCode(), close.getReason()); } break; case OPEN: @@ -400,7 +400,7 @@ public class WebSocketSession extends ContainerLifeCycle implements Session, Inc connection.getIOState().onConnected(); // Connect remote - remote = new WebSocketRemoteEndpoint(connection,outgoingHandler); + remote = new WebSocketRemoteEndpoint(connection, outgoingHandler, getBatchMode()); // Open WebSocket websocket.openSession(this); @@ -410,7 +410,7 @@ public class WebSocketSession extends ContainerLifeCycle implements Session, Inc if (LOG.isDebugEnabled()) { - LOG.debug("open -> {}",dump()); + LOG.debug("open -> {}", dump()); } } @@ -450,11 +450,11 @@ public class WebSocketSession extends ContainerLifeCycle implements Session, Inc List values = entry.getValue(); if (values != null) { - this.parameterMap.put(entry.getKey(),values.toArray(new String[values.size()])); + this.parameterMap.put(entry.getKey(), values.toArray(new String[values.size()])); } else { - this.parameterMap.put(entry.getKey(),new String[0]); + this.parameterMap.put(entry.getKey(), new String[0]); } } } @@ -468,7 +468,15 @@ public class WebSocketSession extends ContainerLifeCycle implements Session, Inc @Override public SuspendToken suspend() { - return connection; + return connection.suspend(); + } + + /** + * @return the default (initial) value for the batching mode. + */ + public BatchMode getBatchMode() + { + return BatchMode.AUTO; } @Override 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 index 8d075b49945..1536b72dbb8 100644 --- 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 @@ -79,7 +79,7 @@ public class JettyAnnotatedEventDriver extends AbstractEventDriver { if (events.onBinary.isStreaming()) { - activeMessage = new MessageInputStream(session.getConnection()); + activeMessage = new MessageInputStream(); final MessageAppender msg = activeMessage; dispatch(new Runnable() { @@ -181,7 +181,7 @@ public class JettyAnnotatedEventDriver extends AbstractEventDriver { if (events.onText.isStreaming()) { - activeMessage = new MessageReader(new MessageInputStream(session.getConnection())); + activeMessage = new MessageReader(new MessageInputStream()); final MessageAppender msg = activeMessage; dispatch(new Runnable() { 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 6e1f1e92838..bed96921fe1 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,6 +26,7 @@ import org.eclipse.jetty.util.annotation.ManagedObject; 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.BatchMode; import org.eclipse.jetty.websocket.api.WebSocketPolicy; import org.eclipse.jetty.websocket.api.WriteCallback; import org.eclipse.jetty.websocket.api.extensions.Extension; @@ -162,10 +163,10 @@ public abstract class AbstractExtension extends ContainerLifeCycle implements Ex this.nextIncoming.incomingFrame(frame); } - protected void nextOutgoingFrame(Frame frame, WriteCallback callback) + protected void nextOutgoingFrame(Frame frame, WriteCallback callback, BatchMode batchMode) { log.debug("nextOutgoingFrame({})",frame); - this.nextOutgoing.outgoingFrame(frame,callback); + this.nextOutgoing.outgoingFrame(frame,callback, batchMode); } public void setBufferPool(ByteBufferPool bufferPool) 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 5be1b81068b..18e4a9da5f6 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 @@ -22,12 +22,16 @@ import java.io.IOException; import java.util.ArrayList; import java.util.List; import java.util.ListIterator; +import java.util.Queue; +import org.eclipse.jetty.util.ConcurrentArrayQueue; +import org.eclipse.jetty.util.IteratingCallback; 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.log.Log; import org.eclipse.jetty.util.log.Logger; +import org.eclipse.jetty.websocket.api.BatchMode; import org.eclipse.jetty.websocket.api.WebSocketPolicy; import org.eclipse.jetty.websocket.api.WriteCallback; import org.eclipse.jetty.websocket.api.extensions.Extension; @@ -47,6 +51,8 @@ public class ExtensionStack extends ContainerLifeCycle implements IncomingFrames { private static final Logger LOG = Log.getLogger(ExtensionStack.class); + private final Queue entries = new ConcurrentArrayQueue<>(); + private final IteratingCallback flusher = new Flusher(); private final ExtensionFactory factory; private List extensions; private IncomingFrames nextIncoming; @@ -76,20 +82,20 @@ public class ExtensionStack extends ContainerLifeCycle implements IncomingFrames // Wire up Extensions if ((extensions != null) && (extensions.size() > 0)) { - ListIterator eiter = extensions.listIterator(); + ListIterator exts = extensions.listIterator(); // Connect outgoings - while (eiter.hasNext()) + while (exts.hasNext()) { - Extension ext = eiter.next(); + Extension ext = exts.next(); ext.setNextOutgoingFrames(nextOutgoing); nextOutgoing = ext; } // Connect incomings - while (eiter.hasPrevious()) + while (exts.hasPrevious()) { - Extension ext = eiter.previous(); + Extension ext = exts.previous(); ext.setNextIncomingFrames(nextIncoming); nextIncoming = ext; } @@ -104,13 +110,13 @@ public class ExtensionStack extends ContainerLifeCycle implements IncomingFrames IncomingFrames websocket = getLastIncoming(); OutgoingFrames network = getLastOutgoing(); - out.append(indent).append(" +- Stack\n"); - out.append(indent).append(" +- Network : ").append(network.toString()).append('\n'); + out.append(indent).append(" +- Stack").append(System.lineSeparator()); + out.append(indent).append(" +- Network : ").append(network.toString()).append(System.lineSeparator()); for (Extension ext : extensions) { - out.append(indent).append(" +- Extension: ").append(ext.toString()).append('\n'); + out.append(indent).append(" +- Extension: ").append(ext.toString()).append(System.lineSeparator()); } - out.append(indent).append(" +- Websocket: ").append(websocket.toString()).append('\n'); + out.append(indent).append(" +- Websocket: ").append(websocket.toString()).append(System.lineSeparator()); } @ManagedAttribute(name = "Extension List", readonly = true) @@ -247,6 +253,8 @@ public class ExtensionStack extends ContainerLifeCycle implements IncomingFrames // Add Extension extensions.add(ext); + addBean(ext); + LOG.debug("Adding Extension: {}",config); // Record RSV Claims @@ -263,14 +271,15 @@ public class ExtensionStack extends ContainerLifeCycle implements IncomingFrames rsvClaims[2] = ext.getName(); } } - - addBean(extensions); } @Override - public void outgoingFrame(Frame frame, WriteCallback callback) + public void outgoingFrame(Frame frame, WriteCallback callback, BatchMode batchMode) { - nextOutgoing.outgoingFrame(frame,callback); + FrameEntry entry = new FrameEntry(frame, callback, batchMode); + LOG.debug("Queuing {}", entry); + entries.offer(entry); + flusher.iterate(); } public void setNextIncoming(IncomingFrames nextIncoming) @@ -299,7 +308,8 @@ public class ExtensionStack extends ContainerLifeCycle implements IncomingFrames { StringBuilder s = new StringBuilder(); s.append("ExtensionStack["); - s.append("extensions="); + s.append("queueSize=").append(entries.size()); + s.append(",extensions="); if (extensions == null) { s.append(""); @@ -331,4 +341,93 @@ public class ExtensionStack extends ContainerLifeCycle implements IncomingFrames s.append("]"); return s.toString(); } + + private static class FrameEntry + { + private final Frame frame; + private final WriteCallback callback; + private final BatchMode batchMode; + + private FrameEntry(Frame frame, WriteCallback callback, BatchMode batchMode) + { + this.frame = frame; + this.callback = callback; + this.batchMode = batchMode; + } + + @Override + public String toString() + { + return frame.toString(); + } + } + + private class Flusher extends IteratingCallback implements WriteCallback + { + private FrameEntry current; + + @Override + protected Action process() throws Exception + { + current = entries.poll(); + LOG.debug("Processing {}", current); + if (current == null) + return Action.IDLE; + nextOutgoing.outgoingFrame(current.frame, this, current.batchMode); + return Action.SCHEDULED; + } + + @Override + protected void completed() + { + // This IteratingCallback never completes. + } + + @Override + public void writeSuccess() + { + // Notify first then call succeeded(), otherwise + // write callbacks may be invoked out of order. + notifyCallbackSuccess(current.callback); + succeeded(); + } + + @Override + public void writeFailed(Throwable x) + { + // Notify first, the call succeeded() to drain the queue. + // We don't want to call failed(x) because that will put + // this flusher into a final state that cannot be exited, + // and the failure of a frame may not mean that the whole + // connection is now invalid. + notifyCallbackFailure(current.callback, x); + succeeded(); + } + + private void notifyCallbackSuccess(WriteCallback callback) + { + try + { + if (callback != null) + callback.writeSuccess(); + } + catch (Throwable x) + { + LOG.debug("Exception while notifying success of callback " + callback, x); + } + } + + private void notifyCallbackFailure(WriteCallback callback, Throwable failure) + { + try + { + if (callback != null) + callback.writeFailed(failure); + } + catch (Throwable x) + { + LOG.debug("Exception while notifying failure of callback " + callback, x); + } + } + } } diff --git a/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/extensions/compress/ByteAccumulator.java b/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/extensions/compress/ByteAccumulator.java index 94e3b7c273b..e896c424264 100644 --- a/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/extensions/compress/ByteAccumulator.java +++ b/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/extensions/compress/ByteAccumulator.java @@ -22,43 +22,27 @@ import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.List; -import org.eclipse.jetty.io.ByteBufferPool; import org.eclipse.jetty.util.BufferUtil; import org.eclipse.jetty.websocket.api.MessageTooLargeException; public class ByteAccumulator { - private static class Buf - { - public Buf(byte[] buffer, int offset, int length) - { - this.buffer = buffer; - this.offset = offset; - this.length = length; - } - - byte[] buffer; - int offset; - int length; - } - + private final List chunks = new ArrayList<>(); private final int maxSize; private int length = 0; - private List buffers; public ByteAccumulator(int maxOverallBufferSize) { this.maxSize = maxOverallBufferSize; - this.buffers = new ArrayList<>(); } - public void addBuffer(byte buf[], int offset, int length) + public void addChunk(byte buf[], int offset, int length) { if (this.length + length > maxSize) { throw new MessageTooLargeException("Frame is too large"); } - buffers.add(new Buf(buf,offset,length)); + chunks.add(new Chunk(buf, offset, length)); this.length += length; } @@ -67,17 +51,29 @@ public class ByteAccumulator return length; } - public ByteBuffer getByteBuffer(ByteBufferPool pool) + public void transferTo(ByteBuffer buffer) { - ByteBuffer ret = pool.acquire(length,false); - BufferUtil.clearToFill(ret); - - for (Buf buf : buffers) + if (buffer.remaining() < length) + throw new IllegalArgumentException(); + int position = buffer.position(); + for (Chunk chunk : chunks) { - ret.put(buf.buffer, buf.offset, buf.length); + buffer.put(chunk.buffer, chunk.offset, chunk.length); } + BufferUtil.flipToFlush(buffer, position); + } - BufferUtil.flipToFlush(ret,0); - return ret; + private static class Chunk + { + private final byte[] buffer; + private final int offset; + private final int length; + + private Chunk(byte[] buffer, int offset, int length) + { + this.buffer = buffer; + this.offset = offset; + this.length = length; + } } } diff --git a/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/extensions/compress/CompressExtension.java b/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/extensions/compress/CompressExtension.java new file mode 100644 index 00000000000..d4030532b33 --- /dev/null +++ b/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/extensions/compress/CompressExtension.java @@ -0,0 +1,345 @@ +// +// ======================================================================== +// Copyright (c) 1995-2014 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://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 java.util.Queue; +import java.util.zip.DataFormatException; +import java.util.zip.Deflater; +import java.util.zip.Inflater; +import java.util.zip.ZipException; + +import org.eclipse.jetty.util.BufferUtil; +import org.eclipse.jetty.util.ConcurrentArrayQueue; +import org.eclipse.jetty.util.IteratingCallback; +import org.eclipse.jetty.util.log.Log; +import org.eclipse.jetty.util.log.Logger; +import org.eclipse.jetty.websocket.api.BadPayloadException; +import org.eclipse.jetty.websocket.api.BatchMode; +import org.eclipse.jetty.websocket.api.WriteCallback; +import org.eclipse.jetty.websocket.api.extensions.Frame; +import org.eclipse.jetty.websocket.common.OpCode; +import org.eclipse.jetty.websocket.common.extensions.AbstractExtension; +import org.eclipse.jetty.websocket.common.frames.DataFrame; + +public abstract class CompressExtension extends AbstractExtension +{ + protected static final byte[] TAIL_BYTES = new byte[]{0x00, 0x00, (byte)0xFF, (byte)0xFF}; + private static final Logger LOG = Log.getLogger(CompressExtension.class); + + private final Queue entries = new ConcurrentArrayQueue<>(); + private final IteratingCallback flusher = new Flusher(); + private final Deflater compressor; + private final Inflater decompressor; + + protected CompressExtension() + { + compressor = new Deflater(Deflater.BEST_COMPRESSION, true); + decompressor = new Inflater(true); + } + + public Deflater getDeflater() + { + return compressor; + } + + public Inflater getInflater() + { + return decompressor; + } + + /** + * Indicates use of RSV1 flag for indicating deflation is in use. + */ + @Override + public boolean isRsv1User() + { + return true; + } + + protected void forwardIncoming(Frame frame, ByteAccumulator accumulator) + { + DataFrame newFrame = new DataFrame(frame); + // Unset RSV1 since it's not compressed anymore. + newFrame.setRsv1(false); + + ByteBuffer buffer = getBufferPool().acquire(accumulator.getLength(), false); + try + { + BufferUtil.flipToFill(buffer); + accumulator.transferTo(buffer); + newFrame.setPayload(buffer); + nextIncomingFrame(newFrame); + } + finally + { + getBufferPool().release(buffer); + } + } + + protected ByteAccumulator decompress(byte[] input) + { + // Since we don't track text vs binary vs continuation state, just grab whatever is the greater value. + int maxSize = Math.max(getPolicy().getMaxTextMessageSize(), getPolicy().getMaxBinaryMessageBufferSize()); + ByteAccumulator accumulator = new ByteAccumulator(maxSize); + + decompressor.setInput(input, 0, input.length); + LOG.debug("Decompressing {} bytes", input.length); + + try + { + // It is allowed to send DEFLATE blocks with BFINAL=1. + // For such blocks, getRemaining() will be > 0 but finished() + // will be true, so we need to check for both. + // When BFINAL=0, finished() will always be false and we only + // check the remaining bytes. + while (decompressor.getRemaining() > 0 && !decompressor.finished()) + { + byte[] output = new byte[Math.min(input.length * 2, 32 * 1024)]; + int decompressed = decompressor.inflate(output); + if (decompressed == 0) + { + if (decompressor.needsInput()) + { + throw new BadPayloadException("Unable to inflate frame, not enough input on frame"); + } + if (decompressor.needsDictionary()) + { + throw new BadPayloadException("Unable to inflate frame, frame erroneously says it needs a dictionary"); + } + } + else + { + accumulator.addChunk(output, 0, decompressed); + } + } + LOG.debug("Decompressed {}->{} bytes", input.length, accumulator.getLength()); + return accumulator; + } + catch (DataFormatException x) + { + throw new BadPayloadException(x); + } + } + + @Override + public void outgoingFrame(Frame frame, WriteCallback callback, BatchMode batchMode) + { + // We use a queue and an IteratingCallback to handle concurrency. + // We must compress and write atomically, otherwise the compression + // context on the other end gets confused. + + if (flusher.isFailed()) + { + notifyCallbackFailure(callback, new ZipException()); + return; + } + + FrameEntry entry = new FrameEntry(frame, callback, batchMode); + LOG.debug("Queuing {}", entry); + entries.offer(entry); + flusher.iterate(); + } + + protected void notifyCallbackSuccess(WriteCallback callback) + { + try + { + if (callback != null) + callback.writeSuccess(); + } + catch (Throwable x) + { + LOG.debug("Exception while notifying success of callback " + callback, x); + } + } + + protected void notifyCallbackFailure(WriteCallback callback, Throwable failure) + { + try + { + if (callback != null) + callback.writeFailed(failure); + } + catch (Throwable x) + { + LOG.debug("Exception while notifying failure of callback " + callback, x); + } + } + + @Override + public String toString() + { + return getClass().getSimpleName(); + } + + private static class FrameEntry + { + private final Frame frame; + private final WriteCallback callback; + private final BatchMode batchMode; + + private FrameEntry(Frame frame, WriteCallback callback, BatchMode batchMode) + { + this.frame = frame; + this.callback = callback; + this.batchMode = batchMode; + } + + @Override + public String toString() + { + return frame.toString(); + } + } + + private class Flusher extends IteratingCallback implements WriteCallback + { + private FrameEntry current; + private ByteBuffer payload; + private boolean finished = true; + + @Override + protected Action process() throws Exception + { + if (finished) + { + current = entries.poll(); + LOG.debug("Processing {}", current); + if (current == null) + return Action.IDLE; + deflate(current); + } + else + { + compress(current, false); + } + return Action.SCHEDULED; + } + + private void deflate(FrameEntry entry) + { + Frame frame = entry.frame; + BatchMode batchMode = entry.batchMode; + if (OpCode.isControlFrame(frame.getOpCode()) || !frame.hasPayload()) + { + nextOutgoingFrame(frame, this, batchMode); + return; + } + + compress(entry, true); + } + + private void compress(FrameEntry entry, boolean first) + { + // Get a chunk of the payload to avoid to blow + // the heap if the payload is a huge mapped file. + Frame frame = entry.frame; + ByteBuffer data = frame.getPayload(); + int remaining = data.remaining(); + int inputLength = Math.min(remaining, 32 * 1024); + LOG.debug("Compressing {}: {} bytes in {} bytes chunk", entry, remaining, inputLength); + + // Avoid to copy the bytes if the ByteBuffer + // is backed by an array. + int inputOffset; + byte[] input; + if (data.hasArray()) + { + input = data.array(); + int position = data.position(); + inputOffset = position + data.arrayOffset(); + data.position(position + inputLength); + } + else + { + input = new byte[inputLength]; + inputOffset = 0; + data.get(input, 0, inputLength); + } + finished = inputLength == remaining; + + compressor.setInput(input, inputOffset, inputLength); + + // Use an additional space in case the content is not compressible. + byte[] output = new byte[inputLength + 64]; + int outputOffset = 0; + int outputLength = 0; + while (true) + { + int space = output.length - outputOffset; + int compressed = compressor.deflate(output, outputOffset, space, Deflater.SYNC_FLUSH); + outputLength += compressed; + if (compressed < space) + { + // Everything was compressed. + break; + } + else + { + // The compressed output is bigger than the uncompressed input. + byte[] newOutput = new byte[output.length * 2]; + System.arraycopy(output, 0, newOutput, 0, output.length); + outputOffset += output.length; + output = newOutput; + } + } + + // Skip the last tail bytes bytes generated by SYNC_FLUSH. + payload = ByteBuffer.wrap(output, 0, outputLength - TAIL_BYTES.length); + LOG.debug("Compressed {}: {}->{} chunk bytes", entry, inputLength, outputLength); + + boolean continuation = frame.getType().isContinuation() || !first; + DataFrame chunk = new DataFrame(frame, continuation); + chunk.setRsv1(true); + chunk.setPayload(payload); + boolean fin = frame.isFin() && finished; + chunk.setFin(fin); + + nextOutgoingFrame(chunk, this, entry.batchMode); + } + + @Override + protected void completed() + { + // This IteratingCallback never completes. + } + + @Override + public void writeSuccess() + { + if (finished) + notifyCallbackSuccess(current.callback); + succeeded(); + } + + @Override + public void writeFailed(Throwable x) + { + notifyCallbackFailure(current.callback, x); + // If something went wrong, very likely the compression context + // will be invalid, so we need to fail this IteratingCallback. + failed(x); + // Now no more frames can be queued, fail those in the queue. + FrameEntry entry; + while ((entry = entries.poll()) != null) + notifyCallbackFailure(entry.callback, x); + } + } +} diff --git a/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/extensions/compress/DeflateFrameExtension.java b/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/extensions/compress/DeflateFrameExtension.java index 61272c70210..8e36e8fd005 100644 --- a/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/extensions/compress/DeflateFrameExtension.java +++ b/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/extensions/compress/DeflateFrameExtension.java @@ -19,39 +19,17 @@ package org.eclipse.jetty.websocket.common.extensions.compress; import java.nio.ByteBuffer; -import java.util.ArrayList; -import java.util.List; -import java.util.zip.DataFormatException; -import java.util.zip.Deflater; -import java.util.zip.Inflater; -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.BadPayloadException; -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.OpCode; -import org.eclipse.jetty.websocket.common.extensions.AbstractExtension; -import org.eclipse.jetty.websocket.common.frames.DataFrame; /** - * Implementation of the deflate-frame extension seen out in the - * wild. + * Implementation of the + * deflate-frame + * extension seen out in the wild. */ -public class DeflateFrameExtension extends AbstractExtension +public class DeflateFrameExtension extends CompressExtension { - private static final boolean BFINAL_HACK = Boolean.parseBoolean(System.getProperty("jetty.websocket.bfinal.hack","true")); - private static final Logger LOG = Log.getLogger(DeflateFrameExtension.class); - - private static final int OVERHEAD = 64; - /** Tail Bytes per Spec */ - private static final byte[] TAIL = new byte[] { 0x00, 0x00, (byte)0xFF, (byte)0xFF }; - private int bufferSize = 64 * 1024; - private Deflater compressor; - private Inflater decompressor; - @Override public String getName() { @@ -61,194 +39,22 @@ public class DeflateFrameExtension extends AbstractExtension @Override public void incomingFrame(Frame frame) { - if (OpCode.isControlFrame(frame.getOpCode()) || !frame.isRsv1()) + // Incoming frames are always non concurrent because + // they are read and parsed with a single thread, and + // therefore there is no need for synchronization. + + if (OpCode.isControlFrame(frame.getOpCode()) || !frame.isRsv1() || !frame.hasPayload()) { - // Cannot modify incoming control frames or ones with RSV1 set. nextIncomingFrame(frame); return; } - if (!frame.hasPayload()) - { - // no payload? nothing to do. - nextIncomingFrame(frame); - return; - } - - // Prime the decompressor ByteBuffer payload = frame.getPayload(); - int inlen = payload.remaining(); - byte compressed[] = new byte[inlen + TAIL.length]; - payload.get(compressed,0,inlen); - System.arraycopy(TAIL,0,compressed,inlen,TAIL.length); + int remaining = payload.remaining(); + byte[] input = new byte[remaining + TAIL_BYTES.length]; + payload.get(input, 0, remaining); + System.arraycopy(TAIL_BYTES, 0, input, remaining, TAIL_BYTES.length); - // Since we don't track text vs binary vs continuation state, just grab whatever is the greater value. - int maxSize = Math.max(getPolicy().getMaxTextMessageSize(),getPolicy().getMaxBinaryMessageBufferSize()); - ByteAccumulator accumulator = new ByteAccumulator(maxSize); - - DataFrame out = new DataFrame(frame); - out.setRsv1(false); // Unset RSV1 - - synchronized (decompressor) - { - decompressor.setInput(compressed,0,compressed.length); - - // Perform decompression - while (decompressor.getRemaining() > 0 && !decompressor.finished()) - { - byte outbuf[] = new byte[Math.min(inlen * 2,bufferSize)]; - try - { - int len = decompressor.inflate(outbuf); - if (len == 0) - { - if (decompressor.needsInput()) - { - throw new BadPayloadException("Unable to inflate frame, not enough input on frame"); - } - if (decompressor.needsDictionary()) - { - throw new BadPayloadException("Unable to inflate frame, frame erroneously says it needs a dictionary"); - } - } - if (len > 0) - { - accumulator.addBuffer(outbuf,0,len); - } - } - catch (DataFormatException e) - { - LOG.warn(e); - throw new BadPayloadException(e); - } - } - } - - // Forward on the frame - out.setPayload(accumulator.getByteBuffer(getBufferPool())); - nextIncomingFrame(out); - } - - /** - * Indicates use of RSV1 flag for indicating deflation is in use. - *

- * Also known as the "COMP" framing header bit - */ - @Override - public boolean isRsv1User() - { - return true; - } - - @Override - public void outgoingFrame(Frame frame, WriteCallback callback) - { - if (OpCode.isControlFrame(frame.getOpCode())) - { - // skip, cannot compress control frames. - nextOutgoingFrame(frame,callback); - return; - } - - if (!frame.hasPayload()) - { - // pass through, nothing to do - nextOutgoingFrame(frame,callback); - return; - } - - if (LOG.isDebugEnabled()) - { - LOG.debug("outgoingFrame({}, {}) - {}",OpCode.name(frame.getOpCode()),callback != null?callback.getClass().getSimpleName():"", - BufferUtil.toDetailString(frame.getPayload())); - } - - // Prime the compressor - byte uncompressed[] = BufferUtil.toArray(frame.getPayload()); - List dframes = new ArrayList<>(); - - synchronized (compressor) - { - // Perform the compression - if (!compressor.finished()) - { - compressor.setInput(uncompressed,0,uncompressed.length); - byte compressed[] = new byte[uncompressed.length + OVERHEAD]; - - while (!compressor.needsInput()) - { - int len = compressor.deflate(compressed,0,compressed.length,Deflater.SYNC_FLUSH); - ByteBuffer outbuf = getBufferPool().acquire(len,true); - BufferUtil.clearToFill(outbuf); - - if (len > 0) - { - outbuf.put(compressed,0,len - 4); - } - - BufferUtil.flipToFlush(outbuf,0); - - if (len > 0 && BFINAL_HACK) - { - /* - * Per the spec, it says that BFINAL 1 or 0 are allowed. - * - * However, Java always uses BFINAL 1, whereas the browsers Chromium and Safari fail to decompress when it encounters BFINAL 1. - * - * This hack will always set BFINAL 0 - */ - byte b0 = outbuf.get(0); - if ((b0 & 1) != 0) // if BFINAL 1 - { - outbuf.put(0,(b0 ^= 1)); // flip bit to BFINAL 0 - } - } - - DataFrame out = new DataFrame(frame); - out.setRsv1(true); - out.setBufferPool(getBufferPool()); - out.setPayload(outbuf); - - if (!compressor.needsInput()) - { - // this is fragmented - out.setFin(false); - } - dframes.add(out); - } - } - } - - // notify outside of synchronize - for (DataFrame df : dframes) - { - if (df.isFin()) - { - nextOutgoingFrame(df,callback); - } - else - { - // non final frames have no callback - nextOutgoingFrame(df,null); - } - } - } - - @Override - public void setConfig(ExtensionConfig config) - { - super.setConfig(config); - - boolean nowrap = true; - compressor = new Deflater(Deflater.BEST_COMPRESSION,nowrap); - compressor.setStrategy(Deflater.DEFAULT_STRATEGY); - - decompressor = new Inflater(nowrap); - } - - @Override - public String toString() - { - return this.getClass().getSimpleName() + "[]"; + forwardIncoming(frame, decompress(input)); } } diff --git a/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/extensions/compress/PerMessageDeflateExtension.java b/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/extensions/compress/PerMessageDeflateExtension.java index 24d978ee8a1..0b0d9e537df 100644 --- a/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/extensions/compress/PerMessageDeflateExtension.java +++ b/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/extensions/compress/PerMessageDeflateExtension.java @@ -19,49 +19,29 @@ package org.eclipse.jetty.websocket.common.extensions.compress; import java.nio.ByteBuffer; -import java.util.zip.DataFormatException; -import java.util.zip.Deflater; -import java.util.zip.Inflater; -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.BadPayloadException; +import org.eclipse.jetty.websocket.api.BatchMode; 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.OpCode; -import org.eclipse.jetty.websocket.common.extensions.AbstractExtension; -import org.eclipse.jetty.websocket.common.frames.DataFrame; /** * Per Message Deflate Compression extension for WebSocket. - *

+ *

* Attempts to follow draft-ietf-hybi-permessage-compression-12 */ -public class PerMessageDeflateExtension extends AbstractExtension +public class PerMessageDeflateExtension extends CompressExtension { - private static final boolean BFINAL_HACK = Boolean.parseBoolean(System.getProperty("jetty.websocket.bfinal.hack","true")); private static final Logger LOG = Log.getLogger(PerMessageDeflateExtension.class); - private static final int OVERHEAD = 64; - /** Tail Bytes per Spec */ - private static final byte[] TAIL = new byte[] - { 0x00, 0x00, (byte)0xFF, (byte)0xFF }; private ExtensionConfig configRequested; private ExtensionConfig configNegotiated; - private Deflater compressor; - private Inflater decompressor; - - private boolean incomingCompressed = false; - private boolean outgoingCompressed = false; - /** - * Context Takeover Control. - *

- * If true, the same LZ77 window is used between messages. Can be overridden with extension parameters. - */ private boolean incomingContextTakeover = true; private boolean outgoingContextTakeover = true; + private boolean incomingCompressed; @Override public String getName() @@ -70,203 +50,35 @@ public class PerMessageDeflateExtension extends AbstractExtension } @Override - public synchronized void incomingFrame(Frame frame) + public void incomingFrame(Frame frame) { - switch (frame.getOpCode()) - { - case OpCode.BINARY: // fall-thru - case OpCode.TEXT: - incomingCompressed = frame.isRsv1(); - break; - case OpCode.CONTINUATION: - if (!incomingCompressed) - { - nextIncomingFrame(frame); - } - break; - default: - // All others are assumed to be control frames - nextIncomingFrame(frame); - return; - } + // Incoming frames are always non concurrent because + // they are read and parsed with a single thread, and + // therefore there is no need for synchronization. - if (!incomingCompressed || !frame.hasPayload()) + // This extension requires the RSV1 bit set only in the first frame. + // Subsequent continuation frames don't have RSV1 set, but are compressed. + if (frame.getType().isData()) + incomingCompressed = frame.isRsv1(); + + if (OpCode.isControlFrame(frame.getOpCode()) || !frame.hasPayload() || !incomingCompressed) { - // nothing to do with this frame nextIncomingFrame(frame); return; } - // Prime the decompressor + boolean appendTail = frame.isFin(); ByteBuffer payload = frame.getPayload(); - int inlen = payload.remaining(); - byte compressed[] = null; + int remaining = payload.remaining(); + byte[] input = new byte[remaining + (appendTail ? TAIL_BYTES.length : 0)]; + payload.get(input, 0, remaining); + if (appendTail) + System.arraycopy(TAIL_BYTES, 0, input, remaining, TAIL_BYTES.length); + + forwardIncoming(frame, decompress(input)); if (frame.isFin()) - { - compressed = new byte[inlen + TAIL.length]; - payload.get(compressed,0,inlen); - System.arraycopy(TAIL,0,compressed,inlen,TAIL.length); incomingCompressed = false; - } - else - { - compressed = new byte[inlen]; - payload.get(compressed,0,inlen); - } - - decompressor.setInput(compressed,0,compressed.length); - - // Since we don't track text vs binary vs continuation state, just grab whatever is the greater value. - int maxSize = Math.max(getPolicy().getMaxTextMessageSize(),getPolicy().getMaxBinaryMessageBufferSize()); - ByteAccumulator accumulator = new ByteAccumulator(maxSize); - - DataFrame out = new DataFrame(frame); - out.setRsv1(false); // Unset RSV1 - - // Perform decompression - while (decompressor.getRemaining() > 0 && !decompressor.finished()) - { - byte outbuf[] = new byte[inlen]; - try - { - int len = decompressor.inflate(outbuf); - if (len == 0) - { - if (decompressor.needsInput()) - { - throw new BadPayloadException("Unable to inflate frame, not enough input on frame"); - } - if (decompressor.needsDictionary()) - { - throw new BadPayloadException("Unable to inflate frame, frame erroneously says it needs a dictionary"); - } - } - if (len > 0) - { - accumulator.addBuffer(outbuf,0,len); - } - } - catch (DataFormatException e) - { - LOG.warn(e); - throw new BadPayloadException(e); - } - } - - // Forward on the frame - out.setPayload(accumulator.getByteBuffer(getBufferPool())); - nextIncomingFrame(out); - } - - /** - * Indicates use of RSV1 flag for indicating deflation is in use. - */ - @Override - public boolean isRsv1User() - { - return true; - } - - @Override - public synchronized void outgoingFrame(Frame frame, WriteCallback callback) - { - if (OpCode.isControlFrame(frame.getOpCode())) - { - // skip, cannot compress control frames. - nextOutgoingFrame(frame,callback); - return; - } - - if (!frame.hasPayload()) - { - // pass through, nothing to do - nextOutgoingFrame(frame,callback); - return; - } - - if (LOG.isDebugEnabled()) - { - LOG.debug("outgoingFrame({}, {}) - {}",OpCode.name(frame.getOpCode()),callback != null?callback.getClass().getSimpleName():"", - BufferUtil.toDetailString(frame.getPayload())); - } - - // Prime the compressor - byte uncompressed[] = BufferUtil.toArray(frame.getPayload()); - - // Perform the compression - if (!compressor.finished()) - { - compressor.setInput(uncompressed,0,uncompressed.length); - byte compressed[] = new byte[uncompressed.length + OVERHEAD]; - - while (!compressor.needsInput()) - { - int len = compressor.deflate(compressed,0,compressed.length,Deflater.SYNC_FLUSH); - ByteBuffer outbuf = getBufferPool().acquire(len,true); - BufferUtil.clearToFill(outbuf); - - if (len > 0) - { - if (len > 4) - { - // Test for the 4 tail octets (0x00 0x00 0xff 0xff) - int idx = len - 4; - boolean found = true; - for (int n = 0; n < TAIL.length; n++) - { - if (compressed[idx + n] != TAIL[n]) - { - found = false; - break; - } - } - if (found) - { - len = len - 4; - } - } - outbuf.put(compressed,0,len); - } - - BufferUtil.flipToFlush(outbuf,0); - - if (len > 0 && BFINAL_HACK) - { - /* - * Per the spec, it says that BFINAL 1 or 0 are allowed. - * - * However, Java always uses BFINAL 1, whereas the browsers Chromium and Safari fail to decompress when it encounters BFINAL 1. - * - * This hack will always set BFINAL 0 - */ - byte b0 = outbuf.get(0); - if ((b0 & 1) != 0) // if BFINAL 1 - { - outbuf.put(0,(b0 ^= 1)); // flip bit to BFINAL 0 - } - } - - DataFrame out = new DataFrame(frame,outgoingCompressed); - out.setRsv1(true); - out.setBufferPool(getBufferPool()); - out.setPayload(outbuf); - - if (!compressor.needsInput()) - { - // this is fragmented - out.setFin(false); - nextOutgoingFrame(out,null); // non final frames have no callback - } - else - { - // pass through the callback - nextOutgoingFrame(out,callback); - } - - outgoingCompressed = !out.isFin(); - } - } } @Override @@ -275,22 +87,20 @@ public class PerMessageDeflateExtension extends AbstractExtension if (frame.isFin() && !incomingContextTakeover) { LOG.debug("Incoming Context Reset"); - decompressor.reset(); + getInflater().reset(); } - super.nextIncomingFrame(frame); } @Override - protected void nextOutgoingFrame(Frame frame, WriteCallback callback) + protected void nextOutgoingFrame(Frame frame, WriteCallback callback, BatchMode batchMode) { if (frame.isFin() && !outgoingContextTakeover) { LOG.debug("Outgoing Context Reset"); - compressor.reset(); + getDeflater().reset(); } - - super.nextOutgoingFrame(frame,callback); + super.nextOutgoingFrame(frame, callback, batchMode); } @Override @@ -299,23 +109,20 @@ public class PerMessageDeflateExtension extends AbstractExtension configRequested = new ExtensionConfig(config); configNegotiated = new ExtensionConfig(config.getName()); - boolean nowrap = true; - compressor = new Deflater(Deflater.BEST_COMPRESSION,nowrap); - compressor.setStrategy(Deflater.DEFAULT_STRATEGY); - - decompressor = new Inflater(nowrap); - for (String key : config.getParameterKeys()) { key = key.trim(); switch (key) { - case "client_max_window_bits": // fallthru + case "client_max_window_bits": case "server_max_window_bits": + { // Not supported by Jetty // Don't negotiate these parameters break; + } case "client_no_context_takeover": + { configNegotiated.setParameter("client_no_context_takeover"); switch (getPolicy().getBehavior()) { @@ -327,7 +134,9 @@ public class PerMessageDeflateExtension extends AbstractExtension break; } break; + } case "server_no_context_takeover": + { configNegotiated.setParameter("server_no_context_takeover"); switch (getPolicy().getBehavior()) { @@ -339,6 +148,11 @@ public class PerMessageDeflateExtension extends AbstractExtension break; } break; + } + default: + { + throw new IllegalArgumentException(); + } } } @@ -348,11 +162,9 @@ public class PerMessageDeflateExtension extends AbstractExtension @Override public String toString() { - StringBuilder str = new StringBuilder(); - str.append(this.getClass().getSimpleName()); - str.append("[requested=").append(configRequested.getParameterizedName()); - str.append(",negotiated=").append(configNegotiated.getParameterizedName()); - str.append(']'); - return str.toString(); + return String.format("%s[requested=%s,negotiated=%s]", + getClass().getSimpleName(), + configRequested.getParameterizedName(), + configNegotiated.getParameterizedName()); } } diff --git a/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/extensions/compress/XWebkitDeflateFrameExtension.java b/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/extensions/compress/XWebkitDeflateFrameExtension.java index 58725dac000..ca7de138a25 100644 --- a/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/extensions/compress/XWebkitDeflateFrameExtension.java +++ b/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/extensions/compress/XWebkitDeflateFrameExtension.java @@ -29,10 +29,4 @@ public class XWebkitDeflateFrameExtension extends DeflateFrameExtension { return "x-webkit-deflate-frame"; } - - @Override - public String toString() - { - return this.getClass().getSimpleName() + "[]"; - } } 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 37ec5d395f7..2684690e409 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 @@ -20,7 +20,13 @@ package org.eclipse.jetty.websocket.common.extensions.fragment; import java.nio.ByteBuffer; +import java.util.Queue; +import org.eclipse.jetty.util.ConcurrentArrayQueue; +import org.eclipse.jetty.util.IteratingCallback; +import org.eclipse.jetty.util.log.Log; +import org.eclipse.jetty.util.log.Logger; +import org.eclipse.jetty.websocket.api.BatchMode; import org.eclipse.jetty.websocket.api.WriteCallback; import org.eclipse.jetty.websocket.api.extensions.ExtensionConfig; import org.eclipse.jetty.websocket.api.extensions.Frame; @@ -33,85 +39,167 @@ import org.eclipse.jetty.websocket.common.frames.DataFrame; */ public class FragmentExtension extends AbstractExtension { - private int maxLength = -1; - + private static final Logger LOG = Log.getLogger(FragmentExtension.class); + + private final Queue entries = new ConcurrentArrayQueue<>(); + private final IteratingCallback flusher = new Flusher(); + private int maxLength; + @Override public String getName() { return "fragment"; } - @Override - public void incomingError(Throwable e) - { - // Pass thru - nextIncomingError(e); - } - @Override public void incomingFrame(Frame frame) { - // Pass thru nextIncomingFrame(frame); } @Override - public void outgoingFrame(Frame frame, WriteCallback callback) + public void outgoingFrame(Frame frame, WriteCallback callback, BatchMode batchMode) { - if (OpCode.isControlFrame(frame.getOpCode())) + ByteBuffer payload = frame.getPayload(); + int length = payload != null ? payload.remaining() : 0; + if (OpCode.isControlFrame(frame.getOpCode()) || maxLength <= 0 || length <= maxLength) { - // Cannot fragment Control Frames - nextOutgoingFrame(frame,callback); + nextOutgoingFrame(frame, callback, batchMode); return; } - int length = frame.getPayloadLength(); - - ByteBuffer payload = frame.getPayload().slice(); - int originalLimit = payload.limit(); - int currentPosition = payload.position(); - - if (maxLength <= 0) - { - // output original frame - nextOutgoingFrame(frame,callback); - return; - } - - boolean continuation = false; - - // break apart payload based on maxLength rules - while (length > maxLength) - { - DataFrame frag = new DataFrame(frame,continuation); - frag.setFin(false); // always false here - payload.position(currentPosition); - payload.limit(Math.min(payload.position() + maxLength,originalLimit)); - frag.setPayload(payload); - - // no callback for beginning and middle parts - nextOutgoingFrame(frag,null); - - length -= maxLength; - continuation = true; - currentPosition = payload.limit(); - } - - // write remaining - DataFrame frag = new DataFrame(frame,continuation); - frag.setFin(frame.isFin()); // use original fin - payload.position(currentPosition); - payload.limit(originalLimit); - frag.setPayload(payload); - - nextOutgoingFrame(frag,callback); + FrameEntry entry = new FrameEntry(frame, callback, batchMode); + LOG.debug("Queuing {}", entry); + entries.offer(entry); + flusher.iterate(); } @Override public void setConfig(ExtensionConfig config) { super.setConfig(config); + maxLength = config.getParameter("maxLength", -1); + } - maxLength = config.getParameter("maxLength",maxLength); + private static class FrameEntry + { + private final Frame frame; + private final WriteCallback callback; + private final BatchMode batchMode; + + private FrameEntry(Frame frame, WriteCallback callback, BatchMode batchMode) + { + this.frame = frame; + this.callback = callback; + this.batchMode = batchMode; + } + + @Override + public String toString() + { + return frame.toString(); + } + } + + private class Flusher extends IteratingCallback implements WriteCallback + { + private FrameEntry current; + private boolean finished = true; + + @Override + protected Action process() throws Exception + { + if (finished) + { + current = entries.poll(); + LOG.debug("Processing {}", current); + if (current == null) + return Action.IDLE; + fragment(current, true); + } + else + { + fragment(current, false); + } + return Action.SCHEDULED; + } + + private void fragment(FrameEntry entry, boolean first) + { + Frame frame = entry.frame; + ByteBuffer payload = frame.getPayload(); + int remaining = payload.remaining(); + int length = Math.min(remaining, maxLength); + finished = length == remaining; + + boolean continuation = frame.getType().isContinuation() || !first; + DataFrame fragment = new DataFrame(frame, continuation); + boolean fin = frame.isFin() && finished; + fragment.setFin(fin); + + int limit = payload.limit(); + int newLimit = payload.position() + length; + payload.limit(newLimit); + ByteBuffer payloadFragment = payload.slice(); + payload.limit(limit); + fragment.setPayload(payloadFragment); + LOG.debug("Fragmented {}->{}", frame, fragment); + payload.position(newLimit); + + nextOutgoingFrame(fragment, this, entry.batchMode); + } + + @Override + protected void completed() + { + // This IteratingCallback never completes. + } + + @Override + public void writeSuccess() + { + // Notify first then call succeeded(), otherwise + // write callbacks may be invoked out of order. + notifyCallbackSuccess(current.callback); + succeeded(); + } + + @Override + public void writeFailed(Throwable x) + { + // Notify first, the call succeeded() to drain the queue. + // We don't want to call failed(x) because that will put + // this flusher into a final state that cannot be exited, + // and the failure of a frame may not mean that the whole + // connection is now invalid. + notifyCallbackFailure(current.callback, x); + succeeded(); + } + + private void notifyCallbackSuccess(WriteCallback callback) + { + try + { + if (callback != null) + callback.writeSuccess(); + } + catch (Throwable x) + { + LOG.debug("Exception while notifying success of callback " + callback, x); + } + } + + private void notifyCallbackFailure(WriteCallback callback, Throwable failure) + { + try + { + if (callback != null) + callback.writeFailed(failure); + } + catch (Throwable x) + { + LOG.debug("Exception while notifying failure of callback " + callback, x); + } + } } } 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 8b7df98e2c7..4ceb3e79826 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,6 +20,7 @@ package org.eclipse.jetty.websocket.common.extensions.identity; import org.eclipse.jetty.util.QuotedStringTokenizer; import org.eclipse.jetty.util.annotation.ManagedObject; +import org.eclipse.jetty.websocket.api.BatchMode; import org.eclipse.jetty.websocket.api.WriteCallback; import org.eclipse.jetty.websocket.api.extensions.ExtensionConfig; import org.eclipse.jetty.websocket.api.extensions.Frame; @@ -56,10 +57,10 @@ public class IdentityExtension extends AbstractExtension } @Override - public void outgoingFrame(Frame frame, WriteCallback callback) + public void outgoingFrame(Frame frame, WriteCallback callback, BatchMode batchMode) { // pass through - nextOutgoingFrame(frame,callback); + nextOutgoingFrame(frame,callback, batchMode); } @Override diff --git a/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/frames/DataFrame.java b/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/frames/DataFrame.java index 8b6be07a34b..af0c9f8527d 100644 --- a/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/frames/DataFrame.java +++ b/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/frames/DataFrame.java @@ -18,7 +18,6 @@ package org.eclipse.jetty.websocket.common.frames; -import org.eclipse.jetty.io.ByteBufferPool; import org.eclipse.jetty.websocket.api.extensions.Frame; import org.eclipse.jetty.websocket.common.OpCode; import org.eclipse.jetty.websocket.common.WebSocketFrame; @@ -28,8 +27,6 @@ import org.eclipse.jetty.websocket.common.WebSocketFrame; */ public class DataFrame extends WebSocketFrame { - private ByteBufferPool pool; - protected DataFrame(byte opcode) { super(opcode); @@ -78,22 +75,6 @@ public class DataFrame extends WebSocketFrame return true; } - public void reset() - { - // TODO: this is rather ugly. - // The ByteBufferPool is set only from extensions that - // compress the payload. It would be better to wrap the - // callback associated with this DataFrame into one that - // releases the buffer and then call the nested callback, - // rather than null-checking whether the pool exists and - // if so then release the buffer. - if (pool!=null) - { - pool.release(this.data); - } - super.reset(); - } - /** * Set the data frame to continuation mode */ @@ -101,12 +82,4 @@ public class DataFrame extends WebSocketFrame { setOpCode(OpCode.CONTINUATION); } - - /** - * Sets the buffer pool used for the payload - */ - public void setBufferPool(ByteBufferPool pool) - { - this.pool = pool; - } } 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 bdb0dd3e4a8..15763393095 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 @@ -32,12 +32,16 @@ import java.util.concurrent.atomic.AtomicLong; import org.eclipse.jetty.io.AbstractConnection; import org.eclipse.jetty.io.ByteBufferPool; +import org.eclipse.jetty.io.Connection; import org.eclipse.jetty.io.EndPoint; import org.eclipse.jetty.util.BufferUtil; import org.eclipse.jetty.util.StringUtil; +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.util.thread.Scheduler; +import org.eclipse.jetty.websocket.api.BatchMode; import org.eclipse.jetty.websocket.api.CloseException; import org.eclipse.jetty.websocket.api.CloseStatus; import org.eclipse.jetty.websocket.api.StatusCode; @@ -55,15 +59,16 @@ import org.eclipse.jetty.websocket.common.WebSocketSession; import org.eclipse.jetty.websocket.common.io.IOState.ConnectionStateListener; /** - * Provides the implementation of {@link LogicalConnection} within the framework of the new {@link Connection} framework of jetty-io + * Provides the implementation of {@link LogicalConnection} within the + * framework of the new {@link Connection} framework of {@code jetty-io}. */ -public abstract class AbstractWebSocketConnection extends AbstractConnection implements LogicalConnection, ConnectionStateListener +public abstract class AbstractWebSocketConnection extends AbstractConnection implements LogicalConnection, ConnectionStateListener, Dumpable { private class Flusher extends FrameFlusher { - private Flusher(Generator generator, EndPoint endpoint) + private Flusher(ByteBufferPool bufferPool, Generator generator, EndPoint endpoint) { - super(generator,endpoint); + super(bufferPool, generator, endpoint, getPolicy().getMaxBinaryMessageBufferSize(), 8); } @Override @@ -149,7 +154,7 @@ public abstract class AbstractWebSocketConnection extends AbstractConnection imp /** * Minimum size of a buffer is the determined to be what would be the maximum framing header size (not including payload) */ - private static final int MIN_BUFFER_SIZE = Generator.OVERHEAD; + private static final int MIN_BUFFER_SIZE = Generator.MAX_HEADER_LENGTH; private final ByteBufferPool bufferPool; private final Scheduler scheduler; @@ -176,7 +181,7 @@ public abstract class AbstractWebSocketConnection extends AbstractConnection imp this.suspendToken = new AtomicBoolean(false); this.ioState = new IOState(); this.ioState.addListener(this); - this.flusher = new Flusher(generator,endp); + this.flusher = new Flusher(bufferPool,generator,endp); this.setInputBufferSize(policy.getInputBufferSize()); this.setMaxIdleTimeout(policy.getIdleTimeout()); } @@ -261,11 +266,6 @@ public abstract class AbstractWebSocketConnection extends AbstractConnection imp super.fillInterested(); } - public void flush() - { - flusher.flush(); - } - @Override public ByteBufferPool getBufferPool() { @@ -379,7 +379,7 @@ public abstract class AbstractWebSocketConnection extends AbstractConnection imp { // Fire out a close frame, indicating abnormal shutdown, then disconnect CloseInfo abnormal = new CloseInfo(StatusCode.SHUTDOWN,"Abnormal Close - " + ioState.getCloseInfo().getReason()); - outgoingFrame(abnormal.asFrame(),new OnDisconnectCallback()); + outgoingFrame(abnormal.asFrame(),new OnDisconnectCallback(), BatchMode.OFF); } else { @@ -390,7 +390,7 @@ public abstract class AbstractWebSocketConnection extends AbstractConnection imp case CLOSING: CloseInfo close = ioState.getCloseInfo(); // append close frame - outgoingFrame(close.asFrame(),new OnDisconnectCallback()); + outgoingFrame(close.asFrame(),new OnDisconnectCallback(), BatchMode.OFF); default: break; } @@ -463,14 +463,14 @@ public abstract class AbstractWebSocketConnection extends AbstractConnection imp * Frame from API, User, or Internal implementation destined for network. */ @Override - public void outgoingFrame(Frame frame, WriteCallback callback) + public void outgoingFrame(Frame frame, WriteCallback callback, BatchMode batchMode) { if (LOG.isDebugEnabled()) { LOG.debug("outgoingFrame({}, {})",frame,callback); } - flusher.enqueue(frame,callback); + flusher.enqueue(frame,callback, batchMode); } private int read(ByteBuffer buffer) @@ -567,10 +567,22 @@ public abstract class AbstractWebSocketConnection extends AbstractConnection imp return this; } + @Override + public String dump() + { + return ContainerLifeCycle.dump(this); + } + + @Override + public void dump(Appendable out, String indent) throws IOException + { + out.append(toString()).append(System.lineSeparator()); + } + @Override public String toString() { - return String.format("%s{g=%s,p=%s}",super.toString(),generator,parser); + return String.format("%s{f=%s,g=%s,p=%s}",super.toString(),flusher,generator,parser); } } diff --git a/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/io/FrameFlusher.java b/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/io/FrameFlusher.java index cb963959209..a7bb3cb8c7d 100644 --- a/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/io/FrameFlusher.java +++ b/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/io/FrameFlusher.java @@ -19,354 +19,377 @@ package org.eclipse.jetty.websocket.common.io; import java.io.EOFException; -import java.io.IOException; import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.List; import java.util.Objects; import java.util.concurrent.atomic.AtomicBoolean; +import org.eclipse.jetty.io.ByteBufferPool; import org.eclipse.jetty.io.EndPoint; import org.eclipse.jetty.util.ArrayQueue; +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; +import org.eclipse.jetty.websocket.api.BatchMode; import org.eclipse.jetty.websocket.api.WriteCallback; import org.eclipse.jetty.websocket.api.extensions.Frame; import org.eclipse.jetty.websocket.common.Generator; import org.eclipse.jetty.websocket.common.OpCode; -import org.eclipse.jetty.websocket.common.frames.DataFrame; +import org.eclipse.jetty.websocket.common.frames.BinaryFrame; /** * Interface for working with bytes destined for {@link EndPoint#write(Callback, ByteBuffer...)} */ -public class FrameFlusher +public class FrameFlusher { - private static final int MAX_GATHER = Integer.getInteger("org.eclipse.jetty.websocket.common.io.FrameFlusher.MAX_GATHER",8); + public static final BinaryFrame FLUSH_FRAME = new BinaryFrame(); private static final Logger LOG = Log.getLogger(FrameFlusher.class); - /** The endpoint to flush to */ + private final ByteBufferPool bufferPool; private final EndPoint endpoint; - - /** The websocket generator */ + private final int bufferSize; private final Generator generator; - + private final int maxGather; private final Object lock = new Object(); - - /** Backlog of frames */ - private final ArrayQueue queue = new ArrayQueue<>(16,16,lock); - - private final FlusherCB flusherCB = new FlusherCB(); - - /** the buffer input size */ - private int bufferSize = 2048; + private final ArrayQueue queue = new ArrayQueue<>(16, 16, lock); + private final Flusher flusher = new Flusher(); + private final AtomicBoolean closed = new AtomicBoolean(); + private volatile Throwable failure; - /** Tracking for failure */ - private Throwable failure; - /** Is WriteBytesProvider closed to more WriteBytes being enqueued? */ - private boolean closed; - - /** - * Create a WriteBytesProvider with specified Generator and "flush" Callback. - * - * @param generator - * the generator to use for converting {@link Frame} objects to network {@link ByteBuffer}s - * @param endpoint - * the endpoint to flush to. - */ - public FrameFlusher(Generator generator, EndPoint endpoint) - { - this.endpoint=endpoint; - this.generator = Objects.requireNonNull(generator); - } - - /** - * Set the buffer size used for generating ByteBuffers from the frames. - *

- * Value usually obtained from {@link AbstractConnection#getInputBufferSize()} - * - * @param bufferSize - * the buffer size to use - */ - public void setBufferSize(int bufferSize) + public FrameFlusher(ByteBufferPool bufferPool, Generator generator, EndPoint endpoint, int bufferSize, int maxGather) { + this.bufferPool = bufferPool; + this.endpoint = endpoint; this.bufferSize = bufferSize; - } - - public int getBufferSize() - { - return bufferSize; + this.generator = Objects.requireNonNull(generator); + this.maxGather = maxGather; } - /** - * Force closure of write bytes - */ - public void close() + public void enqueue(Frame frame, WriteCallback callback, BatchMode batchMode) { - synchronized (lock) + if (closed.get()) { - if (!closed) - { - closed=true; - - EOFException eof = new EOFException("Connection has been disconnected"); - flusherCB.failed(eof); - for (FrameEntry frame : queue) - frame.notifyFailed(eof); - queue.clear(); - } + notifyCallbackFailure(callback, new EOFException("Connection has been closed locally")); + return; } - - } - - /** - * Used to test for the final frame possible to be enqueued, the CLOSE frame. - * - * @return true if close frame has been enqueued already. - */ - public boolean isClosed() - { - synchronized (lock) + if (flusher.isFailed()) { - return closed; + notifyCallbackFailure(callback, failure); + return; } - } - public void enqueue(Frame frame, WriteCallback callback) - { - Objects.requireNonNull(frame); - FrameEntry entry = new FrameEntry(frame,callback); - LOG.debug("enqueue({})",entry); - Throwable failure=null; + FrameEntry entry = new FrameEntry(frame, callback, batchMode); + synchronized (lock) { - if (closed) - { - // Closed for more frames. - LOG.debug("Write is closed: {} {}",frame,callback); - failure=new IOException("Write is closed"); - } - else if (this.failure!=null) - { - failure=this.failure; - } - switch (frame.getOpCode()) { case OpCode.PING: - queue.add(0,entry); + { + // Prepend PINGs so they are processed first. + queue.add(0, entry); break; + } case OpCode.CLOSE: - closed=true; + { + // There may be a chance that other frames are + // added after this close frame, but we will + // fail them later to keep it simple here. + closed.set(true); queue.add(entry); break; + } default: + { queue.add(entry); + break; + } } } - if (failure != null) - { - // no changes when failed - LOG.debug("Write is in failure: {} {}",frame,callback); - entry.notifyFailed(failure); - return; - } - - flush(); + if (LOG.isDebugEnabled()) + LOG.debug("{} queued {}", this, entry); + + flusher.iterate(); } - void flush() + public void close() { - flusherCB.iterate(); + if (closed.compareAndSet(false, true)) + { + LOG.debug("{} closing {}", this); + EOFException eof = new EOFException("Connection has been closed locally"); + flusher.failed(eof); + + // Fail also queued entries. + List entries = new ArrayList<>(); + synchronized (lock) + { + entries.addAll(queue); + queue.clear(); + } + // Notify outside sync block. + for (FrameEntry entry : entries) + notifyCallbackFailure(entry.callback, eof); + } } - + protected void onFailure(Throwable x) { LOG.warn(x); } + protected void notifyCallbackSuccess(WriteCallback callback) + { + try + { + if (callback != null) + callback.writeSuccess(); + } + catch (Throwable x) + { + LOG.debug("Exception while notifying success of callback " + callback, x); + } + } + + protected void notifyCallbackFailure(WriteCallback callback, Throwable failure) + { + try + { + if (callback != null) + callback.writeFailed(failure); + } + catch (Throwable x) + { + LOG.debug("Exception while notifying failure of callback " + callback, x); + } + } + @Override public String toString() { - StringBuilder b = new StringBuilder(); - b.append("WriteBytesProvider["); - if (failure != null) - { - b.append("failure=").append(failure.getClass().getName()); - b.append(":").append(failure.getMessage()).append(','); - } - else - { - b.append("queue.size=").append(queue.size()); - } - b.append(']'); - return b.toString(); + ByteBuffer aggregate = flusher.aggregate; + return String.format("%s[queueSize=%d,aggregateSize=%d,failure=%s]", + getClass().getSimpleName(), + queue.size(), + aggregate == null ? 0 : aggregate.position(), + failure); } - private class FlusherCB extends IteratingCallback + private class Flusher extends IteratingCallback { - private final ArrayQueue active = new ArrayQueue<>(lock); - private final List buffers = new ArrayList<>(MAX_GATHER*2); - private final List succeeded = new ArrayList<>(MAX_GATHER+1); - - @Override - protected void completed() - { - // will never be called as process always returns SCHEDULED or IDLE - throw new IllegalStateException(); - } + private final List entries = new ArrayList<>(maxGather); + private final List buffers = new ArrayList<>(maxGather * 2 + 1); + private ByteBuffer aggregate; + private BatchMode batchMode; @Override protected Action process() throws Exception { + int space = aggregate == null ? bufferSize : BufferUtil.space(aggregate); + BatchMode currentBatchMode = BatchMode.AUTO; synchronized (lock) { - succeeded.clear(); - - // If we exited the loop above without hitting the gatheredBufferLimit - // then all the active frames are done, so we can add some more. - while (buffers.size() (bufferSize >> 2)) + currentBatchMode = BatchMode.OFF; + + // If the aggregate buffer overflows, do not batch. + space -= approxFrameLength; + if (space <= 0) + currentBatchMode = BatchMode.OFF; + + entries.add(entry); } - - if (LOG.isDebugEnabled()) - LOG.debug("process {} active={} buffers={}",FrameFlusher.this,active,buffers); } - - if (buffers.size()==0) - return Action.IDLE; - endpoint.write(this,buffers.toArray(new ByteBuffer[buffers.size()])); + if (LOG.isDebugEnabled()) + LOG.debug("{} processing {} entries: {}", FrameFlusher.this, entries.size(), entries); + + if (entries.isEmpty()) + { + if (batchMode != BatchMode.AUTO) + { + // Nothing more to do, release the aggregate buffer if we need to. + // Releasing it here rather than in succeeded() allows for its reuse. + releaseAggregate(); + return Action.IDLE; + } + + LOG.debug("{} auto flushing", FrameFlusher.this); + return flush(); + } + + batchMode = currentBatchMode; + + return currentBatchMode == BatchMode.OFF ? flush() : batch(); + } + + private Action flush() + { + if (!BufferUtil.isEmpty(aggregate)) + { + buffers.add(aggregate); + if (LOG.isDebugEnabled()) + LOG.debug("{} flushing aggregate {}", FrameFlusher.this, aggregate); + } + + // Do not allocate the iterator here. + for (int i = 0; i < entries.size(); ++i) + { + FrameEntry entry = entries.get(i); + // Skip the "synthetic" frame used for flushing. + if (entry.frame == FLUSH_FRAME) + continue; + buffers.add(entry.generateHeaderBytes()); + ByteBuffer payload = entry.frame.getPayload(); + if (BufferUtil.hasContent(payload)) + buffers.add(payload); + } + + if (LOG.isDebugEnabled()) + LOG.debug("{} flushing {} frames: {}", FrameFlusher.this, entries.size(), entries); + + if (buffers.isEmpty()) + { + releaseAggregate(); + // We may have the FLUSH_FRAME to notify. + succeedEntries(); + return Action.IDLE; + } + + endpoint.write(this, buffers.toArray(new ByteBuffer[buffers.size()])); buffers.clear(); return Action.SCHEDULED; } + private Action batch() + { + if (aggregate == null) + { + aggregate = bufferPool.acquire(bufferSize, true); + if (LOG.isDebugEnabled()) + LOG.debug("{} acquired aggregate buffer {}", FrameFlusher.this, aggregate); + } + + // Do not allocate the iterator here. + for (int i = 0; i < entries.size(); ++i) + { + FrameEntry entry = entries.get(i); + + entry.generateHeaderBytes(aggregate); + + ByteBuffer payload = entry.frame.getPayload(); + if (BufferUtil.hasContent(payload)) + BufferUtil.append(aggregate, payload); + } + if (LOG.isDebugEnabled()) + LOG.debug("{} aggregated {} frames: {}", FrameFlusher.this, entries.size(), entries); + succeeded(); + return Action.SCHEDULED; + } + + private void releaseAggregate() + { + if (aggregate != null && BufferUtil.isEmpty(aggregate)) + { + bufferPool.release(aggregate); + aggregate = null; + } + } + @Override public void succeeded() - { - synchronized (lock) - { - succeeded.addAll(active); - active.clear(); - } - - for (FrameEntry frame:succeeded) - { - frame.notifySucceeded(); - frame.freeBuffers(); - } - + { + succeedEntries(); super.succeeded(); } - + + private void succeedEntries() + { + // Do not allocate the iterator here. + for (int i = 0; i < entries.size(); ++i) + { + FrameEntry entry = entries.get(i); + notifyCallbackSuccess(entry.callback); + entry.release(); + } + entries.clear(); + } + + @Override + protected void completed() + { + // This IteratingCallback never completes. + } @Override public void failed(Throwable x) { - synchronized (lock) + for (FrameEntry entry : entries) { - succeeded.addAll(active); - active.clear(); + notifyCallbackFailure(entry.callback, x); + entry.release(); } - - for (FrameEntry frame : succeeded) - { - frame.notifyFailed(x); - frame.freeBuffers(); - } - succeeded.clear(); - + entries.clear(); super.failed(x); + failure = x; onFailure(x); } } - private class FrameEntry + private class FrameEntry { - protected final AtomicBoolean failed = new AtomicBoolean(false); - protected final Frame frame; - protected final WriteCallback callback; - /** holds reference to header ByteBuffer, as it needs to be released on success/failure */ + private final Frame frame; + private final WriteCallback callback; + private final BatchMode batchMode; private ByteBuffer headerBuffer; - public FrameEntry(Frame frame, WriteCallback callback) + private FrameEntry(Frame frame, WriteCallback callback, BatchMode batchMode) { - this.frame = frame; + this.frame = Objects.requireNonNull(frame); this.callback = callback; + this.batchMode = batchMode; } - public ByteBuffer getHeaderBytes() + private ByteBuffer generateHeaderBytes() { - ByteBuffer buf = generator.generateHeaderBytes(frame); - headerBuffer = buf; - return buf; + return headerBuffer = generator.generateHeaderBytes(frame); } - public ByteBuffer getPayload() + private void generateHeaderBytes(ByteBuffer buffer) { - // There is no need to release this ByteBuffer, as it is just a slice of the user provided payload - return frame.getPayload(); + generator.generateHeaderBytes(frame, buffer); } - public void notifyFailed(Throwable t) - { - freeBuffers(); - if (failed.getAndSet(true) == false) - { - try - { - if (callback!=null) - callback.writeFailed(t); - } - catch (Throwable e) - { - LOG.warn("Uncaught exception",e); - } - } - } - - public void notifySucceeded() - { - freeBuffers(); - if (callback == null) - { - return; - } - try - { - callback.writeSuccess(); - } - catch (Throwable t) - { - LOG.debug(t); - } - } - - public void freeBuffers() + private void release() { if (headerBuffer != null) { generator.getBufferPool().release(headerBuffer); headerBuffer = null; } - - if (frame instanceof DataFrame) - { - ((DataFrame)frame).reset(); - } } + @Override public String toString() { - return "["+callback+","+frame+","+failure+"]"; + return String.format("%s[%s,%s,%s,%s]", getClass().getSimpleName(), frame, callback, batchMode, failure); } } } 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 47771970eae..8189d070a35 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,6 +18,7 @@ package org.eclipse.jetty.websocket.common.io; +import org.eclipse.jetty.websocket.api.BatchMode; import org.eclipse.jetty.websocket.api.WriteCallback; import org.eclipse.jetty.websocket.api.extensions.Frame; import org.eclipse.jetty.websocket.api.extensions.IncomingFrames; @@ -43,7 +44,7 @@ public class FramePipes @Override public void incomingFrame(Frame frame) { - this.outgoing.outgoingFrame(frame,null); + this.outgoing.outgoingFrame(frame,null, BatchMode.OFF); } } @@ -57,7 +58,7 @@ public class FramePipes } @Override - public void outgoingFrame(Frame frame, WriteCallback callback) + public void outgoingFrame(Frame frame, WriteCallback callback, BatchMode batchMode) { try { diff --git a/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/io/payload/DeMaskProcessor.java b/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/io/payload/DeMaskProcessor.java index 9f01bd577c5..ea287f5e63e 100644 --- a/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/io/payload/DeMaskProcessor.java +++ b/jetty-websocket/websocket-common/src/main/java/org/eclipse/jetty/websocket/common/io/payload/DeMaskProcessor.java @@ -24,7 +24,8 @@ import org.eclipse.jetty.websocket.api.extensions.Frame; public class DeMaskProcessor implements PayloadProcessor { - private byte maskBytes[]; + private byte[] maskBytes; + private int maskInt; private int maskOffset; @Override @@ -35,14 +36,14 @@ public class DeMaskProcessor implements PayloadProcessor return; } - int maskInt = ByteBuffer.wrap(maskBytes).getInt(); + int maskInt = this.maskInt; int start = payload.position(); int end = payload.limit(); int offset = this.maskOffset; int remaining; while ((remaining = end - start) > 0) { - if (remaining >= 4 && (offset % 4) == 0) + if (remaining >= 4 && (offset & 3) == 0) { payload.putInt(start,payload.getInt(start) ^ maskInt); start += 4; @@ -58,9 +59,16 @@ public class DeMaskProcessor implements PayloadProcessor maskOffset = offset; } - public void reset(byte mask[]) + public void reset(byte[] mask) { this.maskBytes = mask; + int maskInt = 0; + if (mask != null) + { + for (byte maskByte : mask) + maskInt = (maskInt << 8) + (maskByte & 0xFF); + } + this.maskInt = maskInt; this.maskOffset = 0; } 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 a053dff0572..0a81cbdf8a5 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 @@ -29,30 +29,28 @@ import java.util.concurrent.atomic.AtomicBoolean; 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.LogicalConnection; /** * Support class for reading a (single) WebSocket BINARY message via a InputStream. - *

+ *

* 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 Logger LOG = Log.getLogger(MessageInputStream.class); - // EOF (End of Buffers) - private final static ByteBuffer EOF = ByteBuffer.allocate(0).asReadOnlyBuffer(); + private static final ByteBuffer EOF = ByteBuffer.allocate(0).asReadOnlyBuffer(); private final BlockingDeque buffers = new LinkedBlockingDeque<>(); private AtomicBoolean closed = new AtomicBoolean(false); private final long timeoutMs; private ByteBuffer activeBuffer = null; - public MessageInputStream(LogicalConnection connection) + public MessageInputStream() { - this(connection, -1); + this(-1); } - - public MessageInputStream(LogicalConnection connection, int timeoutMs) + + public MessageInputStream(int timeoutMs) { this.timeoutMs = timeoutMs; } @@ -61,9 +59,7 @@ public class MessageInputStream extends InputStream implements MessageAppender public void appendFrame(ByteBuffer framePayload, boolean fin) throws IOException { if (LOG.isDebugEnabled()) - { - LOG.debug("appendMessage(ByteBuffer,{}): {}",fin,BufferUtil.toDetailString(framePayload)); - } + LOG.debug("Appending {} chunk: {}", fin ? "final" : "non-final", BufferUtil.toDetailString(framePayload)); // If closed, we should just toss incoming payloads into the bit bucket. if (closed.get()) @@ -98,7 +94,7 @@ public class MessageInputStream extends InputStream implements MessageAppender @Override public void close() throws IOException { - if (closed.compareAndSet(false,true)) + if (closed.compareAndSet(false, true)) { buffers.offer(EOF); super.close(); @@ -106,9 +102,9 @@ public class MessageInputStream extends InputStream implements MessageAppender } @Override - public synchronized void mark(int readlimit) + public void mark(int readlimit) { - /* do nothing */ + // Not supported. } @Override @@ -120,62 +116,64 @@ public class MessageInputStream extends InputStream implements MessageAppender @Override public void messageComplete() { - LOG.debug("messageComplete()"); - - // toss an empty ByteBuffer into queue to let it drain + LOG.debug("Message completed"); buffers.offer(EOF); } @Override public int read() throws IOException { - LOG.debug("read()"); - try { if (closed.get()) { + LOG.debug("Stream closed"); return -1; } // grab a fresh buffer while (activeBuffer == null || !activeBuffer.hasRemaining()) { + if (LOG.isDebugEnabled()) + LOG.debug("Waiting {} ms to read", timeoutMs); if (timeoutMs < 0) { - // infinite take + // Wait forever until a buffer is available. activeBuffer = buffers.take(); } else { - // timeout specific - activeBuffer = buffers.poll(timeoutMs,TimeUnit.MILLISECONDS); + // Wait at most for the given timeout. + activeBuffer = buffers.poll(timeoutMs, TimeUnit.MILLISECONDS); if (activeBuffer == null) { - throw new IOException(String.format("Read timeout: %,dms expired",timeoutMs)); + throw new IOException(String.format("Read timeout: %,dms expired", timeoutMs)); } } - + if (activeBuffer == EOF) { + LOG.debug("Reached EOF"); + // Be sure that this stream cannot be reused. closed.set(true); + // Removed buffers that may have remained in the queue. + buffers.clear(); return -1; } } - return activeBuffer.get(); + return activeBuffer.get() & 0xFF; } - catch (InterruptedException e) + catch (InterruptedException x) { - LOG.warn(e); + LOG.debug("Interrupted while waiting to read", x); closed.set(true); return -1; - // throw new IOException(e); } } @Override - public synchronized void reset() throws IOException + public 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 f01d759865d..99364114602 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 @@ -26,6 +26,7 @@ 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.BatchMode; import org.eclipse.jetty.websocket.api.WriteCallback; import org.eclipse.jetty.websocket.api.extensions.OutgoingFrames; import org.eclipse.jetty.websocket.common.BlockingWriteCallback; @@ -38,183 +39,177 @@ import org.eclipse.jetty.websocket.common.frames.BinaryFrame; public class MessageOutputStream extends OutputStream { private static final Logger LOG = Log.getLogger(MessageOutputStream.class); + private final OutgoingFrames outgoing; private final ByteBufferPool bufferPool; private final BlockingWriteCallback blocker; - private long frameCount = 0; + private long frameCount; private BinaryFrame frame; private ByteBuffer buffer; private WriteCallback callback; - private boolean closed = false; + private boolean closed; + + public MessageOutputStream(WebSocketSession session) + { + this(session.getOutgoingHandler(), session.getPolicy().getMaxBinaryMessageBufferSize(), session.getBufferPool()); + } public MessageOutputStream(OutgoingFrames outgoing, int bufferSize, ByteBufferPool bufferPool) { this.outgoing = outgoing; this.bufferPool = bufferPool; this.blocker = new BlockingWriteCallback(); - this.buffer = bufferPool.acquire(bufferSize,true); + this.buffer = bufferPool.acquire(bufferSize, true); BufferUtil.flipToFill(buffer); this.frame = new BinaryFrame(); } - public MessageOutputStream(WebSocketSession session) + @Override + public void write(byte[] bytes, int off, int len) throws IOException { - this(session.getOutgoingHandler(),session.getPolicy().getMaxBinaryMessageBufferSize(),session.getBufferPool()); - } - - private void assertNotClosed() throws IOException - { - if (closed) + try { - IOException e = new IOException("Stream is closed"); - notifyFailure(e); - throw e; + send(bytes, off, len); + } + catch (Throwable x) + { + // Notify without holding locks. + notifyFailure(x); + throw x; } } @Override - public synchronized void close() throws IOException + public void write(int b) throws IOException { - 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(); + send(new byte[]{(byte)b}, 0, 1); + } + catch (Throwable x) + { + // Notify without holding locks. + notifyFailure(x); + throw x; + } + } + + @Override + public void flush() throws IOException + { + try + { + flush(false); + } + catch (Throwable x) + { + // Notify without holding locks. + notifyFailure(x); + throw x; + } + } + + @Override + public void close() throws IOException + { + try + { + flush(true); bufferPool.release(buffer); - LOG.debug("closed"); + LOG.debug("Stream closed, {} frames sent", frameCount); + // Notify without holding locks. + notifySuccess(); } - catch (IOException e) + catch (Throwable x) { - notifyFailure(e); - throw e; + // Notify without holding locks. + notifyFailure(x); + throw x; } } - @Override - public synchronized void flush() throws IOException + private void flush(boolean fin) throws IOException { - LOG.debug("flush()"); - assertNotClosed(); - - // flush whatever is in the buffer with FIN=false - flush(false); - try + synchronized (this) { - super.flush(); - LOG.debug("flushed"); - } - catch (IOException e) - { - notifyFailure(e); - throw e; - } - } + if (closed) + throw new IOException("Stream is closed"); - /** - * 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); + closed = fin; - try - { - outgoing.outgoingFrame(frame,blocker); - // block on write + BufferUtil.flipToFlush(buffer, 0); + LOG.debug("flush({}): {}", fin, BufferUtil.toDetailString(buffer)); + frame.setPayload(buffer); + frame.setFin(fin); + + outgoing.outgoingFrame(frame, blocker, BatchMode.OFF); blocker.block(); - // block success - frameCount++; + + ++frameCount; + // Any flush after the first will be a CONTINUATION frame. frame.setIsContinuation(); + BufferUtil.flipToFill(buffer); } - catch (IOException e) - { - notifyFailure(e); - throw e; - } } - private void notifyFailure(IOException e) + private void send(byte[] bytes, int offset, int length) throws IOException { - if (callback != null) + synchronized (this) { - callback.writeFailed(e); + if (closed) + throw new IOException("Stream is closed"); + + while (length > 0) + { + // There may be no space available, we want + // to handle correctly when space == 0. + int space = buffer.remaining(); + int size = Math.min(space, length); + buffer.put(bytes, offset, size); + offset += size; + length -= size; + if (length > 0) + { + // If we could not write everything, it means + // that the buffer was full, so flush it. + flush(false); + } + } } } public void setCallback(WriteCallback callback) { - this.callback = callback; - } - - @Override - public synchronized void write(byte[] b) throws IOException - { - try + synchronized (this) { - this.write(b,0,b.length); - } - catch (IOException e) - { - notifyFailure(e); - throw e; + this.callback = callback; } } - @Override - public synchronized void write(byte[] b, int off, int len) throws IOException + private void notifySuccess() { - 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) + WriteCallback callback; + synchronized (this) { - 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 + callback = this.callback; + } + if (callback != null) + { + callback.writeSuccess(); } } - @Override - public synchronized void write(int b) throws IOException + private void notifyFailure(Throwable failure) { - assertNotClosed(); - - // buffer up to limit, flush once buffer reached. - buffer.put((byte)b); - if (buffer.remaining() <= 0) + WriteCallback callback; + synchronized (this) { - flush(false); + callback = this.callback; + } + if (callback != null) + { + callback.writeFailed(failure); } } } 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 ef76ecbcbc6..477046b2089 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 @@ -21,11 +21,12 @@ package org.eclipse.jetty.websocket.common.message; import java.io.IOException; import java.io.InputStreamReader; import java.nio.ByteBuffer; +import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; /** * Support class for reading a (single) WebSocket TEXT message via a Reader. - *

+ *

* In compliance to the WebSocket spec, this reader always uses the UTF8 {@link Charset}. */ public class MessageReader extends InputStreamReader implements MessageAppender @@ -34,14 +35,14 @@ public class MessageReader extends InputStreamReader implements MessageAppender public MessageReader(MessageInputStream stream) { - super(stream,StandardCharsets.UTF_8); + super(stream, StandardCharsets.UTF_8); this.stream = stream; } @Override public void appendFrame(ByteBuffer payload, boolean isLast) throws IOException { - this.stream.appendFrame(payload,isLast); + this.stream.appendFrame(payload, isLast); } @Override 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 a077dfef685..aa3852ede86 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 @@ -26,6 +26,7 @@ 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.BatchMode; import org.eclipse.jetty.websocket.api.WriteCallback; import org.eclipse.jetty.websocket.api.extensions.OutgoingFrames; import org.eclipse.jetty.websocket.common.BlockingWriteCallback; @@ -34,165 +35,185 @@ import org.eclipse.jetty.websocket.common.frames.TextFrame; /** * Support for writing a single WebSocket TEXT message via a {@link Writer} - *

+ *

* Note: Per WebSocket spec, all WebSocket TEXT messages must be encoded in UTF-8 */ public class MessageWriter extends Writer { private static final Logger LOG = Log.getLogger(MessageWriter.class); + private final OutgoingFrames outgoing; private final ByteBufferPool bufferPool; private final BlockingWriteCallback blocker; - private long frameCount = 0; + private long frameCount; private TextFrame frame; private ByteBuffer buffer; private Utf8CharBuffer utf; private WriteCallback callback; - private boolean closed = false; + private boolean closed; + + public MessageWriter(WebSocketSession session) + { + this(session.getOutgoingHandler(), session.getPolicy().getMaxTextMessageBufferSize(), session.getBufferPool()); + } public MessageWriter(OutgoingFrames outgoing, int bufferSize, ByteBufferPool bufferPool) { this.outgoing = outgoing; this.bufferPool = bufferPool; this.blocker = new BlockingWriteCallback(); - this.buffer = bufferPool.acquire(bufferSize,true); + this.buffer = bufferPool.acquire(bufferSize, true); BufferUtil.flipToFill(buffer); - this.utf = Utf8CharBuffer.wrap(buffer); this.frame = new TextFrame(); - } - - 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; - } + this.utf = Utf8CharBuffer.wrap(buffer); } @Override - public synchronized void close() throws IOException - { - 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 - { - assertNotClosed(); - - // flush whatever is in the buffer with FIN=false - flush(false); - } - - /** - * Flush whatever is in the buffer. - * - * @param fin - * fin flag - * @throws IOException - */ - private synchronized void flush(boolean fin) throws IOException - { - ByteBuffer data = utf.getByteBuffer(); - frame.setPayload(data); - frame.setFin(fin); - - try - { - outgoing.outgoingFrame(frame,blocker); - // block on write - blocker.block(); - // write success - // clear utf buffer - utf.clear(); - frameCount++; - frame.setIsContinuation(); - } - 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 + public void write(char[] chars, int off, int len) throws IOException { try { - this.write(cbuf,0,cbuf.length); + send(chars, off, len); } - catch (IOException e) + catch (Throwable x) { - notifyFailure(e); - throw e; - } - } - - @Override - public void write(char[] cbuf, int off, int len) throws IOException - { - 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 + // Notify without holding locks. + notifyFailure(x); + throw x; } } @Override public void write(int c) throws IOException { - assertNotClosed(); + try + { + send(new char[]{(char)c}, 0, 1); + } + catch (Throwable x) + { + // Notify without holding locks. + notifyFailure(x); + throw x; + } + } - // buffer up to limit, flush once buffer reached. - utf.append(c); // append with utf logic - if (utf.remaining() <= 0) + @Override + public void flush() throws IOException + { + try { flush(false); } + catch (Throwable x) + { + // Notify without holding locks. + notifyFailure(x); + throw x; + } + } + + @Override + public void close() throws IOException + { + try + { + flush(true); + bufferPool.release(buffer); + LOG.debug("Stream closed, {} frames sent", frameCount); + // Notify without holding locks. + notifySuccess(); + } + catch (Throwable x) + { + // Notify without holding locks. + notifyFailure(x); + throw x; + } + } + + private void flush(boolean fin) throws IOException + { + synchronized (this) + { + if (closed) + throw new IOException("Stream is closed"); + + closed = fin; + + ByteBuffer data = utf.getByteBuffer(); + LOG.debug("flush({}): {}", fin, BufferUtil.toDetailString(buffer)); + frame.setPayload(data); + frame.setFin(fin); + + outgoing.outgoingFrame(frame, blocker, BatchMode.OFF); + blocker.block(); + + ++frameCount; + // Any flush after the first will be a CONTINUATION frame. + frame.setIsContinuation(); + + utf.clear(); + } + } + + private void send(char[] chars, int offset, int length) throws IOException + { + synchronized (this) + { + if (closed) + throw new IOException("Stream is closed"); + + while (length > 0) + { + // There may be no space available, we want + // to handle correctly when space == 0. + int space = utf.remaining(); + int size = Math.min(space, length); + utf.append(chars, offset, size); + offset += size; + length -= size; + if (length > 0) + { + // If we could not write everything, it means + // that the buffer was full, so flush it. + flush(false); + } + } + } + } + + public void setCallback(WriteCallback callback) + { + synchronized (this) + { + this.callback = callback; + } + } + + private void notifySuccess() + { + WriteCallback callback; + synchronized (this) + { + callback = this.callback; + } + if (callback != null) + { + callback.writeSuccess(); + } + } + + private void notifyFailure(Throwable failure) + { + WriteCallback callback; + synchronized (this) + { + callback = this.callback; + } + if (callback != null) + { + callback.writeFailed(failure); + } } } 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 3580ca97e80..c6105b39903 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 @@ -34,7 +34,7 @@ public class SimpleTextMessage implements MessageAppender public SimpleTextMessage(EventDriver onEvent) { this.onEvent = onEvent; - this.utf = new Utf8StringBuilder(); + this.utf = new Utf8StringBuilder(1024); size = 0; finished = false; } diff --git a/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/GeneratorTest.java b/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/GeneratorTest.java index 9557d4678f5..aee29187618 100644 --- a/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/GeneratorTest.java +++ b/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/GeneratorTest.java @@ -18,8 +18,6 @@ package org.eclipse.jetty.websocket.common; -import static org.hamcrest.Matchers.*; - import java.nio.ByteBuffer; import java.util.Arrays; @@ -41,6 +39,8 @@ import org.eclipse.jetty.websocket.common.util.Hex; import org.junit.Assert; import org.junit.Test; +import static org.hamcrest.Matchers.is; + public class GeneratorTest { private static final Logger LOG = Log.getLogger(GeneratorTest.WindowHelper.class); @@ -64,7 +64,7 @@ public class GeneratorTest int completeBufSize = 0; for (Frame f : frames) { - completeBufSize += Generator.OVERHEAD + f.getPayloadLength(); + completeBufSize += Generator.MAX_HEADER_LENGTH + f.getPayloadLength(); } ByteBuffer completeBuf = ByteBuffer.allocate(completeBufSize); diff --git a/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/WebSocketFrameTest.java b/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/WebSocketFrameTest.java index c359a6a3460..d8fc0ddca85 100644 --- a/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/WebSocketFrameTest.java +++ b/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/WebSocketFrameTest.java @@ -18,8 +18,6 @@ package org.eclipse.jetty.websocket.common; -import static org.hamcrest.Matchers.*; - import java.nio.ByteBuffer; import org.eclipse.jetty.io.MappedByteBufferPool; @@ -37,6 +35,8 @@ import org.junit.Before; import org.junit.Rule; import org.junit.Test; +import static org.hamcrest.Matchers.is; + public class WebSocketFrameTest { @Rule @@ -47,7 +47,7 @@ public class WebSocketFrameTest private ByteBuffer generateWholeFrame(Generator generator, Frame frame) { - ByteBuffer buf = ByteBuffer.allocate(frame.getPayloadLength() + Generator.OVERHEAD); + ByteBuffer buf = ByteBuffer.allocate(frame.getPayloadLength() + Generator.MAX_HEADER_LENGTH); generator.generateWholeFrame(frame,buf); BufferUtil.flipToFlush(buf,0); return buf; 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 index a78b7e68703..86e14a61f9a 100644 --- 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 @@ -18,8 +18,6 @@ package org.eclipse.jetty.websocket.common; -import static org.hamcrest.Matchers.containsString; - import java.io.IOException; import java.nio.ByteBuffer; @@ -32,6 +30,8 @@ import org.junit.Rule; import org.junit.Test; import org.junit.rules.TestName; +import static org.hamcrest.Matchers.containsString; + public class WebSocketRemoteEndpointTest { @Rule diff --git a/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/ab/TestABCase2.java b/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/ab/TestABCase2.java index 8dcf7ff4d5e..dc0b2ec67f1 100644 --- a/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/ab/TestABCase2.java +++ b/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/ab/TestABCase2.java @@ -18,8 +18,6 @@ package org.eclipse.jetty.websocket.common.ab; -import static org.hamcrest.Matchers.is; - import java.nio.ByteBuffer; import java.util.Arrays; @@ -41,6 +39,8 @@ import org.eclipse.jetty.websocket.common.test.UnitParser; import org.junit.Assert; import org.junit.Test; +import static org.hamcrest.Matchers.is; + public class TestABCase2 { WebSocketPolicy policy = new WebSocketPolicy(WebSocketBehavior.CLIENT); @@ -288,7 +288,7 @@ public class TestABCase2 byte[] bytes = new byte[126]; Arrays.fill(bytes,(byte)0x00); - ByteBuffer expected = ByteBuffer.allocate(bytes.length + Generator.OVERHEAD); + ByteBuffer expected = ByteBuffer.allocate(bytes.length + Generator.MAX_HEADER_LENGTH); byte b; diff --git a/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/extensions/DummyOutgoingFrames.java b/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/extensions/DummyOutgoingFrames.java index a42f32ebef4..48534e46214 100644 --- a/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/extensions/DummyOutgoingFrames.java +++ b/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/extensions/DummyOutgoingFrames.java @@ -20,6 +20,7 @@ package org.eclipse.jetty.websocket.common.extensions; import org.eclipse.jetty.util.log.Log; import org.eclipse.jetty.util.log.Logger; +import org.eclipse.jetty.websocket.api.BatchMode; import org.eclipse.jetty.websocket.api.WriteCallback; import org.eclipse.jetty.websocket.api.extensions.Frame; import org.eclipse.jetty.websocket.api.extensions.OutgoingFrames; @@ -44,7 +45,7 @@ public class DummyOutgoingFrames implements OutgoingFrames } @Override - public void outgoingFrame(Frame frame, WriteCallback callback) + public void outgoingFrame(Frame frame, WriteCallback callback, BatchMode batchMode) { LOG.debug("outgoingFrame({},{})",frame,callback); if (callback != null) diff --git a/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/extensions/FragmentExtensionTest.java b/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/extensions/FragmentExtensionTest.java index b022c38e4aa..d7d6643e9ac 100644 --- a/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/extensions/FragmentExtensionTest.java +++ b/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/extensions/FragmentExtensionTest.java @@ -18,8 +18,6 @@ package org.eclipse.jetty.websocket.common.extensions; -import static org.hamcrest.Matchers.*; - import java.io.IOException; import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; @@ -29,6 +27,7 @@ import java.util.List; import org.eclipse.jetty.io.MappedByteBufferPool; import org.eclipse.jetty.util.BufferUtil; +import org.eclipse.jetty.websocket.api.BatchMode; import org.eclipse.jetty.websocket.api.WebSocketPolicy; import org.eclipse.jetty.websocket.api.extensions.ExtensionConfig; import org.eclipse.jetty.websocket.api.extensions.Frame; @@ -46,10 +45,12 @@ import org.junit.Assert; import org.junit.Rule; import org.junit.Test; +import static org.hamcrest.Matchers.is; + public class FragmentExtensionTest { @Rule - public LeakTrackingBufferPool bufferPool = new LeakTrackingBufferPool("Test",new MappedByteBufferPool()); + public LeakTrackingBufferPool bufferPool = new LeakTrackingBufferPool("Test", new MappedByteBufferPool()); /** * Verify that incoming frames are passed thru without modification @@ -82,7 +83,7 @@ public class FragmentExtensionTest int len = quote.size(); capture.assertFrameCount(len); - capture.assertHasFrame(OpCode.TEXT,len); + capture.assertHasFrame(OpCode.TEXT, len); String prefix; int i = 0; @@ -90,15 +91,15 @@ public class FragmentExtensionTest { prefix = "Frame[" + i + "]"; - Assert.assertThat(prefix + ".opcode",actual.getOpCode(),is(OpCode.TEXT)); - Assert.assertThat(prefix + ".fin",actual.isFin(),is(true)); - Assert.assertThat(prefix + ".rsv1",actual.isRsv1(),is(false)); - Assert.assertThat(prefix + ".rsv2",actual.isRsv2(),is(false)); - Assert.assertThat(prefix + ".rsv3",actual.isRsv3(),is(false)); + Assert.assertThat(prefix + ".opcode", actual.getOpCode(), is(OpCode.TEXT)); + Assert.assertThat(prefix + ".fin", actual.isFin(), is(true)); + Assert.assertThat(prefix + ".rsv1", actual.isRsv1(), is(false)); + Assert.assertThat(prefix + ".rsv2", actual.isRsv2(), is(false)); + Assert.assertThat(prefix + ".rsv3", actual.isRsv3(), is(false)); - ByteBuffer expected = BufferUtil.toBuffer(quote.get(i),StandardCharsets.UTF_8); - Assert.assertThat(prefix + ".payloadLength",actual.getPayloadLength(),is(expected.remaining())); - ByteBufferAssert.assertEquals(prefix + ".payload",expected,actual.getPayload().slice()); + ByteBuffer expected = BufferUtil.toBuffer(quote.get(i), StandardCharsets.UTF_8); + Assert.assertThat(prefix + ".payloadLength", actual.getPayloadLength(), is(expected.remaining())); + ByteBufferAssert.assertEquals(prefix + ".payload", expected, actual.getPayload().slice()); i++; } } @@ -124,18 +125,18 @@ public class FragmentExtensionTest ext.incomingFrame(ping); capture.assertFrameCount(1); - capture.assertHasFrame(OpCode.PING,1); + capture.assertHasFrame(OpCode.PING, 1); WebSocketFrame actual = capture.getFrames().poll(); - Assert.assertThat("Frame.opcode",actual.getOpCode(),is(OpCode.PING)); - Assert.assertThat("Frame.fin",actual.isFin(),is(true)); - Assert.assertThat("Frame.rsv1",actual.isRsv1(),is(false)); - Assert.assertThat("Frame.rsv2",actual.isRsv2(),is(false)); - Assert.assertThat("Frame.rsv3",actual.isRsv3(),is(false)); + Assert.assertThat("Frame.opcode", actual.getOpCode(), is(OpCode.PING)); + Assert.assertThat("Frame.fin", actual.isFin(), is(true)); + Assert.assertThat("Frame.rsv1", actual.isRsv1(), is(false)); + Assert.assertThat("Frame.rsv2", actual.isRsv2(), is(false)); + Assert.assertThat("Frame.rsv3", actual.isRsv3(), is(false)); - ByteBuffer expected = BufferUtil.toBuffer(payload,StandardCharsets.UTF_8); - Assert.assertThat("Frame.payloadLength",actual.getPayloadLength(),is(expected.remaining())); - ByteBufferAssert.assertEquals("Frame.payload",expected,actual.getPayload().slice()); + ByteBuffer expected = BufferUtil.toBuffer(payload, StandardCharsets.UTF_8); + Assert.assertThat("Frame.payloadLength", actual.getPayloadLength(), is(expected.remaining())); + ByteBufferAssert.assertEquals("Frame.payload", expected, actual.getPayload().slice()); } /** @@ -164,7 +165,7 @@ public class FragmentExtensionTest for (String section : quote) { Frame frame = new TextFrame().setPayload(section); - ext.outgoingFrame(frame,null); + ext.outgoingFrame(frame, null, BatchMode.OFF); } // Expected Frames @@ -195,18 +196,18 @@ public class FragmentExtensionTest // System.out.printf("expect: %s%n",expectedFrame); // Validate Frame - Assert.assertThat(prefix + ".opcode",actualFrame.getOpCode(),is(expectedFrame.getOpCode())); - Assert.assertThat(prefix + ".fin",actualFrame.isFin(),is(expectedFrame.isFin())); - Assert.assertThat(prefix + ".rsv1",actualFrame.isRsv1(),is(expectedFrame.isRsv1())); - Assert.assertThat(prefix + ".rsv2",actualFrame.isRsv2(),is(expectedFrame.isRsv2())); - Assert.assertThat(prefix + ".rsv3",actualFrame.isRsv3(),is(expectedFrame.isRsv3())); + Assert.assertThat(prefix + ".opcode", actualFrame.getOpCode(), is(expectedFrame.getOpCode())); + Assert.assertThat(prefix + ".fin", actualFrame.isFin(), is(expectedFrame.isFin())); + Assert.assertThat(prefix + ".rsv1", actualFrame.isRsv1(), is(expectedFrame.isRsv1())); + Assert.assertThat(prefix + ".rsv2", actualFrame.isRsv2(), is(expectedFrame.isRsv2())); + Assert.assertThat(prefix + ".rsv3", actualFrame.isRsv3(), is(expectedFrame.isRsv3())); // Validate Payload ByteBuffer expectedData = expectedFrame.getPayload().slice(); ByteBuffer actualData = actualFrame.getPayload().slice(); - Assert.assertThat(prefix + ".payloadLength",actualData.remaining(),is(expectedData.remaining())); - ByteBufferAssert.assertEquals(prefix + ".payload",expectedData,actualData); + Assert.assertThat(prefix + ".payloadLength", actualData.remaining(), is(expectedData.remaining())); + ByteBufferAssert.assertEquals(prefix + ".payload", expectedData, actualData); } } @@ -236,7 +237,7 @@ public class FragmentExtensionTest for (String section : quote) { Frame frame = new TextFrame().setPayload(section); - ext.outgoingFrame(frame,null); + ext.outgoingFrame(frame, null, BatchMode.OFF); } // Expected Frames @@ -259,18 +260,18 @@ public class FragmentExtensionTest WebSocketFrame expectedFrame = expectedFrames.get(i); // Validate Frame - Assert.assertThat(prefix + ".opcode",actualFrame.getOpCode(),is(expectedFrame.getOpCode())); - Assert.assertThat(prefix + ".fin",actualFrame.isFin(),is(expectedFrame.isFin())); - Assert.assertThat(prefix + ".rsv1",actualFrame.isRsv1(),is(expectedFrame.isRsv1())); - Assert.assertThat(prefix + ".rsv2",actualFrame.isRsv2(),is(expectedFrame.isRsv2())); - Assert.assertThat(prefix + ".rsv3",actualFrame.isRsv3(),is(expectedFrame.isRsv3())); + Assert.assertThat(prefix + ".opcode", actualFrame.getOpCode(), is(expectedFrame.getOpCode())); + Assert.assertThat(prefix + ".fin", actualFrame.isFin(), is(expectedFrame.isFin())); + Assert.assertThat(prefix + ".rsv1", actualFrame.isRsv1(), is(expectedFrame.isRsv1())); + Assert.assertThat(prefix + ".rsv2", actualFrame.isRsv2(), is(expectedFrame.isRsv2())); + Assert.assertThat(prefix + ".rsv3", actualFrame.isRsv3(), is(expectedFrame.isRsv3())); // Validate Payload ByteBuffer expectedData = expectedFrame.getPayload().slice(); ByteBuffer actualData = actualFrame.getPayload().slice(); - Assert.assertThat(prefix + ".payloadLength",actualData.remaining(),is(expectedData.remaining())); - ByteBufferAssert.assertEquals(prefix + ".payload",expectedData,actualData); + Assert.assertThat(prefix + ".payloadLength", actualData.remaining(), is(expectedData.remaining())); + ByteBufferAssert.assertEquals(prefix + ".payload", expectedData, actualData); } } @@ -293,21 +294,21 @@ public class FragmentExtensionTest String payload = "Are you there?"; Frame ping = new PingFrame().setPayload(payload); - ext.outgoingFrame(ping,null); + ext.outgoingFrame(ping, null, BatchMode.OFF); capture.assertFrameCount(1); - capture.assertHasFrame(OpCode.PING,1); + capture.assertHasFrame(OpCode.PING, 1); WebSocketFrame actual = capture.getFrames().getFirst(); - Assert.assertThat("Frame.opcode",actual.getOpCode(),is(OpCode.PING)); - Assert.assertThat("Frame.fin",actual.isFin(),is(true)); - Assert.assertThat("Frame.rsv1",actual.isRsv1(),is(false)); - Assert.assertThat("Frame.rsv2",actual.isRsv2(),is(false)); - Assert.assertThat("Frame.rsv3",actual.isRsv3(),is(false)); + Assert.assertThat("Frame.opcode", actual.getOpCode(), is(OpCode.PING)); + Assert.assertThat("Frame.fin", actual.isFin(), is(true)); + Assert.assertThat("Frame.rsv1", actual.isRsv1(), is(false)); + Assert.assertThat("Frame.rsv2", actual.isRsv2(), is(false)); + Assert.assertThat("Frame.rsv3", actual.isRsv3(), is(false)); - ByteBuffer expected = BufferUtil.toBuffer(payload,StandardCharsets.UTF_8); - Assert.assertThat("Frame.payloadLength",actual.getPayloadLength(),is(expected.remaining())); - ByteBufferAssert.assertEquals("Frame.payload",expected,actual.getPayload().slice()); + ByteBuffer expected = BufferUtil.toBuffer(payload, StandardCharsets.UTF_8); + Assert.assertThat("Frame.payloadLength", actual.getPayloadLength(), is(expected.remaining())); + ByteBufferAssert.assertEquals("Frame.payload", expected, actual.getPayload().slice()); } } diff --git a/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/extensions/IdentityExtensionTest.java b/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/extensions/IdentityExtensionTest.java index 47052ce16a7..da4dce34011 100644 --- a/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/extensions/IdentityExtensionTest.java +++ b/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/extensions/IdentityExtensionTest.java @@ -18,13 +18,12 @@ package org.eclipse.jetty.websocket.common.extensions; -import static org.hamcrest.Matchers.is; - import java.io.IOException; import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; import org.eclipse.jetty.util.BufferUtil; +import org.eclipse.jetty.websocket.api.BatchMode; import org.eclipse.jetty.websocket.api.extensions.Extension; import org.eclipse.jetty.websocket.api.extensions.Frame; import org.eclipse.jetty.websocket.common.OpCode; @@ -37,6 +36,8 @@ import org.eclipse.jetty.websocket.common.test.OutgoingFramesCapture; import org.junit.Assert; import org.junit.Test; +import static org.hamcrest.Matchers.is; + public class IdentityExtensionTest { /** @@ -54,18 +55,18 @@ public class IdentityExtensionTest ext.incomingFrame(frame); capture.assertFrameCount(1); - capture.assertHasFrame(OpCode.TEXT,1); + capture.assertHasFrame(OpCode.TEXT, 1); WebSocketFrame actual = capture.getFrames().poll(); - Assert.assertThat("Frame.opcode",actual.getOpCode(),is(OpCode.TEXT)); - Assert.assertThat("Frame.fin",actual.isFin(),is(true)); - Assert.assertThat("Frame.rsv1",actual.isRsv1(),is(false)); - Assert.assertThat("Frame.rsv2",actual.isRsv2(),is(false)); - Assert.assertThat("Frame.rsv3",actual.isRsv3(),is(false)); + Assert.assertThat("Frame.opcode", actual.getOpCode(), is(OpCode.TEXT)); + Assert.assertThat("Frame.fin", actual.isFin(), is(true)); + Assert.assertThat("Frame.rsv1", actual.isRsv1(), is(false)); + Assert.assertThat("Frame.rsv2", actual.isRsv2(), is(false)); + Assert.assertThat("Frame.rsv3", actual.isRsv3(), is(false)); - ByteBuffer expected = BufferUtil.toBuffer("hello",StandardCharsets.UTF_8); - Assert.assertThat("Frame.payloadLength",actual.getPayloadLength(),is(expected.remaining())); - ByteBufferAssert.assertEquals("Frame.payload",expected,actual.getPayload().slice()); + ByteBuffer expected = BufferUtil.toBuffer("hello", StandardCharsets.UTF_8); + Assert.assertThat("Frame.payloadLength", actual.getPayloadLength(), is(expected.remaining())); + ByteBufferAssert.assertEquals("Frame.payload", expected, actual.getPayload().slice()); } /** @@ -80,21 +81,21 @@ public class IdentityExtensionTest ext.setNextOutgoingFrames(capture); Frame frame = new TextFrame().setPayload("hello"); - ext.outgoingFrame(frame,null); + ext.outgoingFrame(frame, null, BatchMode.OFF); capture.assertFrameCount(1); - capture.assertHasFrame(OpCode.TEXT,1); + capture.assertHasFrame(OpCode.TEXT, 1); WebSocketFrame actual = capture.getFrames().getFirst(); - Assert.assertThat("Frame.opcode",actual.getOpCode(),is(OpCode.TEXT)); - Assert.assertThat("Frame.fin",actual.isFin(),is(true)); - Assert.assertThat("Frame.rsv1",actual.isRsv1(),is(false)); - Assert.assertThat("Frame.rsv2",actual.isRsv2(),is(false)); - Assert.assertThat("Frame.rsv3",actual.isRsv3(),is(false)); + Assert.assertThat("Frame.opcode", actual.getOpCode(), is(OpCode.TEXT)); + Assert.assertThat("Frame.fin", actual.isFin(), is(true)); + Assert.assertThat("Frame.rsv1", actual.isRsv1(), is(false)); + Assert.assertThat("Frame.rsv2", actual.isRsv2(), is(false)); + Assert.assertThat("Frame.rsv3", actual.isRsv3(), is(false)); - ByteBuffer expected = BufferUtil.toBuffer("hello",StandardCharsets.UTF_8); - Assert.assertThat("Frame.payloadLength",actual.getPayloadLength(),is(expected.remaining())); - ByteBufferAssert.assertEquals("Frame.payload",expected,actual.getPayload().slice()); + ByteBuffer expected = BufferUtil.toBuffer("hello", StandardCharsets.UTF_8); + Assert.assertThat("Frame.payloadLength", actual.getPayloadLength(), is(expected.remaining())); + ByteBufferAssert.assertEquals("Frame.payload", expected, actual.getPayload().slice()); } } diff --git a/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/extensions/compress/CapturedHexPayloads.java b/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/extensions/compress/CapturedHexPayloads.java index 04d7d41f97a..779cac6601f 100644 --- a/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/extensions/compress/CapturedHexPayloads.java +++ b/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/extensions/compress/CapturedHexPayloads.java @@ -21,6 +21,7 @@ package org.eclipse.jetty.websocket.common.extensions.compress; import java.util.ArrayList; import java.util.List; +import org.eclipse.jetty.websocket.api.BatchMode; import org.eclipse.jetty.websocket.api.WriteCallback; import org.eclipse.jetty.websocket.api.extensions.Frame; import org.eclipse.jetty.websocket.api.extensions.OutgoingFrames; @@ -31,7 +32,7 @@ public class CapturedHexPayloads implements OutgoingFrames private List captured = new ArrayList<>(); @Override - public void outgoingFrame(Frame frame, WriteCallback callback) + public void outgoingFrame(Frame frame, WriteCallback callback, BatchMode batchMode) { String hexPayload = Hex.asHex(frame.getPayload()); captured.add(hexPayload); diff --git a/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/extensions/compress/DeflateFrameExtensionTest.java b/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/extensions/compress/DeflateFrameExtensionTest.java index d8dc65e351b..fbb2e3151db 100644 --- a/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/extensions/compress/DeflateFrameExtensionTest.java +++ b/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/extensions/compress/DeflateFrameExtensionTest.java @@ -18,29 +18,35 @@ package org.eclipse.jetty.websocket.common.extensions.compress; -import static org.hamcrest.Matchers.*; - +import java.io.ByteArrayOutputStream; import java.io.IOException; import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; import java.util.Collections; import java.util.List; +import java.util.Random; import java.util.zip.Deflater; import java.util.zip.Inflater; import org.eclipse.jetty.io.MappedByteBufferPool; +import org.eclipse.jetty.io.RuntimeIOException; import org.eclipse.jetty.util.BufferUtil; import org.eclipse.jetty.util.StringUtil; import org.eclipse.jetty.util.TypeUtil; +import org.eclipse.jetty.websocket.api.BatchMode; import org.eclipse.jetty.websocket.api.WebSocketPolicy; +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.api.extensions.IncomingFrames; +import org.eclipse.jetty.websocket.api.extensions.OutgoingFrames; import org.eclipse.jetty.websocket.common.Generator; 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.AbstractExtensionTest; import org.eclipse.jetty.websocket.common.extensions.ExtensionTool.Tester; +import org.eclipse.jetty.websocket.common.frames.BinaryFrame; import org.eclipse.jetty.websocket.common.frames.TextFrame; import org.eclipse.jetty.websocket.common.test.ByteBufferAssert; import org.eclipse.jetty.websocket.common.test.IncomingFramesCapture; @@ -51,11 +57,15 @@ import org.junit.Assert; import org.junit.Rule; import org.junit.Test; +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.greaterThan; +import static org.hamcrest.Matchers.is; + public class DeflateFrameExtensionTest extends AbstractExtensionTest { @Rule - public LeakTrackingBufferPool bufferPool = new LeakTrackingBufferPool("Test",new MappedByteBufferPool()); - + public LeakTrackingBufferPool bufferPool = new LeakTrackingBufferPool("Test", new MappedByteBufferPool()); + private void assertIncoming(byte[] raw, String... expectedTextDatas) { WebSocketPolicy policy = WebSocketPolicy.newClientPolicy(); @@ -81,21 +91,21 @@ public class DeflateFrameExtensionTest extends AbstractExtensionTest int len = expectedTextDatas.length; capture.assertFrameCount(len); - capture.assertHasFrame(OpCode.TEXT,len); + capture.assertHasFrame(OpCode.TEXT, len); - int i=0; - for (WebSocketFrame actual: capture.getFrames()) + int i = 0; + for (WebSocketFrame actual : capture.getFrames()) { String prefix = "Frame[" + i + "]"; - Assert.assertThat(prefix + ".opcode",actual.getOpCode(),is(OpCode.TEXT)); - Assert.assertThat(prefix + ".fin",actual.isFin(),is(true)); - Assert.assertThat(prefix + ".rsv1",actual.isRsv1(),is(false)); // RSV1 should be unset at this point - Assert.assertThat(prefix + ".rsv2",actual.isRsv2(),is(false)); - Assert.assertThat(prefix + ".rsv3",actual.isRsv3(),is(false)); + Assert.assertThat(prefix + ".opcode", actual.getOpCode(), is(OpCode.TEXT)); + Assert.assertThat(prefix + ".fin", actual.isFin(), is(true)); + Assert.assertThat(prefix + ".rsv1", actual.isRsv1(), is(false)); // RSV1 should be unset at this point + Assert.assertThat(prefix + ".rsv2", actual.isRsv2(), is(false)); + Assert.assertThat(prefix + ".rsv3", actual.isRsv3(), is(false)); - ByteBuffer expected = BufferUtil.toBuffer(expectedTextDatas[i],StandardCharsets.UTF_8); - Assert.assertThat(prefix + ".payloadLength",actual.getPayloadLength(),is(expected.remaining())); - ByteBufferAssert.assertEquals(prefix + ".payload",expected,actual.getPayload().slice()); + ByteBuffer expected = BufferUtil.toBuffer(expectedTextDatas[i], StandardCharsets.UTF_8); + Assert.assertThat(prefix + ".payloadLength", actual.getPayloadLength(), is(expected.remaining())); + ByteBufferAssert.assertEquals(prefix + ".payload", expected, actual.getPayload().slice()); i++; } } @@ -111,17 +121,16 @@ public class DeflateFrameExtensionTest extends AbstractExtensionTest ExtensionConfig config = ExtensionConfig.parse("deflate-frame"); ext.setConfig(config); - boolean validating = true; - Generator generator = new Generator(policy,bufferPool,validating); + Generator generator = new Generator(policy, bufferPool, true); generator.configureFromExtensions(Collections.singletonList(ext)); OutgoingNetworkBytesCapture capture = new OutgoingNetworkBytesCapture(generator); ext.setNextOutgoingFrames(capture); Frame frame = new TextFrame().setPayload(text); - ext.outgoingFrame(frame,null); + ext.outgoingFrame(frame, null, BatchMode.OFF); - capture.assertBytes(0,expectedHex); + capture.assertBytes(0, expectedHex); } @Test @@ -134,9 +143,9 @@ public class DeflateFrameExtensionTest extends AbstractExtensionTest tester.parseIncomingHex(// Captured from Blockhead Client - "Hello" then "There" via unit test "c18700000000f248cdc9c90700", // "Hello" "c187000000000ac9482d4a0500" // "There" - ); + ); - tester.assertHasFrames("Hello","There"); + tester.assertHasFrames("Hello", "There"); } @Test @@ -148,7 +157,7 @@ public class DeflateFrameExtensionTest extends AbstractExtensionTest tester.parseIncomingHex(// Captured from Chrome 20.x - "Hello" (sent from browser) "c187832b5c11716391d84a2c5c" // "Hello" - ); + ); tester.assertHasFrames("Hello"); } @@ -163,9 +172,9 @@ public class DeflateFrameExtensionTest extends AbstractExtensionTest tester.parseIncomingHex(// Captured from Chrome 20.x - "Hello" then "There" (sent from browser) "c1877b1971db8951bc12b21e71", // "Hello" "c18759edc8f4532480d913e8c8" // There - ); + ); - tester.assertHasFrames("Hello","There"); + tester.assertHasFrames("Hello", "There"); } @Test @@ -177,7 +186,7 @@ public class DeflateFrameExtensionTest extends AbstractExtensionTest tester.parseIncomingHex(// Captured from Chrome 20.x - "info:" (sent from browser) "c187ca4def7f0081a4b47d4fef" // example payload - ); + ); tester.assertHasFrames("info:"); } @@ -192,9 +201,9 @@ public class DeflateFrameExtensionTest extends AbstractExtensionTest tester.parseIncomingHex(// Captured from Chrome 20.x - "time:" then "time:" once more (sent from browser) "c18782467424a88fb869374474", // "time:" "c1853cfda17f16fcb07f3c" // "time:" - ); + ); - tester.assertHasFrames("time:","time:"); + tester.assertHasFrames("time:", "time:"); } @Test @@ -208,9 +217,9 @@ public class DeflateFrameExtensionTest extends AbstractExtensionTest "c1876b100104" + "41d9cd49de1201", // "time:" "c1852ae3ff01" + "00e2ee012a", // "time:" "c18435558caa" + "37468caa" // "time:" - ); + ); - tester.assertHasFrames("time:","time:","time:"); + tester.assertHasFrames("time:", "time:", "time:"); } @Test @@ -218,7 +227,7 @@ public class DeflateFrameExtensionTest extends AbstractExtensionTest { // What pywebsocket produces for "time:", "time:", "time:" String expected[] = new String[] - { "2AC9CC4DB50200", "2A01110000", "02130000" }; + {"2AC9CC4DB50200", "2A01110000", "02130000"}; // Lets see what we produce CapturedHexPayloads capture = new CapturedHexPayloads(); @@ -226,13 +235,13 @@ public class DeflateFrameExtensionTest extends AbstractExtensionTest init(ext); ext.setNextOutgoingFrames(capture); - ext.outgoingFrame(new TextFrame().setPayload("time:"),null); - ext.outgoingFrame(new TextFrame().setPayload("time:"),null); - ext.outgoingFrame(new TextFrame().setPayload("time:"),null); + ext.outgoingFrame(new TextFrame().setPayload("time:"), null, BatchMode.OFF); + ext.outgoingFrame(new TextFrame().setPayload("time:"), null, BatchMode.OFF); + ext.outgoingFrame(new TextFrame().setPayload("time:"), null, BatchMode.OFF); List actual = capture.getCaptured(); - - Assert.assertThat("Compressed Payloads",actual,contains(expected)); + + Assert.assertThat("Compressed Payloads", actual, contains(expected)); } private void init(DeflateFrameExtension ext) @@ -245,8 +254,7 @@ public class DeflateFrameExtensionTest extends AbstractExtensionTest public void testDeflateBasics() throws Exception { // Setup deflater basics - boolean nowrap = true; - Deflater compressor = new Deflater(Deflater.BEST_COMPRESSION,nowrap); + Deflater compressor = new Deflater(Deflater.BEST_COMPRESSION, true); compressor.setStrategy(Deflater.DEFAULT_STRATEGY); // Text to compress @@ -255,7 +263,7 @@ public class DeflateFrameExtensionTest extends AbstractExtensionTest // Prime the compressor compressor.reset(); - compressor.setInput(uncompressed,0,uncompressed.length); + compressor.setInput(uncompressed, 0, uncompressed.length); compressor.finish(); // Perform compression @@ -265,26 +273,24 @@ public class DeflateFrameExtensionTest extends AbstractExtensionTest while (!compressor.finished()) { byte out[] = new byte[64]; - int len = compressor.deflate(out,0,out.length,Deflater.SYNC_FLUSH); + int len = compressor.deflate(out, 0, out.length, Deflater.SYNC_FLUSH); if (len > 0) { - outbuf.put(out,0,len); + outbuf.put(out, 0, len); } } compressor.end(); - BufferUtil.flipToFlush(outbuf,0); - byte b0 = outbuf.get(0); - if ((b0 & 1) != 0) - { - outbuf.put(0,(b0 ^= 1)); - } + BufferUtil.flipToFlush(outbuf, 0); byte compressed[] = BufferUtil.toArray(outbuf); + // Clear the BFINAL bit that has been set by the compressor.end() call. + // In the real implementation we never end() the compressor. + compressed[0] &= 0xFE; String actual = TypeUtil.toHexString(compressed); String expected = "CaCc4bCbB70200"; // what pywebsocket produces - Assert.assertThat("Compressed data",actual,is(expected)); + Assert.assertThat("Compressed data", actual, is(expected)); } @Test @@ -297,17 +303,16 @@ public class DeflateFrameExtensionTest extends AbstractExtensionTest ext.setPolicy(policy); ext.setConfig(new ExtensionConfig(ext.getName())); - boolean validating = true; - Generator generator = new Generator(policy,bufferPool,validating); + Generator generator = new Generator(policy, bufferPool, true); generator.configureFromExtensions(Collections.singletonList(ext)); OutgoingNetworkBytesCapture capture = new OutgoingNetworkBytesCapture(generator); ext.setNextOutgoingFrames(capture); - ext.outgoingFrame(new TextFrame().setPayload("Hello"),null); - ext.outgoingFrame(new TextFrame().setPayload("There"),null); + ext.outgoingFrame(new TextFrame().setPayload("Hello"), null, BatchMode.OFF); + ext.outgoingFrame(new TextFrame().setPayload("There"), null, BatchMode.OFF); - capture.assertBytes(0,"c107f248cdc9c90700"); + capture.assertBytes(0, "c107f248cdc9c90700"); } @Test @@ -319,15 +324,15 @@ public class DeflateFrameExtensionTest extends AbstractExtensionTest Inflater inflater = new Inflater(true); inflater.reset(); - inflater.setInput(rawbuf,0,rawbuf.length); + inflater.setInput(rawbuf, 0, rawbuf.length); byte outbuf[] = new byte[64]; int len = inflater.inflate(outbuf); inflater.end(); - Assert.assertThat("Inflated length",len,greaterThan(4)); + Assert.assertThat("Inflated length", len, greaterThan(4)); - String actual = StringUtil.toUTF8String(outbuf,0,len); - Assert.assertThat("Inflated text",actual,is("info:")); + String actual = StringUtil.toUTF8String(outbuf, 0, len); + Assert.assertThat("Inflated text", actual, is("info:")); } @Test @@ -335,7 +340,7 @@ public class DeflateFrameExtensionTest extends AbstractExtensionTest { // Captured from PyWebSocket - "Hello" (echo from server) byte rawbuf[] = TypeUtil.fromHexString("c107f248cdc9c90700"); - assertIncoming(rawbuf,"Hello"); + assertIncoming(rawbuf, "Hello"); } @Test @@ -344,7 +349,7 @@ public class DeflateFrameExtensionTest extends AbstractExtensionTest // Captured from PyWebSocket - Long Text (echo from server) byte rawbuf[] = TypeUtil.fromHexString("c1421cca410a80300c44d1abccce9df7" + "f018298634d05631138ab7b7b8fdef1f" + "dc0282e2061d575a45f6f2686bab25e1" + "3fb7296fa02b5885eb3b0379c394f461" + "98cafd03"); - assertIncoming(rawbuf,"It's a big enough umbrella but it's always me that ends up getting wet."); + assertIncoming(rawbuf, "It's a big enough umbrella but it's always me that ends up getting wet."); } @Test @@ -352,7 +357,7 @@ public class DeflateFrameExtensionTest extends AbstractExtensionTest { // Captured from PyWebSocket - "stackoverflow" (echo from server) byte rawbuf[] = TypeUtil.fromHexString("c10f2a2e494ccece2f4b2d4acbc92f0700"); - assertIncoming(rawbuf,"stackoverflow"); + assertIncoming(rawbuf, "stackoverflow"); } /** @@ -361,7 +366,7 @@ public class DeflateFrameExtensionTest extends AbstractExtensionTest @Test public void testServerGeneratedHello() throws IOException { - assertOutgoing("Hello","c107f248cdc9c90700"); + assertOutgoing("Hello", "c107f248cdc9c90700"); } /** @@ -370,6 +375,64 @@ public class DeflateFrameExtensionTest extends AbstractExtensionTest @Test public void testServerGeneratedThere() throws IOException { - assertOutgoing("There","c1070ac9482d4a0500"); + assertOutgoing("There", "c1070ac9482d4a0500"); + } + + @Test + public void testCompressAndDecompressBigPayload() throws Exception + { + byte[] input = new byte[1024 * 1024]; + // Make them not compressible. + new Random().nextBytes(input); + + DeflateFrameExtension clientExtension = new DeflateFrameExtension(); + clientExtension.setBufferPool(bufferPool); + clientExtension.setPolicy(WebSocketPolicy.newClientPolicy()); + clientExtension.setConfig(ExtensionConfig.parse("deflate-frame")); + + final DeflateFrameExtension serverExtension = new DeflateFrameExtension(); + serverExtension.setBufferPool(bufferPool); + serverExtension.setPolicy(WebSocketPolicy.newServerPolicy()); + serverExtension.setConfig(ExtensionConfig.parse("deflate-frame")); + + // Chain the next element to decompress. + clientExtension.setNextOutgoingFrames(new OutgoingFrames() + { + @Override + public void outgoingFrame(Frame frame, WriteCallback callback, BatchMode batchMode) + { + serverExtension.incomingFrame(frame); + callback.writeSuccess(); + } + }); + + final ByteArrayOutputStream result = new ByteArrayOutputStream(input.length); + serverExtension.setNextIncomingFrames(new IncomingFrames() + { + @Override + public void incomingFrame(Frame frame) + { + try + { + result.write(BufferUtil.toArray(frame.getPayload())); + } + catch (IOException x) + { + throw new RuntimeIOException(x); + } + } + + @Override + public void incomingError(Throwable t) + { + } + }); + + BinaryFrame frame = new BinaryFrame(); + frame.setPayload(input); + frame.setFin(true); + clientExtension.outgoingFrame(frame, null, BatchMode.OFF); + + Assert.assertArrayEquals(input, result.toByteArray()); } } diff --git a/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/extensions/compress/DeflateTest.java b/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/extensions/compress/DeflateTest.java index 04d20a50647..d30557ebaa1 100644 --- a/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/extensions/compress/DeflateTest.java +++ b/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/extensions/compress/DeflateTest.java @@ -33,33 +33,29 @@ public class DeflateTest public String deflate(String inputHex, Deflater deflater, int flushMode) { byte uncompressed[] = Hex.asByteArray(inputHex); + deflater.reset(); deflater.setInput(uncompressed,0,uncompressed.length); - deflater.finish(); + if (flushMode != Deflater.SYNC_FLUSH) + deflater.finish(); ByteBuffer out = ByteBuffer.allocate(bufSize); byte buf[] = new byte[64]; - while (!deflater.finished()) - { - int len = deflater.deflate(buf,0,buf.length,flushMode); - out.put(buf,0,len); - } + + int len = deflater.deflate(buf,0,buf.length,flushMode); + out.put(buf,0,len); out.flip(); return Hex.asHex(out); } @Test - @Ignore("just noisy") + @Ignore("noisy") public void deflateAllTypes() { - int levels[] = new int[] - { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 }; - boolean nowraps[] = new boolean[] - { true, false }; - int strategies[] = new int[] - { Deflater.DEFAULT_STRATEGY, Deflater.FILTERED, Deflater.HUFFMAN_ONLY }; - int flushmodes[] = new int[] - { Deflater.NO_FLUSH, Deflater.SYNC_FLUSH, Deflater.FULL_FLUSH }; + int levels[] = new int[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 }; + boolean nowraps[] = new boolean[] { true, false }; + int strategies[] = new int[] { Deflater.DEFAULT_STRATEGY, Deflater.FILTERED, Deflater.HUFFMAN_ONLY }; + int flushmodes[] = new int[] { Deflater.NO_FLUSH, Deflater.SYNC_FLUSH, Deflater.FULL_FLUSH }; String inputHex = Hex.asHex(StringUtil.getUtf8Bytes("time:")); for (int level : levels) diff --git a/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/extensions/compress/PerMessageDeflateExtensionTest.java b/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/extensions/compress/PerMessageDeflateExtensionTest.java index 8d14481b4eb..e840c4c9ba3 100644 --- a/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/extensions/compress/PerMessageDeflateExtensionTest.java +++ b/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/extensions/compress/PerMessageDeflateExtensionTest.java @@ -18,8 +18,6 @@ package org.eclipse.jetty.websocket.common.extensions.compress; -import static org.hamcrest.Matchers.*; - import java.io.IOException; import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; @@ -28,6 +26,7 @@ import java.util.List; import org.eclipse.jetty.io.MappedByteBufferPool; import org.eclipse.jetty.util.BufferUtil; +import org.eclipse.jetty.websocket.api.BatchMode; import org.eclipse.jetty.websocket.api.WebSocketPolicy; import org.eclipse.jetty.websocket.api.extensions.ExtensionConfig; import org.eclipse.jetty.websocket.api.extensions.Frame; @@ -46,19 +45,21 @@ import org.junit.Assert; import org.junit.Rule; import org.junit.Test; +import static org.hamcrest.Matchers.is; + /** * Client side behavioral tests for permessage-deflate extension. - *

+ *

* See: http://tools.ietf.org/html/draft-ietf-hybi-permessage-compression-15 */ public class PerMessageDeflateExtensionTest extends AbstractExtensionTest { @Rule - public LeakTrackingBufferPool bufferPool = new LeakTrackingBufferPool("Test",new MappedByteBufferPool()); - + public LeakTrackingBufferPool bufferPool = new LeakTrackingBufferPool("Test", new MappedByteBufferPool()); + /** * Decode payload example as seen in draft-ietf-hybi-permessage-compression-15. - *

+ *

* Section 8.2.3.4: Using a DEFLATE Block with BFINAL Set to 1 */ @Test @@ -71,14 +72,14 @@ public class PerMessageDeflateExtensionTest extends AbstractExtensionTest tester.parseIncomingHex(// 1 message "0xc1 0x08", // header "0xf3 0x48 0xcd 0xc9 0xc9 0x07 0x00 0x00" // example payload - ); + ); tester.assertHasFrames("Hello"); } /** * Decode payload example as seen in draft-ietf-hybi-permessage-compression-15. - *

+ *

* Section 8.2.3.3: Using a DEFLATE Block with No Compression */ @Test @@ -90,14 +91,14 @@ public class PerMessageDeflateExtensionTest extends AbstractExtensionTest tester.parseIncomingHex(// 1 message / no compression "0xc1 0x0b 0x00 0x05 0x00 0xfa 0xff 0x48 0x65 0x6c 0x6c 0x6f 0x00" // example frame - ); + ); tester.assertHasFrames("Hello"); } /** * Decode payload example as seen in draft-ietf-hybi-permessage-compression-15. - *

+ *

* Section 8.2.3.1: A message compressed using 1 compressed DEFLATE block */ @Test @@ -109,14 +110,14 @@ public class PerMessageDeflateExtensionTest extends AbstractExtensionTest tester.parseIncomingHex(//basic, 1 block, compressed with 0 compression level (aka, uncompressed). "0xc1 0x07 0xf2 0x48 0xcd 0xc9 0xc9 0x07 0x00" // example frame - ); + ); tester.assertHasFrames("Hello"); } /** * Decode payload example as seen in draft-ietf-hybi-permessage-compression-15. - *

+ *

* Section 8.2.3.1: A message compressed using 1 compressed DEFLATE block (with fragmentation) */ @Test @@ -139,7 +140,7 @@ public class PerMessageDeflateExtensionTest extends AbstractExtensionTest /** * Decode payload example as seen in draft-ietf-hybi-permessage-compression-15. - *

+ *

* Section 8.2.3.2: Sharing LZ77 Sliding Window */ @Test @@ -157,12 +158,12 @@ public class PerMessageDeflateExtensionTest extends AbstractExtensionTest "0xc1 0x07", // (HEADER added for this test) "0xf2 0x48 0xcd 0xc9 0xc9 0x07 0x00"); - tester.assertHasFrames("Hello","Hello"); + tester.assertHasFrames("Hello", "Hello"); } /** * Decode payload example as seen in draft-ietf-hybi-permessage-compression-15. - *

+ *

* Section 8.2.3.2: Sharing LZ77 Sliding Window */ @Test @@ -179,14 +180,14 @@ public class PerMessageDeflateExtensionTest extends AbstractExtensionTest // message 2 "0xc1 0x05", // (HEADER added for this test) "0xf2 0x00 0x11 0x00 0x00" - ); + ); - tester.assertHasFrames("Hello","Hello"); + tester.assertHasFrames("Hello", "Hello"); } /** * Decode payload example as seen in draft-ietf-hybi-permessage-compression-15. - *

+ *

* Section 8.2.3.5: Two DEFLATE Blocks in 1 Message */ @Test @@ -199,7 +200,7 @@ public class PerMessageDeflateExtensionTest extends AbstractExtensionTest tester.parseIncomingHex(// 1 message, 1 frame, 2 deflate blocks "0xc1 0x0d", // (HEADER added for this test) "0xf2 0x48 0x05 0x00 0x00 0x00 0xff 0xff 0xca 0xc9 0xc9 0x07 0x00" - ); + ); tester.assertHasFrames("Hello"); } @@ -227,18 +228,18 @@ public class PerMessageDeflateExtensionTest extends AbstractExtensionTest ext.incomingFrame(ping); capture.assertFrameCount(1); - capture.assertHasFrame(OpCode.PING,1); + capture.assertHasFrame(OpCode.PING, 1); WebSocketFrame actual = capture.getFrames().poll(); - Assert.assertThat("Frame.opcode",actual.getOpCode(),is(OpCode.PING)); - Assert.assertThat("Frame.fin",actual.isFin(),is(true)); - Assert.assertThat("Frame.rsv1",actual.isRsv1(),is(false)); - Assert.assertThat("Frame.rsv2",actual.isRsv2(),is(false)); - Assert.assertThat("Frame.rsv3",actual.isRsv3(),is(false)); + Assert.assertThat("Frame.opcode", actual.getOpCode(), is(OpCode.PING)); + Assert.assertThat("Frame.fin", actual.isFin(), is(true)); + Assert.assertThat("Frame.rsv1", actual.isRsv1(), is(false)); + Assert.assertThat("Frame.rsv2", actual.isRsv2(), is(false)); + Assert.assertThat("Frame.rsv3", actual.isRsv3(), is(false)); - ByteBuffer expected = BufferUtil.toBuffer(payload,StandardCharsets.UTF_8); - Assert.assertThat("Frame.payloadLength",actual.getPayloadLength(),is(expected.remaining())); - ByteBufferAssert.assertEquals("Frame.payload",expected,actual.getPayload().slice()); + ByteBuffer expected = BufferUtil.toBuffer(payload, StandardCharsets.UTF_8); + Assert.assertThat("Frame.payloadLength", actual.getPayloadLength(), is(expected.remaining())); + ByteBufferAssert.assertEquals("Frame.payload", expected, actual.getPayload().slice()); } /** @@ -275,7 +276,7 @@ public class PerMessageDeflateExtensionTest extends AbstractExtensionTest int len = quote.size(); capture.assertFrameCount(len); - capture.assertHasFrame(OpCode.TEXT,len); + capture.assertHasFrame(OpCode.TEXT, len); String prefix; int i = 0; @@ -283,15 +284,15 @@ public class PerMessageDeflateExtensionTest extends AbstractExtensionTest { prefix = "Frame[" + i + "]"; - Assert.assertThat(prefix + ".opcode",actual.getOpCode(),is(OpCode.TEXT)); - Assert.assertThat(prefix + ".fin",actual.isFin(),is(true)); - Assert.assertThat(prefix + ".rsv1",actual.isRsv1(),is(false)); - Assert.assertThat(prefix + ".rsv2",actual.isRsv2(),is(false)); - Assert.assertThat(prefix + ".rsv3",actual.isRsv3(),is(false)); + Assert.assertThat(prefix + ".opcode", actual.getOpCode(), is(OpCode.TEXT)); + Assert.assertThat(prefix + ".fin", actual.isFin(), is(true)); + Assert.assertThat(prefix + ".rsv1", actual.isRsv1(), is(false)); + Assert.assertThat(prefix + ".rsv2", actual.isRsv2(), is(false)); + Assert.assertThat(prefix + ".rsv3", actual.isRsv3(), is(false)); - ByteBuffer expected = BufferUtil.toBuffer(quote.get(i),StandardCharsets.UTF_8); - Assert.assertThat(prefix + ".payloadLength",actual.getPayloadLength(),is(expected.remaining())); - ByteBufferAssert.assertEquals(prefix + ".payload",expected,actual.getPayload().slice()); + ByteBuffer expected = BufferUtil.toBuffer(quote.get(i), StandardCharsets.UTF_8); + Assert.assertThat(prefix + ".payloadLength", actual.getPayloadLength(), is(expected.remaining())); + ByteBufferAssert.assertEquals(prefix + ".payload", expected, actual.getPayload().slice()); i++; } } @@ -317,22 +318,22 @@ public class PerMessageDeflateExtensionTest extends AbstractExtensionTest String payload = "Are you there?"; Frame ping = new PingFrame().setPayload(payload); - ext.outgoingFrame(ping,null); + ext.outgoingFrame(ping, null, BatchMode.OFF); capture.assertFrameCount(1); - capture.assertHasFrame(OpCode.PING,1); + capture.assertHasFrame(OpCode.PING, 1); WebSocketFrame actual = capture.getFrames().getFirst(); - Assert.assertThat("Frame.opcode",actual.getOpCode(),is(OpCode.PING)); - Assert.assertThat("Frame.fin",actual.isFin(),is(true)); - Assert.assertThat("Frame.rsv1",actual.isRsv1(),is(false)); - Assert.assertThat("Frame.rsv2",actual.isRsv2(),is(false)); - Assert.assertThat("Frame.rsv3",actual.isRsv3(),is(false)); + Assert.assertThat("Frame.opcode", actual.getOpCode(), is(OpCode.PING)); + Assert.assertThat("Frame.fin", actual.isFin(), is(true)); + Assert.assertThat("Frame.rsv1", actual.isRsv1(), is(false)); + Assert.assertThat("Frame.rsv2", actual.isRsv2(), is(false)); + Assert.assertThat("Frame.rsv3", actual.isRsv3(), is(false)); - ByteBuffer expected = BufferUtil.toBuffer(payload,StandardCharsets.UTF_8); - Assert.assertThat("Frame.payloadLength",actual.getPayloadLength(),is(expected.remaining())); - ByteBufferAssert.assertEquals("Frame.payload",expected,actual.getPayload().slice()); + ByteBuffer expected = BufferUtil.toBuffer(payload, StandardCharsets.UTF_8); + Assert.assertThat("Frame.payloadLength", actual.getPayloadLength(), is(expected.remaining())); + ByteBufferAssert.assertEquals("Frame.payload", expected, actual.getPayload().slice()); } @Test @@ -350,7 +351,7 @@ public class PerMessageDeflateExtensionTest extends AbstractExtensionTest "c1 0b 0a c8 c8 c9 2f 4a 0c 01 62 00 00" // PhloraTora ); - tester.assertHasFrames("ToraTora","AtoraFlora","PhloraTora"); + tester.assertHasFrames("ToraTora", "AtoraFlora", "PhloraTora"); } @Test @@ -368,7 +369,7 @@ public class PerMessageDeflateExtensionTest extends AbstractExtensionTest "c1 04 02 61 00 00" // tora 3 ); - tester.assertHasFrames("tora","tora","tora"); + tester.assertHasFrames("tora", "tora", "tora"); } @Test @@ -386,7 +387,7 @@ public class PerMessageDeflateExtensionTest extends AbstractExtensionTest "c1 8b e2 3e 05 53 e8 f6 cd 9a cd 74 09 52 80 3e 05" // PhloraTora ); - tester.assertHasFrames("ToraTora","AtoraFlora","PhloraTora"); + tester.assertHasFrames("ToraTora", "AtoraFlora", "PhloraTora"); } @Test @@ -404,6 +405,6 @@ public class PerMessageDeflateExtensionTest extends AbstractExtensionTest "c1 84 53 ad a5 34 51 cc a5 34" // tora 3 ); - tester.assertHasFrames("tora","tora","tora"); + tester.assertHasFrames("tora", "tora", "tora"); } } 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 8d7b094ef69..2244c93cb5c 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 @@ -25,6 +25,7 @@ import org.eclipse.jetty.io.ByteBufferPool; 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.BatchMode; import org.eclipse.jetty.websocket.api.StatusCode; import org.eclipse.jetty.websocket.api.SuspendToken; import org.eclipse.jetty.websocket.api.WebSocketPolicy; @@ -204,7 +205,7 @@ public class LocalWebSocketConnection implements LogicalConnection, IncomingFram } @Override - public void outgoingFrame(Frame frame, WriteCallback callback) + public void outgoingFrame(Frame frame, WriteCallback callback, BatchMode batchMode) { } 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 6bf9224d1f6..afb97f97381 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 @@ -21,7 +21,6 @@ package org.eclipse.jetty.websocket.common.io; import java.net.URI; import org.eclipse.jetty.io.ByteBufferPool; -import org.eclipse.jetty.websocket.common.SessionListener; import org.eclipse.jetty.websocket.common.WebSocketSession; import org.eclipse.jetty.websocket.common.events.EventDriver; import org.eclipse.jetty.websocket.common.test.OutgoingFramesCapture; @@ -34,7 +33,7 @@ public class LocalWebSocketSession extends WebSocketSession public LocalWebSocketSession(TestName testname, EventDriver driver, ByteBufferPool bufferPool) { - super(URI.create("ws://localhost/LocalWebSocketSesssion/" + testname.getMethodName()),driver,new LocalWebSocketConnection(testname,bufferPool), new SessionListener[0]); + super(URI.create("ws://localhost/LocalWebSocketSesssion/" + testname.getMethodName()),driver,new LocalWebSocketConnection(testname,bufferPool)); this.id = testname.getMethodName(); outgoingCapture = new OutgoingFramesCapture(); setOutgoingHandler(outgoingCapture); 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 index e1dfc5f9d87..b71149e6a3f 100644 --- 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 @@ -18,8 +18,6 @@ package org.eclipse.jetty.websocket.common.message; -import static org.hamcrest.Matchers.*; - import java.io.IOException; import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; @@ -36,6 +34,8 @@ import org.junit.Rule; import org.junit.Test; import org.junit.rules.TestName; +import static org.hamcrest.Matchers.is; + public class MessageInputStreamTest { @Rule @@ -49,7 +49,7 @@ public class MessageInputStreamTest { LocalWebSocketConnection conn = new LocalWebSocketConnection(testname,bufferPool); - try (MessageInputStream stream = new MessageInputStream(conn)) + try (MessageInputStream stream = new MessageInputStream()) { // Append a single message (simple, short) ByteBuffer payload = BufferUtil.toBuffer("Hello World",StandardCharsets.UTF_8); @@ -72,7 +72,7 @@ public class MessageInputStreamTest { LocalWebSocketConnection conn = new LocalWebSocketConnection(testname,bufferPool); - try (MessageInputStream stream = new MessageInputStream(conn)) + try (MessageInputStream stream = new MessageInputStream()) { final AtomicBoolean hadError = new AtomicBoolean(false); final CountDownLatch startLatch = new CountDownLatch(1); @@ -123,7 +123,7 @@ public class MessageInputStreamTest { LocalWebSocketConnection conn = new LocalWebSocketConnection(testname,bufferPool); - try (MessageInputStream stream = new MessageInputStream(conn)) + try (MessageInputStream stream = new MessageInputStream()) { final AtomicBoolean hadError = new AtomicBoolean(false); @@ -162,7 +162,7 @@ public class MessageInputStreamTest { LocalWebSocketConnection conn = new LocalWebSocketConnection(testname,bufferPool); - try (MessageInputStream stream = new MessageInputStream(conn)) + try (MessageInputStream stream = new MessageInputStream()) { final AtomicBoolean hadError = new AtomicBoolean(false); diff --git a/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/test/BlockheadClient.java b/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/test/BlockheadClient.java index 349b7b49e20..bd02460c663 100644 --- a/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/test/BlockheadClient.java +++ b/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/test/BlockheadClient.java @@ -18,8 +18,6 @@ package org.eclipse.jetty.websocket.common.test; -import static org.hamcrest.Matchers.*; - import java.io.Closeable; import java.io.EOFException; import java.io.IOException; @@ -48,6 +46,7 @@ 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.Logger; +import org.eclipse.jetty.websocket.api.BatchMode; import org.eclipse.jetty.websocket.api.WebSocketPolicy; import org.eclipse.jetty.websocket.api.WriteCallback; import org.eclipse.jetty.websocket.api.extensions.ExtensionConfig; @@ -69,6 +68,10 @@ import org.eclipse.jetty.websocket.common.io.IOState.ConnectionStateListener; import org.eclipse.jetty.websocket.common.io.http.HttpResponseHeaderParser; import org.junit.Assert; +import static org.hamcrest.Matchers.anyOf; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; + /** * A simple websocket client for performing unit tests with. *

@@ -465,7 +468,7 @@ public class BlockheadClient implements IncomingFrames, OutgoingFrames, Connecti } @Override - public void outgoingFrame(Frame frame, WriteCallback callback) + public void outgoingFrame(Frame frame, WriteCallback callback, BatchMode batchMode) { ByteBuffer headerBuf = generator.generateHeaderBytes(frame); if (LOG.isDebugEnabled()) @@ -710,7 +713,7 @@ public class BlockheadClient implements IncomingFrames, OutgoingFrames, Connecti { frame.setMask(clientmask); } - extensionStack.outgoingFrame(frame,null); + extensionStack.outgoingFrame(frame,null, BatchMode.OFF); } public void writeRaw(ByteBuffer buf) throws IOException diff --git a/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/test/BlockheadServer.java b/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/test/BlockheadServer.java index aa23cbde085..c3cd1ad8554 100644 --- a/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/test/BlockheadServer.java +++ b/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/test/BlockheadServer.java @@ -18,8 +18,6 @@ package org.eclipse.jetty.websocket.common.test; -import static org.hamcrest.Matchers.*; - import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; @@ -49,6 +47,7 @@ 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.websocket.api.BatchMode; import org.eclipse.jetty.websocket.api.WebSocketPolicy; import org.eclipse.jetty.websocket.api.WriteCallback; import org.eclipse.jetty.websocket.api.extensions.ExtensionConfig; @@ -66,6 +65,9 @@ import org.eclipse.jetty.websocket.common.extensions.WebSocketExtensionFactory; import org.eclipse.jetty.websocket.common.frames.CloseFrame; import org.junit.Assert; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; + /** * A overly simplistic websocket server used during testing. *

@@ -230,7 +232,7 @@ public class BlockheadServer } @Override - public void outgoingFrame(Frame frame, WriteCallback callback) + public void outgoingFrame(Frame frame, WriteCallback callback, BatchMode batchMode) { ByteBuffer headerBuf = generator.generateHeaderBytes(frame); if (LOG.isDebugEnabled()) @@ -560,7 +562,7 @@ public class BlockheadServer public void write(Frame frame) throws IOException { LOG.debug("write(Frame->{}) to {}",frame,outgoing); - outgoing.outgoingFrame(frame,null); + outgoing.outgoingFrame(frame,null, BatchMode.OFF); } public void write(int b) throws IOException diff --git a/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/test/Fuzzer.java b/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/test/Fuzzer.java index 9cd0bfec238..3d6c6414838 100644 --- a/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/test/Fuzzer.java +++ b/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/test/Fuzzer.java @@ -18,8 +18,6 @@ package org.eclipse.jetty.websocket.common.test; -import static org.hamcrest.Matchers.*; - import java.io.IOException; import java.net.SocketException; import java.nio.ByteBuffer; @@ -41,6 +39,9 @@ import org.eclipse.jetty.websocket.common.WebSocketFrame; import org.eclipse.jetty.websocket.common.io.IOState; import org.junit.Assert; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.is; + /** * Fuzzing utility for the AB tests. */ @@ -103,7 +104,7 @@ public class Fuzzer int buflen = 0; for (Frame f : send) { - buflen += f.getPayloadLength() + Generator.OVERHEAD; + buflen += f.getPayloadLength() + Generator.MAX_HEADER_LENGTH; } ByteBuffer buf = ByteBuffer.allocate(buflen); @@ -260,7 +261,7 @@ public class Fuzzer int buflen = 0; for (Frame f : send) { - buflen += f.getPayloadLength() + Generator.OVERHEAD; + buflen += f.getPayloadLength() + Generator.MAX_HEADER_LENGTH; } ByteBuffer buf = ByteBuffer.allocate(buflen); @@ -295,7 +296,7 @@ public class Fuzzer { f.setMask(MASK); // make sure we have mask set // Using lax generator, generate and send - ByteBuffer fullframe = ByteBuffer.allocate(f.getPayloadLength() + Generator.OVERHEAD); + ByteBuffer fullframe = ByteBuffer.allocate(f.getPayloadLength() + Generator.MAX_HEADER_LENGTH); BufferUtil.clearToFill(fullframe); generator.generateWholeFrame(f,fullframe); BufferUtil.flipToFlush(fullframe,0); diff --git a/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/test/OutgoingFramesCapture.java b/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/test/OutgoingFramesCapture.java index dfa2fe97c65..2510599d3cf 100644 --- a/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/test/OutgoingFramesCapture.java +++ b/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/test/OutgoingFramesCapture.java @@ -18,11 +18,10 @@ package org.eclipse.jetty.websocket.common.test; -import static org.hamcrest.Matchers.*; - import java.util.LinkedList; import org.eclipse.jetty.util.BufferUtil; +import org.eclipse.jetty.websocket.api.BatchMode; import org.eclipse.jetty.websocket.api.WriteCallback; import org.eclipse.jetty.websocket.api.extensions.Frame; import org.eclipse.jetty.websocket.api.extensions.OutgoingFrames; @@ -30,6 +29,9 @@ import org.eclipse.jetty.websocket.common.OpCode; import org.eclipse.jetty.websocket.common.WebSocketFrame; import org.junit.Assert; +import static org.hamcrest.Matchers.greaterThanOrEqualTo; +import static org.hamcrest.Matchers.is; + public class OutgoingFramesCapture implements OutgoingFrames { private LinkedList frames = new LinkedList<>(); @@ -84,7 +86,7 @@ public class OutgoingFramesCapture implements OutgoingFrames } @Override - public void outgoingFrame(Frame frame, WriteCallback callback) + public void outgoingFrame(Frame frame, WriteCallback callback, BatchMode batchMode) { frames.add(WebSocketFrame.copy(frame)); if (callback != null) diff --git a/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/test/OutgoingNetworkBytesCapture.java b/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/test/OutgoingNetworkBytesCapture.java index fb0f17c7b56..1bbf8ac10d2 100644 --- a/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/test/OutgoingNetworkBytesCapture.java +++ b/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/test/OutgoingNetworkBytesCapture.java @@ -18,8 +18,6 @@ package org.eclipse.jetty.websocket.common.test; -import static org.hamcrest.Matchers.*; - import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.List; @@ -27,12 +25,16 @@ import java.util.Locale; import org.eclipse.jetty.util.BufferUtil; import org.eclipse.jetty.util.TypeUtil; +import org.eclipse.jetty.websocket.api.BatchMode; 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.Generator; import org.junit.Assert; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.lessThan; + /** * Capture outgoing network bytes. */ @@ -61,9 +63,9 @@ public class OutgoingNetworkBytesCapture implements OutgoingFrames } @Override - public void outgoingFrame(Frame frame, WriteCallback callback) + public void outgoingFrame(Frame frame, WriteCallback callback, BatchMode batchMode) { - ByteBuffer buf = ByteBuffer.allocate(Generator.OVERHEAD + frame.getPayloadLength()); + ByteBuffer buf = ByteBuffer.allocate(Generator.MAX_HEADER_LENGTH + frame.getPayloadLength()); generator.generateWholeFrame(frame,buf); BufferUtil.flipToFlush(buf,0); captured.add(buf); diff --git a/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/test/UnitGenerator.java b/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/test/UnitGenerator.java index 1dcf19d9d6b..b410f496bc6 100644 --- a/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/test/UnitGenerator.java +++ b/jetty-websocket/websocket-common/src/test/java/org/eclipse/jetty/websocket/common/test/UnitGenerator.java @@ -61,7 +61,7 @@ public class UnitGenerator extends Generator int buflen = 0; for (Frame f : frames) { - buflen += f.getPayloadLength() + Generator.OVERHEAD; + buflen += f.getPayloadLength() + Generator.MAX_HEADER_LENGTH; } ByteBuffer completeBuf = ByteBuffer.allocate(buflen); BufferUtil.clearToFill(completeBuf); @@ -96,7 +96,7 @@ public class UnitGenerator extends Generator int buflen = 0; for (Frame f : frames) { - buflen += f.getPayloadLength() + Generator.OVERHEAD; + buflen += f.getPayloadLength() + Generator.MAX_HEADER_LENGTH; } ByteBuffer completeBuf = ByteBuffer.allocate(buflen); BufferUtil.clearToFill(completeBuf); diff --git a/jetty-websocket/websocket-server/src/main/java/org/eclipse/jetty/websocket/server/HandshakeRFC6455.java b/jetty-websocket/websocket-server/src/main/java/org/eclipse/jetty/websocket/server/HandshakeRFC6455.java index 189e2843776..7f5730099ae 100644 --- a/jetty-websocket/websocket-server/src/main/java/org/eclipse/jetty/websocket/server/HandshakeRFC6455.java +++ b/jetty-websocket/websocket-server/src/main/java/org/eclipse/jetty/websocket/server/HandshakeRFC6455.java @@ -58,6 +58,8 @@ public class HandshakeRFC6455 implements WebSocketHandshake } } + request.complete(); + response.setStatus(HttpServletResponse.SC_SWITCHING_PROTOCOLS); response.complete(); } diff --git a/jetty-websocket/websocket-server/src/main/java/org/eclipse/jetty/websocket/server/UpgradeContext.java b/jetty-websocket/websocket-server/src/main/java/org/eclipse/jetty/websocket/server/UpgradeContext.java deleted file mode 100644 index 51d4dcdaf14..00000000000 --- a/jetty-websocket/websocket-server/src/main/java/org/eclipse/jetty/websocket/server/UpgradeContext.java +++ /dev/null @@ -1,60 +0,0 @@ -// -// ======================================================================== -// Copyright (c) 1995-2014 Mort Bay Consulting Pty. Ltd. -// ------------------------------------------------------------------------ -// All rights reserved. This program and the accompanying materials -// are made available under the terms of the Eclipse Public License v1.0 -// and Apache License v2.0 which accompanies this distribution. -// -// The Eclipse Public License is available at -// http://www.eclipse.org/legal/epl-v10.html -// -// The Apache License v2.0 is available at -// http://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.api.UpgradeRequest; -import org.eclipse.jetty.websocket.api.UpgradeResponse; -import org.eclipse.jetty.websocket.common.LogicalConnection; - -public class UpgradeContext -{ - private LogicalConnection connection; - private UpgradeRequest request; - private UpgradeResponse response; - - public LogicalConnection getConnection() - { - return connection; - } - - public UpgradeRequest getRequest() - { - return request; - } - - public UpgradeResponse getResponse() - { - return response; - } - - public void setConnection(LogicalConnection connection) - { - this.connection = connection; - } - - public void setRequest(UpgradeRequest request) - { - this.request = request; - } - - public void setResponse(UpgradeResponse response) - { - this.response = response; - } -} 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 d7084b14493..31d011878fc 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 @@ -31,18 +31,15 @@ import org.eclipse.jetty.websocket.common.io.AbstractWebSocketConnection; public class WebSocketServerConnection extends AbstractWebSocketConnection { - private final WebSocketServerFactory factory; private final AtomicBoolean opened = new AtomicBoolean(false); - public WebSocketServerConnection(EndPoint endp, Executor executor, Scheduler scheduler, WebSocketPolicy policy, ByteBufferPool bufferPool, - WebSocketServerFactory factory) + public WebSocketServerConnection(EndPoint endp, Executor executor, Scheduler scheduler, WebSocketPolicy policy, ByteBufferPool bufferPool) { super(endp,executor,scheduler,policy,bufferPool); if (policy.getIdleTimeout() > 0) { endp.setIdleTimeout(policy.getIdleTimeout()); } - this.factory = factory; } @Override @@ -57,20 +54,13 @@ public class WebSocketServerConnection extends AbstractWebSocketConnection return getEndPoint().getRemoteAddress(); } - @Override - public void onClose() - { - super.onClose(); - factory.sessionClosed(getSession()); - } - @Override public void onOpen() { boolean beenOpened = opened.getAndSet(true); if (!beenOpened) { - factory.sessionOpened(getSession()); + getSession().open(); } super.onOpen(); } 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 c7c233615e3..9609ce2506e 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 @@ -27,9 +27,7 @@ import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; -import java.util.Queue; import java.util.Set; -import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.CopyOnWriteArraySet; import java.util.concurrent.Executor; import javax.servlet.http.HttpServletRequest; @@ -70,28 +68,12 @@ import org.eclipse.jetty.websocket.servlet.WebSocketServletFactory; public class WebSocketServerFactory extends ContainerLifeCycle implements WebSocketCreator, WebSocketServletFactory, SessionListener { private static final Logger LOG = Log.getLogger(WebSocketServerFactory.class); - private static final ThreadLocal ACTIVE_CONTEXT = new ThreadLocal<>(); - - public static UpgradeContext getActiveUpgradeContext() - { - return ACTIVE_CONTEXT.get(); - } - - protected static void setActiveUpgradeContext(UpgradeContext connection) - { - ACTIVE_CONTEXT.set(connection); - } private final Map handshakes = new HashMap<>(); - { - handshakes.put(HandshakeRFC6455.VERSION,new HandshakeRFC6455()); - } - /** * Have the factory maintain 1 and only 1 scheduler. All connections share this scheduler. */ private final Scheduler scheduler = new ScheduledExecutorScheduler(); - private final Queue sessions = new ConcurrentLinkedQueue<>(); private final String supportedVersions; private final WebSocketPolicy defaultPolicy; private final EventDriverFactory eventDriverFactory; @@ -104,21 +86,23 @@ public class WebSocketServerFactory extends ContainerLifeCycle implements WebSoc public WebSocketServerFactory() { - this(WebSocketPolicy.newServerPolicy(),new MappedByteBufferPool()); + this(WebSocketPolicy.newServerPolicy(), new MappedByteBufferPool()); } public WebSocketServerFactory(WebSocketPolicy policy) { - this(policy,new MappedByteBufferPool()); + this(policy, new MappedByteBufferPool()); } - + public WebSocketServerFactory(ByteBufferPool bufferPool) { - this(WebSocketPolicy.newServerPolicy(),bufferPool); + this(WebSocketPolicy.newServerPolicy(), bufferPool); } public WebSocketServerFactory(WebSocketPolicy policy, ByteBufferPool bufferPool) { + handshakes.put(HandshakeRFC6455.VERSION, new HandshakeRFC6455()); + addBean(scheduler); addBean(bufferPool); @@ -127,7 +111,7 @@ public class WebSocketServerFactory extends ContainerLifeCycle implements WebSoc this.defaultPolicy = policy; this.eventDriverFactory = new EventDriverFactory(defaultPolicy); this.bufferPool = bufferPool; - this.extensionFactory = new WebSocketExtensionFactory(defaultPolicy,this.bufferPool); + this.extensionFactory = new WebSocketExtensionFactory(defaultPolicy, this.bufferPool); this.sessionFactories = new ArrayList<>(); this.sessionFactories.add(new WebSocketSessionFactory(this)); this.creator = this; @@ -138,7 +122,7 @@ public class WebSocketServerFactory extends ContainerLifeCycle implements WebSoc { versions.add(v); } - Collections.sort(versions,Collections.reverseOrder()); // newest first + Collections.sort(versions, Collections.reverseOrder()); // newest first StringBuilder rv = new StringBuilder(); for (int v : versions) { @@ -154,7 +138,7 @@ public class WebSocketServerFactory extends ContainerLifeCycle implements WebSoc @Override public boolean acceptWebSocket(HttpServletRequest request, HttpServletResponse response) throws IOException { - return acceptWebSocket(getCreator(),request,response); + return acceptWebSocket(getCreator(), request, response); } @Override @@ -165,17 +149,7 @@ public class WebSocketServerFactory extends ContainerLifeCycle implements WebSoc ServletUpgradeRequest sockreq = new ServletUpgradeRequest(request); ServletUpgradeResponse sockresp = new ServletUpgradeResponse(response); - UpgradeContext context = getActiveUpgradeContext(); - if (context == null) - { - context = new UpgradeContext(); - setActiveUpgradeContext(context); - } - - context.setRequest(sockreq); - context.setResponse(sockresp); - - Object websocketPojo = creator.createWebSocket(sockreq,sockresp); + Object websocketPojo = creator.createWebSocket(sockreq, sockresp); // Handle response forbidden (and similar paths) if (sockresp.isCommitted()) @@ -192,11 +166,11 @@ public class WebSocketServerFactory extends ContainerLifeCycle implements WebSoc // Send the upgrade EventDriver driver = eventDriverFactory.wrap(websocketPojo); - return upgrade(sockreq,sockresp,driver); + return upgrade(sockreq, sockresp, driver); } catch (URISyntaxException e) { - throw new IOException("Unable to accept websocket due to mangled URI",e); + throw new IOException("Unable to accept websocket due to mangled URI", e); } } @@ -224,17 +198,17 @@ public class WebSocketServerFactory extends ContainerLifeCycle implements WebSoc protected void closeAllConnections() { - for (WebSocketSession session : sessions) + for (WebSocketSession session : openSessions) { session.close(); } - sessions.clear(); + openSessions.clear(); } @Override public WebSocketServletFactory createFactory(WebSocketPolicy policy) { - return new WebSocketServerFactory(policy,bufferPool); + return new WebSocketServerFactory(policy, bufferPool); } private WebSocketSession createSession(URI requestURI, EventDriver websocket, LogicalConnection connection) @@ -250,11 +224,11 @@ public class WebSocketServerFactory extends ContainerLifeCycle implements WebSoc { try { - return impl.createSession(requestURI,websocket,connection); + return impl.createSession(requestURI, websocket, connection); } catch (Throwable e) { - throw new InvalidWebSocketException("Unable to create Session",e); + throw new InvalidWebSocketException("Unable to create Session", e); } } } @@ -285,7 +259,7 @@ public class WebSocketServerFactory extends ContainerLifeCycle implements WebSoc } catch (InstantiationException | IllegalAccessException e) { - throw new WebSocketException("Unable to create instance of " + firstClass,e); + throw new WebSocketException("Unable to create instance of " + firstClass, e); } } @@ -348,7 +322,7 @@ public class WebSocketServerFactory extends ContainerLifeCycle implements WebSoc // Test for "Upgrade" token boolean foundUpgradeToken = false; - Iterator iter = QuoteUtil.splitAt(connection,","); + Iterator iter = QuoteUtil.splitAt(connection, ","); while (iter.hasNext()) { String token = iter.next(); @@ -402,18 +376,16 @@ public class WebSocketServerFactory extends ContainerLifeCycle implements WebSoc { if (protocol == null) { - return new String[] - { null }; + return new String[]{null}; } protocol = protocol.trim(); - if ((protocol == null) || (protocol.length() == 0)) + if (protocol.length() == 0) { - return new String[] - { null }; + return new String[]{null}; } String[] passed = protocol.split("\\s*,\\s*"); String[] protocols = new String[passed.length + 1]; - System.arraycopy(passed,0,protocols,0,passed.length); + System.arraycopy(passed, 0, protocols, 0, passed.length); return protocols; } @@ -423,27 +395,6 @@ public class WebSocketServerFactory extends ContainerLifeCycle implements WebSoc registeredSocketClasses.add(websocketPojo); } - public boolean sessionClosed(WebSocketSession session) - { - return isRunning() && sessions.remove(session); - } - - public boolean sessionOpened(WebSocketSession session) - { - if (LOG.isDebugEnabled()) - { - LOG.debug("Session Opened: {}",session); - } - if (!isRunning()) - { - LOG.warn("Factory is not running"); - return false; - } - boolean ret = sessions.offer(session); - session.open(); - return ret; - } - @Override public void setCreator(WebSocketCreator creator) { @@ -452,16 +403,13 @@ public class WebSocketServerFactory extends ContainerLifeCycle implements WebSoc /** * Upgrade the request/response to a WebSocket Connection. - *

+ *

* This method will not normally return, but will instead throw a UpgradeConnectionException, to exit HTTP handling and initiate WebSocket handling of the * connection. - * - * @param request - * The request to upgrade - * @param response - * The response to upgrade - * @param driver - * The websocket handler implementation to use + * + * @param request The request to upgrade + * @param response The response to upgrade + * @param driver The websocket handler implementation to use * @throws IOException */ public boolean upgrade(ServletUpgradeRequest request, ServletUpgradeResponse response, EventDriver driver) throws IOException @@ -490,10 +438,13 @@ public class WebSocketServerFactory extends ContainerLifeCycle implements WebSoc warn.append(" (:").append(request.getRemotePort()); warn.append(") User Agent: "); String ua = request.getHeader("User-Agent"); - if(ua == null) { + if (ua == null) + { warn.append("[unset] "); - } else { - warn.append('"').append(ua.replaceAll("<","<")).append("\" "); + } + else + { + warn.append('"').append(ua.replaceAll("<", "<")).append("\" "); } warn.append("requested WebSocket version [").append(version); warn.append("], Jetty supports version"); @@ -503,11 +454,11 @@ public class WebSocketServerFactory extends ContainerLifeCycle implements WebSoc } warn.append(": [").append(supportedVersions).append("]"); LOG.warn(warn.toString()); - + // Per RFC 6455 - 4.4 - Supporting Multiple Versions of WebSocket Protocol // Using the examples as outlined - response.setHeader("Sec-WebSocket-Version",supportedVersions); - response.sendError(HttpStatus.BAD_REQUEST_400,"Unsupported websocket version specification"); + response.setHeader("Sec-WebSocket-Version", supportedVersions); + response.sendError(HttpStatus.BAD_REQUEST_400, "Unsupported websocket version specification"); return false; } @@ -527,42 +478,35 @@ public class WebSocketServerFactory extends ContainerLifeCycle implements WebSoc } // Create connection - UpgradeContext context = getActiveUpgradeContext(); - LogicalConnection connection = context.getConnection(); + HttpConnection http = HttpConnection.getCurrentConnection(); + EndPoint endp = http.getEndPoint(); + Executor executor = http.getConnector().getExecutor(); + ByteBufferPool bufferPool = http.getConnector().getByteBufferPool(); + WebSocketServerConnection wsConnection = new WebSocketServerConnection(endp, executor, scheduler, driver.getPolicy(), bufferPool); - if (connection == null) - { - HttpConnection http = HttpConnection.getCurrentConnection(); - EndPoint endp = http.getEndPoint(); - Executor executor = http.getConnector().getExecutor(); - ByteBufferPool bufferPool = http.getConnector().getByteBufferPool(); - WebSocketServerConnection wsConnection = new WebSocketServerConnection(endp,executor,scheduler,driver.getPolicy(),bufferPool,this); - connection = wsConnection; + extensionStack.setPolicy(driver.getPolicy()); + extensionStack.configure(wsConnection.getParser()); + extensionStack.configure(wsConnection.getGenerator()); - extensionStack.setPolicy(driver.getPolicy()); - extensionStack.configure(wsConnection.getParser()); - extensionStack.configure(wsConnection.getGenerator()); - - LOG.debug("HttpConnection: {}",http); - LOG.debug("AsyncWebSocketConnection: {}",connection); - } + LOG.debug("HttpConnection: {}", http); + LOG.debug("WebSocketConnection: {}", wsConnection); // Setup Session - WebSocketSession session = createSession(request.getRequestURI(),driver,connection); + WebSocketSession session = createSession(request.getRequestURI(), driver, wsConnection); session.setPolicy(driver.getPolicy()); session.setUpgradeRequest(request); // set true negotiated extension list back to response response.setExtensions(extensionStack.getNegotiatedExtensions()); session.setUpgradeResponse(response); - connection.setSession(session); + wsConnection.setSession(session); // Setup Incoming Routing - connection.setNextIncomingFrames(extensionStack); + wsConnection.setNextIncomingFrames(extensionStack); extensionStack.setNextIncoming(session); // Setup Outgoing Routing session.setOutgoingHandler(extensionStack); - extensionStack.setNextOutgoing(connection); + extensionStack.setNextOutgoing(wsConnection); // Start Components try @@ -571,7 +515,7 @@ public class WebSocketServerFactory extends ContainerLifeCycle implements WebSoc } catch (Exception e) { - throw new IOException("Unable to start Session",e); + throw new IOException("Unable to start Session", e); } try { @@ -579,17 +523,17 @@ public class WebSocketServerFactory extends ContainerLifeCycle implements WebSoc } catch (Exception e) { - throw new IOException("Unable to start Extension Stack",e); + throw new IOException("Unable to start Extension Stack", e); } // Tell jetty about the new connection - request.setServletAttribute(HttpConnection.UPGRADE_CONNECTION_ATTRIBUTE,connection); + request.setServletAttribute(HttpConnection.UPGRADE_CONNECTION_ATTRIBUTE, wsConnection); // Process (version specific) handshake response - LOG.debug("Handshake Response: {}",handshaker); - handshaker.doHandshakeResponse(request,response); + LOG.debug("Handshake Response: {}", handshaker); + handshaker.doHandshakeResponse(request, response); - LOG.debug("Websocket upgrade {} {} {} {}",request.getRequestURI(),version,response.getAcceptedSubProtocol(),connection); + LOG.debug("Websocket upgrade {} {} {} {}", request.getRequestURI(), version, response.getAcceptedSubProtocol(), wsConnection); return true; } } diff --git a/jetty-websocket/websocket-server/src/test/java/org/eclipse/jetty/websocket/server/BatchModeTest.java b/jetty-websocket/websocket-server/src/test/java/org/eclipse/jetty/websocket/server/BatchModeTest.java new file mode 100644 index 00000000000..835d107c096 --- /dev/null +++ b/jetty-websocket/websocket-server/src/test/java/org/eclipse/jetty/websocket/server/BatchModeTest.java @@ -0,0 +1,103 @@ +// +// ======================================================================== +// Copyright (c) 1995-2014 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://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.net.URI; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; + +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.ServerConnector; +import org.eclipse.jetty.websocket.api.RemoteEndpoint; +import org.eclipse.jetty.websocket.api.Session; +import org.eclipse.jetty.websocket.api.WebSocketAdapter; +import org.eclipse.jetty.websocket.client.WebSocketClient; +import org.eclipse.jetty.websocket.server.helper.EchoSocket; +import org.eclipse.jetty.websocket.servlet.WebSocketServletFactory; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +public class BatchModeTest +{ + private Server server; + private ServerConnector connector; + private WebSocketClient client; + + @Before + public void prepare() throws Exception + { + server = new Server(); + connector = new ServerConnector(server); + server.addConnector(connector); + + WebSocketHandler handler = new WebSocketHandler() + { + @Override + public void configure(WebSocketServletFactory factory) + { + factory.register(EchoSocket.class); + } + }; + + server.setHandler(handler); + + client = new WebSocketClient(); + server.addBean(client, true); + + server.start(); + } + + @After + public void dispose() throws Exception + { + server.stop(); + } + + @Test + public void testBatchModeAuto() throws Exception + { + URI uri = URI.create("ws://localhost:" + connector.getLocalPort()); + + final CountDownLatch latch = new CountDownLatch(1); + WebSocketAdapter adapter = new WebSocketAdapter() + { + @Override + public void onWebSocketText(String message) + { + latch.countDown(); + } + }; + try (Session session = client.connect(adapter, uri).get()) + { + RemoteEndpoint remote = session.getRemote(); + + Future future = remote.sendStringByFuture("batch_mode_on"); + // The write is aggregated and therefore completes immediately. + future.get(1, TimeUnit.MICROSECONDS); + + // Wait for the echo. + Assert.assertTrue(latch.await(5, TimeUnit.SECONDS)); + } + } + + // TODO: currently not possible to configure the Jetty WebSocket Session with the batch mode. +} 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 index c79b0e5df99..46dbdd92405 100644 --- 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 @@ -18,8 +18,6 @@ package org.eclipse.jetty.websocket.server; -import static org.hamcrest.Matchers.*; - import java.util.concurrent.TimeUnit; import org.eclipse.jetty.websocket.common.WebSocketFrame; @@ -32,6 +30,8 @@ import org.junit.Assert; import org.junit.BeforeClass; import org.junit.Test; +import static org.hamcrest.Matchers.is; + public class FirefoxTest { private static SimpleServletServer server; @@ -52,8 +52,7 @@ public class FirefoxTest @Test public void testConnectionKeepAlive() throws Exception { - BlockheadClient client = new BlockheadClient(server.getServerUri()); - try + try (BlockheadClient client = new BlockheadClient(server.getServerUri())) { // Odd Connection Header value seen in Firefox client.setConnectionValue("keep-alive, Upgrade"); @@ -66,13 +65,9 @@ public class FirefoxTest client.write(new TextFrame().setPayload(msg)); // Read frame (hopefully text frame) - IncomingFramesCapture capture = client.readFrames(1,TimeUnit.MILLISECONDS,500); + 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(); + Assert.assertThat("Text Frame.status code", tf.getPayloadAsUTF8(), is(msg)); } } } 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 34b5e40ac31..5dcecd300c8 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 @@ -18,8 +18,6 @@ package org.eclipse.jetty.websocket.server; -import static org.hamcrest.Matchers.*; - import java.net.URI; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; @@ -27,6 +25,8 @@ import java.util.concurrent.TimeUnit; import org.eclipse.jetty.io.MappedByteBufferPool; import org.eclipse.jetty.toolchain.test.EventQueue; import org.eclipse.jetty.toolchain.test.TestTracker; +import org.eclipse.jetty.websocket.api.BatchMode; +import org.eclipse.jetty.websocket.api.RemoteEndpoint; import org.eclipse.jetty.websocket.api.Session; import org.eclipse.jetty.websocket.client.WebSocketClient; import org.eclipse.jetty.websocket.common.test.LeakTrackingBufferPool; @@ -38,6 +38,8 @@ import org.junit.BeforeClass; import org.junit.Rule; import org.junit.Test; +import static org.hamcrest.Matchers.is; + public class WebSocketOverSSLTest { @Rule @@ -84,7 +86,10 @@ public class WebSocketOverSSLTest // Generate text frame String msg = "this is an echo ... cho ... ho ... o"; - session.getRemote().sendString(msg); + RemoteEndpoint remote = session.getRemote(); + remote.sendString(msg); + if (remote.getBatchMode() == BatchMode.ON) + remote.flush(); // Read frame (hopefully text frame) clientSocket.messages.awaitEventCount(1,500,TimeUnit.MILLISECONDS); @@ -122,7 +127,10 @@ public class WebSocketOverSSLTest Session session = fut.get(5,TimeUnit.SECONDS); // Generate text frame - session.getRemote().sendString("session.isSecure"); + RemoteEndpoint remote = session.getRemote(); + remote.sendString("session.isSecure"); + if (remote.getBatchMode() == BatchMode.ON) + remote.flush(); // Read frame (hopefully text frame) clientSocket.messages.awaitEventCount(1,500,TimeUnit.MILLISECONDS); @@ -160,7 +168,10 @@ public class WebSocketOverSSLTest Session session = fut.get(5,TimeUnit.SECONDS); // Generate text frame - session.getRemote().sendString("session.upgradeRequest.requestURI"); + RemoteEndpoint remote = session.getRemote(); + remote.sendString("session.upgradeRequest.requestURI"); + if (remote.getBatchMode() == BatchMode.ON) + remote.flush(); // Read frame (hopefully text frame) clientSocket.messages.awaitEventCount(1,500,TimeUnit.MILLISECONDS); diff --git a/jetty-websocket/websocket-server/src/test/java/org/eclipse/jetty/websocket/server/WebSocketServerSessionTest.java b/jetty-websocket/websocket-server/src/test/java/org/eclipse/jetty/websocket/server/WebSocketServerSessionTest.java index 28d32c08123..8b3732bfb71 100644 --- a/jetty-websocket/websocket-server/src/test/java/org/eclipse/jetty/websocket/server/WebSocketServerSessionTest.java +++ b/jetty-websocket/websocket-server/src/test/java/org/eclipse/jetty/websocket/server/WebSocketServerSessionTest.java @@ -18,8 +18,6 @@ package org.eclipse.jetty.websocket.server; -import static org.hamcrest.Matchers.*; - import java.net.URI; import java.util.Queue; import java.util.concurrent.TimeUnit; @@ -36,6 +34,8 @@ import org.junit.BeforeClass; import org.junit.Test; import org.junit.runner.RunWith; +import static org.hamcrest.Matchers.is; + /** * Testing various aspects of the server side support for WebSocket {@link Session} */ @@ -61,8 +61,7 @@ public class WebSocketServerSessionTest public void testDisconnect() throws Exception { URI uri = server.getServerUri().resolve("/test/disconnect"); - BlockheadClient client = new BlockheadClient(uri); - try + try (BlockheadClient client = new BlockheadClient(uri)) { client.connect(); client.sendStandardRequest(); @@ -70,11 +69,7 @@ public class WebSocketServerSessionTest client.write(new TextFrame().setPayload("harsh-disconnect")); - client.awaitDisconnect(1,TimeUnit.SECONDS); - } - finally - { - client.close(); + client.awaitDisconnect(1, TimeUnit.SECONDS); } } @@ -82,8 +77,7 @@ public class WebSocketServerSessionTest public void testUpgradeRequestResponse() throws Exception { URI uri = server.getServerUri().resolve("/test?snack=cashews&amount=handful&brand=off"); - BlockheadClient client = new BlockheadClient(uri); - try + try (BlockheadClient client = new BlockheadClient(uri)) { client.connect(); client.sendStandardRequest(); @@ -93,24 +87,19 @@ public class WebSocketServerSessionTest client.write(new TextFrame().setPayload("getParameterMap|snack")); client.write(new TextFrame().setPayload("getParameterMap|amount")); client.write(new TextFrame().setPayload("getParameterMap|brand")); - client.write(new TextFrame().setPayload("getParameterMap|cost")); // intentionall invalid + client.write(new TextFrame().setPayload("getParameterMap|cost")); // intentionally invalid // Read frame (hopefully text frame) - IncomingFramesCapture capture = client.readFrames(4,TimeUnit.MILLISECONDS,500); + IncomingFramesCapture capture = client.readFrames(4, TimeUnit.SECONDS, 5); Queue frames = capture.getFrames(); WebSocketFrame tf = frames.poll(); - Assert.assertThat("Parameter Map[snack]",tf.getPayloadAsUTF8(),is("[cashews]")); + Assert.assertThat("Parameter Map[snack]", tf.getPayloadAsUTF8(), is("[cashews]")); tf = frames.poll(); - Assert.assertThat("Parameter Map[amount]",tf.getPayloadAsUTF8(),is("[handful]")); + Assert.assertThat("Parameter Map[amount]", tf.getPayloadAsUTF8(), is("[handful]")); tf = frames.poll(); - Assert.assertThat("Parameter Map[brand]",tf.getPayloadAsUTF8(),is("[off]")); + Assert.assertThat("Parameter Map[brand]", tf.getPayloadAsUTF8(), is("[off]")); tf = frames.poll(); - Assert.assertThat("Parameter Map[cost]",tf.getPayloadAsUTF8(),is("")); - } - finally - { - client.close(); + Assert.assertThat("Parameter Map[cost]", tf.getPayloadAsUTF8(), is("")); } } - } 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 7ea355b8d6e..dd60c94fa8f 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 @@ -83,8 +83,9 @@ public class BrowserDebugTool implements WebSocketCreator String ua = req.getHeader("User-Agent"); String rexts = req.getHeader("Sec-WebSocket-Extensions"); + LOG.debug("User-Agent: {}", ua); - LOG.debug("Sec-WebSocket-Extensions: {}", rexts); + LOG.debug("Sec-WebSocket-Extensions (Request) : {}", rexts); return new BrowserSocket(ua,rexts); } @@ -113,6 +114,10 @@ public class BrowserDebugTool implements WebSocketCreator { LOG.debug("Configuring WebSocketServerFactory ..."); + // factory.getExtensionFactory().unregister("deflate-frame"); + // factory.getExtensionFactory().unregister("permessage-deflate"); + // factory.getExtensionFactory().unregister("x-webkit-deflate-frame"); + // 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/browser/BrowserSocket.java b/jetty-websocket/websocket-server/src/test/java/org/eclipse/jetty/websocket/server/browser/BrowserSocket.java index b232b0f2d8d..83d3bcebc51 100644 --- a/jetty-websocket/websocket-server/src/test/java/org/eclipse/jetty/websocket/server/browser/BrowserSocket.java +++ b/jetty-websocket/websocket-server/src/test/java/org/eclipse/jetty/websocket/server/browser/BrowserSocket.java @@ -135,8 +135,10 @@ public class BrowserSocket } else { - writeMessage("Client Sec-WebSocket-Extensions: " + this.requestedExtensions); + writeMessage("Client requested Sec-WebSocket-Extensions: " + this.requestedExtensions); + writeMessage("Negotiated Sec-WebSocket-Extensions: " + session.getUpgradeResponse().getHeader("Sec-WebSocket-Extensions")); } + break; } case "many": @@ -225,7 +227,7 @@ public class BrowserSocket } // Async write - session.getRemote().sendString(message, null); + session.getRemote().sendString(message,null); } private void writeMessage(String format, Object... args) diff --git a/jetty-websocket/websocket-server/src/test/java/org/eclipse/jetty/websocket/server/examples/MyEchoSocket.java b/jetty-websocket/websocket-server/src/test/java/org/eclipse/jetty/websocket/server/examples/MyEchoSocket.java index 5b7a4b869bc..4a8d3b05d33 100644 --- a/jetty-websocket/websocket-server/src/test/java/org/eclipse/jetty/websocket/server/examples/MyEchoSocket.java +++ b/jetty-websocket/websocket-server/src/test/java/org/eclipse/jetty/websocket/server/examples/MyEchoSocket.java @@ -20,6 +20,9 @@ package org.eclipse.jetty.websocket.server.examples; import java.io.IOException; +import org.eclipse.jetty.io.RuntimeIOException; +import org.eclipse.jetty.websocket.api.BatchMode; +import org.eclipse.jetty.websocket.api.RemoteEndpoint; import org.eclipse.jetty.websocket.api.WebSocketAdapter; /** @@ -38,11 +41,14 @@ public class MyEchoSocket extends WebSocketAdapter try { // echo the data back - getRemote().sendString(message); + RemoteEndpoint remote = getRemote(); + remote.sendString(message); + if (remote.getBatchMode() == BatchMode.ON) + remote.flush(); } catch (IOException e) { - e.printStackTrace(); + throw new RuntimeIOException(e); } } } 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 742f7634dca..431437f4433 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 @@ -18,10 +18,13 @@ package org.eclipse.jetty.websocket.server.examples.echo; +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.BatchMode; +import org.eclipse.jetty.websocket.api.RemoteEndpoint; import org.eclipse.jetty.websocket.api.Session; import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage; import org.eclipse.jetty.websocket.api.annotations.WebSocket; @@ -35,24 +38,30 @@ public class BigEchoSocket private static final Logger LOG = Log.getLogger(BigEchoSocket.class); @OnWebSocketMessage - public void onBinary(Session session, byte buf[], int offset, int length) + public void onBinary(Session session, byte buf[], int offset, int length) throws IOException { if (!session.isOpen()) { LOG.warn("Session is closed"); return; } - session.getRemote().sendBytes(ByteBuffer.wrap(buf,offset,length),null); + RemoteEndpoint remote = session.getRemote(); + remote.sendBytes(ByteBuffer.wrap(buf, offset, length), null); + if (remote.getBatchMode() == BatchMode.ON) + remote.flush(); } @OnWebSocketMessage - public void onText(Session session, String message) + public void onText(Session session, String message) throws IOException { if (!session.isOpen()) { LOG.warn("Session is closed"); return; } - session.getRemote().sendString(message,null); + RemoteEndpoint remote = session.getRemote(); + remote.sendString(message, null); + if (remote.getBatchMode() == BatchMode.ON) + remote.flush(); } } diff --git a/jetty-websocket/websocket-server/src/test/java/org/eclipse/jetty/websocket/server/helper/EchoSocket.java b/jetty-websocket/websocket-server/src/test/java/org/eclipse/jetty/websocket/server/helper/EchoSocket.java index c5d1e1c6516..a3dc6950451 100644 --- a/jetty-websocket/websocket-server/src/test/java/org/eclipse/jetty/websocket/server/helper/EchoSocket.java +++ b/jetty-websocket/websocket-server/src/test/java/org/eclipse/jetty/websocket/server/helper/EchoSocket.java @@ -18,10 +18,13 @@ package org.eclipse.jetty.websocket.server.helper; +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.BatchMode; +import org.eclipse.jetty.websocket.api.RemoteEndpoint; import org.eclipse.jetty.websocket.api.Session; import org.eclipse.jetty.websocket.api.annotations.OnWebSocketConnect; import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage; @@ -38,13 +41,16 @@ public class EchoSocket private Session session; @OnWebSocketMessage - public void onBinary(byte buf[], int offset, int len) + public void onBinary(byte buf[], int offset, int len) throws IOException { LOG.debug("onBinary(byte[{}],{},{})",buf.length,offset,len); // echo the message back. ByteBuffer data = ByteBuffer.wrap(buf,offset,len); - this.session.getRemote().sendBytes(data,null); + RemoteEndpoint remote = this.session.getRemote(); + remote.sendBytes(data, null); + if (remote.getBatchMode() == BatchMode.ON) + remote.flush(); } @OnWebSocketConnect @@ -54,11 +60,14 @@ public class EchoSocket } @OnWebSocketMessage - public void onText(String message) + public void onText(String message) throws IOException { LOG.debug("onText({})",message); // echo the message back. - this.session.getRemote().sendString(message,null); + RemoteEndpoint remote = session.getRemote(); + remote.sendString(message, null); + if (remote.getBatchMode() == BatchMode.ON) + remote.flush(); } } diff --git a/jetty-websocket/websocket-server/src/test/java/org/eclipse/jetty/websocket/server/helper/RFCSocket.java b/jetty-websocket/websocket-server/src/test/java/org/eclipse/jetty/websocket/server/helper/RFCSocket.java index e0c13fee2f5..d90a8f24989 100644 --- a/jetty-websocket/websocket-server/src/test/java/org/eclipse/jetty/websocket/server/helper/RFCSocket.java +++ b/jetty-websocket/websocket-server/src/test/java/org/eclipse/jetty/websocket/server/helper/RFCSocket.java @@ -18,10 +18,13 @@ package org.eclipse.jetty.websocket.server.helper; +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.BatchMode; +import org.eclipse.jetty.websocket.api.RemoteEndpoint; import org.eclipse.jetty.websocket.api.Session; import org.eclipse.jetty.websocket.api.annotations.OnWebSocketConnect; import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage; @@ -35,13 +38,16 @@ public class RFCSocket private Session session; @OnWebSocketMessage - public void onBinary(byte buf[], int offset, int len) + public void onBinary(byte buf[], int offset, int len) throws IOException { LOG.debug("onBinary(byte[{}],{},{})",buf.length,offset,len); // echo the message back. ByteBuffer data = ByteBuffer.wrap(buf,offset,len); - this.session.getRemote().sendBytes(data,null); + RemoteEndpoint remote = session.getRemote(); + remote.sendBytes(data, null); + if (remote.getBatchMode() == BatchMode.ON) + remote.flush(); } @OnWebSocketConnect @@ -51,7 +57,7 @@ public class RFCSocket } @OnWebSocketMessage - public void onText(String message) + public void onText(String message) throws IOException { LOG.debug("onText({})",message); // Test the RFC 6455 close code 1011 that should close @@ -62,6 +68,9 @@ public class RFCSocket } // echo the message back. - this.session.getRemote().sendString(message,null); + RemoteEndpoint remote = session.getRemote(); + remote.sendString(message, null); + if (remote.getBatchMode() == BatchMode.ON) + remote.flush(); } } 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 129963b33f9..467a4b527f4 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,11 +18,14 @@ package org.eclipse.jetty.websocket.server.helper; +import java.io.IOException; import java.util.List; import java.util.Map; import org.eclipse.jetty.util.log.Log; import org.eclipse.jetty.util.log.Logger; +import org.eclipse.jetty.websocket.api.BatchMode; +import org.eclipse.jetty.websocket.api.RemoteEndpoint; import org.eclipse.jetty.websocket.api.Session; import org.eclipse.jetty.websocket.api.annotations.OnWebSocketConnect; import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage; @@ -61,7 +64,7 @@ public class SessionSocket if (values == null) { - session.getRemote().sendString("",null); + sendString(""); return; } @@ -78,21 +81,22 @@ public class SessionSocket delim = true; } valueStr.append(']'); - session.getRemote().sendString(valueStr.toString(),null); + System.err.println("valueStr = " + valueStr); + sendString(valueStr.toString()); return; } if ("session.isSecure".equals(message)) { String issecure = String.format("session.isSecure=%b",session.isSecure()); - session.getRemote().sendString(issecure,null); + sendString(issecure); return; } if ("session.upgradeRequest.requestURI".equals(message)) { String response = String.format("session.upgradeRequest.requestURI=%s",session.getUpgradeRequest().getRequestURI().toASCIIString()); - session.getRemote().sendString(response,null); + sendString(response); return; } @@ -103,11 +107,19 @@ public class SessionSocket } // echo the message back. - this.session.getRemote().sendString(message,null); + sendString(message); } catch (Throwable t) { LOG.warn(t); } } + + protected void sendString(String text) throws IOException + { + RemoteEndpoint remote = session.getRemote(); + remote.sendString(text, null); + if (remote.getBatchMode() == BatchMode.ON) + remote.flush(); + } } diff --git a/jetty-websocket/websocket-servlet/src/main/java/org/eclipse/jetty/websocket/servlet/PostUpgradedHttpServletRequest.java b/jetty-websocket/websocket-servlet/src/main/java/org/eclipse/jetty/websocket/servlet/PostUpgradedHttpServletRequest.java deleted file mode 100644 index 5caca7d508b..00000000000 --- a/jetty-websocket/websocket-servlet/src/main/java/org/eclipse/jetty/websocket/servlet/PostUpgradedHttpServletRequest.java +++ /dev/null @@ -1,190 +0,0 @@ -// -// ======================================================================== -// Copyright (c) 1995-2014 Mort Bay Consulting Pty. Ltd. -// ------------------------------------------------------------------------ -// All rights reserved. This program and the accompanying materials -// are made available under the terms of the Eclipse Public License v1.0 -// and Apache License v2.0 which accompanies this distribution. -// -// The Eclipse Public License is available at -// http://www.eclipse.org/legal/epl-v10.html -// -// The Apache License v2.0 is available at -// http://www.opensource.org/licenses/apache2.0.php -// -// You may elect to redistribute this code under either of these licenses. -// ======================================================================== -// - -package org.eclipse.jetty.websocket.servlet; - -import java.io.BufferedReader; -import java.io.IOException; -import java.io.UnsupportedEncodingException; -import java.util.Collection; - -import javax.servlet.AsyncContext; -import javax.servlet.RequestDispatcher; -import javax.servlet.ServletException; -import javax.servlet.ServletInputStream; -import javax.servlet.ServletRequest; -import javax.servlet.ServletResponse; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletRequestWrapper; -import javax.servlet.http.HttpServletResponse; -import javax.servlet.http.HttpUpgradeHandler; -import javax.servlet.http.Part; - -public class PostUpgradedHttpServletRequest extends HttpServletRequestWrapper -{ - private static final String UNSUPPORTED_WITH_WEBSOCKET_UPGRADE = "Feature unsupported with a Upgraded to WebSocket HttpServletRequest"; - - public PostUpgradedHttpServletRequest(HttpServletRequest request) - { - super(request); - } - - @Override - public boolean authenticate(HttpServletResponse response) throws IOException, ServletException - { - throw new UnsupportedOperationException(UNSUPPORTED_WITH_WEBSOCKET_UPGRADE); - } - - @Override - public String changeSessionId() - { - throw new UnsupportedOperationException(UNSUPPORTED_WITH_WEBSOCKET_UPGRADE); - } - - @Override - public AsyncContext getAsyncContext() - { - throw new UnsupportedOperationException(UNSUPPORTED_WITH_WEBSOCKET_UPGRADE); - } - - @Override - public String getCharacterEncoding() - { - throw new UnsupportedOperationException(UNSUPPORTED_WITH_WEBSOCKET_UPGRADE); - } - - @Override - public int getContentLength() - { - throw new UnsupportedOperationException(UNSUPPORTED_WITH_WEBSOCKET_UPGRADE); - } - - @Override - public long getContentLengthLong() - { - throw new UnsupportedOperationException(UNSUPPORTED_WITH_WEBSOCKET_UPGRADE); - } - - @Override - public String getContentType() - { - throw new UnsupportedOperationException(UNSUPPORTED_WITH_WEBSOCKET_UPGRADE); - } - - @Override - public ServletInputStream getInputStream() throws IOException - { - throw new UnsupportedOperationException(UNSUPPORTED_WITH_WEBSOCKET_UPGRADE); - } - - @Override - public Part getPart(String name) throws IOException, ServletException - { - throw new UnsupportedOperationException(UNSUPPORTED_WITH_WEBSOCKET_UPGRADE); - } - - @Override - public Collection getParts() throws IOException, ServletException - { - throw new UnsupportedOperationException(UNSUPPORTED_WITH_WEBSOCKET_UPGRADE); - } - - @Override - public BufferedReader getReader() throws IOException - { - throw new UnsupportedOperationException(UNSUPPORTED_WITH_WEBSOCKET_UPGRADE); - } - - @Override - public ServletRequest getRequest() - { - throw new UnsupportedOperationException(UNSUPPORTED_WITH_WEBSOCKET_UPGRADE); - } - - @Override - public RequestDispatcher getRequestDispatcher(String path) - { - throw new UnsupportedOperationException(UNSUPPORTED_WITH_WEBSOCKET_UPGRADE); - } - - @Override - public boolean isAsyncStarted() - { - return false; - } - - @Override - public boolean isAsyncSupported() - { - return false; - } - - @Override - public boolean isWrapperFor(Class wrappedType) - { - throw new UnsupportedOperationException(UNSUPPORTED_WITH_WEBSOCKET_UPGRADE); - } - - @Override - public boolean isWrapperFor(ServletRequest wrapped) - { - throw new UnsupportedOperationException(UNSUPPORTED_WITH_WEBSOCKET_UPGRADE); - } - - @Override - public void login(String username, String password) throws ServletException - { - throw new UnsupportedOperationException(UNSUPPORTED_WITH_WEBSOCKET_UPGRADE); - } - - @Override - public void logout() throws ServletException - { - throw new UnsupportedOperationException(UNSUPPORTED_WITH_WEBSOCKET_UPGRADE); - } - - @Override - public void setCharacterEncoding(String enc) throws UnsupportedEncodingException - { - throw new UnsupportedOperationException(UNSUPPORTED_WITH_WEBSOCKET_UPGRADE); - } - - @Override - public void setRequest(ServletRequest request) - { - throw new UnsupportedOperationException(UNSUPPORTED_WITH_WEBSOCKET_UPGRADE); - } - - @Override - public AsyncContext startAsync() throws IllegalStateException - { - throw new UnsupportedOperationException(UNSUPPORTED_WITH_WEBSOCKET_UPGRADE); - } - - @Override - public AsyncContext startAsync(ServletRequest servletRequest, ServletResponse servletResponse) throws IllegalStateException - { - throw new UnsupportedOperationException(UNSUPPORTED_WITH_WEBSOCKET_UPGRADE); - } - - @Override - public T upgrade(Class handlerClass) throws IOException, ServletException - { - throw new UnsupportedOperationException(UNSUPPORTED_WITH_WEBSOCKET_UPGRADE); - } -} 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 986239e712b..35a7832b666 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 @@ -31,7 +31,6 @@ import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; - import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpSession; @@ -45,148 +44,140 @@ import org.eclipse.jetty.websocket.api.util.WSURI; */ public class ServletUpgradeRequest extends UpgradeRequest { - private final HttpServletRequest req; + private final UpgradeHttpServletRequest request; - public ServletUpgradeRequest(HttpServletRequest request) throws URISyntaxException + public ServletUpgradeRequest(HttpServletRequest httpRequest) throws URISyntaxException { - super(WSURI.toWebsocket(request.getRequestURL(),request.getQueryString())); - this.req = new PostUpgradedHttpServletRequest(request); + super(WSURI.toWebsocket(httpRequest.getRequestURL(), httpRequest.getQueryString())); + this.request = new UpgradeHttpServletRequest(httpRequest); - // Copy Request Line Details - setMethod(request.getMethod()); - setHttpVersion(request.getProtocol()); - - // Copy parameters - Map> pmap = new HashMap<>(); - if (request.getParameterMap() != null) + // Parse protocols. + Enumeration requestProtocols = request.getHeaders("Sec-WebSocket-Protocol"); + if (requestProtocols != null) { - for (Map.Entry entry : request.getParameterMap().entrySet()) + List protocols = new ArrayList<>(2); + while (requestProtocols.hasMoreElements()) { - pmap.put(entry.getKey(),Arrays.asList(entry.getValue())); + String candidate = requestProtocols.nextElement(); + Collections.addAll(protocols, parseProtocols(candidate)); } - } - super.setParameterMap(pmap); - - // Copy Cookies - Cookie rcookies[] = request.getCookies(); - if (rcookies != null) - { - List cookies = new ArrayList<>(); - for (Cookie rcookie : rcookies) - { - HttpCookie hcookie = new HttpCookie(rcookie.getName(),rcookie.getValue()); - // no point handling domain/path/expires/secure/httponly on client request cookies - cookies.add(hcookie); - } - super.setCookies(cookies); + setSubProtocols(protocols); } - // Copy Headers - Enumeration headerNames = request.getHeaderNames(); - while (headerNames.hasMoreElements()) - { - String name = headerNames.nextElement(); - List values = Collections.list(request.getHeaders(name)); - setHeader(name,values); - } - - // Parse Sub Protocols - Enumeration protocols = request.getHeaders("Sec-WebSocket-Protocol"); - List subProtocols = new ArrayList<>(); - String protocol = null; - while ((protocol == null) && (protocols != null) && protocols.hasMoreElements()) - { - String candidate = protocols.nextElement(); - for (String p : parseProtocols(candidate)) - { - subProtocols.add(p); - } - } - setSubProtocols(subProtocols); - - // Parse Extension Configurations + // Parse extensions. Enumeration e = request.getHeaders("Sec-WebSocket-Extensions"); setExtensions(ExtensionConfig.parseEnum(e)); + + // Copy cookies. + Cookie[] requestCookies = request.getCookies(); + if (requestCookies != null) + { + List cookies = new ArrayList<>(); + for (Cookie requestCookie : requestCookies) + { + HttpCookie cookie = new HttpCookie(requestCookie.getName(), requestCookie.getValue()); + // No point handling domain/path/expires/secure/httponly on client request cookies + cookies.add(cookie); + } + setCookies(cookies); + } + + setHeaders(request.getHeaders()); + + // Copy parameters. + Map requestParams = request.getParameterMap(); + if (requestParams != null) + { + Map> params = new HashMap<>(requestParams.size()); + for (Map.Entry entry : requestParams.entrySet()) + params.put(entry.getKey(), Arrays.asList(entry.getValue())); + setParameterMap(params); + } + + setSession(request.getSession(false)); + + setHttpVersion(request.getProtocol()); + setMethod(request.getMethod()); } public X509Certificate[] getCertificates() { - return (X509Certificate[])req.getAttribute("javax.servlet.request.X509Certificate"); + return (X509Certificate[])request.getAttribute("javax.servlet.request.X509Certificate"); } - + /** * Return the underlying HttpServletRequest that existed at Upgrade time. - *

+ *

* Note: many features of the HttpServletRequest are invalid when upgraded, * especially ones that deal with body content, streams, readers, and responses. - * + * * @return a limited version of the underlying HttpServletRequest */ public HttpServletRequest getHttpServletRequest() { - return req; + return request; } /** * Equivalent to {@link HttpServletRequest#getLocalAddr()} - * + * * @return the local address */ public String getLocalAddress() { - return req.getLocalAddr(); + return request.getLocalAddr(); } /** * Equivalent to {@link HttpServletRequest#getLocalName()} - * + * * @return the local host name */ public String getLocalHostName() { - return req.getLocalName(); + return request.getLocalName(); } /** * Equivalent to {@link HttpServletRequest#getLocalPort()} - * + * * @return the local port */ public int getLocalPort() { - return req.getLocalPort(); - } - - /** - * Equivalent to {@link HttpServletRequest#getLocale()} - * - * @return the preferred Locale for the client - */ - public Locale getLocale() - { - return req.getLocale(); - } - - /** - * Equivalent to {@link HttpServletRequest#getLocales()} - * - * @return an Enumeration of preferred Locale objects - */ - public Enumeration getLocales() - { - return req.getLocales(); + return request.getLocalPort(); } /** * Return a {@link InetSocketAddress} for the local socket. - *

+ *

* Warning: this can cause a DNS lookup - * + * * @return the local socket address */ public InetSocketAddress getLocalSocketAddress() { - return new InetSocketAddress(req.getLocalAddr(),req.getLocalPort()); + return new InetSocketAddress(getLocalAddress(), getLocalPort()); + } + + /** + * Equivalent to {@link HttpServletRequest#getLocale()} + * + * @return the preferred Locale for the client + */ + public Locale getLocale() + { + return request.getLocale(); + } + + /** + * Equivalent to {@link HttpServletRequest#getLocales()} + * + * @return an Enumeration of preferred Locale objects + */ + public Enumeration getLocales() + { + return request.getLocales(); } /** @@ -195,136 +186,118 @@ public class ServletUpgradeRequest extends UpgradeRequest @Deprecated public Principal getPrincipal() { - return req.getUserPrincipal(); + return getUserPrincipal(); } - + /** * Equivalent to {@link HttpServletRequest#getUserPrincipal()} */ public Principal getUserPrincipal() { - return req.getUserPrincipal(); + return request.getUserPrincipal(); } /** * Equivalent to {@link HttpServletRequest#getRemoteAddr()} - * + * * @return the remote address */ public String getRemoteAddress() { - return req.getRemoteAddr(); + return request.getRemoteAddr(); } /** * Equivalent to {@link HttpServletRequest#getRemoteHost()} - * + * * @return the remote host name */ public String getRemoteHostName() { - return req.getRemoteHost(); + return request.getRemoteHost(); } /** * Equivalent to {@link HttpServletRequest#getRemotePort()} - * + * * @return the remote port */ public int getRemotePort() { - return req.getRemotePort(); + return request.getRemotePort(); } /** * Return a {@link InetSocketAddress} for the remote socket. - *

+ *

* Warning: this can cause a DNS lookup - * + * * @return the remote socket address */ public InetSocketAddress getRemoteSocketAddress() { - return new InetSocketAddress(req.getRemoteAddr(),req.getRemotePort()); + return new InetSocketAddress(getRemoteAddress(), getRemotePort()); } public Map getServletAttributes() { - Map attributes = new HashMap(); - - for (String name : Collections.list(req.getAttributeNames())) - { - attributes.put(name,req.getAttribute(name)); - } - - return attributes; + return request.getAttributes(); } public Map> getServletParameters() { - Map> parameters = new HashMap>(); - - for (String name : Collections.list(req.getParameterNames())) - { - parameters.put(name,Collections.unmodifiableList(Arrays.asList(req.getParameterValues(name)))); - } - - return parameters; + return getParameterMap(); } /** * Return the HttpSession if it exists. - *

- * Note: this is equivalent to {@link HttpServletRequest#getSession()} and will not create a new HttpSession. + *

+ * Note: this is equivalent to {@link HttpServletRequest#getSession(boolean)} + * and will not create a new HttpSession. */ @Override public HttpSession getSession() { - return this.req.getSession(false); + return request.getSession(false); } - protected String[] parseProtocols(String protocol) + public void setServletAttribute(String name, Object value) { - if (protocol == null) - { - return new String[] {}; - } - protocol = protocol.trim(); - if ((protocol == null) || (protocol.length() == 0)) - { - return new String[] {}; - } - String[] passed = protocol.split("\\s*,\\s*"); - String[] protocols = new String[passed.length]; - System.arraycopy(passed,0,protocols,0,passed.length); - return protocols; - } - - public void setServletAttribute(String name, Object o) - { - this.req.setAttribute(name,o); + request.setAttribute(name, value); } public Object getServletAttribute(String name) { - return req.getAttribute(name); + return request.getAttribute(name); } public boolean isUserInRole(String role) { - return req.isUserInRole(role); + return request.isUserInRole(role); } public String getRequestPath() { - // Since this can be called from a filter, we need to be smart about determining the target request path - String contextPath = req.getContextPath(); - String requestPath = req.getRequestURI(); + // Since this can be called from a filter, we need to be smart about determining the target request path. + String contextPath = request.getContextPath(); + String requestPath = request.getRequestURI(); if (requestPath.startsWith(contextPath)) - { requestPath = requestPath.substring(contextPath.length()); - } - return requestPath; } + + private String[] parseProtocols(String protocol) + { + if (protocol == null) + return new String[0]; + protocol = protocol.trim(); + if (protocol.length() == 0) + return new String[0]; + return protocol.split("\\s*,\\s*"); + } + + public void complete() + { + request.complete(); + } } diff --git a/jetty-websocket/websocket-servlet/src/main/java/org/eclipse/jetty/websocket/servlet/ServletUpgradeResponse.java b/jetty-websocket/websocket-servlet/src/main/java/org/eclipse/jetty/websocket/servlet/ServletUpgradeResponse.java index 450376d5c12..91043660bfc 100644 --- a/jetty-websocket/websocket-servlet/src/main/java/org/eclipse/jetty/websocket/servlet/ServletUpgradeResponse.java +++ b/jetty-websocket/websocket-servlet/src/main/java/org/eclipse/jetty/websocket/servlet/ServletUpgradeResponse.java @@ -75,16 +75,18 @@ public class ServletUpgradeResponse extends UpgradeResponse public void sendError(int statusCode, String message) throws IOException { setSuccess(false); - complete(); + commitHeaders(); response.sendError(statusCode, message); + response = null; } @Override public void sendForbidden(String message) throws IOException { setSuccess(false); - complete(); + commitHeaders(); response.sendError(HttpServletResponse.SC_FORBIDDEN, message); + response = null; } @Override @@ -102,14 +104,20 @@ public class ServletUpgradeResponse extends UpgradeResponse } public void complete() + { + commitHeaders(); + response = null; + } + + private void commitHeaders() { // Transfer all headers to the real HTTP response - for (Map.Entry> entry : getHeaders().entrySet()) - { - for (String value : entry.getValue()) - { - response.addHeader(entry.getKey(), value); - } - } + for (Map.Entry> entry : getHeaders().entrySet()) + { + for (String value : entry.getValue()) + { + response.addHeader(entry.getKey(), value); + } + } } } diff --git a/jetty-websocket/websocket-servlet/src/main/java/org/eclipse/jetty/websocket/servlet/UpgradeHttpServletRequest.java b/jetty-websocket/websocket-servlet/src/main/java/org/eclipse/jetty/websocket/servlet/UpgradeHttpServletRequest.java new file mode 100644 index 00000000000..cc672d8f2b6 --- /dev/null +++ b/jetty-websocket/websocket-servlet/src/main/java/org/eclipse/jetty/websocket/servlet/UpgradeHttpServletRequest.java @@ -0,0 +1,589 @@ +// +// ======================================================================== +// Copyright (c) 1995-2014 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.websocket.servlet; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.net.InetSocketAddress; +import java.security.Principal; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import javax.servlet.AsyncContext; +import javax.servlet.DispatcherType; +import javax.servlet.RequestDispatcher; +import javax.servlet.ServletContext; +import javax.servlet.ServletException; +import javax.servlet.ServletInputStream; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.Cookie; +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; + +public class UpgradeHttpServletRequest implements HttpServletRequest +{ + private static final String UNSUPPORTED_WITH_WEBSOCKET_UPGRADE = "Feature unsupported with a Upgraded to WebSocket HttpServletRequest"; + + private HttpServletRequest request; + private final ServletContext context; + private final DispatcherType dispatcher; + private final String method; + private final String protocol; + private final String scheme; + private final boolean secure; + private final String requestURI; + private final StringBuffer requestURL; + private final String pathInfo; + private final String pathTranslated; + private final String servletPath; + private final String query; + private final String authType; + private final Cookie[] cookies; + private final String remoteUser; + private final Principal principal; + + private final Map> headers = new HashMap<>(8); + private final Map parameters = new HashMap<>(2); + private final Map attributes = new HashMap<>(2); + private final List locales = new ArrayList<>(2); + + private HttpSession session; + + private final InetSocketAddress localAddress; + private final String localName; + private final InetSocketAddress remoteAddress; + private final String remoteName; + private final InetSocketAddress serverAddress; + + public UpgradeHttpServletRequest(HttpServletRequest httpRequest) + { + // The original request object must be held temporarily for the duration of the handshake + // in order to be able to implement methods such as isUserInRole() and setAttribute(). + request = httpRequest; + context = httpRequest.getServletContext(); + dispatcher = httpRequest.getDispatcherType(); + + method = httpRequest.getMethod(); + protocol = httpRequest.getProtocol(); + scheme = httpRequest.getScheme(); + secure = httpRequest.isSecure(); + requestURI = httpRequest.getRequestURI(); + requestURL = httpRequest.getRequestURL(); + pathInfo = httpRequest.getPathInfo(); + pathTranslated = httpRequest.getPathTranslated(); + servletPath = httpRequest.getServletPath(); + query = httpRequest.getQueryString(); + authType = httpRequest.getAuthType(); + cookies = request.getCookies(); + + remoteUser = httpRequest.getRemoteUser(); + principal = httpRequest.getUserPrincipal(); + + Enumeration headerNames = httpRequest.getHeaderNames(); + while (headerNames.hasMoreElements()) + { + String name = headerNames.nextElement(); + headers.put(name, Collections.list(httpRequest.getHeaders(name))); + } + + parameters.putAll(request.getParameterMap()); + + Enumeration attributeNames = httpRequest.getAttributeNames(); + while (attributeNames.hasMoreElements()) + { + String name = attributeNames.nextElement(); + attributes.put(name, httpRequest.getAttribute(name)); + } + + localAddress = InetSocketAddress.createUnresolved(httpRequest.getLocalAddr(), httpRequest.getLocalPort()); + localName = httpRequest.getLocalName(); + remoteAddress = InetSocketAddress.createUnresolved(httpRequest.getRemoteAddr(), httpRequest.getRemotePort()); + remoteName = httpRequest.getRemoteHost(); + serverAddress = InetSocketAddress.createUnresolved(httpRequest.getServerName(), httpRequest.getServerPort()); + } + + public HttpServletRequest getHttpServletRequest() + { + return request; + } + + @Override + public String getAuthType() + { + return authType; + } + + @Override + public Cookie[] getCookies() + { + return cookies; + } + + @Override + public String getHeader(String name) + { + List values = headers.get(name); + if (values == null || values.isEmpty()) + return null; + return values.get(0); + } + + @Override + public Enumeration getHeaders(String name) + { + List values = headers.get(name); + if (values == null) + return Collections.emptyEnumeration(); + return Collections.enumeration(values); + } + + @Override + public Enumeration getHeaderNames() + { + return Collections.enumeration(headers.keySet()); + } + + public Map> getHeaders() + { + return Collections.unmodifiableMap(headers); + } + + @Override + public long getDateHeader(String name) + { + // TODO + throw new UnsupportedOperationException(UNSUPPORTED_WITH_WEBSOCKET_UPGRADE); + } + + @Override + public int getIntHeader(String name) + { + // TODO + throw new UnsupportedOperationException(UNSUPPORTED_WITH_WEBSOCKET_UPGRADE); + } + + @Override + public String getMethod() + { + return method; + } + + @Override + public String getPathInfo() + { + return pathInfo; + } + + @Override + public String getPathTranslated() + { + return pathTranslated; + } + + @Override + public String getContextPath() + { + return context.getContextPath(); + } + + @Override + public String getQueryString() + { + return query; + } + + @Override + public String getRemoteUser() + { + return remoteUser; + } + + @Override + public boolean isUserInRole(String role) + { + HttpServletRequest request = getHttpServletRequest(); + if (request != null) + return request.isUserInRole(role); + throw new UnsupportedOperationException(UNSUPPORTED_WITH_WEBSOCKET_UPGRADE); + } + + @Override + public Principal getUserPrincipal() + { + return principal; + } + + @Override + public String getRequestURI() + { + return requestURI; + } + + @Override + public StringBuffer getRequestURL() + { + return requestURL; + } + + @Override + public String getServletPath() + { + return servletPath; + } + + @Override + public HttpSession getSession(boolean create) + { + HttpServletRequest request = getHttpServletRequest(); + if (request != null) + return session = request.getSession(create); + return session; + } + + @Override + public HttpSession getSession() + { + return getSession(true); + } + + @Override + public String getRequestedSessionId() + { + throw new UnsupportedOperationException(UNSUPPORTED_WITH_WEBSOCKET_UPGRADE); + } + + @Override + public boolean isRequestedSessionIdValid() + { + throw new UnsupportedOperationException(UNSUPPORTED_WITH_WEBSOCKET_UPGRADE); + } + + @Override + public boolean isRequestedSessionIdFromCookie() + { + throw new UnsupportedOperationException(UNSUPPORTED_WITH_WEBSOCKET_UPGRADE); + } + + @Override + public boolean isRequestedSessionIdFromURL() + { + throw new UnsupportedOperationException(UNSUPPORTED_WITH_WEBSOCKET_UPGRADE); + } + + @Override + public boolean isRequestedSessionIdFromUrl() + { + throw new UnsupportedOperationException(UNSUPPORTED_WITH_WEBSOCKET_UPGRADE); + } + + @Override + public Object getAttribute(String name) + { + return attributes.get(name); + } + + @Override + public Enumeration getAttributeNames() + { + return Collections.enumeration(attributes.keySet()); + } + + public Map getAttributes() + { + return Collections.unmodifiableMap(attributes); + } + + @Override + public String getParameter(String name) + { + String[] values = parameters.get(name); + if (values == null || values.length == 0) + return null; + return values[0]; + } + + @Override + public Enumeration getParameterNames() + { + return Collections.enumeration(parameters.keySet()); + } + + @Override + public String[] getParameterValues(String name) + { + return parameters.get(name); + } + + @Override + public Map getParameterMap() + { + return parameters; + } + + @Override + public String getProtocol() + { + return protocol; + } + + @Override + public String getScheme() + { + return scheme; + } + + @Override + public String getServerName() + { + return serverAddress.getHostString(); + } + + @Override + public int getServerPort() + { + return serverAddress.getPort(); + } + + @Override + public String getRemoteAddr() + { + return remoteAddress.getHostString(); + } + + @Override + public int getRemotePort() + { + return remoteAddress.getPort(); + } + + @Override + public String getRemoteHost() + { + return remoteName; + } + + @Override + public void setAttribute(String name, Object value) + { + HttpServletRequest request = getHttpServletRequest(); + if (request != null) + request.setAttribute(name, value); + attributes.put(name, value); + } + + @Override + public void removeAttribute(String name) + { + HttpServletRequest request = getHttpServletRequest(); + if (request != null) + request.removeAttribute(name); + attributes.remove(name); + } + + @Override + public Locale getLocale() + { + if (locales.isEmpty()) + return Locale.getDefault(); + return locales.get(0); + } + + @Override + public Enumeration getLocales() + { + return Collections.enumeration(locales); + } + + @Override + public boolean isSecure() + { + return secure; + } + + @Override + public String getRealPath(String path) + { + return context.getRealPath(path); + } + + @Override + public String getLocalName() + { + return localName; + } + + @Override + public String getLocalAddr() + { + return localAddress.getHostString(); + } + + @Override + public int getLocalPort() + { + return localAddress.getPort(); + } + + @Override + public ServletContext getServletContext() + { + return context; + } + + @Override + public DispatcherType getDispatcherType() + { + return dispatcher; + } + + @Override + public boolean authenticate(HttpServletResponse response) throws IOException, ServletException + { + throw new UnsupportedOperationException(UNSUPPORTED_WITH_WEBSOCKET_UPGRADE); + } + + @Override + public String changeSessionId() + { + throw new UnsupportedOperationException(UNSUPPORTED_WITH_WEBSOCKET_UPGRADE); + } + + @Override + public AsyncContext getAsyncContext() + { + throw new UnsupportedOperationException(UNSUPPORTED_WITH_WEBSOCKET_UPGRADE); + } + + @Override + public String getCharacterEncoding() + { + throw new UnsupportedOperationException(UNSUPPORTED_WITH_WEBSOCKET_UPGRADE); + } + + @Override + public int getContentLength() + { + throw new UnsupportedOperationException(UNSUPPORTED_WITH_WEBSOCKET_UPGRADE); + } + + @Override + public long getContentLengthLong() + { + throw new UnsupportedOperationException(UNSUPPORTED_WITH_WEBSOCKET_UPGRADE); + } + + @Override + public String getContentType() + { + throw new UnsupportedOperationException(UNSUPPORTED_WITH_WEBSOCKET_UPGRADE); + } + + @Override + public ServletInputStream getInputStream() throws IOException + { + throw new UnsupportedOperationException(UNSUPPORTED_WITH_WEBSOCKET_UPGRADE); + } + + @Override + public Part getPart(String name) throws IOException, ServletException + { + throw new UnsupportedOperationException(UNSUPPORTED_WITH_WEBSOCKET_UPGRADE); + } + + @Override + public Collection getParts() throws IOException, ServletException + { + throw new UnsupportedOperationException(UNSUPPORTED_WITH_WEBSOCKET_UPGRADE); + } + + @Override + public BufferedReader getReader() throws IOException + { + throw new UnsupportedOperationException(UNSUPPORTED_WITH_WEBSOCKET_UPGRADE); + } + + @Override + public RequestDispatcher getRequestDispatcher(String path) + { + throw new UnsupportedOperationException(UNSUPPORTED_WITH_WEBSOCKET_UPGRADE); + } + + @Override + public boolean isAsyncStarted() + { + return false; + } + + @Override + public boolean isAsyncSupported() + { + return false; + } + + @Override + public void login(String username, String password) throws ServletException + { + throw new UnsupportedOperationException(UNSUPPORTED_WITH_WEBSOCKET_UPGRADE); + } + + @Override + public void logout() throws ServletException + { + throw new UnsupportedOperationException(UNSUPPORTED_WITH_WEBSOCKET_UPGRADE); + } + + @Override + public void setCharacterEncoding(String enc) throws UnsupportedEncodingException + { + throw new UnsupportedOperationException(UNSUPPORTED_WITH_WEBSOCKET_UPGRADE); + } + + @Override + public AsyncContext startAsync() throws IllegalStateException + { + throw new UnsupportedOperationException(UNSUPPORTED_WITH_WEBSOCKET_UPGRADE); + } + + @Override + public AsyncContext startAsync(ServletRequest servletRequest, ServletResponse servletResponse) throws IllegalStateException + { + throw new UnsupportedOperationException(UNSUPPORTED_WITH_WEBSOCKET_UPGRADE); + } + + @Override + public T upgrade(Class handlerClass) throws IOException, ServletException + { + throw new UnsupportedOperationException(UNSUPPORTED_WITH_WEBSOCKET_UPGRADE); + } + + public void complete() + { + request = null; + } +} diff --git a/jetty-xml/src/main/java/org/eclipse/jetty/xml/XmlAppendable.java b/jetty-xml/src/main/java/org/eclipse/jetty/xml/XmlAppendable.java new file mode 100644 index 00000000000..5a11c7505cd --- /dev/null +++ b/jetty-xml/src/main/java/org/eclipse/jetty/xml/XmlAppendable.java @@ -0,0 +1,164 @@ +// +// ======================================================================== +// Copyright (c) 1995-2014 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.xml; + +import java.io.Closeable; +import java.io.IOException; +import java.util.Map; +import java.util.Stack; + +public class XmlAppendable +{ + private final String SPACES=" "; + private final Appendable _out; + private final int _indent; + private final Stack _tags = new Stack<>(); + private String _space=""; + + public XmlAppendable(Appendable out) throws IOException + { + this(out,2); + } + + public XmlAppendable(Appendable out, int indent) throws IOException + { + _out=out; + _indent=indent; + _out.append("\n"); + } + + public XmlAppendable open(String tag, Map attributes) throws IOException + { + _out.append(_space).append('<').append(tag); + attributes(attributes); + + _out.append(">\n"); + _space=_space+SPACES.substring(0,_indent); + _tags.push(tag); + return this; + } + + public XmlAppendable open(String tag) throws IOException + { + _out.append(_space).append('<').append(tag).append(">\n"); + _space=_space+SPACES.substring(0,_indent); + _tags.push(tag); + return this; + } + + public XmlAppendable content(String s) throws IOException + { + if (s!=null) + { + for (int i=0;i': + _out.append(">"); + break; + case '&': + _out.append("&"); + break; + case '\'': + _out.append("'"); + break; + case '"': + _out.append("""); + break; + default: + _out.append(c); + } + } + } + + return this; + } + + public XmlAppendable cdata(String s) throws IOException + { + _out.append(""); + return this; + } + + public XmlAppendable tag(String tag) throws IOException + { + _out.append(_space).append('<').append(tag).append("/>\n"); + return this; + } + + public XmlAppendable tag(String tag, Map attributes) throws IOException + { + _out.append(_space).append('<').append(tag); + attributes(attributes); + _out.append("/>\n"); + return this; + } + + public XmlAppendable tag(String tag,String content) throws IOException + { + _out.append(_space).append('<').append(tag).append('>'); + content(content); + _out.append("\n"); + return this; + } + + public XmlAppendable tag(String tag, Map attributes,String content) throws IOException + { + _out.append(_space).append('<').append(tag); + attributes(attributes); + _out.append('>'); + content(content); + _out.append("\n"); + return this; + } + + public XmlAppendable close() throws IOException + { + if (_tags.isEmpty()) + throw new IllegalStateException("Tags closed"); + String tag=_tags.pop(); + _space=_space.substring(0,_space.length()-_indent); + _out.append(_space).append("\n"); + if (_tags.isEmpty() && _out instanceof Closeable) + ((Closeable)_out).close(); + return this; + } + + private void attributes(Map attributes) throws IOException + { + for (String k:attributes.keySet()) + { + String v = attributes.get(k); + _out.append(' ').append(k).append("=\""); + content(v); + _out.append('"'); + } + } + + public void literal(String xml) throws IOException + { + _out.append(xml); + } + +} diff --git a/jetty-xml/src/main/java/org/eclipse/jetty/xml/XmlParser.java b/jetty-xml/src/main/java/org/eclipse/jetty/xml/XmlParser.java index d512dee023d..31ef0afc39e 100644 --- a/jetty-xml/src/main/java/org/eclipse/jetty/xml/XmlParser.java +++ b/jetty-xml/src/main/java/org/eclipse/jetty/xml/XmlParser.java @@ -186,7 +186,7 @@ public class XmlParser public synchronized void addContentHandler(String trigger, ContentHandler observer) { if (_observerMap == null) - _observerMap = new HashMap(); + _observerMap = new HashMap<>(); _observerMap.put(trigger, observer); } @@ -251,6 +251,51 @@ public class XmlParser return doc; } + + /* ------------------------------------------------------------ */ + protected InputSource resolveEntity(String pid, String sid) + { + if (LOG.isDebugEnabled()) + LOG.debug("resolveEntity(" + pid + ", " + sid + ")"); + + if (sid!=null && sid.endsWith(".dtd")) + _dtd=sid; + + URL entity = null; + if (pid != null) + entity = (URL) _redirectMap.get(pid); + if (entity == null) + entity = (URL) _redirectMap.get(sid); + if (entity == null) + { + String dtd = sid; + if (dtd.lastIndexOf('/') >= 0) + dtd = dtd.substring(dtd.lastIndexOf('/') + 1); + + if (LOG.isDebugEnabled()) + LOG.debug("Can't exact match entity in redirect map, trying " + dtd); + entity = (URL) _redirectMap.get(dtd); + } + + if (entity != null) + { + try + { + InputStream in = entity.openStream(); + if (LOG.isDebugEnabled()) + LOG.debug("Redirected entity " + sid + " --> " + entity); + InputSource is = new InputSource(in); + is.setSystemId(sid); + return is; + } + catch (IOException e) + { + LOG.ignore(e); + } + } + return null; + } + /* ------------------------------------------------------------ */ /* ------------------------------------------------------------ */ private class NoopHandler extends DefaultHandler @@ -414,45 +459,7 @@ public class XmlParser /* ------------------------------------------------------------ */ public InputSource resolveEntity(String pid, String sid) { - if (LOG.isDebugEnabled()) - LOG.debug("resolveEntity(" + pid + ", " + sid + ")"); - - if (sid!=null && sid.endsWith(".dtd")) - _dtd=sid; - - URL entity = null; - if (pid != null) - entity = (URL) _redirectMap.get(pid); - if (entity == null) - entity = (URL) _redirectMap.get(sid); - if (entity == null) - { - String dtd = sid; - if (dtd.lastIndexOf('/') >= 0) - dtd = dtd.substring(dtd.lastIndexOf('/') + 1); - - if (LOG.isDebugEnabled()) - LOG.debug("Can't exact match entity in redirect map, trying " + dtd); - entity = (URL) _redirectMap.get(dtd); - } - - if (entity != null) - { - try - { - InputStream in = entity.openStream(); - if (LOG.isDebugEnabled()) - LOG.debug("Redirected entity " + sid + " --> " + entity); - InputSource is = new InputSource(in); - is.setSystemId(sid); - return is; - } - catch (IOException e) - { - LOG.ignore(e); - } - } - return null; + return XmlParser.this.resolveEntity(pid,sid); } } diff --git a/jetty-xml/src/test/java/org/eclipse/jetty/xml/XmlAppendableTest.java b/jetty-xml/src/test/java/org/eclipse/jetty/xml/XmlAppendableTest.java new file mode 100644 index 00000000000..dd8f3c355eb --- /dev/null +++ b/jetty-xml/src/test/java/org/eclipse/jetty/xml/XmlAppendableTest.java @@ -0,0 +1,54 @@ +// +// ======================================================================== +// Copyright (c) 1995-2014 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.xml; + +import java.util.HashMap; +import java.util.Map; + +import org.junit.Assert; +import org.junit.Test; + +public class XmlAppendableTest +{ + @Test + public void test() throws Exception + { + StringBuilder b = new StringBuilder(); + XmlAppendable out = new XmlAppendable(b); + Map attr = new HashMap<>(); + + out.open("test"); + + attr.put("name","attr value"); + attr.put("noval",null); + attr.put("quotes","'\""); + + out.tag("tag"); + out.tag("tag",attr); + out.tag("tag",attr,"content"); + + out.open("level1").tag("tag","content").tag("tag","content").close(); + out.open("level1",attr).open("level2").tag("tag","content").tag("tag","content").close().close(); + + out.close(); + + String s = b.toString(); + Assert.assertEquals("\n\n \n \n content\n \n content\n content\n \n \n \n content\n content\n \n \n\n",s); + } +} 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 aac989d82db..502e4b1c48d 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 @@ -304,11 +304,22 @@ public class AnnotationTest extends HttpServlet out.println("@Resource(name=\"minAmount\")"); out.println("private Double minAmount;"); out.println(""); - out.println("

Result: "+envResult+": "+(maxAmount.compareTo(new Double(55))==0?" PASS":" FAIL")+""); + if (maxAmount==null) + out.println("

Result: "+envResult+": FAIL"); + else + out.println("

Result: "+envResult+": "+(maxAmount.compareTo(new Double(55))==0?" PASS":" FAIL")+""); out.println("
JNDI Lookup Result: "+envLookupResult+""); - out.println("
Result: "+envResult2+": "+(minAmount.compareTo(new Double("0.99"))==0?" PASS":" FAIL")+""); + + if (minAmount==null) + out.println("

Result: "+envResult2+": FAIL"); + else + out.println("
Result: "+envResult2+": "+(minAmount.compareTo(new Double("0.99"))==0?" PASS":" FAIL")+""); out.println("
JNDI Lookup Result: "+envLookupResult2+""); - out.println("
Result: "+envResult3+": "+(avgAmount.compareTo(new Double("1.25"))==0?" PASS":" FAIL")+""); + + if (avgAmount==null) + out.println("

Result: "+envResult3+": FAIL"); + else + out.println("
Result: "+envResult3+": "+(avgAmount.compareTo(new Double("1.25"))==0?" PASS":" FAIL")+""); out.println("
JNDI Lookup Result: "+envLookupResult3+"

"); out.println("

@Resource Injection for UserTransaction

");