/** * $RCSfile$ * $Revision$ * $Date$ * * Copyright (C) 2003 Jive Software. All rights reserved. * * This software is the proprietary information of Jive Software. Use is subject to license terms. */ package org.jivesoftware.webchat; import java.io.*; import java.util.*; import javax.servlet.*; import javax.servlet.http.*; import org.jivesoftware.smack.*; import org.jivesoftware.smack.filter.*; import org.jivesoftware.smack.packet.*; import org.jivesoftware.smack.util.StringUtils; /** * An extension of HttpServlet customized to handle transactions between N webclients * and M chats located on a given XMPP server. While N >= M in the case of group chats, * the code will currently, never the less, hold onto N connections to the XMPP server.
* * It is assumed that all JSP pages are in the context root. The init params should be: * * * @author Bill Lynch * @author loki der quaeler */ public class JiveChatServlet extends HttpServlet implements HttpSessionListener, PacketListener { static public final String JIVE_WEB_CHAT_RESOURCE_NAME = "WebChat"; static protected long PACKET_RESPONSE_TIMEOUT_MS = 5000; static protected String CHAT_LAUNCHER_URI_SUFFIX = "/chat-launcher.jsp"; static protected String CREATE_ACCOUNT_URI = "/account_creation.jsp"; static protected String LOGIN_URI = "/index.jsp"; static protected String ERRORS_ATTRIBUTE_STRING = "messenger.servlet.errors"; static protected String NICKNAME_ATTRIBUTE_STRING = "messenger.servlet.nickname"; static protected String ROOM_ATTRIBUTE_STRING = "messenger.servlet.room"; static protected String HOST_PARAM_STRING = "host"; static protected String PORT_PARAM_STRING = "port"; static protected String SSL_PARAM_STRING = "SSLEnabled"; static protected String COMMAND_PARAM_STRING = "command"; static protected String NICKNAME_PARAM_STRING = "nickname"; static protected String PASSWORD_PARAM_STRING = "password"; static protected String RETYPED_PASSWORD_PARAM_STRING = "password_zwei"; static protected String ROOM_PARAM_STRING = "room"; static protected String USERNAME_PARAM_STRING = "username"; static protected String ANON_LOGIN_COMMAND_STRING = "anon_login"; static protected String CREATE_ACCOUNT_COMMAND_STRING = "create_account"; static protected String LOGIN_COMMAND_STRING = "login"; static protected String LOGOUT_COMMAND_STRING = "logout"; static protected String READ_COMMAND_STRING = "read"; static protected String SILENCE_COMMAND_STRING = "silence"; static protected String WRITE_COMMAND_STRING = "write"; static protected String MESSAGE_REQUEST_STRING = "message"; // is this value used elsewhere? (if not, why a string?) PENDING static protected String ERROR_RETURN_CODE_STRING = "error"; static protected String SUCCESS_RETURN_CODE_STRING = "success"; // k/v :: S(session id) / ChatData static protected Map SESSION_CHATDATA_MAP = new HashMap(); // k/v :: S(unique root of packet ids) / ChatData static protected Map PACKET_ROOT_CHATDATA_MAP = new HashMap(); static protected EmoticonFilter EMOTICONFILTER = new EmoticonFilter(); static protected URLFilter URLFILTER = new URLFilter(); protected String host; protected int port; protected boolean sslEnabled; public void init (ServletConfig config) throws ServletException { ServletContext context = null; String portParameter = null; super.init(config); // XMPPConnection.DEBUG_ENABLED = true; context = config.getServletContext(); this.host = context.getInitParameter(HOST_PARAM_STRING); if (this.host == null) { throw new ServletException("Init parameter \"" + HOST_PARAM_STRING + "\" must be set."); } this.port = -1; portParameter = context.getInitParameter(PORT_PARAM_STRING); if (portParameter != null) { try { this.port = Integer.parseInt(portParameter); } catch (NumberFormatException nfe) { throw new ServletException("Init parameter \"" + PORT_PARAM_STRING + "\" must be a valid number.", nfe); } } this.sslEnabled = Boolean.valueOf(context.getInitParameter(SSL_PARAM_STRING)).booleanValue(); } /** * Take care of closing down everything we're holding on to, then bubble up the destroy * to our superclass. */ public void destroy () { synchronized (SESSION_CHATDATA_MAP) { for (Iterator i = SESSION_CHATDATA_MAP.values().iterator(); i.hasNext(); ) { ChatData chatData = (ChatData)i.next(); if (chatData.groupChat != null) { chatData.groupChat.leave(); } chatData.connection.close(); } } super.destroy(); } protected void service (HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { HttpSession session = request.getSession(); String sessionID = session.getId(); String path = request.getContextPath(); String command = request.getParameter(COMMAND_PARAM_STRING); if (READ_COMMAND_STRING.equals(command)) { ChatData chatData = (ChatData)SESSION_CHATDATA_MAP.get(sessionID); StringBuffer reply = null; boolean foundData = false; Message message = null; int i = 0; if (chatData == null) { this.writeData("Must login first.", response); return; } reply = new StringBuffer(); reply.append("\n"); reply.append("Chat Read Page\n"); reply.append("\n"); reply.append(""); reply.append("\n"); this.writeData(reply.toString(), response); } else if (WRITE_COMMAND_STRING.equals(command)) { String message = request.getParameter(MESSAGE_REQUEST_STRING); ChatData chatData = (ChatData)SESSION_CHATDATA_MAP.get(sessionID); if (message == null) { this.writeData("Parameter \"" + MESSAGE_REQUEST_STRING + "\" is required.", response); return; } else if (chatData == null) { this.writeData("Must login first.", response); return; } try { StringBuffer reply = new StringBuffer(); chatData.groupChat.sendMessage(message.trim()); reply.append("\n"); reply.append("\n"); reply.append( "\n"); reply.append("Chat Form"); reply.append("\n"); reply.append("
\n"); reply.append("\n"); reply.append("\n"); reply.append("
"); this.writeData(reply.toString(), response); } catch (XMPPException e) { // PENDING - better handling e.printStackTrace(); } } else if (LOGOUT_COMMAND_STRING.equals(command)) { ChatData chatData = null; synchronized (SESSION_CHATDATA_MAP) { chatData = (ChatData)SESSION_CHATDATA_MAP.remove(sessionID); } if (chatData != null) { if (chatData.groupChat != null) { chatData.groupChat.leave(); } synchronized (PACKET_ROOT_CHATDATA_MAP) { Packet p = new IQ(); String root = this.getPacketIDRoot(p); PACKET_ROOT_CHATDATA_MAP.remove(root); } chatData.connection.close(); } } else if (ANON_LOGIN_COMMAND_STRING.equals(command)) { String returnCode = this.handleLogin(request, true); if (SUCCESS_RETURN_CODE_STRING.equals(returnCode)) { response.sendRedirect(path + CHAT_LAUNCHER_URI_SUFFIX); } else { // error, return to the original page to display errors and allow re-attempts RequestDispatcher rd = request.getRequestDispatcher(LOGIN_URI); rd.forward(request, response); } } else if (LOGIN_COMMAND_STRING.equals(command)) { String returnCode = this.handleLogin(request, false); if (SUCCESS_RETURN_CODE_STRING.equals(returnCode)) { response.sendRedirect(path + CHAT_LAUNCHER_URI_SUFFIX); } else { // error, return to the original page to display errors and allow re-attempts RequestDispatcher rd = request.getRequestDispatcher(LOGIN_URI); rd.forward(request, response); } } else if (CREATE_ACCOUNT_COMMAND_STRING.equals(command)) { String returnCode = this.createAccount(request); if (SUCCESS_RETURN_CODE_STRING.equals(returnCode)) { response.sendRedirect(path + LOGIN_URI); } else { // error, return to the original page to display errors and allow re-attempts RequestDispatcher rd = request.getRequestDispatcher(CREATE_ACCOUNT_URI); rd.forward(request, response); } } else if (SILENCE_COMMAND_STRING.equals(command)) { // do nothing } else if (command != null) { this.writeData(("Invalid command: " + command), response); } else { this.writeData("Jive Messenger Chat Servlet", response); } } protected String getPacketIDRoot (Packet p) { if (p == null) { return null; } return p.getPacketID().substring(0, 5); } /** * Creates an account for the user data specified. */ private String createAccount (HttpServletRequest request) { String sessionID = request.getSession().getId(); String username = request.getParameter(USERNAME_PARAM_STRING); String password = request.getParameter(PASSWORD_PARAM_STRING); String retypedPassword = request.getParameter(RETYPED_PASSWORD_PARAM_STRING); Map errors = new HashMap(); // PENDING: validate already taken username if ((username == null) || (username.trim().length() == 0)) { errors.put("empty_username", ""); } if ((password == null) || (password.trim().length() == 0)) { errors.put("empty_password", ""); } if ((retypedPassword == null) || (retypedPassword.trim().length() == 0)) { errors.put("empty_password_two", ""); } if ((retypedPassword != null) && (password != null) && (! retypedPassword.equals(password))) { errors.put("mismatch_password", ""); } // If there were no errors, continue if (errors.size() == 0) { ChatData chatData = (ChatData)SESSION_CHATDATA_MAP.get(sessionID); // If a connection already exists for this session, close it before creating // another. if (chatData != null) { if (chatData.groupChat != null) { chatData.groupChat.leave(); } chatData.connection.close(); } chatData = new ChatData(); try { AccountManager am = null; // Create connection. if (! this.sslEnabled) { if (port != -1) { chatData.connection = new XMPPConnection(this.host, this.port); } else { chatData.connection = new XMPPConnection(this.host); } } else { if (port != -1) { chatData.connection = new SSLXMPPConnection(this.host, this.port); } else { chatData.connection = new SSLXMPPConnection(this.host); } } am = chatData.connection.getAccountManager(); // PENDING check whether the server even supports account creation am.createAccount(username, password); } catch (XMPPException e) { errors.put("general", "The server reported an error in account creation: " + e.getXMPPError().getMessage()); } } if (errors.size() > 0) { request.setAttribute(ERRORS_ATTRIBUTE_STRING, errors); return ERROR_RETURN_CODE_STRING; } return SUCCESS_RETURN_CODE_STRING; } /** * Handles login logic. */ private String handleLogin (HttpServletRequest request, boolean anonymous) { String sessionID = request.getSession().getId(); String username = request.getParameter(USERNAME_PARAM_STRING); String password = request.getParameter(PASSWORD_PARAM_STRING); String room = request.getParameter(ROOM_PARAM_STRING); String nickname = request.getParameter(NICKNAME_PARAM_STRING); Map errors = new HashMap(); // Validate parameters if ((! anonymous) && ((username == null) || (username.trim().length() == 0))) { errors.put(USERNAME_PARAM_STRING, ""); } if ((! anonymous) && ((password == null) || (password.trim().length() == 0))) { errors.put(PASSWORD_PARAM_STRING, ""); } if ((room == null) || (room.trim().length() == 0)) { errors.put(ROOM_PARAM_STRING, ""); } if ((nickname == null) || (nickname.trim().length() == 0)) { errors.put(NICKNAME_PARAM_STRING, ""); } // If there were no errors, continue if (errors.size() == 0) { ChatData chatData = (ChatData)SESSION_CHATDATA_MAP.get(sessionID); // If a connection already exists for this session, close it before creating // another. if (chatData != null) { if (chatData.groupChat != null) { chatData.groupChat.leave(); } chatData.connection.close(); } chatData = new ChatData(); try { // Create connection. if (! this.sslEnabled) { if (port != -1) { chatData.connection = new XMPPConnection(this.host, this.port); } else { chatData.connection = new XMPPConnection(this.host); } } else { if (port != -1) { chatData.connection = new SSLXMPPConnection(this.host, this.port); } else { chatData.connection = new SSLXMPPConnection(this.host); } } if (anonymous) { Authentication a = new Authentication(); PacketCollector pc = chatData.connection.createPacketCollector( new PacketIDFilter(a.getPacketID())); Authentication responsePacket = null; a.setType(IQ.Type.SET); chatData.connection.sendPacket(a); responsePacket = (Authentication)pc.nextResult(PACKET_RESPONSE_TIMEOUT_MS); if (responsePacket == null) { // throw new XMPPException("No response from the server."); } // check for error response pc.cancel(); // since GroupChat isn't setting the 'from' in it's message sends, // i can't see a problem in not doing anything with the unique resource // we've just been given by the server. if GroupChat starts setting the // from, it would probably grab the information from the XMPPConnection // instance it holds, and as such we would then need to introduce the // concept of anonymous logins to XMPPConnection, or tell GroupChat what // to do what username is null or blank but a resource exists... PENDING } else { chatData.connection.login(username, password, JIVE_WEB_CHAT_RESOURCE_NAME); } chatData.connection.addPacketListener(this, new PacketTypeFilter(Presence.class)); synchronized (SESSION_CHATDATA_MAP) { SESSION_CHATDATA_MAP.put(sessionID, chatData); } synchronized (PACKET_ROOT_CHATDATA_MAP) { Packet p = new IQ(); String root = this.getPacketIDRoot(p); // PENDING -- we won't do anything with this packet, so it will ultimately look // to the server as though a packet has disappeared -- is this ok with the // server? PACKET_ROOT_CHATDATA_MAP.put(root, chatData); } // Join groupChat room. chatData.groupChat = chatData.connection.createGroupChat(room); chatData.groupChat.join(nickname); // Put the user's nickname in the session - this is used by the view to correctly // display the user's messages in a different color: request.getSession().setAttribute(NICKNAME_ATTRIBUTE_STRING, nickname); request.getSession().setAttribute(ROOM_ATTRIBUTE_STRING, room); } catch (XMPPException e) { XMPPError err = e.getXMPPError(); errors.put("general", ((err != null) ? err.getMessage() : e.getMessage())); if (chatData.groupChat != null) { chatData.groupChat.leave(); } } } if (errors.size() > 0) { request.setAttribute(ERRORS_ATTRIBUTE_STRING, errors); return ERROR_RETURN_CODE_STRING; } return SUCCESS_RETURN_CODE_STRING; } private void writeData (String data, HttpServletResponse response) { try { PrintWriter responseWriter = response.getWriter(); response.setContentType("text/html"); responseWriter.println(data); responseWriter.close(); } catch (IOException ioe) { // PENDING } } // a hack class to hold a data glom (really hacky) private class ChatData { private XMPPConnection connection; private GroupChat groupChat; private Set newJoins = new HashSet(); private Set newDepartures = new HashSet(); } /** * Replaces all instances of oldString with newString in string. * * PENDING - why is this final? * PENDING - take this out -- it fails under some cases... * * @param string the String to search to perform replacements on * @param oldString the String that should be replaced by newString * @param newString the String that will replace all instances of oldString * * @return a String will all instances of oldString replaced by newString */ static public final String replace (String string, String oldString, String newString) { int i = 0; // MAY RETURN THIS BLOCK if (string == null) { return null; } if (newString == null) { return string; } // Make sure that oldString appears at least once before doing any processing. if (( i=string.indexOf(oldString, i)) >= 0) { // Use char []'s, as they are more efficient to deal with. char[] string2 = string.toCharArray(); char[] newString2 = newString.toCharArray(); int oLength = oldString.length(); StringBuffer buf = new StringBuffer(string2.length); int j = 1; buf.append(string2, 0, i).append(newString2); i += oLength; // Replace all remaining instances of oldString with newString. while ((i=string.indexOf(oldString, i)) > 0) { buf.append(string2, j, (i - j)).append(newString2); i += oLength; j = i; } buf.append(string2, j, (string2.length - j)); return buf.toString(); } return string; } /** * * HttpSessionListener implementation * */ public void sessionCreated (HttpSessionEvent event) { } public void sessionDestroyed (HttpSessionEvent event) { String sessionID = event.getSession().getId(); ChatData chatData = null; synchronized (SESSION_CHATDATA_MAP) { chatData = (ChatData)SESSION_CHATDATA_MAP.remove(sessionID); } if (chatData != null) { if (chatData.groupChat != null) { chatData.groupChat.leave(); } synchronized (PACKET_ROOT_CHATDATA_MAP) { Packet p = new IQ(); String root = this.getPacketIDRoot(p); PACKET_ROOT_CHATDATA_MAP.remove(root); } chatData.connection.close(); } } /** * * PacketListener implementation * */ public void processPacket (Packet packet) { Presence presence = (Presence)packet; String root = null; ChatData chatData = null; String userName = null; // MAY RETURN THIS BLOCK if (presence.getMode() == Presence.Mode.INVISIBLE) { return; } root = this.getPacketIDRoot(presence); chatData = (ChatData)PACKET_ROOT_CHATDATA_MAP.get(root); // MAY RETURN THIS BLOCK if (chatData == null) { return; } userName = StringUtils.parseResource(packet.getFrom()); if (presence.getType() == Presence.Type.UNAVAILABLE) { synchronized (chatData.newDepartures) { synchronized (chatData.newJoins) { chatData.newJoins.remove(userName); chatData.newDepartures.add(userName); } } } else if (presence.getType() == Presence.Type.AVAILABLE) { synchronized (chatData.newJoins) { synchronized (chatData.newDepartures) { chatData.newDepartures.remove(userName); chatData.newJoins.add(userName); } } } } }