diff options
author | Thomas Becker | 2013-06-13 14:58:43 +0000 |
---|---|---|
committer | Thomas Becker | 2013-06-13 14:58:57 +0000 |
commit | df17ef8b3a01c208ff94cb1333b4ad573b77c307 (patch) | |
tree | 6f4cac60aa527b81d42eaae2c57162d2ec9fab93 | |
parent | b4913ef38ce6d73e42fd81302b61f49896ff4ae4 (diff) | |
download | org.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.
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(); + } +} |