View Javadoc

1   //
2   //  ========================================================================
3   //  Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd.
4   //  ------------------------------------------------------------------------
5   //  All rights reserved. This program and the accompanying materials
6   //  are made available under the terms of the Eclipse Public License v1.0
7   //  and Apache License v2.0 which accompanies this distribution.
8   //
9   //      The Eclipse Public License is available at
10  //      http://www.eclipse.org/legal/epl-v10.html
11  //
12  //      The Apache License v2.0 is available at
13  //      http://www.opensource.org/licenses/apache2.0.php
14  //
15  //  You may elect to redistribute this code under either of these licenses.
16  //  ========================================================================
17  //
18  
19  package com.acme;
20  
21  import java.io.IOException;
22  import java.io.PrintWriter;
23  import java.util.HashMap;
24  import java.util.LinkedList;
25  import java.util.Map;
26  import java.util.Queue;
27  import java.util.concurrent.atomic.AtomicReference;
28  
29  import javax.servlet.AsyncContext;
30  import javax.servlet.AsyncEvent;
31  import javax.servlet.AsyncListener;
32  import javax.servlet.DispatcherType;
33  import javax.servlet.ServletException;
34  import javax.servlet.http.HttpServlet;
35  import javax.servlet.http.HttpServletRequest;
36  import javax.servlet.http.HttpServletResponse;
37  
38  // Simple asynchronous Chat room.
39  // This does not handle duplicate usernames or multiple frames/tabs from the same browser
40  // Some code is duplicated for clarity.
41  @SuppressWarnings("serial")
42  public class ChatServlet extends HttpServlet
43  {
44  
45      // inner class to hold message queue for each chat room member
46      class Member implements AsyncListener
47      {
48          final String _name;
49          final AtomicReference<AsyncContext> _async=new AtomicReference<>();
50          final Queue<String> _queue = new LinkedList<String>();
51          
52          Member(String name)
53          {
54              _name=name;
55          }
56          
57          @Override
58          public void onTimeout(AsyncEvent event) throws IOException
59          {
60              AsyncContext async = _async.get();
61              if (async!=null && _async.compareAndSet(async,null))
62              {
63                  HttpServletResponse response = (HttpServletResponse)async.getResponse();
64                  response.setContentType("text/json;charset=utf-8");
65                  PrintWriter out=response.getWriter();
66                  out.print("{action:\"poll\"}");
67                  async.complete();
68              }
69          }
70          
71          @Override
72          public void onStartAsync(AsyncEvent event) throws IOException
73          {
74              event.getAsyncContext().addListener(this);
75          }
76          
77          @Override
78          public void onError(AsyncEvent event) throws IOException
79          {
80          }
81          
82          @Override
83          public void onComplete(AsyncEvent event) throws IOException
84          {
85          }
86      }
87  
88      Map<String,Map<String,Member>> _rooms = new HashMap<String,Map<String, Member>>();
89  
90  
91      // Handle Ajax calls from browser
92      @Override
93      protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException
94      {
95          // Ajax calls are form encoded
96          String action = request.getParameter("action");
97          String message = request.getParameter("message");
98          String username = request.getParameter("user");
99  
100         if (action.equals("join"))
101             join(request,response,username);
102         else if (action.equals("poll"))
103             poll(request,response,username);
104         else if (action.equals("chat"))
105             chat(request,response,username,message);
106     }
107 
108     private synchronized void join(HttpServletRequest request,HttpServletResponse response,String username)
109     throws IOException
110     {
111         Member member = new Member(username);
112         Map<String,Member> room=_rooms.get(request.getPathInfo());
113         if (room==null)
114         {
115             room=new HashMap<String,Member>();
116             _rooms.put(request.getPathInfo(),room);
117         }
118         room.put(username,member);
119         response.setContentType("text/json;charset=utf-8");
120         PrintWriter out=response.getWriter();
121         out.print("{action:\"join\"}");
122     }
123 
124     private synchronized void poll(HttpServletRequest request,HttpServletResponse response,String username)
125     throws IOException
126     {
127         Map<String,Member> room=_rooms.get(request.getPathInfo());
128         if (room==null)
129         {
130             response.sendError(503);
131             return;
132         }
133         final Member member = room.get(username);
134         if (member==null)
135         {
136             response.sendError(503);
137             return;
138         }
139 
140         synchronized(member)
141         {
142             if (member._queue.size()>0)
143             {
144                 // Send one chat message
145                 response.setContentType("text/json;charset=utf-8");
146                 StringBuilder buf=new StringBuilder();
147 
148                 buf.append("{\"action\":\"poll\",");
149                 buf.append("\"from\":\"");
150                 buf.append(member._queue.poll());
151                 buf.append("\",");
152 
153                 String message = member._queue.poll();
154                 int quote=message.indexOf('"');
155                 while (quote>=0)
156                 {
157                     message=message.substring(0,quote)+'\\'+message.substring(quote);
158                     quote=message.indexOf('"',quote+2);
159                 }
160                 buf.append("\"chat\":\"");
161                 buf.append(message);
162                 buf.append("\"}");
163                 byte[] bytes = buf.toString().getBytes("utf-8");
164                 response.setContentLength(bytes.length);
165                 response.getOutputStream().write(bytes);
166             }
167             else
168             {
169                 AsyncContext async = request.startAsync();
170                 async.setTimeout(10000);
171                 async.addListener(member);
172                 if (!member._async.compareAndSet(null,async))
173                     throw new IllegalStateException();
174             }
175         }
176     }
177 
178     private synchronized void chat(HttpServletRequest request,HttpServletResponse response,String username,String message)
179     throws IOException
180     {
181         Map<String,Member> room=_rooms.get(request.getPathInfo());
182         if (room!=null)
183         {
184             // Post chat to all members
185             for (Member m:room.values())
186             {
187                 synchronized (m)
188                 {
189                     m._queue.add(username); // from
190                     m._queue.add(message);  // chat
191 
192                     // wakeup member if polling
193                     AsyncContext async=m._async.get();
194                     if (async!=null & m._async.compareAndSet(async,null))
195                         async.dispatch();
196                 }
197             }
198         }
199 
200         response.setContentType("text/json;charset=utf-8");
201         PrintWriter out=response.getWriter();
202         out.print("{action:\"chat\"}");
203     }
204 
205     // Serve the HTML with embedded CSS and Javascript.
206     // This should be static content and should use real JS libraries.
207     @Override
208     protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException
209     {
210         if (request.getParameter("action")!=null)
211             doPost(request,response);
212         else
213             getServletContext().getNamedDispatcher("default").forward(request,response);
214     }
215 
216 }