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;
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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)
|
||||
LOG.debug("doPost called. join={},message={},username={}", join, message, username);
|
||||
if (username == null)
|
||||
{
|
||||
room=new HashMap<String,Member>();
|
||||
_rooms.put(request.getPathInfo(),room);
|
||||
}
|
||||
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);
|
||||
LOG.debug("no paramter user set, sending 503");
|
||||
response.sendError(503, "user==null");
|
||||
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
|
||||
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("\",");
|
||||
|
||||
String message = member._queue.poll();
|
||||
int quote=message.indexOf('"');
|
||||
while (quote>=0)
|
||||
LOG.debug("Queue size: {}", member._queue.size());
|
||||
if (member._queue.size() > 0)
|
||||
{
|
||||
message=message.substring(0,quote)+'\\'+message.substring(quote);
|
||||
quote=message.indexOf('"',quote+2);
|
||||
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();
|
||||
}
|
||||
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)
|
||||
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)
|
||||
{
|
||||
// Post chat to all members
|
||||
for (Member m:room.values())
|
||||
{
|
||||
synchronized (m)
|
||||
{
|
||||
m._queue.add(username); // from
|
||||
m._queue.add(message); // chat
|
||||
LOG.debug("user: {} in room: {} doesn't exist. Creating new user.", username, room);
|
||||
member = new Member(username);
|
||||
room.put(username, member);
|
||||
}
|
||||
return member;
|
||||
}
|
||||
|
||||
// wakeup member if polling
|
||||
AsyncContext async=m._async.get();
|
||||
if (async!=null & m._async.compareAndSet(async,null))
|
||||
async.dispatch();
|
||||
private Map<String, Member> getRoom(String path)
|
||||
{
|
||||
Map<String, Member> room = _rooms.get(path);
|
||||
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.
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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.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);
|
||||
send(room._username, 'has joined!', room._poll, true);
|
||||
},
|
||||
_joined:function ()
|
||||
{
|
||||
send('chat', room._username, 'has joined!', room._startPolling);
|
||||
},
|
||||
_startPolling:function ()
|
||||
{
|
||||
send('poll', room._username, null, room._poll);
|
||||
},
|
||||
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>
|
||||
|
|
|
@ -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