Index: lams_tool_chat/src/java/org/lamsfoundation/lams/tool/chat/JabberHTTPBind/JHBServlet.java =================================================================== diff -u --- lams_tool_chat/src/java/org/lamsfoundation/lams/tool/chat/JabberHTTPBind/JHBServlet.java (revision 0) +++ lams_tool_chat/src/java/org/lamsfoundation/lams/tool/chat/JabberHTTPBind/JHBServlet.java (revision c07351b3fba3779036610103e4becd1e9842bf97) @@ -0,0 +1,534 @@ +/* JabberHTTPBind - An implementation of JEP-0124 (HTTP Binding) + * Please see http://www.jabber.org/jeps/jep-0124.html for details. + * + * Copyright (c) 2005 Stefan Strigler + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ +package org.lamsfoundation.lams.tool.chat.JabberHTTPBind; + +import java.io.BufferedReader; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.PrintWriter; +import java.net.UnknownHostException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; + +import org.apache.log4j.Logger; +import org.lamsfoundation.lams.tool.chat.service.ChatService; +import org.w3c.dom.Document; +import org.w3c.dom.NamedNodeMap; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; +import org.xml.sax.SAXException; + +/** + * An implementation of JEP-0124 (HTTP Binding). See + * http://www.jabber.org/jeps/jep-0124.html for details. + * + * @author Stefan Strigler + */ +public final class JHBServlet extends HttpServlet { + + static Logger log = Logger.getLogger(JHBServlet.class.getName()); + + public static final String APP_VERSION = "0.3"; + public static final String APP_NAME = "Jabber HTTP Binding Servlet"; + + public static final boolean DEBUG = true; + public static final int DEBUG_LEVEL = 2; + + private DocumentBuilder db; + private Janitor janitor; + + public void init() throws ServletException { + DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); + + try { + db = dbf.newDocumentBuilder(); + } catch (ParserConfigurationException e) { + log("failed to create DocumentBuilderFactory", e); + } + + janitor = new Janitor(); // cleans up sessions + new Thread(janitor).start(); + } + + public void destroy() { + Session.stopSessions(); + janitor.stop(); + } + + public static String hex(byte[] array) { + StringBuffer sb = new StringBuffer(); + for (int i = 0; i < array.length; ++i) { + sb.append(Integer.toHexString((array[i] & 0xFF) | 0x100) + .toLowerCase().substring(1, 3)); + } + return sb.toString(); + } + + public static String sha1(String message) { + try { + MessageDigest sha = MessageDigest.getInstance("SHA-1"); + return hex(sha.digest(message.getBytes())); + } catch (NoSuchAlgorithmException e) { + } + return null; + } + + public static void dbg(String msg) { + dbg(msg,0); + } + + public static void dbg(String msg,int lvl) { + if (!DEBUG) + return; + if (lvl > DEBUG_LEVEL) + return; + log.debug("["+lvl+"] "+msg); + } + + /** + * We only need to respond to POST requests ... + * + * @param request + * The servlet request we are processing + * @param response + * The servlet response we are producing + * + * @exception IOException + * if an input/output error occurs + * @exception ServletException + * if a servlet error occurs + */ + public void doPost(HttpServletRequest request, HttpServletResponse response) + throws IOException, ServletException { + log.debug("starting doPost"); + int rid = 0; + try { + String contents = ""; + String line = ""; + // Safari sends a carriage-return after the POST data which causes + // SAX parser to fail. + BufferedReader bfr = request.getReader(); + while ((line = bfr.readLine()) != null) + contents += line + "\n"; + contents.trim(); + + String cEnc = request.getCharacterEncoding(); + if (cEnc == null) + cEnc = "UTF-8"; + ByteArrayInputStream bis = new ByteArrayInputStream(contents.getBytes(cEnc)); + + /* + * parse request + */ + Document doc; + synchronized (db) { + doc = db.parse(bis); + } + + Node rootNode = doc.getDocumentElement(); + if (rootNode == null || !rootNode.getNodeName().equals("body")) + // not a -tag - don't know what to do with it + response.sendError(HttpServletResponse.SC_BAD_REQUEST); + else { + + /* + * we got a request - let's look if there something + * useful we could do with it + */ + + NamedNodeMap attribs = rootNode.getAttributes(); + if (attribs.getNamedItem("sid") != null) { + + /*********************************************************** + * lookup existing session *** + */ + + Session sess = Session.getSession(attribs.getNamedItem( + "sid").getNodeValue()); + + if (sess != null) { + dbg("incoming request for "+sess.getSID(),3); + + /******************************************************* + * check if request is valid + */ + // check if rid valid + if (attribs.getNamedItem("rid") == null) { + // rid missing + dbg("rid missing",1); + response.sendError(HttpServletResponse.SC_UNAUTHORIZED); + sess.terminate(); + } else { + try { + rid = Integer.parseInt(attribs.getNamedItem( + "rid").getNodeValue()); + } catch (NumberFormatException e) { + dbg("rid not a number",1); + response.sendError(HttpServletResponse.SC_BAD_REQUEST); + return; + } + Response r = sess.getResponse(rid); + if (r != null) { // resend + dbg("resend rid "+rid,2); + r.send(); + return; + } + if (!sess.checkValidRID(rid)) { + dbg("invalid rid "+rid,1); + response.sendError(HttpServletResponse.SC_NOT_FOUND); + sess.terminate(); + return; + } + } + + dbg("found valid rid "+rid,3); + + /* too many simultaneous requests? */ + if (sess.numPendingRequests() >= Session.MAX_REQUESTS) { + dbg("too many simultaneous requests: "+sess.numPendingRequests(),1); + response.sendError(HttpServletResponse.SC_FORBIDDEN); + // no pardon - kick it + sess.terminate(); + return; + } + + + /******************************************************* + * we got a valid request start processing it + */ + + Response jresp = new Response(response, db.newDocument()); + jresp.setRID(rid); + jresp.setContentType(sess.getContent()); + sess.addResponse(jresp); + + synchronized (sess.sock) { + /* wait till it's our turn */ + + /* NOTE: This only works when having MAX_REQUESTS set to 1 or 2 + * Otherwise we would fail on the condition that incoming data + * from the client has to be forwarded as soon as possible + * (the request might be pending waiting for others to timeout + * first) + */ + + /* wait 'till we are the lowest rid that's pending */ + + int lastrid = sess.getLastDoneRID(); + while (rid != lastrid+1) { + if (sess.isStatus(Session.SESS_TERM)) { + dbg("session terminated for "+rid,1); + response.sendError(HttpServletResponse.SC_UNAUTHORIZED); + sess.sock.notifyAll(); + return; + } + try { + dbg(rid+" waiting for "+(lastrid+1),2); + sess.sock.wait(); + dbg("bell for "+rid,2); + lastrid = sess.getLastDoneRID(); + } catch (InterruptedException e) { } + } + + dbg("handling response "+rid,3); + /* + * check key + */ + String key = sess.getKey(); + if (key != null) { + dbg("checking keys for "+rid,3); + if (attribs.getNamedItem("key") == null + || !sha1( + attribs.getNamedItem("key") + .getNodeValue()) + .equals(key)) { + dbg("Key sequence error",1); + response + .sendError(HttpServletResponse.SC_NOT_FOUND); + sess.terminate(); + return; + } + if (attribs.getNamedItem("newkey") != null) + sess.setKey(attribs.getNamedItem("newkey") + .getNodeValue()); + else + sess.setKey(attribs.getNamedItem("key") + .getNodeValue()); + dbg("key valid for "+rid,3); + } + + /* + * check if we got sth to forward to remote jabber + * server + */ + if (rootNode.hasChildNodes()) + sess.sendNodes(rootNode.getChildNodes()); + else { + /* + * check if polling too frequently only empty polls + * are considered harmfull + */ + long now = System.currentTimeMillis(); + if (sess.getHold() == 0 && + now - sess.getLastPoll() < Session.MIN_POLLING * 1000) { + // indeed (s)he's violating our rules + dbg("polling too frequently! [now:" + + now + + ", last:" + + sess.getLastPoll() + + "(" + + (now - sess.getLastPoll()) + + ")]",1); + + response.sendError(HttpServletResponse.SC_FORBIDDEN); + // no pardon - kick it + sess.terminate(); + return; + } + // mark last empty poll + sess.setLastPoll(); + + // trigger sendNodes + //sess.sendNodes(rid,null); + } + + /* + * send response + */ + /* + * TODO: check for errors to inform client of + */ + // global one's + // errors local to session + /* request to terminate session? */ + if (attribs.getNamedItem("type") != null) { + String rType = attribs.getNamedItem("type") + .getNodeValue(); + if (rType.equals("terminate")) { + sess.terminate(); + jresp.send(); + return; + } + } + + /* check incoming queue */ + NodeList nl = sess.checkInQ(rid); + // add items to response + if (nl != null) + for (int i = 0; i < nl.getLength(); i++) + jresp.addNode(nl.item(i)); + + /* check for streamid (digest auth!) */ + if (!sess.authidSent && sess.getAuthid() != null) { + sess.authidSent = true; + jresp.setAttribute("authid", sess.getAuthid()); + } + + /* finally send back response */ + jresp.send(); + sess.setLastDoneRID(jresp.getRID()); + sess.sock.notifyAll(); + } + } else { + /* session not found! */ + response.sendError(HttpServletResponse.SC_UNAUTHORIZED); + } + } else { + + /*********************************************************** + * request to create a new session *** + */ + + if (attribs.getNamedItem("rid") == null) { + response.sendError(HttpServletResponse.SC_BAD_REQUEST); + return; + } else { + try { + rid = Integer.parseInt(attribs.getNamedItem("rid") + .getNodeValue()); + } catch (NumberFormatException e) { + response + .sendError(HttpServletResponse.SC_BAD_REQUEST); + return; + } + } + + Response jresp = new Response(response, db.newDocument()); + jresp.setRID(rid); + + if (attribs.getNamedItem("to") == null + || attribs.getNamedItem("to").getNodeValue() == "") { + /* + * ERROR: 'to' attribute missing or emtpy + */ + + if (attribs.getNamedItem("content") != null) + jresp.setContentType(attribs + .getNamedItem("content").getNodeValue()); + else + jresp.setContentType(Session.DEFAULT_CONTENT); + + jresp.setAttribute("type", "terminate"); + jresp.setAttribute("condition", "improper-addressing"); + + jresp.send(); + return; + } + + /* + * really create new session + */ + try { + Session sess = new Session(attribs.getNamedItem("to") + .getNodeValue()); + + if (attribs.getNamedItem("content") != null) + sess.setContent(attribs.getNamedItem("content") + .getNodeValue()); + + if (attribs.getNamedItem("wait") != null) + sess.setWait(Integer.parseInt(attribs.getNamedItem( + "wait").getNodeValue())); + + if (attribs.getNamedItem("hold") != null) + sess.setHold(Integer.parseInt(attribs.getNamedItem( + "hold").getNodeValue())); + + if (attribs.getNamedItem("xml:lang") != null) + sess.setXMLLang(attribs.getNamedItem("xml:lang") + .getNodeValue()); + + if (attribs.getNamedItem("newkey") != null) + sess.setKey(attribs.getNamedItem("newkey") + .getNodeValue()); + + sess.addResponse(jresp); + + /* + * send back response + */ + jresp.setContentType(sess.getContent()); + + jresp.setAttribute("sid", sess.getSID()); + jresp.setAttribute("wait", String.valueOf(sess + .getWait())); + jresp.setAttribute("inactivity", String + .valueOf(Session.MAX_INACTIVITY)); + jresp.setAttribute("polling", String + .valueOf(Session.MIN_POLLING)); + + jresp.setAttribute("requests", String + .valueOf(Session.MAX_REQUESTS)); + + if (sess.getAuthid() != null) { + sess.authidSent = true; + jresp.setAttribute("authid", sess.getAuthid()); + } + + jresp.send(); + sess.setLastDoneRID(jresp.getRID()); + } catch (UnknownHostException uhe) { + /* + * ERROR: remote host unknown + */ + + if (attribs.getNamedItem("content") != null) + jresp.setContentType(attribs + .getNamedItem("content").getNodeValue()); + else + jresp.setContentType(Session.DEFAULT_CONTENT); + + jresp.setAttribute("type", "terminate"); + jresp.setAttribute("condition", "host-unknown"); + + jresp.send(); + } catch (IOException ioe) { + /* + * ERROR: could not connect to remote host + */ + + if (attribs.getNamedItem("content") != null) + jresp.setContentType(attribs + .getNamedItem("content").getNodeValue()); + else + jresp.setContentType(Session.DEFAULT_CONTENT); + + jresp.setAttribute("type", "terminate"); + jresp.setAttribute("condition", + "remote-connection-failed"); + + jresp.send(); + } catch (NumberFormatException nfe) { + response.sendError(HttpServletResponse.SC_BAD_REQUEST); + return; + } + } + } + } catch (SAXException se) { + /* + * ERROR: Parser error + */ + if (DEBUG) + System.err.println(se.toString()); + response.sendError(HttpServletResponse.SC_BAD_REQUEST); + } catch (Exception e) { + System.err.println(e.toString()); + e.printStackTrace(); + try { + Response jresp = new Response(response, db.newDocument()); + jresp.setAttribute("type", "terminate"); + jresp.setAttribute("condition", "internal-server-error"); + jresp.send(); + } catch (Exception e2) { + e2.printStackTrace(); + response.sendError(HttpServletResponse.SC_BAD_REQUEST); + } + } + } + + public void doGet(HttpServletRequest request, HttpServletResponse response) + throws IOException, ServletException { + log.debug("starting doGet"); + /* + * This one's just for convenience to inform of improper use of + * component + */ + + response.setContentType("text/html"); + PrintWriter writer = response.getWriter(); + String title = APP_NAME + " v" + APP_VERSION; + writer + .println(""); + writer.println("\n\n" + title + + "\n\n\n"); + writer.println("

" + title + "

"); + writer + .println("This is an implementation of JEP-0124 (HTTP-Binding). Please see http://www.jabber.org/jeps/jep-0124.html for details."); + writer.println("\n\n"); + } +} Index: lams_tool_chat/src/java/org/lamsfoundation/lams/tool/chat/JabberHTTPBind/Janitor.java =================================================================== diff -u --- lams_tool_chat/src/java/org/lamsfoundation/lams/tool/chat/JabberHTTPBind/Janitor.java (revision 0) +++ lams_tool_chat/src/java/org/lamsfoundation/lams/tool/chat/JabberHTTPBind/Janitor.java (revision c07351b3fba3779036610103e4becd1e9842bf97) @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2005 Stefan Strigler + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ +package org.lamsfoundation.lams.tool.chat.JabberHTTPBind; + +import java.util.Enumeration; + +/** + * @author Stefan Strigler + */ +public class Janitor implements Runnable { + public static final int SLEEPMILLIS = 1000; + + private boolean keep_running = true; + + /* + * @see java.lang.Runnable#run() + */ + public void run() { + while (this.keep_running) { + for (Enumeration e = Session.getSessions(); e.hasMoreElements();) { + Session sess = (Session) e.nextElement(); + + // stop inactive sessions + if (System.currentTimeMillis() - sess.getLastActive() > Session.MAX_INACTIVITY * 1000) { + if (JHBServlet.DEBUG) + System.err.println("Session timed out: " + sess.getSID()); + sess.terminate(); + } + } + try { + Thread.sleep(SLEEPMILLIS); + } catch (InterruptedException ie) { + ie.printStackTrace(); + } + } + } + + public void stop() { + this.keep_running = false; + } + +} \ No newline at end of file Index: lams_tool_chat/src/java/org/lamsfoundation/lams/tool/chat/JabberHTTPBind/Response.java =================================================================== diff -u --- lams_tool_chat/src/java/org/lamsfoundation/lams/tool/chat/JabberHTTPBind/Response.java (revision 0) +++ lams_tool_chat/src/java/org/lamsfoundation/lams/tool/chat/JabberHTTPBind/Response.java (revision c07351b3fba3779036610103e4becd1e9842bf97) @@ -0,0 +1,154 @@ +/* + * Copyright (C) 2005 Stefan Strigler + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ +package org.lamsfoundation.lams.tool.chat.JabberHTTPBind; + +import java.io.StringWriter; + +import javax.servlet.http.HttpServletResponse; +import javax.xml.transform.Transformer; +import javax.xml.transform.TransformerFactory; +import javax.xml.transform.dom.DOMSource; +import javax.xml.transform.stream.StreamResult; + +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.Node; + +/** + * @author Stefan Strigler + */ +public class Response { + private static TransformerFactory tff = TransformerFactory.newInstance(); + public static final String STATUS_LEAVING = "leaving"; + public static final String STATUS_PENDING = "pending"; + public static final String STATUS_DONE = "done"; + + private HttpServletResponse response; + private long cDate; + private Document doc; + private Element body; + private int rid; + + private String status; + + /** + * creates new high level response object specific to http binding + * responses + * + * @param response low level response object + * @param doc empty document to start with + */ + public Response(HttpServletResponse response, Document doc) { + this.response = response; + this.doc = doc; + + this.body = this.doc.createElement("body"); + this.doc.appendChild(this.body); + + this.body.setAttribute("xmlns","http://jabber.org/protocol/httpbind"); + + this.cDate = System.currentTimeMillis(); + + setStatus(STATUS_PENDING); + } + + /** + * adds an attribute to request's body element + * + * @param key attribute key + * @param val attribute value + * @return the response + */ + public Response setAttribute(String key, String val) { + this.body.setAttribute(key,val); + return this; + } + + /** + * sets content type header value of low-level response object + * + * @param type the content-type definition e.g. 'text/xml' + * @return the response + */ + public Response setContentType(String type) { + this.response.setContentType(type); + return this; + } + + /** + * adds node as child of replies body element + * + * @param n The node to add + * @return Returns the response again + */ + public Response addNode(Node n) { + /* make sure we set proper namespace for all nodes + * which must be 'jabber:client' + */ + try { + if (((Element) n).getAttribute("xmlns") == "") + ((Element) n).setAttribute("xmlns","jabber:client"); + } catch (ClassCastException e) { /* ? skip! */ } + this.body.appendChild(this.doc.importNode(n,true)); + return this; + } + + /** + * sends this response + */ + public synchronized void send() { + StringWriter strWtr = new StringWriter(); + StreamResult strResult = new StreamResult(strWtr); + try { + Transformer tf = tff.newTransformer(); + tf.setOutputProperty("omit-xml-declaration", "yes"); + tf.transform(new DOMSource(this.doc.getDocumentElement()), strResult); + this.response.getWriter().println(strResult.getWriter().toString()); + JHBServlet.dbg("sent response for "+this.getRID(),3); + } catch (Exception e) { + JHBServlet.dbg("XML.toString(Document): " + e,1); + } + setStatus(STATUS_DONE); + } + /** + * @return Returns the status. + */ + public synchronized String getStatus() { + return status; + } + /** + * @param status The status to set. + */ + public synchronized void setStatus(String status) { + JHBServlet.dbg("response status "+status+" for "+this.getRID(),3); + this.status = status; + } + + public int getRID() { return this.rid; } + + public Response setRID(int rid) { + this.rid = rid; + return this; + } + /** + * @return Returns the cDate. + */ + public synchronized long getCDate() { + return cDate; + } +} Index: lams_tool_chat/src/java/org/lamsfoundation/lams/tool/chat/JabberHTTPBind/Session.java =================================================================== diff -u --- lams_tool_chat/src/java/org/lamsfoundation/lams/tool/chat/JabberHTTPBind/Session.java (revision 0) +++ lams_tool_chat/src/java/org/lamsfoundation/lams/tool/chat/JabberHTTPBind/Session.java (revision c07351b3fba3779036610103e4becd1e9842bf97) @@ -0,0 +1,585 @@ +/* + * Copyright (C) 2005 Stefan Strigler + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ + +package org.lamsfoundation.lams.tool.chat.JabberHTTPBind; + +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStreamWriter; +import java.io.StringReader; +import java.io.StringWriter; +import java.net.Socket; +import java.net.UnknownHostException; +import java.util.Enumeration; +import java.util.Hashtable; +import java.util.Iterator; +import java.util.NoSuchElementException; +import java.util.Random; +import java.util.TreeMap; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.transform.Transformer; +import javax.xml.transform.TransformerFactory; +import javax.xml.transform.dom.DOMSource; +import javax.xml.transform.stream.StreamResult; + +import org.w3c.dom.Document; +import org.w3c.dom.NodeList; +import org.xml.sax.InputSource; + +/** + * this class reflects a session within http binding definition + * + * @author Stefan Strigler + */ +public class Session { + + /** + * Default HTTP Content-Type header. + */ + public static final String DEFAULT_CONTENT = "text/xml; charset=utf-8"; + + /** + * Longest allowable inactivity period (in seconds). + */ + public static final int MAX_INACTIVITY = 60; + + /** + * Maximum number of simultaneous requests allowed. + */ + public static final int MAX_REQUESTS = 2; + + /* + * ####### CONSTANTS ####### + */ + + /** + * Default value for longest time (in seconds) that the connection manager + * is allowed to wait before responding to any request during the session. + * This enables the client to prevent its TCP connection from expiring due + * to inactivity, as well as to limit the delay before it discovers any + * network failure. + */ + public static final int MAX_WAIT = 300; + + /** + * Shortest allowable polling interval (in seconds). + */ + public static final int MIN_POLLING = 2; + + /** + * Time to sleep on reading in MSEC. + */ + private static final int READ_TIMEOUT = 1; + + protected static final String SESS_START = "starting"; + protected static final String SESS_ACTIVE = "active"; + protected static final String SESS_TERM = "term"; + + /* + * ####### static ####### + */ + + private static Hashtable sessions = new Hashtable(); + + private static TransformerFactory tff = TransformerFactory.newInstance(); + + private static String createSessionID(int len) { + String charlist = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_"; + + Random rand = new Random(); + + String str = new String(); + for (int i = 0; i < len; i++) + str += charlist.charAt(rand.nextInt(charlist.length())); + return str; + } + + public static Session getSession(String sid) { + return (Session) sessions.get(sid); + } + + public static Enumeration getSessions() { + return sessions.elements(); + } + + public static void stopSessions() { + for (Enumeration e = sessions.elements(); e.hasMoreElements();) + ((Session) e.nextElement()).terminate(); + } + + /* *** + * END static + */ + + private String authid; // stream id given by remote jabber server + public boolean authidSent = false; + private String content = DEFAULT_CONTENT; + private DocumentBuilder db; + private int hold = MAX_REQUESTS - 1; + private String inQueue = ""; + private InputStreamReader isr; + private String key; + private long lastActive; + private long lastPoll = 0; + private int lastSentRid = 0; + private OutputStreamWriter osw; + private TreeMap outQueue; + private TreeMap responses; + private String status = SESS_START; + private String sid; + public Socket sock; + private String to; + private int wait = MAX_WAIT; + private String xmllang = "en"; + + /** + * Create a new session and connect to jabber server host denoted by + * to. + * + * @param to + * hostname of jabber server to connect to. + * @throws UnknownHostException + * @throws IOException + */ + public Session(String to) throws UnknownHostException, IOException { + + this.to = to; + this.setLastActive(); + + try { + this.db = DocumentBuilderFactory.newInstance().newDocumentBuilder(); + } catch (Exception e) { + } + + // connect to jabber server + try { + this.sock = new Socket(to, 5222); + + if (JHBServlet.DEBUG && this.sock.isConnected()) + System.err.println("Succesfully connected to " + to); + + // instantiate + this.osw = new OutputStreamWriter(this.sock.getOutputStream(), + "UTF-8"); + + this.osw + .write(""); + this.osw.flush(); + + // create session id + while (sessions.get(this.sid = createSessionID(24)) != null) + ; + + if (JHBServlet.DEBUG) + System.err.println("creating session with id " + this.sid); + + // register session + sessions.put(this.sid, this); + + // create list of responses + responses = new TreeMap(); + + // create list of nodes to send + outQueue = new TreeMap(); + + this.isr = new InputStreamReader(this.sock.getInputStream(), + "UTF-8"); + + String stream = this.readFromSocket(0); + + Pattern p = Pattern + .compile(".*\\.*"); + Matcher m = p.matcher(stream); + if (m.matches()) + this.authid = m.group(1); + else JHBServlet.dbg("failed to get authid",2); + + this.setStatus(SESS_ACTIVE); + } catch (UnknownHostException uhe) { + throw uhe; + } catch (IOException ioe) { + throw ioe; + } + } + + /** + * Adds new response to list of known responses. Truncates list to allowed + * size. + * + * @param resp + * the response to add + * @return this session object + */ + public synchronized Response addResponse(Response r) { + while (this.responses.size() > 0 + && this.responses.size() >= Session.MAX_REQUESTS) + this.responses.remove(this.responses.firstKey()); + return (Response) this.responses.put(new Integer(r.getRID()), r); + } + + /** + * checks InputStream from server for incoming packets blocks until request + * timeout or packets available + * + * @return nl - NodeList of incoming Nodes + */ + public NodeList checkInQ(int rid) { + NodeList nl = null; + + inQueue += this.readFromSocket(rid); + +// ((Response) this.responses.get(new Integer(rid))).setStatus(Response.STATUS_LEAVING); + + if (this.authid == null) { + Pattern p = Pattern + .compile(".*\\(.*)"); + Matcher m = p.matcher(inQueue); + if (m.matches()) { + this.authid = m.group(1); + inQueue = m.group(2); + } else JHBServlet.dbg("failed to get authid",2); + } + + // try to parse it + if (!inQueue.equals("")) { + try { + /* + * wrap inQueue with element so that multiple nodes can be + * parsed + */ + Document doc = db.parse(new InputSource(new StringReader( + "" + inQueue + ""))); + nl = doc.getFirstChild().getChildNodes(); + inQueue = ""; // reset! + } catch (Exception e3) { /* skip this */ + } + } + this.setLastActive(); + return nl; + } + + /** + * Checks whether given request ID is valid within context of this session. + * + * @param rid + * Request ID to be checked + * @return true if rid is valid + */ + public synchronized boolean checkValidRID(int rid) { + try { + if (rid <= ((Integer) this.responses.lastKey()).intValue() + + MAX_REQUESTS && + rid >= ((Integer) this.responses.firstKey()).intValue()) + return true; + else { + JHBServlet.dbg("invalid request id: " + rid + + " (last: " + + ((Integer) this.responses.lastKey()).intValue() + + ")",1); + return false; + } + } catch (NoSuchElementException e) { + return false; + } + } + + public String getAuthid() { + return this.authid; + } + + public String getContent() { + return this.content; + } + + public int getHold() { + return this.hold; + } + + /** + * @return Returns the key. + */ + public synchronized String getKey() { + return key; + } + + /** + * @return Returns the lastActive. + */ + public synchronized long getLastActive() { + return lastActive; + } + + /** + * @return Returns the lastPoll. + */ + public synchronized long getLastPoll() { + return lastPoll; + } + + /** + * lookup response for given request id + * + * @param rid + * Request id associated with response + * @return the response if found, null otherwise + */ + public synchronized Response getResponse(int rid) { + return (Response) this.responses.get(new Integer(rid)); + } + + public String getSID() { + return this.sid; + } + + /* + * ######## getters ######### + */ + + public String getTo() { + return this.to; + } + + public int getWait() { + return this.wait; + } + + public String getXMLLang() { + return this.xmllang; + } + + public synchronized int numPendingRequests() { + int num_pending = 0; + Iterator it = this.responses.values().iterator(); + while (it.hasNext()) { + Response r = (Response) it.next(); + if (!r.getStatus().equals(Response.STATUS_DONE)) + num_pending++; + } + return num_pending; + } + private int lastDoneRID; + public synchronized int getLastDoneRID() { + return this.lastDoneRID; + +// Iterator it = this.responses.values().iterator(); +// int last_done = 0; +// while (it.hasNext()) { + /* TODO better to traverse in reverted order to be able to stop + * on first found + */ +// Response r = (Response) it.next(); +// if (r.getStatus().equals(Response.STATUS_DONE)) +// last_done = r.getRID(); +// } +// return last_done; + } + + /** + * reads from socket + * + * @return string that was read + */ + private String readFromSocket(int rid) { + String retval = ""; + char buf[] = new char[16]; + int c = 0; + + Response r = this.getResponse(rid); + +// synchronized (this.sock) { + while (!this.sock.isClosed() && !this.isStatus(SESS_TERM)) { + this.setLastActive(); + try { + if (this.isr.ready()) { + while (this.isr.ready() && + (c = this.isr.read(buf, 0, buf.length)) >= 0) + retval += new String(buf, 0, c); + break; // got sth. to send + } else { + if ((this.hold == 0 && + System.currentTimeMillis() - this.getLastActive() > 200) || + /* makes polling clients feel a little bit more responsive */ + (this.hold > 0 && ( + (r != null && System.currentTimeMillis() - r.getCDate() >= this.getWait()*1000 ) || + this.numPendingRequests() > this.getHold() || + !retval.equals("")))) { + JHBServlet.dbg("readFromSocket done for "+rid,3); + break; // time exeeded + } + + try { + Thread.sleep(READ_TIMEOUT); // wait for incoming + // packets + } catch (InterruptedException ie) { + System.err.println(ie.toString()); + } + } + } catch (IOException e) { + System.err.println("Can't read from socket"); + this.terminate(); + } + } +// } + /* + * TODO if (this.sock.isClosed()) indicate an error + * (remote-connection-failed) + */ + + return retval; + } + + /** + * sends all nodes in list to remote jabber server make sure that nodes get + * sent in requested order + * + * @param nl + * list of nodes to send + * @return the session itself + */ +// public synchronized Session sendNodes(int rid, NodeList aNL) { + public Session sendNodes(NodeList nl) { + // init lastSentRid +// if (lastSentRid == 0) +// lastSentRid = rid - 1; + + // add to queue +// outQueue.put(new Integer(rid), aNL); + + // build a string + String out = ""; + StreamResult strResult = new StreamResult(); + +// while (!outQueue.isEmpty() +// && ((Integer) outQueue.firstKey()).intValue() == lastSentRid + 1) { +// lastSentRid = ((Integer) outQueue.firstKey()).intValue(); +// NodeList nl = (NodeList) outQueue.remove(outQueue.firstKey()); +// if (nl == null) +// continue; + try { + Transformer tf = tff.newTransformer(); + tf.setOutputProperty("omit-xml-declaration", "yes"); + // loop list + for (int i = 0; i < nl.getLength(); i++) { + strResult.setWriter(new StringWriter()); + tf.transform(new DOMSource(nl.item(i)), strResult); + String tStr = strResult.getWriter().toString(); + out += tStr; + } + } catch (Exception e) { + System.err.println("XML.toString(Document): " + e); + } +// } + try { + this.osw.write(out); + this.osw.flush(); + } catch (IOException ioe) { + System.err.println(this.sid + " failed to write to stream"); + } + + return this; + } + + public Session setContent(String content) { + this.content = content; + return this; + } + + /* + * ######## setters ######### + */ + + public Session setHold(int hold) { + if (hold < MAX_REQUESTS && hold >= 0) + this.hold = hold; + return this; + } + + /** + * @param key + * The key to set. + */ + public synchronized void setKey(String key) { + this.key = key; + } + + /** + * set lastActive to current timestamp. + */ + public synchronized void setLastActive() { + this.lastActive = System.currentTimeMillis(); + } + + public synchronized void setLastDoneRID(int rid) { + this.lastDoneRID = rid; + } + + /** + * set lastPoll to current timestamp. + */ + public synchronized void setLastPoll() { + this.lastPoll = System.currentTimeMillis(); + } + + public int setWait(int wait) { + if (wait < 0) + wait = 0; + if (wait > MAX_WAIT) + wait = MAX_WAIT; + this.wait = wait; + return wait; + } + + public Session setXMLLang(String xmllang) { + this.xmllang = xmllang; + return this; + } + + public synchronized void setStatus(String status) { + this.status = status; + } + + public synchronized boolean isStatus(String status) { + return (this.status == status); + } + + /** + * kill this session + * + */ + public void terminate() { + JHBServlet.dbg("terminating session " + this.getSID(),2); + this.setStatus(SESS_TERM); + synchronized (this.sock) { + if (!this.sock.isClosed()) { + try { + this.osw.write(""); + this.osw.flush(); + this.sock.close(); + } catch (IOException ie) { } + } + this.sock.notifyAll(); + } + sessions.remove(this.sid); + } +}