You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@mina.apache.org by be...@apache.org on 2012/04/20 17:45:00 UTC

svn commit: r1328417 - in /mina/vysper/trunk/server/extensions/xep0124-xep0206-bosh/src: main/java/org/apache/vysper/xmpp/extension/xep0124/ test/java/org/apache/vysper/xmpp/extension/xep0124/

Author: berndf
Date: Fri Apr 20 15:45:00 2012
New Revision: 1328417

URL: http://svn.apache.org/viewvc?rev=1328417&view=rev
Log:
+ rename writeBoshResponse() (again)
+ don't create standard stanzas over and over again
+ create less short-lived objects when merging
+ move helper methods from BoshHandler to static utilities
+ improve error handling and logging

Added:
    mina/vysper/trunk/server/extensions/xep0124-xep0206-bosh/src/main/java/org/apache/vysper/xmpp/extension/xep0124/BoshStanzaUtils.java
Modified:
    mina/vysper/trunk/server/extensions/xep0124-xep0206-bosh/src/main/java/org/apache/vysper/xmpp/extension/xep0124/BoshBackedSessionContext.java
    mina/vysper/trunk/server/extensions/xep0124-xep0206-bosh/src/main/java/org/apache/vysper/xmpp/extension/xep0124/BoshEndpoint.java
    mina/vysper/trunk/server/extensions/xep0124-xep0206-bosh/src/main/java/org/apache/vysper/xmpp/extension/xep0124/BoshHandler.java
    mina/vysper/trunk/server/extensions/xep0124-xep0206-bosh/src/main/java/org/apache/vysper/xmpp/extension/xep0124/BoshRequest.java
    mina/vysper/trunk/server/extensions/xep0124-xep0206-bosh/src/test/java/org/apache/vysper/xmpp/extension/xep0124/BoshBackedSessionContextTest.java
    mina/vysper/trunk/server/extensions/xep0124-xep0206-bosh/src/test/java/org/apache/vysper/xmpp/extension/xep0124/BoshHandlerTest.java

Modified: mina/vysper/trunk/server/extensions/xep0124-xep0206-bosh/src/main/java/org/apache/vysper/xmpp/extension/xep0124/BoshBackedSessionContext.java
URL: http://svn.apache.org/viewvc/mina/vysper/trunk/server/extensions/xep0124-xep0206-bosh/src/main/java/org/apache/vysper/xmpp/extension/xep0124/BoshBackedSessionContext.java?rev=1328417&r1=1328416&r2=1328417&view=diff
==============================================================================
--- mina/vysper/trunk/server/extensions/xep0124-xep0206-bosh/src/main/java/org/apache/vysper/xmpp/extension/xep0124/BoshBackedSessionContext.java (original)
+++ mina/vysper/trunk/server/extensions/xep0124-xep0206-bosh/src/main/java/org/apache/vysper/xmpp/extension/xep0124/BoshBackedSessionContext.java Fri Apr 20 15:45:00 2012
@@ -20,6 +20,7 @@
 package org.apache.vysper.xmpp.extension.xep0124;
 
 import java.io.IOException;
+import java.util.ArrayList;
 import java.util.LinkedList;
 import java.util.Queue;
 import java.util.SortedMap;
@@ -201,7 +202,7 @@ public class BoshBackedSessionContext ex
     synchronized public void write(Stanza stanza) {
         if (stanza == null) throw new IllegalArgumentException("stanza must not be null.");
         LOGGER.debug("adding server stanza for writing to BOSH client");
-        writeBOSHResponse(boshHandler.wrapStanza(stanza));
+        writeBoshResponse(BoshStanzaUtils.wrapStanza(stanza));
     }
 
     /**
@@ -212,9 +213,9 @@ public class BoshBackedSessionContext ex
      * 
      * @param responseStanza The BOSH response to write
      */
