/* Copyright (c) 2008 Bluendo S.r.L. * See about.html for details about license. * * $Id: BasicXmlStream.java 1578 2009-06-16 11:07:59Z luca $ */ package it.yup.xmlstream; import it.yup.transport.TransportListener; //#mdebug import it.yup.util.Logger; //#enddebug import it.yup.xml.Element; import it.yup.xmpp.Contact; import it.yup.xmpp.XMPPClient; import it.yup.xmpp.packets.Iq; import java.util.Date; import java.util.Enumeration; import java.util.Hashtable; import java.util.Vector; public abstract class BasicXmlStream implements TransportListener, StreamEventListener { /* * The registration for mySelf is used for unmatched stanza registration; */ private static EventQueryRegistration unmatchedStanzaReg = null; /** packets waiting for being sent */ protected Vector sendQueue = new Vector(10); /** Storing XPath like queries and relative packet listeners */ public static Vector eventListeners = new Vector(10); /* EVENT CONSTANTS */ public static String STREAM_CONNECTED = "_01"; public static String STREAM_INITIALIZED = "_02"; // public static String STREAM_DISCONNECTED = "_03"; public static String STREAM_ERROR = "_04"; // public static String STREAM_RESOURCE_BOUND = "_05"; // public static String STREAM_SESSION_OPENED = "_06"; public static String STREAM_ACCOUNT_REGISTERED = "_07"; public static String CONNECTION_LOST = "_08"; public static String STREAM_TERMINATED = "_09"; // public static String AUTHENTICACTION_FAILED = "_09"; public static String REGISTRATION_FAILED = "_10"; public static String CONNECTION_FAILED = "_11"; public static String NOT_AUTHORIZED = "_12"; //public static String STREAM_REGISTRATION_ERROR = "_08"; public static String TLS_INITIALIZED = "_13"; public static String STREAM_AUTHENTICATED = "_14"; public static String COMPRESSION_INITIALIZED = "_15"; public static String UNMATCHED_STANZA = "_16"; /** Session ID for this stream */ protected String SID = null; /* configuration properties */ public static final String USERNAME = "1"; public static final String PASSWORD = "2"; /* User related data */ /** Session jid */ public String jid = null; /** Password used for authentication */ protected String password = null; /** * Stream features, mapping namespace to relevant dom Element */ protected Hashtable features = new Hashtable(); /** Initializers */ protected Vector initializers = new Vector(); /** Iterate through initializers in subsequent {@link BasicXmlStream#nextInitializer()} * calls */ protected Enumeration initializerInterator = null; /** * Class used for associating packet listeners and queries */ protected static class ListenerRegistration { public EventQuery query; public Object listener; public boolean oneTime; public ListenerRegistration(EventQuery query, Object listener, boolean oneTime) { this.query = query; this.listener = listener; this.oneTime = oneTime; } } protected BasicXmlStream() { // prepare the default initializers // #ifdef TLS if (XMPPClient.getInstance().addTLS) initializers.addElement(new TLSInitializer()); // #endif // #ifdef COMPRESSION if (XMPPClient.getInstance().addCompression) initializers .addElement(new CompressionInitializer()); // #endif initializers.addElement(new SASLAuthenticator()); initializers.addElement(new ResourceBinding()); initializers.addElement(new SessionOpener()); eventListeners.removeAllElements(); EventQuery eq = new EventQuery(BasicXmlStream.UNMATCHED_STANZA, null, null); unmatchedStanzaReg = addEventListener(eq, this); } /** Initialize the stream * @param jid * jid with or without resource; in the first case the resource is taken as a request * (the server may override it) * @param domain * @param password * */ public abstract void initialize(String jid, String password); /** * Send a XMPP packet. It's possible to set a maximum wait time in order to send a packet * also when cheap connections aren't available. * * @param packetToSend * the XMPP packet to send * @param maxWait * maximum time a packet can wait before sending it (-1 for sending it * only when a cheap connection is available). This paramenter is only for * compatibility with future extensions */ public void send(Element packetToSend, int maxWait) { // prepare the packet to send packetToSend.queueTime = new Date().getTime(); packetToSend.maxWait = maxWait; synchronized (sendQueue) { this.sendQueue.addElement(packetToSend); } tryToSend(); } /** Restart a stream (used during initialization)*/ protected abstract void restart(); protected Vector getPacketsToSend(boolean onlyUrgent) { Vector packetsToSend = new Vector(); synchronized (sendQueue) { if (onlyUrgent) { // try to send the most urgent Enumeration en = sendQueue.elements(); // the packets due in the next second long aBitLater = (new Date()).getTime() + 1000; while (en.hasMoreElements()) { Element ithPacket = ((Element) en.nextElement()); if (ithPacket.maxWait > 0 && (ithPacket.queueTime + 1000 * ithPacket.maxWait) > aBitLater) { packetsToSend.addElement(ithPacket); // this is the first place to look for an error sendQueue.removeElement(ithPacket); } } } else { Enumeration en = sendQueue.elements(); while (en.hasMoreElements()) { packetsToSend.addElement(en.nextElement()); } sendQueue.removeAllElements(); } } return packetsToSend; } /** * Method starting the send process, only if necessary * */ protected abstract void tryToSend(); /** * Add an event listener, it may be either a {@link PacketListener} or a {@link StreamEventListener} * @param query * @param listener either a {@link PacketListener} or a {@link StreamEventListener} * @return the registration object that may be used for unregistering the listener */ public static EventQueryRegistration addEventListener(EventQuery query, Object listener) { ListenerRegistration ld = new ListenerRegistration(query, listener, false); synchronized (eventListeners) { eventListeners.addElement(ld); } return new EventQueryRegistration(ld, eventListeners); } /** * Remove an event listener passing the {@link EventQueryRegistration} received from * {@link BasicXmlStream#addEventListener(EventQuery, Object)} or * {@link BasicXmlStream#addOnetimeEventListener(EventQuery, Object)} * @param registration */ public static void removeEventListener(EventQueryRegistration registration) { registration.remove(); } /** * Add an event listener that can be fired only one, it may be either a * {@link PacketListener} or a {@link StreamEventListener} * @param query * @param listener either a {@link PacketListener} or a {@link StreamEventListener} * @return the registration object that may be used for unregistering the listener */ public static EventQueryRegistration addOnetimeEventListener( EventQuery query, Object listener) { ListenerRegistration ld = new ListenerRegistration(query, listener, true); synchronized (eventListeners) { eventListeners.addElement(ld); } return new EventQueryRegistration(ld, eventListeners); } /** * Call the packet listeners registered for this packet * @param stanza */ protected void promotePacket(Element stanza) { boolean matched = false; try { // #ifdef TIMING long t1 = System.currentTimeMillis(); // #endif // XXX transform into a preprocessor macro // Uncomment for logging the number of listeners //System.out.println("---->" + eventListeners.size()); Enumeration enPacketListener = null; synchronized (eventListeners) { enPacketListener = eventListeners.elements(); } while (enPacketListener.hasMoreElements()) { ListenerRegistration listenerData = (ListenerRegistration) enPacketListener .nextElement(); //Uncomment for dumping registered listeners // EventQuery q = listenerData.query; // String tab = ">>"; // while(q!=null) { // System.out.println(tab + q.event); // if(q.tagAttrNames != null) { // for(int i=0; i<q.tagAttrNames.length; i++) { // System.out.println(tab +">" + q.tagAttrNames[i] +": " + q.tagAttrValues[i]); // } // } // q = q.child; // // tab += ">>"; // // } if (areMatching(stanza, listenerData.query)) { // #ifdef TIMING long t2 = System.currentTimeMillis(); // #endif matched = true; ((PacketListener) listenerData.listener) .packetReceived(stanza); if (listenerData.oneTime == true) { synchronized (eventListeners) { eventListeners.removeElement(listenerData); } } // #ifdef TIMING EventQuery q = listenerData.query; System.out.println("L " + q.event + ":" + (System.currentTimeMillis() - t2)); // #endif } } // #ifdef TIMING System.out.println("Promote: " + (System.currentTimeMillis() - t1)); // #endif } catch (RuntimeException e) { // XXX don't knwow if here we must do something like closing the stream // #mdebug e.printStackTrace(); Logger.log(new String(stanza.toXml())); Logger.log("[BasicXmlStream::promotePacket] RuntimeException: " + e.getClass().getName() + "\n" + e.getMessage()); // #enddebug } if (matched == false) { try { dispatchEvent(BasicXmlStream.UNMATCHED_STANZA, stanza); } catch (Exception e) { // #mdebug e.printStackTrace(); Logger.log(new String(stanza.toXml())); Logger.log("[BasicXmlStream::promotePacket] RuntimeException: " + e.getClass().getName() + "\n" + e.getMessage()); // #enddebug } } } /** * Verify if a packet matches a query * @param receivedPacket * @param query * @return */ protected boolean areMatching(Element receivedPacket, EventQuery query) { /* better stating first a condition that fails immediatly the check * (just readability issue) */ if (!query.event.equals(receivedPacket.name) && !query.event.equals(EventQuery.ANY_PACKET)) { return false; } // then check all the attributes if the query has any if (query.tagAttrNames != null) { for (int l = 0; l < query.tagAttrNames.length; l++) { String lthName = query.tagAttrNames[l]; String lthValue = query.tagAttrValues[l]; if ("xmlns".equals(lthName) && lthValue.equals(receivedPacket.uri)) { continue; } else { String val = receivedPacket.getAttribute(lthName); if (val == null || !val.equals(lthValue)) { return false; } } } } /* a packet with no child doesn't match a query with a child sub-query */ Element[] children = receivedPacket.getChildren(); if (query.child != null && children != null && children.length == 0) { return false; } // all attributes verified, check the children if (query.child != null) { for (int i = 0; i < children.length; i++) { Element ithChild = children[i]; if (areMatching(ithChild, query.child)) { return true; } } return false; } return true; } /** * Dispatch an XmlStream event * */ protected void dispatchEvent(String event, Object source) { // Vector listeners = BasicXmlStream.eventListeners; // Enumeration en = eventListeners.elements(); // while (en.hasMoreElements()) { // ListenerRegistration listenerData = (ListenerRegistration) en // .nextElement(); // if (listenerData.query.event.equals(EventQuery.ANY_EVENT) // || event.equals(listenerData.query.event)) { // ((StreamEventListener) listenerData.listener).gotStreamEvent( // event, source); // if (listenerData.oneTime) { // synchronized (eventListeners) { // eventListeners.removeElement(listenerData); // } // } // } // } ListenerRegistration[] regs = null; synchronized (eventListeners) { regs = new ListenerRegistration[eventListeners.size()]; eventListeners.copyInto(regs); } for (int i = 0; i < regs.length; i++) { ListenerRegistration listenerData = regs[i]; if (listenerData.query.event.equals(EventQuery.ANY_EVENT) || event.equals(listenerData.query.event)) { ((StreamEventListener) listenerData.listener).gotStreamEvent( event, source); if (listenerData.oneTime) { synchronized (eventListeners) { eventListeners.removeElement(listenerData); } } } } } public void gotStreamEvent(String event, Object source) { if (event.equals(BasicXmlStream.UNMATCHED_STANZA) && source instanceof Element) { Element sSource = (Element) source; if (sSource.name.equals(Iq.IQ)) { String type = sSource.getAttribute(Iq.ATT_TYPE); if (type.equals(Iq.T_GET) || type.equals(Iq.T_SET)) { Element replyIq = new Element(sSource); replyIq.setAttribute(Iq.ATT_TO, replyIq .getAttribute(Iq.ATT_FROM)); replyIq.delAttribute(Iq.ATT_FROM); replyIq.setAttribute(Iq.ATT_TYPE, Iq.T_ERROR); Element error = replyIq.addElement(null, Iq.T_ERROR); error.setAttribute(Iq.ATT_TYPE, "cancel"); error.addElement("urn:ietf:params:xml:ns:xmpp-stanzas", "feature-not-implemented"); XMPPClient.getInstance().sendPacket(replyIq); } } } } /** * Start the feature chain * @param features */ protected void processFeatures(Element features[]) { this.features.clear(); this.initializerInterator = null; for (int i = 0; i < features.length; i++) { this.features.put(features[i].uri, features[i]); } // received a set of features trigger the stream initialization nextInitializer(); } /** * Call the next stream initialiazer. * Dispatch {@link XmlStream#STREAM_INITIALIZATION_FINISHED} when all the * initializers have been processed */ public void nextInitializer() { if (initializerInterator == null) { initializerInterator = initializers.elements(); } while (initializerInterator.hasMoreElements()) { Initializer initializer = (Initializer) initializerInterator .nextElement(); if (initializer.matchFeatures(features)) { initializer.start(this); return; } } initializerInterator = null; dispatchEvent(BasicXmlStream.STREAM_INITIALIZED, null); } public void addInitializer(Initializer initializer, int position) { this.initializers.insertElementAt(initializer, position); } public void removeInitializer(Initializer initializer) { this.initializers.removeElement(initializer); } /** * Initializer that binds a resource */ private class ResourceBinding extends Initializer implements PacketListener { public ResourceBinding() { super("urn:ietf:params:xml:ns:xmpp-bind", false); } public void start(BasicXmlStream xmlStream) { this.stream = xmlStream; Iq iq = new Iq(null, "set"); Element bind = new Element(namespace, "bind"); String s = Contact.resource(xmlStream.jid); if (s != null) { bind.addElementAndContent(namespace, "resource", s); } iq.addElement(bind); EventQuery q = new EventQuery("iq", new String[] { "id" }, new String[] { iq.getAttribute("id") }); BasicXmlStream.addOnetimeEventListener(q, this); stream.send(iq, -1); } public void packetReceived(Element e) { if (Iq.T_RESULT.equals(e.getAttribute("type"))) { Element bind = e.getChildByName(null, "bind"); Element jid = null; if (bind != null && (jid = bind.getChildByName(null, "jid")) != null && jid.getText() != null) { stream.jid = jid.getText(); } stream.nextInitializer(); } else { stream.dispatchEvent(BasicXmlStream.STREAM_ERROR, "cannot bind resource"); } } } /** * Initialiazer that opens a session * */ private class SessionOpener extends Initializer implements PacketListener { public SessionOpener() { super("urn:ietf:params:xml:ns:xmpp-session", true); } public void start(BasicXmlStream xmlStream) { this.stream = xmlStream; Iq iq = new Iq(null, "set"); Element session = new Element(namespace, "session"); iq.addElement(session); EventQuery q = new EventQuery("iq", new String[] { "id" }, new String[] { iq.getAttribute("id") }); BasicXmlStream.addOnetimeEventListener(q, this); stream.send(iq, -1); } public void packetReceived(Element e) { if ("result".equals(e.getAttribute("type"))) { stream.nextInitializer(); } else { stream.dispatchEvent(BasicXmlStream.STREAM_ERROR, "cannot start session"); } } } }