Skip to main content
aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorThomas Becker2013-06-13 14:58:43 +0000
committerThomas Becker2013-06-13 14:58:57 +0000
commitdf17ef8b3a01c208ff94cb1333b4ad573b77c307 (patch)
tree6f4cac60aa527b81d42eaae2c57162d2ec9fab93
parentb4913ef38ce6d73e42fd81302b61f49896ff4ae4 (diff)
downloadorg.eclipse.jetty.project-df17ef8b3a01c208ff94cb1333b4ad573b77c307.tar.gz
org.eclipse.jetty.project-df17ef8b3a01c208ff94cb1333b4ad573b77c307.tar.xz
org.eclipse.jetty.project-df17ef8b3a01c208ff94cb1333b4ad573b77c307.zip
408709 refactor test-webapp's chat application. Now there's only a single request for user login and initial chat message.
-rw-r--r--tests/test-webapps/test-jetty-webapp/src/main/java/com/acme/ChatServlet.java213
-rw-r--r--tests/test-webapps/test-jetty-webapp/src/main/resources/jetty-logging.properties2
-rw-r--r--tests/test-webapps/test-jetty-webapp/src/main/webapp/chat/index.html41
-rw-r--r--tests/test-webapps/test-jetty-webapp/src/test/java/org/eclipse/jetty/ChatServletTest.java92
4 files changed, 220 insertions, 128 deletions
diff --git a/tests/test-webapps/test-jetty-webapp/src/main/java/com/acme/ChatServlet.java b/tests/test-webapps/test-jetty-webapp/src/main/java/com/acme/ChatServlet.java
index 42040f56fd..dabfacd81c 100644
--- a/tests/test-webapps/test-jetty-webapp/src/main/java/com/acme/ChatServlet.java
+++ b/tests/test-webapps/test-jetty-webapp/src/main/java/com/acme/ChatServlet.java
@@ -19,13 +19,11 @@
package com.acme;
import java.io.IOException;
-import java.io.PrintWriter;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.Map;
import java.util.Queue;
import java.util.concurrent.atomic.AtomicReference;
-
import javax.servlet.AsyncContext;
import javax.servlet.AsyncEvent;
import javax.servlet.AsyncListener;
@@ -34,57 +32,70 @@ import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+
// Simple asynchronous Chat room.
// This does not handle duplicate usernames or multiple frames/tabs from the same browser
// Some code is duplicated for clarity.
@SuppressWarnings("serial")
public class ChatServlet extends HttpServlet
{
+ private static final Logger LOG = Log.getLogger(ChatServlet.class);
+
+ private long asyncTimeout = 10000;
+
+ public void init()
+ {
+ String parameter = getServletConfig().getInitParameter("asyncTimeout");
+ if (parameter != null)
+ asyncTimeout = Long.parseLong(parameter);
+ }
// inner class to hold message queue for each chat room member
class Member implements AsyncListener
{
final String _name;
- final AtomicReference<AsyncContext> _async=new AtomicReference<>();
- final Queue<String> _queue = new LinkedList<String>();
-
+ final AtomicReference<AsyncContext> _async = new AtomicReference<>();
+ final Queue<String> _queue = new LinkedList<>();
+
Member(String name)
{
- _name=name;
+ _name = name;
}
-
+
@Override
public void onTimeout(AsyncEvent event) throws IOException
{
+ LOG.debug("resume request");
AsyncContext async = _async.get();
- if (async!=null && _async.compareAndSet(async,null))
+ if (async != null && _async.compareAndSet(async, null))
{
HttpServletResponse response = (HttpServletResponse)async.getResponse();
response.setContentType("text/json;charset=utf-8");
- PrintWriter out=response.getWriter();
- out.print("{action:\"poll\"}");
+ response.getOutputStream().write("{action:\"poll\"}".getBytes());
async.complete();
}
}
-
+
@Override
public void onStartAsync(AsyncEvent event) throws IOException
{
event.getAsyncContext().addListener(this);
}
-
+
@Override
public void onError(AsyncEvent event) throws IOException
{
}
-
+
@Override
public void onComplete(AsyncEvent event) throws IOException
{
}
}
- Map<String,Map<String,Member>> _rooms = new HashMap<String,Map<String, Member>>();
+ Map<String, Map<String, Member>> _rooms = new HashMap<>();
// Handle Ajax calls from browser
@@ -92,113 +103,119 @@ public class ChatServlet extends HttpServlet
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException
{
// Ajax calls are form encoded
- String action = request.getParameter("action");
+ boolean join = Boolean.parseBoolean(request.getParameter("join"));
String message = request.getParameter("message");
String username = request.getParameter("user");
- if (action.equals("join"))
- join(request,response,username);
- else if (action.equals("poll"))
- poll(request,response,username);
- else if (action.equals("chat"))
- chat(request,response,username,message);
- }
+ LOG.debug("doPost called. join={},message={},username={}", join, message, username);
+ if (username == null)
+ {
+ LOG.debug("no paramter user set, sending 503");
+ response.sendError(503, "user==null");
+ return;
+ }
- private synchronized void join(HttpServletRequest request,HttpServletResponse response,String username)
- throws IOException
- {
- Member member = new Member(username);
- Map<String,Member> room=_rooms.get(request.getPathInfo());
- if (room==null)
+ Map<String, Member> room = getRoom(request.getPathInfo());
+ Member member = getMember(username, room);
+
+ if (message != null)
{
- room=new HashMap<String,Member>();
- _rooms.put(request.getPathInfo(),room);
+ sendMessageToAllMembers(message, username, room);
+ }
+ // If a message is set, we only want to enter poll mode if the user is a new user. This is necessary to avoid
+ // two parallel requests per user (one is already in async wait and the new one). Sending a message will
+ // dispatch to an existing poll request if necessary and the client will issue a new request to receive the
+ // next message or long poll again.
+ if (message == null || join)
+ {
+ synchronized (member)
+ {
+ LOG.debug("Queue size: {}", member._queue.size());
+ if (member._queue.size() > 0)
+ {
+ sendSingleMessage(response, member);
+ }
+ else
+ {
+ LOG.debug("starting async");
+ AsyncContext async = request.startAsync();
+ async.setTimeout(asyncTimeout);
+ async.addListener(member);
+ if (!member._async.compareAndSet(null, async))
+ throw new IllegalStateException();
+ }
+ }
}
- room.put(username,member);
- response.setContentType("text/json;charset=utf-8");
- PrintWriter out=response.getWriter();
- out.print("{action:\"join\"}");
}
- private synchronized void poll(HttpServletRequest request,HttpServletResponse response,String username)
- throws IOException
+ private Member getMember(String username, Map<String, Member> room)
{
- Map<String,Member> room=_rooms.get(request.getPathInfo());
- if (room==null)
+ Member member = room.get(username);
+ if (member == null)
{
- response.sendError(503);
- return;
+ LOG.debug("user: {} in room: {} doesn't exist. Creating new user.", username, room);
+ member = new Member(username);
+ room.put(username, member);
}
- final Member member = room.get(username);
- if (member==null)
+ return member;
+ }
+
+ private Map<String, Member> getRoom(String path)
+ {
+ Map<String, Member> room = _rooms.get(path);
+ if (room == null)
{
- response.sendError(503);
- return;
+ LOG.debug("room: {} doesn't exist. Creating new room.", path);
+ room = new HashMap<>();
+ _rooms.put(path, room);
}
+ return room;
+ }
- synchronized(member)
- {
- if (member._queue.size()>0)
- {
- // Send one chat message
- response.setContentType("text/json;charset=utf-8");
- StringBuilder buf=new StringBuilder();
+ private void sendSingleMessage(HttpServletResponse response, Member member) throws IOException
+ {
+ response.setContentType("text/json;charset=utf-8");
+ StringBuilder buf = new StringBuilder();
- buf.append("{\"action\":\"poll\",");
- buf.append("\"from\":\"");
- buf.append(member._queue.poll());
- buf.append("\",");
+ buf.append("{\"from\":\"");
+ buf.append(member._queue.poll());
+ buf.append("\",");
- String message = member._queue.poll();
- int quote=message.indexOf('"');
- while (quote>=0)
- {
- message=message.substring(0,quote)+'\\'+message.substring(quote);
- quote=message.indexOf('"',quote+2);
- }
- buf.append("\"chat\":\"");
- buf.append(message);
- buf.append("\"}");
- byte[] bytes = buf.toString().getBytes("utf-8");
- response.setContentLength(bytes.length);
- response.getOutputStream().write(bytes);
- }
- else
- {
- AsyncContext async = request.startAsync();
- async.setTimeout(10000);
- async.addListener(member);
- if (!member._async.compareAndSet(null,async))
- throw new IllegalStateException();
- }
+ String returnMessage = member._queue.poll();
+ int quote = returnMessage.indexOf('"');
+ while (quote >= 0)
+ {
+ returnMessage = returnMessage.substring(0, quote) + '\\' + returnMessage.substring(quote);
+ quote = returnMessage.indexOf('"', quote + 2);
}
+ buf.append("\"chat\":\"");
+ buf.append(returnMessage);
+ buf.append("\"}");
+ byte[] bytes = buf.toString().getBytes("utf-8");
+ response.setContentLength(bytes.length);
+ response.getOutputStream().write(bytes);
}
- private synchronized void chat(HttpServletRequest request,HttpServletResponse response,String username,String message)
- throws IOException
+ private void sendMessageToAllMembers(String message, String username, Map<String, Member> room)
{
- Map<String,Member> room=_rooms.get(request.getPathInfo());
- if (room!=null)
+ LOG.debug("Sending message: {} from: {}", message, username);
+ for (Member m : room.values())
{
- // Post chat to all members
- for (Member m:room.values())
+ synchronized (m)
{
- synchronized (m)
- {
- m._queue.add(username); // from
- m._queue.add(message); // chat
+ m._queue.add(username); // from
+ m._queue.add(message); // chat
- // wakeup member if polling
- AsyncContext async=m._async.get();
- if (async!=null & m._async.compareAndSet(async,null))
- async.dispatch();
+ // wakeup member if polling
+ AsyncContext async = m._async.get();
+ LOG.debug("Async found: {}", async);
+ if (async != null & m._async.compareAndSet(async, null))
+ {
+ LOG.debug("dispatch");
+ async.dispatch();
}
}
}
-
- response.setContentType("text/json;charset=utf-8");
- PrintWriter out=response.getWriter();
- out.print("{action:\"chat\"}");
}
// Serve the HTML with embedded CSS and Javascript.
@@ -206,10 +223,10 @@ public class ChatServlet extends HttpServlet
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException
{
- if (request.getParameter("action")!=null)
- doPost(request,response);
+ if (request.getParameter("action") != null)
+ doPost(request, response);
else
- getServletContext().getNamedDispatcher("default").forward(request,response);
+ getServletContext().getNamedDispatcher("default").forward(request, response);
}
}
diff --git a/tests/test-webapps/test-jetty-webapp/src/main/resources/jetty-logging.properties b/tests/test-webapps/test-jetty-webapp/src/main/resources/jetty-logging.properties
new file mode 100644
index 0000000000..7d9b55efd4
--- /dev/null
+++ b/tests/test-webapps/test-jetty-webapp/src/main/resources/jetty-logging.properties
@@ -0,0 +1,2 @@
+org.eclipse.jetty.util.log.class=org.eclipse.jetty.util.log.StdErrLog
+com.acme.LEVEL=DEBUG
diff --git a/tests/test-webapps/test-jetty-webapp/src/main/webapp/chat/index.html b/tests/test-webapps/test-jetty-webapp/src/main/webapp/chat/index.html
index cc8f833154..53e4795b1c 100644
--- a/tests/test-webapps/test-jetty-webapp/src/main/webapp/chat/index.html
+++ b/tests/test-webapps/test-jetty-webapp/src/main/webapp/chat/index.html
@@ -30,38 +30,28 @@
req.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
req.send(body);
}
- ;
- function send(action, user, message, handler)
+ function send(user, message, handler, join)
{
if (message) message = message.replace('%', '%25').replace('&', '%26').replace('=', '%3D');
if (user) user = user.replace('%', '%25').replace('&', '%26').replace('=', '%3D');
- xhr('POST', 'chat', 'action=' + action + '&user=' + user + '&message=' + message, handler);
+ var requestBody = 'user=' + user + (message ? '&message=' + message : '') + (join ? '&join=true' : '');
+ xhr('POST', 'chat', requestBody , handler);
}
- ;
-
var room = {
- join:function (name)
+ join: function (name)
{
this._username = name;
$('join').className = 'hidden';
$('joined').className = '';
$('phrase').focus();
- send('join', room._username, null, room._joined);
- },
- _joined:function ()
- {
- send('chat', room._username, 'has joined!', room._startPolling);
- },
- _startPolling:function ()
- {
- send('poll', room._username, null, room._poll);
+ send(room._username, 'has joined!', room._poll, true);
},
- chat:function (text)
+ chat: function (text)
{
if (text != null && text.length > 0)
- send('chat', room._username, text);
+ send(room._username, text, room._poll, false);
},
- _poll:function (m)
+ _poll: function (m)
{
//console.debug(m);
if (m.chat)
@@ -79,10 +69,9 @@
chat.appendChild(lineBreak);
chat.scrollTop = chat.scrollHeight - chat.clientHeight;
}
- if (m.action == 'poll')
- send('poll', room._username, null, room._poll);
+ send(room._username, null, room._poll, false);
},
- _end:''
+ _end: ''
};
</script>
<style type='text/css'>
@@ -106,7 +95,7 @@
padding: 4px;
background-color: #e0e0e0;
border: 1px solid black;
- border-top: 0px
+ border-top: 0
}
input#phrase {
@@ -122,14 +111,6 @@
div.hidden {
display: none;
}
-
- span.from {
- font-weight: bold;
- }
-
- span.alert {
- font-style: italic;
- }
</style>
</head>
<body>
diff --git a/tests/test-webapps/test-jetty-webapp/src/test/java/org/eclipse/jetty/ChatServletTest.java b/tests/test-webapps/test-jetty-webapp/src/test/java/org/eclipse/jetty/ChatServletTest.java
new file mode 100644
index 0000000000..abc991e793
--- /dev/null
+++ b/tests/test-webapps/test-jetty-webapp/src/test/java/org/eclipse/jetty/ChatServletTest.java
@@ -0,0 +1,92 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty;
+
+import com.acme.ChatServlet;
+import org.eclipse.jetty.servlet.ServletHolder;
+import org.eclipse.jetty.servlet.ServletTester;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import static org.hamcrest.Matchers.is;
+import static org.junit.Assert.assertThat;
+
+@RunWith(JUnit4.class)
+public class ChatServletTest
+{
+
+ private final ServletTester tester = new ServletTester();
+
+ @Before
+ public void setUp() throws Exception
+ {
+ tester.setContextPath("/");
+
+ ServletHolder dispatch = tester.addServlet(ChatServlet.class, "/chat/*");
+ dispatch.setInitParameter("asyncTimeout","500");
+ tester.start();
+ }
+
+ @After
+ public void tearDown() throws Exception
+ {
+ tester.stop();
+ }
+
+ @Test
+ public void testLogin() throws Exception
+ {
+ assertResponse("user=test&join=true&message=has%20joined!", "{\"from\":\"test\",\"chat\":\"has joined!\"}");
+ }
+
+ @Test
+ public void testChat() throws Exception
+ {
+ assertResponse("user=test&message=has%20joined!", "{\"from\":\"test\",\"chat\":\"has joined!\"}");
+ assertResponse("user=test&message=message", "");
+ }
+
+ @Test
+ public void testPoll() throws Exception
+ {
+ assertResponse("user=test", "{action:\"poll\"}");
+ }
+
+ private void assertResponse(String requestBody, String expectedResponse) throws Exception
+ {
+ String response = tester.getResponses(createRequestString(requestBody));
+ assertThat(response.contains(expectedResponse), is(true));
+ }
+
+ private String createRequestString(String body)
+ {
+ StringBuilder req1 = new StringBuilder();
+ req1.append("POST /chat/ HTTP/1.1\r\n");
+ req1.append("Host: tester\r\n");
+ req1.append("Content-length: " + body.length() + "\r\n");
+ req1.append("Content-type: application/x-www-form-urlencoded\r\n");
+ req1.append("Connection: close\r\n");
+ req1.append("\r\n");
+ req1.append(body);
+ return req1.toString();
+ }
+}

Back to the top