View Javadoc

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