-    /*package*/ void writeBOSHResponse(Stanza responseStanza) {
+    /*package*/ void writeBoshResponse(Stanza responseStanza) {
         if (responseStanza == null) throw new IllegalArgumentException();
-        final boolean isEmtpyResponse = responseStanza == BoshHandler.EMPTY_BOSH_RESPONSE;
+        final boolean isEmtpyResponse = responseStanza == BoshStanzaUtils.EMPTY_BOSH_RESPONSE;
         
         BoshRequest req;
         BoshResponse boshResponse;
@@ -283,8 +284,8 @@ public class BoshBackedSessionContext ex
         final Long rid = br.getRid();
         requestsWindow.put(rid, br);
         BoshRequest req = requestsWindow.remove(requestsWindow.firstKey());
-        Stanza body = boshHandler.getTerminateResponse();
-        body = boshHandler.addAttribute(body, "condition", condition);
+        Stanza body = BoshStanzaUtils.TERMINATE_BOSH_RESPONSE;
+        body = BoshStanzaUtils.addAttribute(body, "condition", condition);
         BoshResponse boshResponse = getBoshResponse(body, null);
         if (LOGGER.isDebugEnabled()) {
             LOGGER.debug("rid = {} - BOSH writing response: {}", rid, new String(boshResponse.getContent()));
@@ -302,7 +303,7 @@ public class BoshBackedSessionContext ex
         // respond to all the queued HTTP requests with termination responses
         while (!requestsWindow.isEmpty()) {
             BoshRequest req = requestsWindow.remove(requestsWindow.firstKey());
-            Stanza body = boshHandler.getTerminateResponse();
+            Stanza body = BoshStanzaUtils.TERMINATE_BOSH_RESPONSE;
             BoshResponse boshResponse = getBoshResponse(body, null);
             if (LOGGER.isDebugEnabled()) {
                 LOGGER.debug("rid = {} - BOSH writing response: {}", req.getRid(), new String(boshResponse.getContent()));
@@ -411,19 +412,23 @@ public class BoshBackedSessionContext ex
      * specified by the client in its request, whichever is lower.
      * @param version the BOSH version
      */
-    public void setBoshVersion(String version) {
-        String[] v = boshVersion.split("\\.");
-        int major = Integer.parseInt(v[0]);
-        int minor = Integer.parseInt(v[1]);
-        v = version.split("\\.");
-
-        if (v.length == 2) {
-            int clientMajor = Integer.parseInt(v[0]);
-            int clientMinor = Integer.parseInt(v[1]);
-
-            if (clientMajor < major || (clientMajor == major && clientMinor < minor)) {
-                boshVersion = version;
+    public void setBoshVersion(String version) throws NumberFormatException {
+        try {
+            String[] v = boshVersion.split("\\.");
+            int major = Integer.parseInt(v[0]);
+            int minor = Integer.parseInt(v[1]);
+            v = version.split("\\.");
+
+            if (v.length == 2) {
+                int clientMajor = Integer.parseInt(v[0]);
+                int clientMinor = Integer.parseInt(v[1]);
+    
+                if (clientMajor < major || (clientMajor == major && clientMinor < minor)) {
+                    boshVersion = version;
+                }
             }
+        } catch (NumberFormatException e) {
+            throw e;
         }
     }
 
@@ -474,7 +479,7 @@ public class BoshBackedSessionContext ex
         }
         LOGGER.debug("rid = {} - BOSH request expired", req.getRid());
         while (!requestsWindow.isEmpty() && requestsWindow.firstKey() <= req.getRid()) {
-            writeBOSHResponse(BoshHandler.EMPTY_BOSH_RESPONSE);
+            writeBoshResponse(BoshStanzaUtils.EMPTY_BOSH_RESPONSE);
         }
     }
 
@@ -484,8 +489,9 @@ public class BoshBackedSessionContext ex
      * 
      * @param req the HTTP request
      */
-    public void insertRequest(BoshRequest br) {
+    public void insertRequest(final BoshRequest br) {
 
+        final Stanza stanza = br.getBody();
         final Long rid = br.getRid();
         LOGGER.debug("rid = {} - inserting new BOSH request", rid);
         
@@ -517,14 +523,14 @@ public class BoshBackedSessionContext ex
                 }
                 return;
             }
-            if (requestsWindow.size() + 1 > currentRequests && !"terminate".equals(br.getBody().getAttributeValue("type"))
-                    && br.getBody().getAttributeValue("pause") == null) {
+            if (requestsWindow.size() + 1 > currentRequests && !"terminate".equals(stanza.getAttributeValue("type"))
+                    && stanza.getAttributeValue("pause") == null) {
                 LOGGER.warn("BOSH Overactivity: Too many simultaneous requests");
                 error(br, "policy-violation");
                 return;
             }
-            if (requestsWindow.size() + 1 == currentRequests && !"terminate".equals(br.getBody().getAttributeValue("type"))
-                    && br.getBody().getAttributeValue("pause") == null && br.getBody().getInnerElements().isEmpty()) {
+            if (requestsWindow.size() + 1 == currentRequests && !"terminate".equals(stanza.getAttributeValue("type"))
+                    && stanza.getAttributeValue("pause") == null && stanza.getInnerElements().isEmpty()) {
                 if (!requestsWindow.isEmpty()
                         && br.getTimestamp() - requestsWindow.get(requestsWindow.lastKey()).getTimestamp() < polling * 1000) {
                     LOGGER.warn("BOSH Overactivity: Too frequent requests");
@@ -532,7 +538,7 @@ public class BoshBackedSessionContext ex
                     return;
                 }
             }
-            if ((wait == 0 || hold == 0) && br.getBody().getInnerElements().isEmpty()) {
+            if ((wait == 0 || hold == 0) && stanza.getInnerElements().isEmpty()) {
                 if (latestEmptyPollingRequest != null && br.getTimestamp() - latestEmptyPollingRequest.getTimestamp() < polling * 1000) {
                     LOGGER.warn("BOSH Overactivity for polling: Too frequent requests");
                     error(br, "policy-violation");
@@ -561,7 +567,7 @@ public class BoshBackedSessionContext ex
 
         if (isClientAcknowledgements()) {
             synchronized (sentResponses) {
-                if (br.getBody().getAttribute("ack") == null) {
+                if (stanza.getAttribute("ack") == null) {
                     // if there is no ack attribute present then the client confirmed it received all the responses to all the previous requests
                     // and we clear the cache
                     sentResponses.clear();
@@ -570,7 +576,7 @@ public class BoshBackedSessionContext ex
                     // the connection manager MAY inform the client of the situation. In this case it SHOULD include a 'report' attribute set
                     // to one greater than the 'ack' attribute it received from the client, and a 'time' attribute set to the number of milliseconds
                     // since it sent the response associated with the 'report' attribute.
-                    long ack = Long.parseLong(br.getBody().getAttributeValue("ack"));
+                    long ack = Long.parseLong(stanza.getAttributeValue("ack"));
                     if (ack < sentResponses.lastKey() && sentResponses.containsKey(ack + 1)) {
                         long delta = System.currentTimeMillis() - sentResponses.get(ack + 1).getTimestamp();
                         if (delta >= brokenConnectionReportTimeout) {
@@ -585,15 +591,17 @@ public class BoshBackedSessionContext ex
         // we cannot pause if there are missing requests, this is tested with
         // br.getRid().equals(requestsWindow.lastKey()) && highestReadRid.equals(br.getRid())
         synchronized (requestsWindow) {
-            if (br.getBody().getAttribute("pause") != null && rid.equals(requestsWindow.lastKey()) && highestReadRid.equals(rid)) {
-                int pause = Integer.parseInt(br.getBody().getAttributeValue("pause"));
-                if (pause > maxpause) {
-                    // do not allow to pause more than maxpause
-                    pause = maxpause;
-                }
-                if (pause < 0) {
-                    pause = 0;
+            final String pauseAttribute = stanza.getAttributeValue("pause");
+            if (pauseAttribute != null && rid.equals(requestsWindow.lastKey()) && highestReadRid.equals(rid)) {
+                int pause;
+                try {
+                    pause = Integer.parseInt(pauseAttribute);
+                } catch (NumberFormatException e) {
+                    error(br, "bad-request");
+                    return;
                 }
+                pause = Math.max(0, pause);
+                pause = Math.min(pause, maxpause);
                 respondToPause(pause);
                 return;
             }
@@ -602,19 +610,21 @@ public class BoshBackedSessionContext ex
         // If there are delayed responses waiting to be sent to the BOSH client, then we wrap them all in
         // a <body/> element and send them as a HTTP response to the current HTTP request.
         Stanza delayedResponse;
-        Stanza mergedResponse = null;
+        ArrayList<Stanza> mergeCandidates = null; // do not create until there is a delayed response
         while ((delayedResponse = delayedResponseQueue.poll()) != null) {
-            mergedResponse = boshHandler.mergeResponses(mergedResponse, delayedResponse);
+            if (mergeCandidates == null) mergeCandidates = new ArrayList<Stanza>();
+            mergeCandidates.add(delayedResponse);
         }
+        Stanza mergedResponse = BoshStanzaUtils.mergeResponses(mergeCandidates);
         if (mergedResponse != null) {
-            writeBOSHResponse(mergedResponse);
+            writeBoshResponse(mergedResponse);
             return;
         }
 
         // If there are more suspended enqueued requests than it is allowed by the BOSH 'hold' parameter,
         // than we release the oldest one by sending an empty response.
         if (requestsWindow.size() > hold) {
-            writeBOSHResponse(BoshHandler.EMPTY_BOSH_RESPONSE);
+            writeBoshResponse(BoshStanzaUtils.EMPTY_BOSH_RESPONSE);
         }
     }
     
@@ -626,15 +636,15 @@ public class BoshBackedSessionContext ex
             if (boshRequest == null) {
                 break;
             }
-            writeBOSHResponse(BoshHandler.EMPTY_BOSH_RESPONSE);
+            writeBoshResponse(BoshStanzaUtils.EMPTY_BOSH_RESPONSE);
         }
     }
     
     protected void sendBrokenConnectionReport(long report, long delta) {
-        Stanza body = boshHandler.getTerminateResponse();
-        body = boshHandler.addAttribute(body, "report", Long.toString(report));
-        body = boshHandler.addAttribute(body, "time", Long.toString(delta));
-        writeBOSHResponse(body);
+        Stanza body = BoshStanzaUtils.TERMINATE_BOSH_RESPONSE;
+        body = BoshStanzaUtils.addAttribute(body, "report", Long.toString(report));
+        body = BoshStanzaUtils.addAttribute(body, "time", Long.toString(delta));
+        writeBoshResponse(body);
     }
     
     protected void addContinuationExpirationListener(final AsyncContext context) {
@@ -678,7 +688,7 @@ public class BoshBackedSessionContext ex
 
     protected BoshResponse getBoshResponse(Stanza stanza, Long ack) {
         if (ack != null) {
-            stanza = boshHandler.addAttribute(stanza, "ack", ack.toString());
+            stanza = BoshStanzaUtils.addAttribute(stanza, "ack", ack.toString());
         }
         byte[] content = new Renderer(stanza).getComplete().getBytes();
         return new BoshResponse(contentType, content);

Modified: mina/vysper/trunk/server/extensions/xep0124-xep0206-bosh/src/main/java/org/apache/vysper/xmpp/extension/xep0124/BoshEndpoint.java
URL: http://svn.apache.org/viewvc/mina/vysper/trunk/server/extensions/xep0124-xep0206-bosh/src/main/java/org/apache/vysper/xmpp/extension/xep0124/BoshEndpoint.java?rev=1328417&r1=1328416&r2=1328417&view=diff
==============================================================================
--- mina/vysper/trunk/server/extensions/xep0124-xep0206-bosh/src/main/java/org/apache/vysper/xmpp/extension/xep0124/BoshEndpoint.java (original)
+++ mina/vysper/trunk/server/extensions/xep0124-xep0206-bosh/src/main/java/org/apache/vysper/xmpp/extension/xep0124/BoshEndpoint.java Fri Apr 20 15:45:00 2012
@@ -36,6 +36,7 @@ import org.slf4j.LoggerFactory;
 
 /**
  * Allows HTTP clients to communicate via the BOSH protocol with Vysper.
+ * This endpoint creates an (embedded) servlet container answering HTTP.
  * <p>
  * See http://xmpp.org/extensions/xep-0124.html and
  * http://xmpp.org/extensions/xep-0206.html

Modified: mina/vysper/trunk/server/extensions/xep0124-xep0206-bosh/src/main/java/org/apache/vysper/xmpp/extension/xep0124/BoshHandler.java
URL: http://svn.apache.org/viewvc/mina/vysper/trunk/server/extensions/xep0124-xep0206-bosh/src/main/java/org/apache/vysper/xmpp/extension/xep0124/BoshHandler.java?rev=1328417&r1=1328416&r2=1328417&view=diff
==============================================================================
--- mina/vysper/trunk/server/extensions/xep0124-xep0206-bosh/src/main/java/org/apache/vysper/xmpp/extension/xep0124/BoshHandler.java (original)
+++ mina/vysper/trunk/server/extensions/xep0124-xep0206-bosh/src/main/java/org/apache/vysper/xmpp/extension/xep0124/BoshHandler.java Fri Apr 20 15:45:00 2012
@@ -23,9 +23,11 @@ import java.io.IOException;
 import java.util.Map;
 import java.util.concurrent.ConcurrentHashMap;
 
+import javax.servlet.AsyncContext;
 import javax.servlet.http.HttpServletRequest;
 
 import org.apache.vysper.xml.fragment.Attribute;
+import org.apache.vysper.xml.fragment.Renderer;
 import org.apache.vysper.xml.fragment.XMLElement;
 import org.apache.vysper.xmpp.protocol.NamespaceURIs;
 import org.apache.vysper.xmpp.server.ServerRuntimeContext;
@@ -46,13 +48,6 @@ import org.slf4j.LoggerFactory;
 public class BoshHandler {
 
     private static final Logger LOGGER = LoggerFactory.getLogger(BoshHandler.class);
-    
-    /**
-     * the empty BOSH response.
-     * <p>
-     * Looks like <code>&lt;body xmlns='http://jabber.org/protocol/httpbind'/&gt;</code>
-     */
-    protected static final Stanza EMPTY_BOSH_RESPONSE = new StanzaBuilder("body", NamespaceURIs.XEP0124_BOSH).build();
 
     private ServerRuntimeContext serverRuntimeContext;
 
@@ -115,9 +110,12 @@ public class BoshHandler {
                 return;
             }
         } else {
-            BoshBackedSessionContext session = sessions.get(body.getAttributeValue("sid"));
+            final String sid = body.getAttributeValue("sid");
+            BoshBackedSessionContext session = null;
+            if (sid != null) session = sessions.get(sid);
             if (session == null) {
-                LOGGER.warn("Received an invalid 'sid'!");
+                LOGGER.warn("Received an invalid sid = '{}', should terminating connection", sid);
+                // TODO terminate connection
                 return;
             }
             synchronized (session) {
@@ -137,37 +135,40 @@ public class BoshHandler {
     }
     
     private void processSession(BoshBackedSessionContext session, BoshRequest br) {
+        final Stanza stanza = br.getBody();
         if (session.getState() == SessionState.ENCRYPTED) {
-            if (br.getBody().getInnerElements().isEmpty()) {
+            if (stanza.getInnerElements().isEmpty()) {
                 // session needs authentication
                 return;
             }
-            for (XMLElement element : br.getBody().getInnerElements()) {
+            for (XMLElement element : stanza.getInnerElements()) {
                 if (element.getNamespaceURI().equals(NamespaceURIs.URN_IETF_PARAMS_XML_NS_XMPP_SASL)) {
                     processStanza(session, element);
                 }
             }
         } else if (session.getState() == SessionState.AUTHENTICATED) {
-            if ("true".equals(br.getBody().getAttributeValue(NamespaceURIs.URN_XMPP_XBOSH, "restart"))) {
+            if ("true".equals(stanza.getAttributeValue(NamespaceURIs.URN_XMPP_XBOSH, "restart"))) {
                 // restart request
-                session.writeBOSHResponse(getRestartResponse());
+                session.writeBoshResponse(BoshStanzaUtils.RESTART_BOSH_RESPONSE);
             } else {
                 // any other request
-                for (XMLElement element : br.getBody().getInnerElements()) {
+                for (XMLElement element : stanza.getInnerElements()) {
                     processStanza(session, element);
                 }
                 
                 // if the client solicited the session termination
-                if ("terminate".equals(br.getBody().getAttributeValue("type"))) {
+                if ("terminate".equals(stanza.getAttributeValue("type"))) {
                     terminateSession(session);
                 }
             }
+        } else {
+            LOGGER.debug("processing session while in state = " + session.getState());
         }
     }
 
     private void terminateSession(BoshBackedSessionContext session) {
         sessions.remove(session.getSessionId());
-        session.writeBOSHResponse(getTerminateResponse());
+        session.writeBoshResponse(BoshStanzaUtils.TERMINATE_BOSH_RESPONSE);
         session.close();
     }
 
@@ -184,37 +185,55 @@ public class BoshHandler {
     }
 
     private void createSession(BoshRequest br) throws IOException {
+        final Stanza stanza = br.getBody();
+
         BoshBackedSessionContext session = new BoshBackedSessionContext(this, serverRuntimeContext, inactivityChecker);
-        if (br.getBody().getAttribute("content") != null) {
-            session.setContentType(br.getBody().getAttributeValue("content"));
-        }
-        if (br.getBody().getAttribute("wait") != null) {
-            int wait = Integer.parseInt(br.getBody().getAttributeValue("wait"));
+        
+        final String contentAttribute = stanza.getAttributeValue("content");
+        if (contentAttribute != null) session.setContentType(contentAttribute);
+        
+        String waitAttribute = null;
+        try {
+            waitAttribute = stanza.getAttributeValue("wait");
+            final int wait = Integer.parseInt(waitAttribute);
             session.setWait(wait);
+        } catch (NumberFormatException e) {
+            LOGGER.warn("wait value is expected to be an Integer, not {}", waitAttribute);
         }
-        if (br.getBody().getAttribute("hold") != null) {
-            int hold = Integer.parseInt(br.getBody().getAttributeValue("hold"));
+
+        final String holdAttribute = stanza.getAttributeValue("hold");
+        try {
+            int hold = Integer.parseInt(holdAttribute);
             session.setHold(hold);
+        } catch (NumberFormatException e) {
+            LOGGER.warn("hold value is expected to be an Integer, not {}", waitAttribute);
         }
-        if (br.getBody().getAttribute("ver") != null) {
-            String ver = br.getBody().getAttributeValue("ver");
-            session.setBoshVersion(ver);
-        }
-        if (br.getBody().getAttribute(NamespaceURIs.XML, "lang") != null) {
-            String lang = br.getBody().getAttributeValue(NamespaceURIs.XML, "lang");
-            session.setXMLLang(lang);
+
+        final String versionAttribute = stanza.getAttributeValue("ver");
+        if (versionAttribute != null) {
+            try {
+                session.setBoshVersion(versionAttribute);
+            } catch (NumberFormatException e) {
+                LOGGER.warn("bosh version is expected to be of form nn.mm, not {}", waitAttribute);
+            }
         }
-        if ("1".equals(br.getBody().getAttributeValue("ack"))) {
+
+        final String langAttribute = stanza.getAttributeValue(NamespaceURIs.XML, "lang");
+        if (langAttribute != null) session.setXMLLang(langAttribute);
+        
+        if ("1".equals(stanza.getAttributeValue("ack"))) {
             session.setClientAcknowledgements(true);
         }
         session.insertRequest(br);
         sessions.put(session.getSessionId(), session);
 
-        session.writeBOSHResponse(getSessionCreationResponse(session));
+        LOGGER.info("BOSH session created with session id = {}", session.getSessionId());
+
+        session.writeBoshResponse(getSessionCreationResponse(session));
     }
 
     private Stanza getSessionCreationResponse(BoshBackedSessionContext session) {
-        StanzaBuilder body = new StanzaBuilder("body", NamespaceURIs.XEP0124_BOSH);
+        StanzaBuilder body = BoshStanzaUtils.createBoshStanzaBuilder();
         body.addAttribute("wait", Integer.toString(session.getWait()));
         body.addAttribute("inactivity", Integer.toString(session.getInactivity()));
         body.addAttribute("polling", Integer.toString(session.getPolling()));
@@ -234,71 +253,11 @@ public class BoshHandler {
                 .getAuthenticationMethods(), session);
         body.addPreparedElement(features);
         return body.build();
-    }
 
-    /**
-     * Creates a BOSH response by wrapping a stanza in a &lt;body/&gt; element
-     * @param stanza the XMPP stanza to wrap
-     * @return the BOSH response
-     */
-    public Stanza wrapStanza(Stanza stanza) {
-        StanzaBuilder body = new StanzaBuilder("body", NamespaceURIs.XEP0124_BOSH);
-        body.addPreparedElement(stanza);
-        return body.build();
     }
-    
-    /**
-     * Creates a BOSH response by merging 2 other BOSH responses, this is useful when sending more than one message as
-     * a response to a HTTP request.
-     * @param response1 the first BOSH response to merge
-     * @param response2 the second BOSH response to merge
-     * @return the merged BOSH response
-     */
-    public Stanza mergeResponses(Stanza response1, Stanza response2) {
-        if (response1 == null && response2 == null) {
-            return null;
-        }
-        if (response1 == null) {
-            return response2;
-        }
-        if (response2 == null) {
-            return response1;
-        }
-        StanzaBuilder body = new StanzaBuilder("body", NamespaceURIs.XEP0124_BOSH);
-        for (XMLElement element : response1.getInnerElements()) {
-            body.addPreparedElement(element);
-        }
-        for (XMLElement element : response2.getInnerElements()) {
-            body.addPreparedElement(element);
-        }
-        return body.build();
-    }
-    
-    private Stanza getRestartResponse() {
-        Stanza features = new ServerResponses().getFeaturesForSession();
-        return wrapStanza(features);
-    }
-    
-    /**
-     * Creates a session termination BOSH response
-     * @return the termination BOSH body
-    */
-    public Stanza getTerminateResponse() {
-        StanzaBuilder stanzaBuilder = new StanzaBuilder("body", NamespaceURIs.XEP0124_BOSH);
-        stanzaBuilder.addAttribute("type", "terminate");
-        return stanzaBuilder.build();
-    }
-    
-    /**
-     * Adds a custom attribute to a BOSH body.
-     * 
-     * @param stanza the BOSH body
-     * @param attributeName the name of the attribute
-     * @param attributeValue the value of the attribute
-     * @return a new BOSH body identical with the one provided except it also has the newly added attribute
-     */
+
     public Stanza addAttribute(Stanza stanza, String attributeName, String attributeValue) {
-        StanzaBuilder stanzaBuilder = new StanzaBuilder("body", NamespaceURIs.XEP0124_BOSH);
+        StanzaBuilder stanzaBuilder = BoshStanzaUtils.createBoshStanzaBuilder();
         for (Attribute attr : stanza.getAttributes()) {
             stanzaBuilder.addAttribute(attr);
         }
@@ -308,5 +267,4 @@ public class BoshHandler {
         }
         return stanzaBuilder.build();
     }
-
 }

Modified: mina/vysper/trunk/server/extensions/xep0124-xep0206-bosh/src/main/java/org/apache/vysper/xmpp/extension/xep0124/BoshRequest.java
URL: http://svn.apache.org/viewvc/mina/vysper/trunk/server/extensions/xep0124-xep0206-bosh/src/main/java/org/apache/vysper/xmpp/extension/xep0124/BoshRequest.java?rev=1328417&r1=1328416&r2=1328417&view=diff
==============================================================================
--- mina/vysper/trunk/server/extensions/xep0124-xep0206-bosh/src/main/java/org/apache/vysper/xmpp/extension/xep0124/BoshRequest.java (original)
+++ mina/vysper/trunk/server/extensions/xep0124-xep0206-bosh/src/main/java/org/apache/vysper/xmpp/extension/xep0124/BoshRequest.java Fri Apr 20 15:45:00 2012
@@ -68,18 +68,15 @@ public class BoshRequest implements Comp
 
     @Override
     public boolean equals(Object obj) {
-        if (this == obj)
-            return true;
-        if (obj == null)
-            return false;
-        if (getClass() != obj.getClass())
-            return false;
+        if (this == obj) return true;
+        if (obj == null) return false;
+        if (getClass() != obj.getClass()) return false;
         BoshRequest other = (BoshRequest) obj;
         if (rid == null) {
-            if (other.rid != null)
-                return false;
-        } else if (!rid.equals(other.rid))
-            return false;
+            if (other.rid != null) return false;
+        } else {
+            if (!rid.equals(other.rid)) return false;
+        }
         return true;
     }
 

Added: mina/vysper/trunk/server/extensions/xep0124-xep0206-bosh/src/main/java/org/apache/vysper/xmpp/extension/xep0124/BoshStanzaUtils.java
URL: http://svn.apache.org/viewvc/mina/vysper/trunk/server/extensions/xep0124-xep0206-bosh/src/main/java/org/apache/vysper/xmpp/extension/xep0124/BoshStanzaUtils.java?rev=1328417&view=auto
==============================================================================
--- mina/vysper/trunk/server/extensions/xep0124-xep0206-bosh/src/main/java/org/apache/vysper/xmpp/extension/xep0124/BoshStanzaUtils.java (added)
+++ mina/vysper/trunk/server/extensions/xep0124-xep0206-bosh/src/main/java/org/apache/vysper/xmpp/extension/xep0124/BoshStanzaUtils.java Fri Apr 20 15:45:00 2012
@@ -0,0 +1,95 @@
+package org.apache.vysper.xmpp.extension.xep0124;
+
+import org.apache.vysper.xml.fragment.Attribute;
+import org.apache.vysper.xml.fragment.XMLElement;
+import org.apache.vysper.xmpp.protocol.NamespaceURIs;
+import org.apache.vysper.xmpp.server.response.ServerResponses;
+import org.apache.vysper.xmpp.stanza.Stanza;
+import org.apache.vysper.xmpp.stanza.StanzaBuilder;
+
+import java.util.Collection;
+
+/**
+ */
+public class BoshStanzaUtils {
+    
+    /**
+     * the empty BOSH response.
+     * <p>
+     * Looks like <code>&lt;body xmlns='http://jabber.org/protocol/httpbind'/&gt;</code>
+     */
+    protected static final Stanza EMPTY_BOSH_RESPONSE = createBoshStanzaBuilder().build();
+    
+    protected static final Stanza RESTART_BOSH_RESPONSE = wrapStanza(new ServerResponses().getFeaturesForSession());
+    
+    protected static final Stanza TERMINATE_BOSH_RESPONSE = createTerminateResponse();
+
+    /**
+     * Creates a new BOSH response builder
+     */
+    public static StanzaBuilder createBoshStanzaBuilder() {
+        return new StanzaBuilder("body", NamespaceURIs.XEP0124_BOSH);
+    }
+
+    /**
+     * Creates a BOSH response by wrapping a stanza in a &lt;body/&gt; element
+     * @param stanza the XMPP stanza to wrap
+     * @return the BOSH response
+     */
+    public static Stanza wrapStanza(Stanza stanza) {
+        StanzaBuilder body = createBoshStanzaBuilder();
+        body.addPreparedElement(stanza);
+        return body.build();
+    }
+
+    /**
+     * Creates a unified BOSH response by merging BOSH responses, this is useful when sending more than one message as
+     * a response to a HTTP request.
+     * @param response1 the first BOSH response to merge
+     * @param response2 the second BOSH response to merge
+     * @return the merged BOSH response
+     */
+    public static Stanza mergeResponses(Collection<Stanza> mergeCandidates) {
+        if (mergeCandidates == null || mergeCandidates.size() == 0) {
+            return null;
+        }
+        StanzaBuilder body = createBoshStanzaBuilder();
+        for (Stanza mergee : mergeCandidates) {
+            if (mergee == null) continue;
+            for (XMLElement element : mergee.getInnerElements()) {
+                body.addPreparedElement(element);
+            }
+        }
+        return body.build();
+    }
+
+    /**
+     * Creates a session termination BOSH response
+     * @return the termination BOSH body
+    */
+    private static Stanza createTerminateResponse() {
+        StanzaBuilder stanzaBuilder = createBoshStanzaBuilder();
+        stanzaBuilder.addAttribute("type", "terminate");
+        return stanzaBuilder.build();
+    }
+
+    /**
+     * Adds a custom attribute to a BOSH body.
+     * 
+     * @param stanza the BOSH body
+     * @param attributeName the name of the attribute
+     * @param attributeValue the value of the attribute
+     * @return a new BOSH body identical with the one provided except it also has the newly added attribute
+     */
+    public static Stanza addAttribute(Stanza stanza, String attributeName, String attributeValue) {
+        StanzaBuilder stanzaBuilder = createBoshStanzaBuilder();
+        for (Attribute attr : stanza.getAttributes()) {
+            stanzaBuilder.addAttribute(attr);
+        }
+        stanzaBuilder.addAttribute(attributeName, attributeValue);
+        for (XMLElement element : stanza.getInnerElements()) {
+            stanzaBuilder.addPreparedElement(element);
+        }
+        return stanzaBuilder.build();
+    }
+}

Modified: mina/vysper/trunk/server/extensions/xep0124-xep0206-bosh/src/test/java/org/apache/vysper/xmpp/extension/xep0124/BoshBackedSessionContextTest.java
URL: http://svn.apache.org/viewvc/mina/vysper/trunk/server/extensions/xep0124-xep0206-bosh/src/test/java/org/apache/vysper/xmpp/extension/xep0124/BoshBackedSessionContextTest.java?rev=1328417&r1=1328416&r2=1328417&view=diff
==============================================================================
--- mina/vysper/trunk/server/extensions/xep0124-xep0206-bosh/src/test/java/org/apache/vysper/xmpp/extension/xep0124/BoshBackedSessionContextTest.java (original)
+++ mina/vysper/trunk/server/extensions/xep0124-xep0206-bosh/src/test/java/org/apache/vysper/xmpp/extension/xep0124/BoshBackedSessionContextTest.java Fri Apr 20 15:45:00 2012
@@ -19,11 +19,7 @@
  */
 package org.apache.vysper.xmpp.extension.xep0124;
 
-import static org.easymock.EasyMock.anyLong;
-import static org.easymock.EasyMock.createControl;
-import static org.easymock.EasyMock.eq;
-import static org.easymock.EasyMock.expect;
-import static org.easymock.EasyMock.expectLastCall;
+import static org.easymock.EasyMock.*;
 import static org.junit.Assert.assertEquals;
 
 import java.io.IOException;
@@ -33,6 +29,7 @@ import javax.servlet.AsyncEvent;
 import javax.servlet.AsyncListener;
 import javax.servlet.http.HttpServletRequest;
 
+import org.apache.commons.lang.StringUtils;
 import org.apache.vysper.xml.fragment.Renderer;
 import org.apache.vysper.xmpp.addressing.EntityImpl;
 import org.apache.vysper.xmpp.protocol.NamespaceURIs;
@@ -84,16 +81,16 @@ public class BoshBackedSessionContextTes
         asyncContext.setTimeout(anyLong());
         asyncContext.dispatch();
         expectLastCall().atLeastOnce();
-        httpServletRequest.setAttribute(eq("request"), EasyMock.<BoshRequest> notNull());
-        asyncContext.addListener(EasyMock.<AsyncListener> anyObject());
+        httpServletRequest.setAttribute(eq("request"), EasyMock.<BoshRequest>notNull());
+        asyncContext.addListener(EasyMock.<AsyncListener>anyObject());
         Capture<BoshResponse> captured = new Capture<BoshResponse>();
         httpServletRequest.setAttribute(eq("response"), EasyMock.<BoshResponse> capture(captured));
         mocksControl.replay();
 
         BoshBackedSessionContext boshBackedSessionContext = new BoshBackedSessionContext(boshHandler, serverRuntimeContext, inactivityChecker);
-        Stanza body = new StanzaBuilder("body", NamespaceURIs.XEP0124_BOSH).build();
+        Stanza body = BoshStanzaUtils.EMPTY_BOSH_RESPONSE;
         boshBackedSessionContext.insertRequest(new BoshRequest(httpServletRequest, body, 1L));
-        boshBackedSessionContext.writeBOSHResponse(body);
+        boshBackedSessionContext.writeBoshResponse(body);
         mocksControl.verify();
         
         BoshResponse boshResponse = captured.getValue();
@@ -121,7 +118,7 @@ public class BoshBackedSessionContextTes
 
     @Test
     public void testRequestExpired() throws IOException {
-        Stanza emtpyStanza = new StanzaBuilder("body", NamespaceURIs.XEP0124_BOSH).build();
+        Stanza emtpyStanza = BoshStanzaUtils.EMPTY_BOSH_RESPONSE;
 
         // addRequest
         HttpServletRequest httpServletRequest = mocksControl.createMock(HttpServletRequest.class);
@@ -129,7 +126,7 @@ public class BoshBackedSessionContextTes
         expect(httpServletRequest.startAsync()).andReturn(asyncContext).atLeastOnce();
         expect(httpServletRequest.getAsyncContext()).andReturn(asyncContext).atLeastOnce();
         asyncContext.setTimeout(anyLong());
-        httpServletRequest.setAttribute(eq("request"), EasyMock.<BoshRequest> notNull());
+        httpServletRequest.setAttribute(eq("request"), EasyMock.<BoshRequest>notNull());
 
         expect(asyncContext.getRequest()).andReturn(httpServletRequest).atLeastOnce();
         asyncContext.dispatch();
@@ -166,6 +163,7 @@ public class BoshBackedSessionContextTes
         HttpServletRequest httpServletRequest2 = mocksControl.createMock(HttpServletRequest.class);
         AsyncContext asyncContext1 = mocksControl.createMock(AsyncContext.class);
         AsyncContext asyncContext2 = mocksControl.createMock(AsyncContext.class);
+        BoshStanzaUtils boshStanzaUtils = mocksControl.createMock(BoshStanzaUtils.class);
 
         expect(httpServletRequest1.startAsync()).andReturn(asyncContext1).atLeastOnce();
         expect(httpServletRequest1.getAsyncContext()).andReturn(asyncContext1).atLeastOnce();
@@ -179,16 +177,15 @@ public class BoshBackedSessionContextTes
 
         asyncContext2.setTimeout(anyLong());
         Capture<BoshRequest> br2 = new Capture<BoshRequest>();
-        httpServletRequest2.setAttribute(eq("request"), EasyMock.<BoshRequest> capture(br2));
+        httpServletRequest2.setAttribute(eq("request"), EasyMock.<BoshRequest>capture(br2));
 
         asyncContext1.addListener(EasyMock.<AsyncListener> anyObject());
-        asyncContext2.addListener(EasyMock.<AsyncListener> anyObject());
+        asyncContext2.addListener(EasyMock.<AsyncListener>anyObject());
 
         asyncContext1.dispatch();
         expectLastCall().atLeastOnce();
 
-        Stanza body = new StanzaBuilder("body", NamespaceURIs.XEP0124_BOSH).build();
-        expect(boshHandler.addAttribute(eq(body), eq("ack"), Long.toString(EasyMock.anyLong()))).andReturn(body);
+        Stanza body = BoshStanzaUtils.EMPTY_BOSH_RESPONSE;
 
         // write0
         Capture<BoshResponse> captured = new Capture<BoshResponse>();
@@ -199,15 +196,18 @@ public class BoshBackedSessionContextTes
 
         boshBackedSessionContext.setHold(2);
         // consecutive writes with RID 1 and 2
+        long maxRID = 2L;
         boshBackedSessionContext.insertRequest(new BoshRequest(httpServletRequest1, body, 1L));
-        boshBackedSessionContext.insertRequest(new BoshRequest(httpServletRequest2, body, 2L));
-        boshBackedSessionContext.writeBOSHResponse(body);
+        boshBackedSessionContext.insertRequest(new BoshRequest(httpServletRequest2, body, maxRID));
+        boshBackedSessionContext.writeBoshResponse(body);
         mocksControl.verify();
 
         assertEquals(httpServletRequest1, br1.getValue().getHttpServletRequest());
         assertEquals(httpServletRequest2, br2.getValue().getHttpServletRequest());
 
-        assertEquals(new Renderer(body).getComplete(), new String(captured.getValue().getContent()));
+        // expect ack for newest/largest RID
+        final Stanza ackedResponse = BoshStanzaUtils.addAttribute(body, "ack", Long.toString(maxRID));
+        assertEquals(new Renderer(ackedResponse).getComplete(), new String(captured.getValue().getContent()));
         assertEquals(BoshServlet.XML_CONTENT_TYPE, captured.getValue().getContentType());
     }
 
@@ -218,29 +218,30 @@ public class BoshBackedSessionContextTes
         expect(httpServletRequest.startAsync()).andReturn(asyncContext).atLeastOnce();
         expect(httpServletRequest.getAsyncContext()).andReturn(asyncContext).atLeastOnce();
         asyncContext.setTimeout(anyLong());
-        httpServletRequest.setAttribute(eq("request"), EasyMock.<BoshRequest> notNull());
+        httpServletRequest.setAttribute(eq("request"), EasyMock.<BoshRequest>notNull());
 
-        asyncContext.addListener(EasyMock.<AsyncListener> anyObject());
+        asyncContext.addListener(EasyMock.<AsyncListener>anyObject());
 
         asyncContext.dispatch();
         expectLastCall().atLeastOnce();
 
-        Stanza body1 = mocksControl.createMock(Stanza.class);
-        Stanza body2 = mocksControl.createMock(Stanza.class);
-        Stanza body = new StanzaBuilder("body", NamespaceURIs.XEP0124_BOSH).build();
-        expect(boshHandler.mergeResponses(EasyMock.<Stanza> anyObject(), EasyMock.<Stanza> anyObject()))
-                .andReturn(body);
-        expectLastCall().times(2);
-
-        httpServletRequest.setAttribute(eq("response"), EasyMock.<BoshResponse> anyObject());
+        Stanza body = BoshStanzaUtils.createBoshStanzaBuilder().startInnerElement("presence").endInnerElement().build();
+        
+        Capture<BoshResponse> captured = new Capture<BoshResponse>();
+        httpServletRequest.setAttribute(eq("response"), EasyMock.<BoshResponse> capture(captured));
 
         mocksControl.replay();
 
         BoshBackedSessionContext boshBackedSessionContext = new BoshBackedSessionContext(boshHandler,
                 serverRuntimeContext, inactivityChecker);
-        boshBackedSessionContext.writeBOSHResponse(body1);
-        boshBackedSessionContext.writeBOSHResponse(body2);
+        boshBackedSessionContext.writeBoshResponse(body); // queued for merging
+        boshBackedSessionContext.writeBoshResponse(body); // queued for merging
+        boshBackedSessionContext.writeBoshResponse(body); // queued for merging
         boshBackedSessionContext.insertRequest(new BoshRequest(httpServletRequest, body, 1L));
+
         mocksControl.verify();
+
+        final String mergedAllBodiesStanza = new String(captured.getValue().getContent());
+        assertEquals(3, StringUtils.countMatches(mergedAllBodiesStanza, "<presence"));
     }
 }

Modified: mina/vysper/trunk/server/extensions/xep0124-xep0206-bosh/src/test/java/org/apache/vysper/xmpp/extension/xep0124/BoshHandlerTest.java
URL: http://svn.apache.org/viewvc/mina/vysper/trunk/server/extensions/xep0124-xep0206-bosh/src/test/java/org/apache/vysper/xmpp/extension/xep0124/BoshHandlerTest.java?rev=1328417&r1=1328416&r2=1328417&view=diff
==============================================================================
--- mina/vysper/trunk/server/extensions/xep0124-xep0206-bosh/src/test/java/org/apache/vysper/xmpp/extension/xep0124/BoshHandlerTest.java (original)
+++ mina/vysper/trunk/server/extensions/xep0124-xep0206-bosh/src/test/java/org/apache/vysper/xmpp/extension/xep0124/BoshHandlerTest.java Fri Apr 20 15:45:00 2012
@@ -27,6 +27,7 @@ import static org.easymock.EasyMock.eq;
 import static org.easymock.EasyMock.expect;
 
 import java.io.IOException;
+import java.util.Arrays;
 import java.util.Collections;
 
 import javax.servlet.AsyncContext;
@@ -152,7 +153,7 @@ public class BoshHandlerTest {
 
     @Test
     public void testGetEmptyResponse() {
-        Stanza response = BoshHandler.EMPTY_BOSH_RESPONSE;
+        Stanza response = BoshStanzaUtils.EMPTY_BOSH_RESPONSE;
         assertNotNull(response);
         assertEquals("body", response.getName());
         assertEquals(NamespaceURIs.XEP0124_BOSH, response.getNamespaceURI());
@@ -164,7 +165,7 @@ public class BoshHandlerTest {
     public void testWrapStanza() {
         StanzaBuilder stanzaBuilder = new StanzaBuilder("iq", NamespaceURIs.JABBER_CLIENT);
         Stanza stanza = stanzaBuilder.build();
-        Stanza body = boshHandler.wrapStanza(stanza);
+        Stanza body = BoshStanzaUtils.wrapStanza(stanza);
         assertNotNull(body);
         assertEquals("body", body.getName());
         assertEquals(NamespaceURIs.XEP0124_BOSH, body.getNamespaceURI());
@@ -177,9 +178,9 @@ public class BoshHandlerTest {
     public void testMergeResponses() {
         Stanza response1 = createPingStanzaResponse("vysper.org", "user1@vysper.org/resource", "100");
         Stanza response2 = createPingStanzaResponse("vysper.org", "user1@vysper.org/resource", "101");
-        assertEquals(response1, boshHandler.mergeResponses(response1, null));
-        assertEquals(response1, boshHandler.mergeResponses(null, response1));
-        Stanza merged = boshHandler.mergeResponses(response1, response2);
+        assertEquals(response1, BoshStanzaUtils.mergeResponses(Arrays.asList(response1, null)));
+        assertEquals(response1, BoshStanzaUtils.mergeResponses(Arrays.asList(null, response1)));
+        Stanza merged = BoshStanzaUtils.mergeResponses(Arrays.asList(response1, response2));
         assertNotNull(merged);
         assertEquals("body", merged.getName());
         assertEquals(NamespaceURIs.XEP0124_BOSH, merged.getNamespaceURI());
@@ -189,7 +190,7 @@ public class BoshHandlerTest {
     }
 
     private Stanza createSessionRequest() {
-        StanzaBuilder body = new StanzaBuilder("body", NamespaceURIs.XEP0124_BOSH);
+        StanzaBuilder body = BoshStanzaUtils.createBoshStanzaBuilder();
         body.addAttribute("rid", "100");
         body.addAttribute("to", "vysper.org");
         body.addAttribute(NamespaceURIs.XML, "lang", "en");
@@ -201,7 +202,7 @@ public class BoshHandlerTest {
     }
 
     private Stanza createSaslRequest() {
-        StanzaBuilder body = new StanzaBuilder("body", NamespaceURIs.XEP0124_BOSH);
+        StanzaBuilder body = BoshStanzaUtils.createBoshStanzaBuilder();
         body.addAttribute("rid", "101");
         body.addAttribute("sid", "200");
         body.startInnerElement("auth", NamespaceURIs.URN_IETF_PARAMS_XML_NS_XMPP_SASL)
@@ -210,7 +211,7 @@ public class BoshHandlerTest {
     }
 
     private static Stanza createPingStanzaResponse(String from, String to, String id) {
-        StanzaBuilder body = new StanzaBuilder("body", NamespaceURIs.XEP0124_BOSH);
+        StanzaBuilder body = BoshStanzaUtils.createBoshStanzaBuilder();
         body.startInnerElement("iq", NamespaceURIs.JABBER_CLIENT).addAttribute("from", from)
                 .addAttribute("type", "result").addAttribute("to", to).addAttribute("id", id);
         body.endInnerElement();