View Javadoc

1   // ========================================================================
2   // Copyright (c) 2008-2009 Mort Bay Consulting Pty. Ltd.
3   // ------------------------------------------------------------------------
4   // All rights reserved. This program and the accompanying materials
5   // are made available under the terms of the Eclipse Public License v1.0
6   // and Apache License v2.0 which accompanies this distribution.
7   // The Eclipse Public License is available at 
8   // http://www.eclipse.org/legal/epl-v10.html
9   // The Apache License v2.0 is available at
10  // http://www.opensource.org/licenses/apache2.0.php
11  // You may elect to redistribute this code under either of these licenses. 
12  // ========================================================================
13  package com.acme;
14  
15  import java.io.IOException;
16  import java.io.PrintWriter;
17  import java.util.HashMap;
18  import java.util.LinkedList;
19  import java.util.Map;
20  import java.util.Queue;
21  
22  import javax.servlet.ServletException;
23  import javax.servlet.http.HttpServlet;
24  import javax.servlet.http.HttpServletRequest;
25  import javax.servlet.http.HttpServletResponse;
26  
27  import org.eclipse.jetty.continuation.Continuation;
28  import org.eclipse.jetty.continuation.ContinuationSupport;
29  
30  
31  
32  // Simple asynchronous Chat room.
33  // This does not handle duplicate usernames or multiple frames/tabs from the same browser
34  // Some code is duplicated for clarity.
35  public class ChatServlet extends HttpServlet
36  {
37      
38      // inner class to hold message queue for each chat room member
39      class Member
40      {
41          String _name;
42          Continuation _continuation;
43          Queue<String> _queue = new LinkedList<String>();
44      }
45  
46      Map<String,Map<String,Member>> _rooms = new HashMap<String,Map<String, Member>>();
47      
48      
49      // Handle Ajax calls from browser
50      protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException
51      {   
52          // Ajax calls are form encoded
53          String action = request.getParameter("action");
54          String message = request.getParameter("message");
55          String username = request.getParameter("user");
56  
57          if (action.equals("join"))
58              join(request,response,username);
59          else if (action.equals("poll"))
60              poll(request,response,username);
61          else if (action.equals("chat"))
62              chat(request,response,username,message);
63      }
64  
65      private synchronized void join(HttpServletRequest request,HttpServletResponse response,String username)
66      throws IOException
67      {
68          Member member = new Member();
69          member._name=username;
70          Map<String,Member> room=_rooms.get(request.getPathInfo());
71          if (room==null)
72          {
73              room=new HashMap<String,Member>();
74              _rooms.put(request.getPathInfo(),room);
75          }
76          room.put(username,member); 
77          response.setContentType("text/json;charset=utf-8");
78          PrintWriter out=response.getWriter();
79          out.print("{action:\"join\"}");
80      }
81  
82      private synchronized void poll(HttpServletRequest request,HttpServletResponse response,String username)
83      throws IOException
84      {
85          Map<String,Member> room=_rooms.get(request.getPathInfo());
86          if (room==null)
87          {
88              response.sendError(503);
89              return;
90          }
91          Member member = room.get(username);
92          if (room==null)
93          {
94              response.sendError(503);
95              return;
96          }
97  
98          synchronized(member)
99          {
100             if (member._queue.size()>0)
101             {
102                 // Send one chat message
103                 response.setContentType("text/json;charset=utf-8");
104                 StringBuilder buf=new StringBuilder();
105 
106                 buf.append("{\"action\":\"poll\",");
107                 buf.append("\"from\":\"");
108                 buf.append(member._queue.poll());
109                 buf.append("\",");
110 
111                 String message = member._queue.poll();
112                 int quote=message.indexOf('"');
113                 while (quote>=0)
114                 {
115                     message=message.substring(0,quote)+'\\'+message.substring(quote);
116                     quote=message.indexOf('"',quote+2);
117                 }
118                 buf.append("\"chat\":\"");
119                 buf.append(message);
120                 buf.append("\"}");
121                 byte[] bytes = buf.toString().getBytes("utf-8");
122                 response.setContentLength(bytes.length);
123                 response.getOutputStream().write(bytes);
124             }
125             else 
126             {
127                 Continuation continuation = ContinuationSupport.getContinuation(request,response);
128                 if (continuation.isInitial()) 
129                 {
130                     // No chat in queue, so suspend and wait for timeout or chat
131                     continuation.suspend();
132                     member._continuation=continuation;
133                 }
134                 else
135                 {
136                     // Timeout so send empty response
137                     response.setContentType("text/json;charset=utf-8");
138                     PrintWriter out=response.getWriter();
139                     out.print("{action:\"poll\"}");
140                 }
141             }
142         }
143     }
144 
145     private synchronized void chat(HttpServletRequest request,HttpServletResponse response,String username,String message)
146     throws IOException
147     {
148         Map<String,Member> room=_rooms.get(request.getPathInfo());
149         // Post chat to all members
150         for (Member m:room.values())
151         {
152             synchronized (m)
153             {
154                 m._queue.add(username); // from
155                 m._queue.add(message);  // chat
156 
157                 // wakeup member if polling
158                 if (m._continuation!=null)
159                 {
160                     m._continuation.resume();
161                     m._continuation=null;
162                 }
163             }
164         }
165 
166         response.setContentType("text/json;charset=utf-8");
167         PrintWriter out=response.getWriter();
168         out.print("{action:\"chat\"}");  
169     }
170 
171     
172     // Serve the HTML with embedded CSS and Javascript.
173     // This should be static content and should use real JS libraries.
174     protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException
175     {
176         if (!request.getRequestURI().endsWith("/"))
177         {
178             response.sendRedirect(request.getRequestURI()+"/");
179             return;
180         }
181         if (request.getParameter("action")!=null)
182         {
183             doPost(request,response);
184             return;
185         }
186         
187         response.setContentType("text/html");
188         PrintWriter out=response.getWriter();
189         out.println("<html><head>");
190         out.println("    <title>async chat</title>");
191         out.println("    <script type='text/javascript'>");
192         out.println("      function $() { return document.getElementById(arguments[0]); }");
193         out.println("      function $F() { return document.getElementById(arguments[0]).value; }");
194         out.println("      function getKeyCode(ev) { if (window.event) return window.event.keyCode; return ev.keyCode; } ");
195         out.println("      function xhr(method,uri,body,handler) {");
196         out.println("        var req=(window.XMLHttpRequest)?new XMLHttpRequest():new ActiveXObject('Microsoft.XMLHTTP');");
197         out.println("        req.onreadystatechange=function() { if (req.readyState==4 && handler) { eval('var o='+req.responseText);handler(o);} }");
198         out.println("        req.open(method,uri,true);");
199         out.println("        req.setRequestHeader('Content-Type','application/x-www-form-urlencoded');");
200         out.println("        req.send(body);");
201         out.println("      };");
202         out.println("      function send(action,user,message,handler){");
203         out.println("        if (message) message=message.replace('%','%25').replace('&','%26').replace('=','%3D');");
204         out.println("        if (user) user=user.replace('%','%25').replace('&','%26').replace('=','%3D');");
205         out.println("        xhr('POST','chat','action='+action+'&user='+user+'&message='+message,handler);");
206         out.println("      };");
207         out.println("      ");
208         out.println("      var room = {");
209         out.println("        join: function(name) {");
210         out.println("          this._username=name;");
211         out.println("          $('join').className='hidden';");
212         out.println("          $('joined').className='';");
213         out.println("          $('phrase').focus();");
214         out.println("          send('join', room._username,null);");
215         out.println("          send('chat', room._username,'has joined!');");
216         out.println("          send('poll', room._username,null, room._poll);");
217         out.println("        },");
218         out.println("        chat: function(text) {");
219         out.println("          if (text != null && text.length>0 )");
220         out.println("              send('chat',room._username,text);");
221         out.println("        },");
222         out.println("        _poll: function(m) {");
223         out.println("          //console.debug(m);");
224         out.println("          if (m.chat){");
225         out.println("            var chat=document.getElementById('chat');");
226         out.println("            var spanFrom = document.createElement('span');");
227         out.println("            spanFrom.className='from';");
228         out.println("            spanFrom.innerHTML=m.from+':&nbsp;';");
229         out.println("            var spanText = document.createElement('span');");
230         out.println("            spanText.className='text';");
231         out.println("            spanText.innerHTML=m.chat;");
232         out.println("            var lineBreak = document.createElement('br');");
233         out.println("            chat.appendChild(spanFrom);");
234         out.println("            chat.appendChild(spanText);");
235         out.println("            chat.appendChild(lineBreak);");
236         out.println("            chat.scrollTop = chat.scrollHeight - chat.clientHeight;   ");
237         out.println("          }");
238         out.println("          if (m.action=='poll')");
239         out.println("            send('poll', room._username,null, room._poll);");
240         out.println("        },");
241         out.println("        _end:''");
242         out.println("      };");
243         out.println("    </script>");
244         out.println("    <style type='text/css'>");
245         out.println("    div { border: 0px solid black; }");
246         out.println("    div#chat { clear: both; width: 40em; height: 20ex; overflow: auto; background-color: #f0f0f0; padding: 4px; border: 1px solid black; }");
247         out.println("    div#input { clear: both; width: 40em; padding: 4px; background-color: #e0e0e0; border: 1px solid black; border-top: 0px }");
248         out.println("    input#phrase { width:30em; background-color: #e0f0f0; }");
249         out.println("    input#username { width:14em; background-color: #e0f0f0; }");
250         out.println("    div.hidden { display: none; }");
251         out.println("    span.from { font-weight: bold; }");
252         out.println("    span.alert { font-style: italic; }");
253         out.println("    </style>");
254         out.println("</head><body>");
255         out.println("<div id='chat'></div>");
256         out.println("<div id='input'>");
257         out.println("  <div id='join' >");
258         out.println("    Username:&nbsp;<input id='username' type='text'/><input id='joinB' class='button' type='submit' name='join' value='Join'/>");
259         out.println("  </div>");
260         out.println("  <div id='joined' class='hidden'>");
261         out.println("    Chat:&nbsp;<input id='phrase' type='text'></input>");
262         out.println("    <input id='sendB' class='button' type='submit' name='join' value='Send'/>");
263         out.println("  </div>");
264         out.println("</div>");
265         out.println("<script type='text/javascript'>");
266         out.println("$('username').setAttribute('autocomplete','OFF');");
267         out.println("$('username').onkeyup = function(ev) { var keyc=getKeyCode(ev); if (keyc==13 || keyc==10) { room.join($F('username')); return false; } return true; } ;        ");
268         out.println("$('joinB').onclick = function(event) { room.join($F('username')); return false; };");
269         out.println("$('phrase').setAttribute('autocomplete','OFF');");
270         out.println("$('phrase').onkeyup = function(ev) {   var keyc=getKeyCode(ev); if (keyc==13 || keyc==10) { room.chat($F('phrase')); $('phrase').value=''; return false; } return true; };");
271         out.println("$('sendB').onclick = function(event) { room.chat($F('phrase')); $('phrase').value=''; return false; };");
272         out.println("</script>");
273         out.println("</body></html>");
274     }
275     
276 }