diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/handler/BufferedResponseHandler.java b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/BufferedResponseHandler.java
new file mode 100644
index 00000000000..7feceac3eb8
--- /dev/null
+++ b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/BufferedResponseHandler.java
@@ -0,0 +1,322 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2016 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server.handler;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.util.Queue;
+import java.util.Set;
+import java.util.concurrent.ConcurrentLinkedQueue;
+
+import javax.servlet.ServletContext;
+import javax.servlet.ServletException;
+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.http.pathmap.PathSpecSet;
+import org.eclipse.jetty.server.HttpChannel;
+import org.eclipse.jetty.server.HttpOutput;
+import org.eclipse.jetty.server.HttpOutput.Interceptor;
+import org.eclipse.jetty.server.Request;
+import org.eclipse.jetty.server.Response;
+import org.eclipse.jetty.util.BufferUtil;
+import org.eclipse.jetty.util.Callback;
+import org.eclipse.jetty.util.IncludeExclude;
+import org.eclipse.jetty.util.IteratingCallback;
+import org.eclipse.jetty.util.StringUtil;
+import org.eclipse.jetty.util.URIUtil;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+
+/**
+ * Buffered Response Handler
+ *
+ * A Handler that can apply a {@link org.eclipse.jetty.server.HttpOutput.Interceptor}
+ * mechanism to buffer the entire response content until the output is closed.
+ * This allows the commit to be delayed until the response is complete and thus
+ * headers and response status can be changed while writing the body.
+ *
+ * Note that the decision to buffer is influenced by the headers and status at the
+ * first write, and thus subsequent changes to those headers will not influence the
+ * decision to buffer or not.
+ *
+ * Note also that there are no memory limits to the size of the buffer, thus
+ * this handler can represent an unbounded memory commitment if the content
+ * generated can also be unbounded.
+ *
+ */
+public class BufferedResponseHandler extends HandlerWrapper
+{
+ static final Logger LOG = Log.getLogger(BufferedResponseHandler.class);
+
+ private final IncludeExclude _methods = new IncludeExclude<>();
+ private final IncludeExclude _paths = new IncludeExclude<>(PathSpecSet.class);
+ private final IncludeExclude _mimeTypes = new IncludeExclude<>();
+
+ /* ------------------------------------------------------------ */
+ public BufferedResponseHandler()
+ {
+ // include only GET requests
+
+ _methods.include(HttpMethod.GET.asString());
+ // Exclude images, aduio and video from buffering
+ for (String type:MimeTypes.getKnownMimeTypes())
+ {
+ if (type.startsWith("image/")||
+ type.startsWith("audio/")||
+ type.startsWith("video/"))
+ _mimeTypes.exclude(type);
+ }
+ LOG.debug("{} mime types {}",this,_mimeTypes);
+ }
+
+ /* ------------------------------------------------------------ */
+ public IncludeExclude getMethodIncludeExclude()
+ {
+ return _methods;
+ }
+
+ /* ------------------------------------------------------------ */
+ public IncludeExclude getPathIncludeExclude()
+ {
+ return _paths;
+ }
+
+ /* ------------------------------------------------------------ */
+ public IncludeExclude getMimeIncludeExclude()
+ {
+ return _mimeTypes;
+ }
+
+ /* ------------------------------------------------------------ */
+ /**
+ * @see org.eclipse.jetty.server.handler.HandlerWrapper#handle(java.lang.String, org.eclipse.jetty.server.Request, javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse)
+ */
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
+ {
+ ServletContext context = baseRequest.getServletContext();
+ String path = context==null?baseRequest.getRequestURI():URIUtil.addPaths(baseRequest.getServletPath(),baseRequest.getPathInfo());
+ LOG.debug("{} handle {} in {}",this,baseRequest,context);
+
+ HttpOutput out = baseRequest.getResponse().getHttpOutput();
+
+ // Are we already being gzipped?
+ HttpOutput.Interceptor interceptor = out.getInterceptor();
+ while (interceptor!=null)
+ {
+ if (interceptor instanceof BufferedInterceptor)
+ {
+ LOG.debug("{} already intercepting {}",this,request);
+ _handler.handle(target,baseRequest, request, response);
+ return;
+ }
+ interceptor=interceptor.getNextInterceptor();
+ }
+
+ // If not a supported method - no Vary because no matter what client, this URI is always excluded
+ if (!_methods.matches(baseRequest.getMethod()))
+ {
+ LOG.debug("{} excluded by method {}",this,request);
+ _handler.handle(target,baseRequest, request, response);
+ return;
+ }
+
+ // If not a supported URI- no Vary because no matter what client, this URI is always excluded
+ // Use pathInfo because this is be
+ if (!isPathBufferable(path))
+ {
+ LOG.debug("{} excluded by path {}",this,request);
+ _handler.handle(target,baseRequest, request, response);
+ return;
+ }
+
+ // If the mime type is known from the path, then apply mime type filtering
+ String mimeType = context==null?null:context.getMimeType(path);
+ if (mimeType!=null)
+ {
+ mimeType = MimeTypes.getContentTypeWithoutCharset(mimeType);
+ if (!isMimeTypeBufferable(mimeType))
+ {
+ LOG.debug("{} excluded by path suffix mime type {}",this,request);
+ // handle normally without setting vary header
+ _handler.handle(target,baseRequest, request, response);
+ return;
+ }
+ }
+
+ // install interceptor and handle
+ out.setInterceptor(new BufferedInterceptor(baseRequest.getHttpChannel(),out.getInterceptor()));
+
+ if (_handler!=null)
+ _handler.handle(target,baseRequest, request, response);
+ }
+
+ /* ------------------------------------------------------------ */
+ protected boolean isMimeTypeBufferable(String mimetype)
+ {
+ return _mimeTypes.matches(mimetype);
+ }
+
+ /* ------------------------------------------------------------ */
+ protected boolean isPathBufferable(String requestURI)
+ {
+ if (requestURI == null)
+ return true;
+
+ return _paths.matches(requestURI);
+ }
+
+ /* ------------------------------------------------------------ */
+ /* ------------------------------------------------------------ */
+ /* ------------------------------------------------------------ */
+ private class BufferedInterceptor implements HttpOutput.Interceptor
+ {
+ final Interceptor _next;
+ final HttpChannel _channel;
+ final Queue _buffers=new ConcurrentLinkedQueue<>();
+ Boolean _aggregating;
+ ByteBuffer _aggregate;
+
+ public BufferedInterceptor(HttpChannel httpChannel, Interceptor interceptor)
+ {
+ _next=interceptor;
+ _channel=httpChannel;
+ }
+
+ @Override
+ public void write(ByteBuffer content, boolean last, Callback callback)
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("{} write last={} {}",this,last,BufferUtil.toDetailString(content));
+ // if we are not committed, have to decide if we should aggregate or not
+ if (_aggregating==null)
+ {
+ Response response = _channel.getResponse();
+ int sc = response.getStatus();
+ if (sc>0 && (sc<200 || sc==204 || sc==205 || sc>=300))
+ _aggregating=Boolean.FALSE; // No body
+ else
+ {
+ String ct = response.getContentType();
+ if (ct==null)
+ _aggregating=Boolean.TRUE;
+ else
+ {
+ ct=MimeTypes.getContentTypeWithoutCharset(ct);
+ _aggregating=isMimeTypeBufferable(StringUtil.asciiToLowerCase(ct));
+ }
+ }
+ }
+
+ // If we are not aggregating, then handle normally
+ if (!_aggregating.booleanValue())
+ {
+ getNextInterceptor().write(content,last,callback);
+ return;
+ }
+
+ // If last
+ if (last)
+ {
+ // Add the current content to the buffer list without a copy
+ if (BufferUtil.length(content)>0)
+ _buffers.add(content);
+
+ if (LOG.isDebugEnabled())
+ LOG.debug("{} committing {}",this,_buffers.size());
+ commit(_buffers,callback);
+ }
+ else
+ {
+ if (LOG.isDebugEnabled())
+ LOG.debug("{} aggregating",this);
+
+ // Aggregate the content into buffer chain
+ while (BufferUtil.hasContent(content))
+ {
+ // Do we need a new aggregate buffer
+ if (BufferUtil.space(_aggregate)==0)
+ {
+ int size = Math.max(_channel.getHttpConfiguration().getOutputBufferSize(),BufferUtil.length(content));
+ _aggregate=BufferUtil.allocate(size); // TODO use a buffer pool
+ _buffers.add(_aggregate);
+ }
+
+ BufferUtil.append(_aggregate,content);
+ }
+ callback.succeeded();
+ }
+ }
+
+ @Override
+ public Interceptor getNextInterceptor()
+ {
+ return _next;
+ }
+
+ @Override
+ public boolean isOptimizedForDirectBuffers()
+ {
+ return false;
+ }
+
+ protected void commit(Queue buffers, Callback callback)
+ {
+ // If only 1 buffer
+ if (_buffers.size()==1)
+ // just flush it with the last callback
+ getNextInterceptor().write(_buffers.remove(),true,callback);
+ else
+ {
+ // Create an iterating callback to do the writing
+ IteratingCallback icb = new IteratingCallback()
+ {
+ @Override
+ protected Action process() throws Exception
+ {
+ ByteBuffer buffer = _buffers.poll();
+ if (buffer==null)
+ return Action.SUCCEEDED;
+
+ getNextInterceptor().write(buffer,_buffers.isEmpty(),this);
+ return Action.SCHEDULED;
+ }
+
+ @Override
+ protected void onCompleteSuccess()
+ {
+ // Signal last callback
+ callback.succeeded();
+ }
+
+ @Override
+ protected void onCompleteFailure(Throwable cause)
+ {
+ // Signal last callback
+ callback.failed(cause);
+ }
+ };
+ icb.iterate();
+ }
+ }
+ }
+}
diff --git a/jetty-server/src/test/java/org/eclipse/jetty/server/handler/BufferedResponseHandlerTest.java b/jetty-server/src/test/java/org/eclipse/jetty/server/handler/BufferedResponseHandlerTest.java
new file mode 100644
index 00000000000..a0c485c2362
--- /dev/null
+++ b/jetty-server/src/test/java/org/eclipse/jetty/server/handler/BufferedResponseHandlerTest.java
@@ -0,0 +1,239 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2016 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server.handler;
+
+import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.Matchers.not;
+import static org.junit.Assert.assertThat;
+
+import java.io.IOException;
+import java.util.Arrays;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.eclipse.jetty.server.HttpConfiguration;
+import org.eclipse.jetty.server.HttpConnectionFactory;
+import org.eclipse.jetty.server.LocalConnector;
+import org.eclipse.jetty.server.Request;
+import org.eclipse.jetty.server.Server;
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+/**
+ * Resource Handler test
+ *
+ * TODO: increase the testing going on here
+ */
+public class BufferedResponseHandlerTest
+{
+ private static Server _server;
+ private static HttpConfiguration _config;
+ private static LocalConnector _local;
+ private static ContextHandler _contextHandler;
+ private static BufferedResponseHandler _bufferedHandler;
+ private static TestHandler _test;
+
+ @BeforeClass
+ public static void setUp() throws Exception
+ {
+ _server = new Server();
+ _config = new HttpConfiguration();
+ _config.setOutputBufferSize(1024);
+ _config.setOutputAggregationSize(256);
+ _local = new LocalConnector(_server,new HttpConnectionFactory(_config));
+ _server.addConnector(_local);
+
+ _bufferedHandler = new BufferedResponseHandler();
+ _bufferedHandler.getPathIncludeExclude().include("/include/*");
+ _bufferedHandler.getPathIncludeExclude().exclude("*.exclude");
+ _bufferedHandler.getMimeIncludeExclude().exclude("text/excluded");
+ _bufferedHandler.setHandler(_test=new TestHandler());
+
+ _contextHandler = new ContextHandler("/ctx");
+ _contextHandler.setHandler(_bufferedHandler);
+
+ _server.setHandler(_contextHandler);
+ _server.start();
+
+ // BufferedResponseHandler.LOG.setDebugEnabled(true);
+ }
+
+ @AfterClass
+ public static void tearDown() throws Exception
+ {
+ _server.stop();
+ }
+
+ @Before
+ public void before()
+ {
+ _test._bufferSize=-1;
+ _test._mimeType=null;
+ _test._content=new byte[128];
+ Arrays.fill(_test._content,(byte)'X');
+ _test._content[_test._content.length-1]='\n';
+ _test._writes=10;
+ _test._flush=false;
+ _test._close=false;
+ }
+
+ @Test
+ public void testNormal() throws Exception
+ {
+ String response = _local.getResponse("GET /ctx/path HTTP/1.1\r\nHost: localhost\r\n\r\n");
+ assertThat(response,containsString(" 200 OK"));
+ assertThat(response,containsString("Write: 0"));
+ assertThat(response,containsString("Write: 7"));
+ assertThat(response,not(containsString("Content-Length: ")));
+ assertThat(response,not(containsString("Write: 8")));
+ assertThat(response,not(containsString("Write: 9")));
+ assertThat(response,not(containsString("Written: true")));
+ }
+
+ @Test
+ public void testIncluded() throws Exception
+ {
+ String response = _local.getResponse("GET /ctx/include/path HTTP/1.1\r\nHost: localhost\r\n\r\n");
+ assertThat(response,containsString(" 200 OK"));
+ assertThat(response,containsString("Write: 0"));
+ assertThat(response,containsString("Write: 9"));
+ assertThat(response,containsString("Written: true"));
+ }
+
+ @Test
+ public void testExcludedByPath() throws Exception
+ {
+ String response = _local.getResponse("GET /ctx/include/path.exclude HTTP/1.1\r\nHost: localhost\r\n\r\n");
+ assertThat(response,containsString(" 200 OK"));
+ assertThat(response,containsString("Write: 0"));
+ assertThat(response,containsString("Write: 7"));
+ assertThat(response,not(containsString("Content-Length: ")));
+ assertThat(response,not(containsString("Write: 8")));
+ assertThat(response,not(containsString("Write: 9")));
+ assertThat(response,not(containsString("Written: true")));
+ }
+
+ @Test
+ public void testExcludedByMime() throws Exception
+ {
+ _test._mimeType="text/excluded";
+ String response = _local.getResponse("GET /ctx/include/path HTTP/1.1\r\nHost: localhost\r\n\r\n");
+ assertThat(response,containsString(" 200 OK"));
+ assertThat(response,containsString("Write: 0"));
+ assertThat(response,containsString("Write: 7"));
+ assertThat(response,not(containsString("Content-Length: ")));
+ assertThat(response,not(containsString("Write: 8")));
+ assertThat(response,not(containsString("Write: 9")));
+ assertThat(response,not(containsString("Written: true")));
+ }
+
+ @Test
+ public void testFlushed() throws Exception
+ {
+ _test._flush=true;
+ String response = _local.getResponse("GET /ctx/include/path HTTP/1.1\r\nHost: localhost\r\n\r\n");
+ assertThat(response,containsString(" 200 OK"));
+ assertThat(response,containsString("Write: 0"));
+ assertThat(response,containsString("Write: 9"));
+ assertThat(response,containsString("Written: true"));
+ }
+
+ @Test
+ public void testClosed() throws Exception
+ {
+ _test._close=true;
+ String response = _local.getResponse("GET /ctx/include/path HTTP/1.1\r\nHost: localhost\r\n\r\n");
+ assertThat(response,containsString(" 200 OK"));
+ assertThat(response,containsString("Write: 0"));
+ assertThat(response,containsString("Write: 9"));
+ assertThat(response,not(containsString("Written: true")));
+ }
+
+ @Test
+ public void testBufferSizeSmall() throws Exception
+ {
+ _test._bufferSize=16;
+ String response = _local.getResponse("GET /ctx/include/path HTTP/1.1\r\nHost: localhost\r\n\r\n");
+ assertThat(response,containsString(" 200 OK"));
+ assertThat(response,containsString("Write: 0"));
+ assertThat(response,containsString("Write: 9"));
+ assertThat(response,containsString("Written: true"));
+ }
+
+ @Test
+ public void testBufferSizeBig() throws Exception
+ {
+ _test._bufferSize=4096;
+ String response = _local.getResponse("GET /ctx/include/path HTTP/1.1\r\nHost: localhost\r\n\r\n");
+ assertThat(response,containsString(" 200 OK"));
+ assertThat(response,containsString("Content-Length: "));
+ assertThat(response,containsString("Write: 0"));
+ assertThat(response,containsString("Write: 9"));
+ assertThat(response,containsString("Written: true"));
+ }
+
+ @Test
+ public void testOne() throws Exception
+ {
+ _test._writes=1;
+ String response = _local.getResponse("GET /ctx/include/path HTTP/1.1\r\nHost: localhost\r\n\r\n");
+ assertThat(response,containsString(" 200 OK"));
+ assertThat(response,containsString("Content-Length: "));
+ assertThat(response,containsString("Write: 0"));
+ assertThat(response,not(containsString("Write: 1")));
+ assertThat(response,containsString("Written: true"));
+ }
+
+ public static class TestHandler extends AbstractHandler
+ {
+ int _bufferSize;
+ String _mimeType;
+ byte[] _content;
+ int _writes;
+ boolean _flush;
+ boolean _close;
+
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
+ {
+ baseRequest.setHandled(true);
+ if (_bufferSize>0)
+ response.setBufferSize(_bufferSize);
+ if (_mimeType!=null)
+ response.setContentType(_mimeType);
+
+ for (int i=0;i<_writes;i++)
+ {
+ response.addHeader("Write",Integer.toString(i));
+ response.getOutputStream().write(_content);
+ if (_flush)
+ response.getOutputStream().flush();
+ }
+
+ if (_close)
+ response.getOutputStream().close();
+ response.addHeader("Written","true");
+ }
+
+ }
+}
diff --git a/jetty-websocket/websocket-server/src/main/java/org/eclipse/jetty/websocket/server/WebSocketUpgradeFilter.java b/jetty-websocket/websocket-server/src/main/java/org/eclipse/jetty/websocket/server/WebSocketUpgradeFilter.java
index a189155d486..b9f4d702d6b 100644
--- a/jetty-websocket/websocket-server/src/main/java/org/eclipse/jetty/websocket/server/WebSocketUpgradeFilter.java
+++ b/jetty-websocket/websocket-server/src/main/java/org/eclipse/jetty/websocket/server/WebSocketUpgradeFilter.java
@@ -36,6 +36,8 @@ import javax.servlet.http.HttpServletResponse;
import org.eclipse.jetty.http.pathmap.MappedResource;
import org.eclipse.jetty.http.pathmap.PathMappings;
import org.eclipse.jetty.http.pathmap.PathSpec;
+import org.eclipse.jetty.http.pathmap.RegexPathSpec;
+import org.eclipse.jetty.http.pathmap.ServletPathSpec;
import org.eclipse.jetty.io.ByteBufferPool;
import org.eclipse.jetty.io.MappedByteBufferPool;
import org.eclipse.jetty.servlet.FilterHolder;
@@ -142,6 +144,26 @@ public class WebSocketUpgradeFilter extends ContainerLifeCycle implements Filter
{
pathmap.put(spec,creator);
}
+
+ /**
+ * @deprecated use new {@link #addMapping(org.eclipse.jetty.http.pathmap.PathSpec, WebSocketCreator)} instead
+ */
+ @Deprecated
+ public void addMapping(org.eclipse.jetty.websocket.server.pathmap.PathSpec spec, WebSocketCreator creator)
+ {
+ if (spec instanceof org.eclipse.jetty.websocket.server.pathmap.ServletPathSpec)
+ {
+ addMapping(new ServletPathSpec(spec.getSpec()), creator);
+ }
+ else if (spec instanceof org.eclipse.jetty.websocket.server.pathmap.RegexPathSpec)
+ {
+ addMapping(new RegexPathSpec(spec.getSpec()), creator);
+ }
+ else
+ {
+ throw new RuntimeException("Unsupported (Deprecated) PathSpec implementation: " + spec.getClass().getName());
+ }
+ }
@Override
public void destroy()
diff --git a/jetty-websocket/websocket-server/src/main/java/org/eclipse/jetty/websocket/server/pathmap/PathSpec.java b/jetty-websocket/websocket-server/src/main/java/org/eclipse/jetty/websocket/server/pathmap/PathSpec.java
new file mode 100644
index 00000000000..e0a3d672e57
--- /dev/null
+++ b/jetty-websocket/websocket-server/src/main/java/org/eclipse/jetty/websocket/server/pathmap/PathSpec.java
@@ -0,0 +1,38 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2016 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.websocket.server.pathmap;
+
+/**
+ * @deprecated moved to jetty-http {@link org.eclipse.jetty.http.pathmap.PathSpec} (this facade will be removed in Jetty 9.4)
+ */
+@Deprecated
+public abstract class PathSpec
+{
+ private final String spec;
+
+ protected PathSpec(String spec)
+ {
+ this.spec = spec;
+ }
+
+ public String getSpec()
+ {
+ return spec;
+ }
+}
diff --git a/jetty-websocket/websocket-server/src/main/java/org/eclipse/jetty/websocket/server/pathmap/RegexPathSpec.java b/jetty-websocket/websocket-server/src/main/java/org/eclipse/jetty/websocket/server/pathmap/RegexPathSpec.java
index 3111a6c23a2..e6e004acb49 100644
--- a/jetty-websocket/websocket-server/src/main/java/org/eclipse/jetty/websocket/server/pathmap/RegexPathSpec.java
+++ b/jetty-websocket/websocket-server/src/main/java/org/eclipse/jetty/websocket/server/pathmap/RegexPathSpec.java
@@ -22,7 +22,7 @@ package org.eclipse.jetty.websocket.server.pathmap;
* @deprecated moved to jetty-http {@link org.eclipse.jetty.http.pathmap.RegexPathSpec} (this facade will be removed in Jetty 9.4)
*/
@Deprecated
-public class RegexPathSpec extends org.eclipse.jetty.http.pathmap.RegexPathSpec
+public class RegexPathSpec extends PathSpec
{
public RegexPathSpec(String regex)
{
diff --git a/jetty-websocket/websocket-server/src/main/java/org/eclipse/jetty/websocket/server/pathmap/ServletPathSpec.java b/jetty-websocket/websocket-server/src/main/java/org/eclipse/jetty/websocket/server/pathmap/ServletPathSpec.java
index da52817bfb8..852aae68de2 100644
--- a/jetty-websocket/websocket-server/src/main/java/org/eclipse/jetty/websocket/server/pathmap/ServletPathSpec.java
+++ b/jetty-websocket/websocket-server/src/main/java/org/eclipse/jetty/websocket/server/pathmap/ServletPathSpec.java
@@ -22,7 +22,7 @@ package org.eclipse.jetty.websocket.server.pathmap;
* @deprecated moved to jetty-http {@link org.eclipse.jetty.http.pathmap.ServletPathSpec} (this facade will be removed in Jetty 9.4)
*/
@Deprecated
-public class ServletPathSpec extends org.eclipse.jetty.http.pathmap.ServletPathSpec
+public class ServletPathSpec extends PathSpec
{
public ServletPathSpec(String servletPathSpec)
{
diff --git a/tests/test-sessions/test-hash-sessions/src/test/java/org/eclipse/jetty/server/session/RedirectSessionTest.java b/tests/test-sessions/test-hash-sessions/src/test/java/org/eclipse/jetty/server/session/RedirectSessionTest.java
new file mode 100644
index 00000000000..c35222b67c5
--- /dev/null
+++ b/tests/test-sessions/test-hash-sessions/src/test/java/org/eclipse/jetty/server/session/RedirectSessionTest.java
@@ -0,0 +1,114 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2016 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.server.session;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+
+import java.io.IOException;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import javax.servlet.http.HttpSession;
+
+import org.eclipse.jetty.client.HttpClient;
+import org.eclipse.jetty.client.api.ContentResponse;
+import org.eclipse.jetty.servlet.ServletContextHandler;
+import org.junit.Test;
+
+
+/**
+ * RedirectSessionTest
+ *
+ * Test that creating a session and then doing a redirect preserves the session.
+ */
+public class RedirectSessionTest
+{
+
+
+
+ @Test
+ public void testSessionRedirect() throws Exception
+ {
+ AbstractTestServer testServer = new HashTestServer(0, -1, 60, SessionCache.NEVER_EVICT);
+ ServletContextHandler testServletContextHandler = testServer.addContext("/context");
+ testServletContextHandler.addServlet(Servlet1.class, "/one");
+ testServletContextHandler.addServlet(Servlet2.class, "/two");
+
+
+
+
+ try
+ {
+ testServer.start();
+ int serverPort=testServer.getPort();
+ HttpClient client = new HttpClient();
+ client.setFollowRedirects(true); //ensure client handles redirects
+ client.start();
+ try
+ {
+ //make a request to the first servlet, which will redirect
+ ContentResponse response = client.GET("http://localhost:" + serverPort + "/context/one");
+ assertEquals(HttpServletResponse.SC_OK, response.getStatus());
+ }
+ finally
+ {
+ client.stop();
+ }
+ }
+ finally
+ {
+ testServer.stop();
+ }
+
+ }
+
+
+ public static class Servlet1 extends HttpServlet
+ {
+ @Override
+ protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException
+ {
+ //create a session
+ HttpSession session = request.getSession(true);
+ assertNotNull(session);
+ session.setAttribute("servlet1", "servlet1");
+ response.sendRedirect("/context/two");
+ }
+ }
+
+ public static class Servlet2 extends HttpServlet
+ {
+ @Override
+ protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException
+ {
+ //the session should exist after the redirect
+ HttpSession sess = request.getSession(false);
+ assertNotNull(sess);
+ assertNotNull(sess.getAttribute("servlet1"));
+ assertEquals("servlet1", sess.getAttribute("servlet1"));
+
+ }
+ }
+
+
+
+}