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")); + + } + } + + + +}