408709 refactor test-webapp's chat application. Now there's only a single request for user login and initial chat message.
This commit is contained in:
parent
b4913ef38c
commit
df17ef8b3a
|
@ -19,13 +19,11 @@
|
||||||
package com.acme;
|
package com.acme;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.PrintWriter;
|
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Queue;
|
import java.util.Queue;
|
||||||
import java.util.concurrent.atomic.AtomicReference;
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
|
|
||||||
import javax.servlet.AsyncContext;
|
import javax.servlet.AsyncContext;
|
||||||
import javax.servlet.AsyncEvent;
|
import javax.servlet.AsyncEvent;
|
||||||
import javax.servlet.AsyncListener;
|
import javax.servlet.AsyncListener;
|
||||||
|
@ -34,57 +32,70 @@ import javax.servlet.http.HttpServlet;
|
||||||
import javax.servlet.http.HttpServletRequest;
|
import javax.servlet.http.HttpServletRequest;
|
||||||
import javax.servlet.http.HttpServletResponse;
|
import javax.servlet.http.HttpServletResponse;
|
||||||
|
|
||||||
|
import org.eclipse.jetty.util.log.Log;
|
||||||
|
import org.eclipse.jetty.util.log.Logger;
|
||||||
|
|
||||||
// Simple asynchronous Chat room.
|
// Simple asynchronous Chat room.
|
||||||
// This does not handle duplicate usernames or multiple frames/tabs from the same browser
|
// This does not handle duplicate usernames or multiple frames/tabs from the same browser
|
||||||
// Some code is duplicated for clarity.
|
// Some code is duplicated for clarity.
|
||||||
@SuppressWarnings("serial")
|
@SuppressWarnings("serial")
|
||||||
public class ChatServlet extends HttpServlet
|
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
|
// inner class to hold message queue for each chat room member
|
||||||
class Member implements AsyncListener
|
class Member implements AsyncListener
|
||||||
{
|
{
|
||||||
final String _name;
|
final String _name;
|
||||||
final AtomicReference<AsyncContext> _async=new AtomicReference<>();
|
final AtomicReference<AsyncContext> _async = new AtomicReference<>();
|
||||||
final Queue<String> _queue = new LinkedList<String>();
|
final Queue<String> _queue = new LinkedList<>();
|
||||||
|
|
||||||
Member(String name)
|
Member(String name)
|
||||||
{
|
{
|
||||||
_name=name;
|
_name = name;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onTimeout(AsyncEvent event) throws IOException
|
public void onTimeout(AsyncEvent event) throws IOException
|
||||||
{
|
{
|
||||||
|
LOG.debug("resume request");
|
||||||
AsyncContext async = _async.get();
|
AsyncContext async = _async.get();
|
||||||
if (async!=null && _async.compareAndSet(async,null))
|
if (async != null && _async.compareAndSet(async, null))
|
||||||
{
|
{
|
||||||
HttpServletResponse response = (HttpServletResponse)async.getResponse();
|
HttpServletResponse response = (HttpServletResponse)async.getResponse();
|
||||||
response.setContentType("text/json;charset=utf-8");
|
response.setContentType("text/json;charset=utf-8");
|
||||||
PrintWriter out=response.getWriter();
|
response.getOutputStream().write("{action:\"poll\"}".getBytes());
|
||||||
out.print("{action:\"poll\"}");
|
|
||||||
async.complete();
|
async.complete();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onStartAsync(AsyncEvent event) throws IOException
|
public void onStartAsync(AsyncEvent event) throws IOException
|
||||||
{
|
{
|
||||||
event.getAsyncContext().addListener(this);
|
event.getAsyncContext().addListener(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onError(AsyncEvent event) throws IOException
|
public void onError(AsyncEvent event) throws IOException
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onComplete(AsyncEvent event) throws IOException
|
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
|
// Handle Ajax calls from browser
|
||||||
|
@ -92,113 +103,119 @@ public class ChatServlet extends HttpServlet
|
||||||
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException
|
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException
|
||||||
{
|
{
|
||||||
// Ajax calls are form encoded
|
// Ajax calls are form encoded
|
||||||
String action = request.getParameter("action");
|
boolean join = Boolean.parseBoolean(request.getParameter("join"));
|
||||||
String message = request.getParameter("message");
|
String message = request.getParameter("message");
|
||||||
String username = request.getParameter("user");
|
String username = request.getParameter("user");
|
||||||
|
|
||||||
if (action.equals("join"))
|
LOG.debug("doPost called. join={},message={},username={}", join, message, username);
|
||||||
join(request,response,username);
|
if (username == null)
|
||||||
else if (action.equals("poll"))
|
|
||||||
poll(request,response,username);
|
|
||||||
else if (action.equals("chat"))
|
|
||||||
chat(request,response,username,message);
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
{
|
{
|
||||||
room=new HashMap<String,Member>();
|
LOG.debug("no paramter user set, sending 503");
|
||||||
_rooms.put(request.getPathInfo(),room);
|
response.sendError(503, "user==null");
|
||||||
}
|
|
||||||
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
|
|
||||||
{
|
|
||||||
Map<String,Member> room=_rooms.get(request.getPathInfo());
|
|
||||||
if (room==null)
|
|
||||||
{
|
|
||||||
response.sendError(503);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
final Member member = room.get(username);
|
|
||||||
if (member==null)
|
|
||||||
{
|
|
||||||
response.sendError(503);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
synchronized(member)
|
Map<String, Member> room = getRoom(request.getPathInfo());
|
||||||
|
Member member = getMember(username, room);
|
||||||
|
|
||||||
|
if (message != null)
|
||||||
{
|
{
|
||||||
if (member._queue.size()>0)
|
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)
|
||||||
{
|
{
|
||||||
// Send one chat message
|
LOG.debug("Queue size: {}", member._queue.size());
|
||||||
response.setContentType("text/json;charset=utf-8");
|
if (member._queue.size() > 0)
|
||||||
StringBuilder buf=new StringBuilder();
|
|
||||||
|
|
||||||
buf.append("{\"action\":\"poll\",");
|
|
||||||
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);
|
sendSingleMessage(response, member);
|
||||||
quote=message.indexOf('"',quote+2);
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
LOG.debug("starting async");
|
||||||
|
AsyncContext async = request.startAsync();
|
||||||
|
async.setTimeout(asyncTimeout);
|
||||||
|
async.addListener(member);
|
||||||
|
if (!member._async.compareAndSet(null, async))
|
||||||
|
throw new IllegalStateException();
|
||||||
}
|
}
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private synchronized void chat(HttpServletRequest request,HttpServletResponse response,String username,String message)
|
private Member getMember(String username, Map<String, Member> room)
|
||||||
throws IOException
|
|
||||||
{
|
{
|
||||||
Map<String,Member> room=_rooms.get(request.getPathInfo());
|
Member member = room.get(username);
|
||||||
if (room!=null)
|
if (member == null)
|
||||||
{
|
{
|
||||||
// Post chat to all members
|
LOG.debug("user: {} in room: {} doesn't exist. Creating new user.", username, room);
|
||||||
for (Member m:room.values())
|
member = new Member(username);
|
||||||
{
|
room.put(username, member);
|
||||||
synchronized (m)
|
}
|
||||||
{
|
return member;
|
||||||
m._queue.add(username); // from
|
}
|
||||||
m._queue.add(message); // chat
|
|
||||||
|
|
||||||
// wakeup member if polling
|
private Map<String, Member> getRoom(String path)
|
||||||
AsyncContext async=m._async.get();
|
{
|
||||||
if (async!=null & m._async.compareAndSet(async,null))
|
Map<String, Member> room = _rooms.get(path);
|
||||||
async.dispatch();
|
if (room == null)
|
||||||
|
{
|
||||||
|
LOG.debug("room: {} doesn't exist. Creating new room.", path);
|
||||||
|
room = new HashMap<>();
|
||||||
|
_rooms.put(path, room);
|
||||||
|
}
|
||||||
|
return room;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void sendSingleMessage(HttpServletResponse response, Member member) throws IOException
|
||||||
|
{
|
||||||
|
response.setContentType("text/json;charset=utf-8");
|
||||||
|
StringBuilder buf = new StringBuilder();
|
||||||
|
|
||||||
|
buf.append("{\"from\":\"");
|
||||||
|
buf.append(member._queue.poll());
|
||||||
|
buf.append("\",");
|
||||||
|
|
||||||
|
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 void sendMessageToAllMembers(String message, String username, Map<String, Member> room)
|
||||||
|
{
|
||||||
|
LOG.debug("Sending message: {} from: {}", message, username);
|
||||||
|
for (Member m : room.values())
|
||||||
|
{
|
||||||
|
synchronized (m)
|
||||||
|
{
|
||||||
|
m._queue.add(username); // from
|
||||||
|
m._queue.add(message); // chat
|
||||||
|
|
||||||
|
// 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.
|
// Serve the HTML with embedded CSS and Javascript.
|
||||||
|
@ -206,10 +223,10 @@ public class ChatServlet extends HttpServlet
|
||||||
@Override
|
@Override
|
||||||
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException
|
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException
|
||||||
{
|
{
|
||||||
if (request.getParameter("action")!=null)
|
if (request.getParameter("action") != null)
|
||||||
doPost(request,response);
|
doPost(request, response);
|
||||||
else
|
else
|
||||||
getServletContext().getNamedDispatcher("default").forward(request,response);
|
getServletContext().getNamedDispatcher("default").forward(request, response);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,2 @@
|
||||||
|
org.eclipse.jetty.util.log.class=org.eclipse.jetty.util.log.StdErrLog
|
||||||
|
com.acme.LEVEL=DEBUG
|
|
@ -30,38 +30,28 @@
|
||||||
req.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
|
req.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
|
||||||
req.send(body);
|
req.send(body);
|
||||||
}
|
}
|
||||||
;
|
function send(user, message, handler, join)
|
||||||
function send(action, user, message, handler)
|
|
||||||
{
|
{
|
||||||
if (message) message = message.replace('%', '%25').replace('&', '%26').replace('=', '%3D');
|
if (message) message = message.replace('%', '%25').replace('&', '%26').replace('=', '%3D');
|
||||||
if (user) user = user.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 = {
|
var room = {
|
||||||
join:function (name)
|
join: function (name)
|
||||||
{
|
{
|
||||||
this._username = name;
|
this._username = name;
|
||||||
$('join').className = 'hidden';
|
$('join').className = 'hidden';
|
||||||
$('joined').className = '';
|
$('joined').className = '';
|
||||||
$('phrase').focus();
|
$('phrase').focus();
|
||||||
send('join', room._username, null, room._joined);
|
send(room._username, 'has joined!', room._poll, true);
|
||||||
},
|
},
|
||||||
_joined:function ()
|
chat: function (text)
|
||||||
{
|
|
||||||
send('chat', room._username, 'has joined!', room._startPolling);
|
|
||||||
},
|
|
||||||
_startPolling:function ()
|
|
||||||
{
|
|
||||||
send('poll', room._username, null, room._poll);
|
|
||||||
},
|
|
||||||
chat:function (text)
|
|
||||||
{
|
{
|
||||||
if (text != null && text.length > 0)
|
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);
|
//console.debug(m);
|
||||||
if (m.chat)
|
if (m.chat)
|
||||||
|
@ -79,10 +69,9 @@
|
||||||
chat.appendChild(lineBreak);
|
chat.appendChild(lineBreak);
|
||||||
chat.scrollTop = chat.scrollHeight - chat.clientHeight;
|
chat.scrollTop = chat.scrollHeight - chat.clientHeight;
|
||||||
}
|
}
|
||||||
if (m.action == 'poll')
|
send(room._username, null, room._poll, false);
|
||||||
send('poll', room._username, null, room._poll);
|
|
||||||
},
|
},
|
||||||
_end:''
|
_end: ''
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
<style type='text/css'>
|
<style type='text/css'>
|
||||||
|
@ -106,7 +95,7 @@
|
||||||
padding: 4px;
|
padding: 4px;
|
||||||
background-color: #e0e0e0;
|
background-color: #e0e0e0;
|
||||||
border: 1px solid black;
|
border: 1px solid black;
|
||||||
border-top: 0px
|
border-top: 0
|
||||||
}
|
}
|
||||||
|
|
||||||
input#phrase {
|
input#phrase {
|
||||||
|
@ -122,14 +111,6 @@
|
||||||
div.hidden {
|
div.hidden {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
span.from {
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
span.alert {
|
|
||||||
font-style: italic;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue