org.apache.vysper.xmpp.extension.xep0124.BoshBackedSessionContext.java Source code

Java tutorial

Introduction

Here is the source code for org.apache.vysper.xmpp.extension.xep0124.BoshBackedSessionContext.java

Source

/*
 *  Licensed to the Apache Software Foundation (ASF) under one
 *  or more contributor license agreements.  See the NOTICE file
 *  distributed with this work for additional information
 *  regarding copyright ownership.  The ASF licenses this file
 *  to you under the Apache License, Version 2.0 (the
 *  "License"); you may not use this file except in compliance
 *  with the License.  You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing,
 *  software distributed under the License is distributed on an
 *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 *  KIND, either express or implied.  See the License for the
 *  specific language governing permissions and limitations
 *  under the License.
 *
 */
package org.apache.vysper.xmpp.extension.xep0124;

import java.io.IOException;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.Queue;
import java.util.SortedMap;
import java.util.TreeMap;

import javax.servlet.AsyncContext;
import javax.servlet.AsyncEvent;
import javax.servlet.AsyncListener;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;

import org.apache.commons.lang.StringUtils;
import org.apache.vysper.xml.fragment.Renderer;
import org.apache.vysper.xml.fragment.XMLElement;
import org.apache.vysper.xmpp.protocol.SessionStateHolder;
import org.apache.vysper.xmpp.server.AbstractSessionContext;
import org.apache.vysper.xmpp.server.ServerRuntimeContext;
import org.apache.vysper.xmpp.server.SessionState;
import org.apache.vysper.xmpp.stanza.Stanza;
import org.apache.vysper.xmpp.stanza.StanzaBuilder;
import org.apache.vysper.xmpp.writer.StanzaWriter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Keeps the session state of a BOSH client
 *
 * @author The Apache MINA Project (dev@mina.apache.org)
 */
public class BoshBackedSessionContext extends AbstractSessionContext implements StanzaWriter {

    private final static Logger LOGGER = LoggerFactory.getLogger(BoshBackedSessionContext.class);

    public final static String HTTP_SESSION_ATTRIBUTE = "org.apache.vysper.xmpp.extension.xep0124.BoshBackedSessionContext";

    public final static String BOSH_REQUEST_ATTRIBUTE = "boshRequest";
    public final static String BOSH_RESPONSE_ATTRIBUTE = "boshResponse";

    protected static final Charset UTF8_CHARSET = Charset.forName("UTF-8");

    private final int maxpauseSeconds = 120;

    private final int inactivitySeconds = 60;

    private final int pollingSeconds = 15;

    private final int maximumSentResponses = 100;

    /*
     * The number of milliseconds that will have to pass for a response to be reported missing to the client by
     * responding with a report and time attributes. See Response Acknowledgements in XEP-0124.
     */
    private final int brokenConnectionReportTimeoutMillis = 1000;

    /*
     * Keeps the suspended HTTP requests (does not respond to them) until the server has an asynchronous message
     * to send to the client. (Comet HTTP Long Polling technique - described in XEP-0124)
     * 
     * The BOSH requests are sorted by their RIDs.
     */
    protected final RequestsWindow requestsWindow;

    /*
     * Keeps the asynchronous messages sent from server that cannot be delivered to the client because there are
     * no available HTTP requests to respond to (requestsWindow is empty).
     */
    private final Queue<Stanza> delayedResponseQueue = new LinkedList<Stanza>();

    /*
     * A cache of sent responses to the BOSH client, kept in the event of delivery failure and retransmission requests.
     * these sent responses are moved to the sentResponsesBacklog when the client acks their receival.
     * See Broken Connections in XEP-0124.
     */
    private final SortedMap<Long, BoshResponse> sentResponses = new TreeMap<Long, BoshResponse>();

    /**
     * backlog of sent responses which have been acked by the client (and thus shouldn't ever been needed to be resent.
     */
    private final ResponsesBuffer sentResponsesBacklog = new ResponsesBuffer();

    private int parallelRequestsCount = 2;

    private String boshVersion = "1.9";

    private String contentType = BoshServlet.XML_CONTENT_TYPE;

    private int wait = 60;

    private int hold = 1;

    private int currentInactivitySeconds = inactivitySeconds;

    /**
     * must be synchronized along with requestsWindow 
     */
    private Long latestEmptyPollingRequestTimestamp = 0L;

    /*
     * Indicate if the BOSH client will use acknowledgements throughout the session and that the absence of an 'ack'
     * attribute in any request is meaningful.
     */
    private boolean clientAcknowledgements;

    /*
     * The timestamp of the latest response wrote to the client is used to measure the inactivity period.
     * When reaching the maximum inactivity the session will automatically close.
     */
    private long latestWriteTimestamp = System.currentTimeMillis();

    private final InactivityChecker inactivityChecker;

    private Long lastInactivityExpireTime;

    private boolean isWatchedByInactivityChecker;

    private boolean propagateSessionContextToHTTPSession = false;

    /**
     * Creates a new context for a session
     * @param serverRuntimeContext
     * @param inactivityChecker
     */
    public BoshBackedSessionContext(ServerRuntimeContext serverRuntimeContext, BoshHandler boshHandler,
            InactivityChecker inactivityChecker) {
        super(serverRuntimeContext, new SessionStateHolder());

        // in BOSH we jump directly to the encrypted state
        sessionStateHolder.setState(SessionState.ENCRYPTED);

        requestsWindow = new RequestsWindow(getSessionId());
        this.inactivityChecker = inactivityChecker;
        updateInactivityChecker();
    }

    public boolean isWatchedByInactivityChecker() {
        return isWatchedByInactivityChecker;
    }

    private synchronized void updateInactivityChecker() {
        Long newInactivityExpireTime = null;
        if (requestsWindow.isEmpty()) {
            newInactivityExpireTime = latestWriteTimestamp + currentInactivitySeconds * 1000;
            if (newInactivityExpireTime.equals(lastInactivityExpireTime)) {
                return;
            }
        } else if (!isWatchedByInactivityChecker) {
            return;
        }
        isWatchedByInactivityChecker = inactivityChecker.updateExpireTime(this, lastInactivityExpireTime,
                newInactivityExpireTime);
        lastInactivityExpireTime = newInactivityExpireTime;
    }

    public SessionStateHolder getStateHolder() {
        return sessionStateHolder;
    }

    public StanzaWriter getResponseWriter() {
        return this;
    }

    public void setIsReopeningXMLStream() {
        // BOSH does not use XML streams, the BOSH equivalent for reopening an XML stream is to restart the BOSH connection,
        // and this is done in BoshHandler when the client requests it
    }

    /**
     * true, iff this session context will be stored to the related BOSH HTTP session.
     * @return
     */
    public boolean propagateSessionContext() {
        return propagateSessionContextToHTTPSession;
    }

    /*
    * This method is synchronized on the session object to prevent concurrent writes to the same BOSH client
    */
    synchronized public void write(Stanza stanza) {
        if (stanza == null)
            throw new IllegalArgumentException("stanza must not be null.");
        LOGGER.debug("SID = " + getSessionId() + " - adding server stanza for writing to BOSH client");
        writeBoshResponse(BoshStanzaUtils.wrapStanza(stanza));
    }

    /**
     * Writes a server-to-client XMPP stanza as a BOSH response (wrapped in a &lt;body/&gt; element) if there are 
     * available HTTP requests to respond to, otherwise the response is queued to be sent later 
     * (when a HTTP request becomes available).
     * <p>
     * (package access)
     * 
     * @param responseStanza The BOSH response to write
     */
    /*package*/ void writeBoshResponse(Stanza responseStanza) {
        if (responseStanza == null)
            throw new IllegalArgumentException();
        final boolean isEmtpyResponse = responseStanza == BoshStanzaUtils.EMPTY_BOSH_RESPONSE;

        final ArrayList<BoshRequest> boshRequestsForRID = new ArrayList<BoshRequest>(1);
        BoshResponse boshResponse;
        final Long rid;
        synchronized (requestsWindow) {
            BoshRequest req = requestsWindow.pollNext();
            if (req == null) {
                if (isEmtpyResponse)
                    return; // do not delay empty responses, everything's good.
                // delay sending until request comes available
                final boolean accepted = delayedResponseQueue.offer(responseStanza);
                if (!accepted) {
                    LOGGER.debug(
                            "SID = " + getSessionId()
                                    + " - stanza not queued. BOSH delayedResponseQueue is full: {}",
                            delayedResponseQueue.size());
                    // TODO do not silently drop this stanza
                }
                return;
            }

            rid = req.getRid();
            // in rare cases, we have same RID in two separate requests
            boshRequestsForRID.add(req);

            // collect more requests for this RID
            while (rid.equals(requestsWindow.firstRid())) {
                final BoshRequest sameRidRequest = requestsWindow.pollNext();
                boshRequestsForRID.add(sameRidRequest);
                LOGGER.warn("SID = " + getSessionId() + " - rid = {} - multi requests ({}) per RID.", rid,
                        boshRequestsForRID.size());
            }

            long highestContinuousRid = requestsWindow.getHighestContinuousRid();
            final Long ack = rid.equals(highestContinuousRid) ? null : highestContinuousRid;
            boshResponse = getBoshResponse(responseStanza, ack);
            if (LOGGER.isDebugEnabled()) {
                String emptyHint = isEmtpyResponse ? "empty " : StringUtils.EMPTY;
                LOGGER.debug("SID = " + getSessionId() + " - rid = " + rid + " - BOSH writing {}response: {}",
                        emptyHint, new String(boshResponse.getContent()));
            }
        }

        synchronized (sentResponses) {
            if (isResponseSavable(boshRequestsForRID.get(0), responseStanza)) {
                sentResponses.put(rid, boshResponse);
                // The number of responses to non-pause requests kept in the buffer SHOULD be either the same as the maximum
                // number of simultaneous requests allowed or, if Acknowledgements are being used, the number of responses
                // that have not yet been acknowledged (this part is handled in insertRequest(BoshRequest)), or 
                // the hard limit maximumSentResponses (not in the specification) that prevents excessive memory consumption.
                if (sentResponses.size() > maximumSentResponses
                        || (!isClientAcknowledgements() && sentResponses.size() > parallelRequestsCount)) {
                    final Long key = sentResponses.firstKey();
                    sentResponsesBacklog.add(key, sentResponses.remove(key));
                }
            }
        }

        if (sentResponses.size() > maximumSentResponses + 10) {
            synchronized (sentResponses) {
                LOGGER.warn("stored sent responses ({}) exeeds maximum ({}). purging.", sentResponses.size(),
                        maximumSentResponses);
                while (sentResponses.size() > maximumSentResponses) {
                    final Long key = sentResponses.firstKey();
                    sentResponsesBacklog.add(key, sentResponses.remove(key));
                }
            }
        }

        for (BoshRequest boshRequest : boshRequestsForRID) {
            try {
                final AsyncContext asyncContext = saveResponse(boshRequest, boshResponse);
                asyncContext.dispatch();
            } catch (Exception e) {
                LOGGER.warn("SID = " + getSessionId() + " - exception in async processing rid = {}",
                        boshRequest.getRid(), e);
            }
        }
        setLatestWriteTimestamp();
        updateInactivityChecker();
    }

    private String logRIDSequence() {
        final String logMsg = requestsWindow.logRequestWindow();
        return logMsg + ", sent buffer = " + sentResponses.size();
    }

    private void setLatestWriteTimestamp() {
        latestWriteTimestamp = System.currentTimeMillis();
    }

    private static boolean isResponseSavable(BoshRequest req, Stanza response) {
        // responses to pause requests are not saved
        if (req.getBody().getAttributeValue("pause") != null) {
            return false;
        }
        // responses with binding error are not saved
        for (XMLElement element : response.getInnerElements()) {
            if ("iq".equals(element.getName()) && "error".equals(element.getAttributeValue("type"))) {
                for (XMLElement subelement : element.getInnerElements()) {
                    if ("bind".equals(subelement.getName())) {
                        return false;
                    }
                }
            }
        }
        return true;
    }

    public void sendError(String condition) {
        sendError(null, condition);
    }

    /**
     * Writes an error to the client and closes the connection
     * @param condition the error condition
     */
    protected void sendError(BoshRequest req, String condition) {
        req = req == null ? requestsWindow.pollNext() : req;
        if (req == null) {
            LOGGER.warn("SID = " + getSessionId() + " - no request for sending BOSH error " + condition);
            endSession(SessionTerminationCause.CONNECTION_ABORT);
            return;
        }
        final Long rid = req.getRid();
        Stanza body = BoshStanzaUtils.createTerminateResponse(condition).build();
        BoshResponse boshResponse = getBoshResponse(body, null);
        if (LOGGER.isDebugEnabled()) {
            LOGGER.debug("SID = " + getSessionId() + " - rid = {} - BOSH writing response: {}", rid,
                    new String(boshResponse.getContent()));
        }

        try {
            final AsyncContext asyncContext = saveResponse(req, boshResponse);
            asyncContext.dispatch();
        } catch (Exception e) {
            LOGGER.warn("SID = " + getSessionId() + " - exception in async processing", e);
        }
        close();
    }

    /*
     * Terminates the BOSH session
     */
    public void close() {
        // respond to all the queued HTTP requests with termination responses
        synchronized (requestsWindow) {
            BoshRequest next;
            while ((next = requestsWindow.pollNext()) != null) {
                Stanza body = BoshStanzaUtils.TERMINATE_BOSH_RESPONSE;
                BoshResponse boshResponse = getBoshResponse(body, null);
                if (LOGGER.isDebugEnabled()) {
                    LOGGER.debug("SID = " + getSessionId() + " - rid = {} - BOSH writing response: {}",
                            next.getRid(), new String(boshResponse.getContent()));
                }

                try {
                    final AsyncContext asyncContext = saveResponse(next, boshResponse);
                    asyncContext.dispatch();
                } catch (Exception e) {
                    LOGGER.warn("SID = " + getSessionId() + " - exception in async processing", e);
                }
            }

            inactivityChecker.updateExpireTime(this, lastInactivityExpireTime, null);
            lastInactivityExpireTime = null;
        }

        LOGGER.info("SID = " + getSessionId() + " - session closed");
    }

    public void switchToTLS(boolean delayed, boolean clientTls) {
        // BOSH cannot switch dynamically (because STARTTLS cannot be used with HTTP),
        // SSL can be enabled/disabled in BoshEndpoint#setSSLEnabled()
    }

    /**
     * Setter for the Content-Type header of the HTTP responses sent to the BOSH client associated with this session
     * @param contentType
     */
    public void setContentType(String contentType) {
        this.contentType = contentType;
    }

    /**
     * Getter for the Content-Type header
     * @return the configured Content-Type
     */
    public String getContentType() {
        return contentType;
    }

    /**
     * Getter for the maximum length of a temporary session pause (in seconds) that a client can request
     * @return
     */
    public int getMaxPause() {
        return maxpauseSeconds;
    }

    /**
     * Setter for the BOSH 'wait' parameter, the longest time (in seconds) that the connection manager is allowed to
     * wait before responding to any request during the session. The BOSH client can only configure this parameter to
     * a lower value than the default value from this session context.
     * @param wait the BOSH 'wait' parameter
     */
    public void setWait(int wait) {
        this.wait = Math.min(wait, this.wait);
    }

    /**
     * Getter for the BOSH 'wait' parameter
     * @return The BOSH 'wait' parameter
     */
    public int getWait() {
        return wait;
    }

    /**
     * Setter for the BOSH 'hold' parameter, the maximum number of HTTP requests the connection manager is allowed to
     * keep waiting at any one time during the session. The value of this parameter can trigger the modification of
     * the BOSH 'requests' parameter.
     * @param hold
     */
    public void setHold(int hold) {
        this.hold = hold;
        if (hold >= 2) {
            parallelRequestsCount = hold + 1;
        }
    }

    /**
     * Getter for the BOSH 'hold' parameter
     * @return the BOSH 'hold' parameter
     */
    public int getHold() {
        return hold;
    }

    /**
     * Setter for the client acknowledgements throughout the session
     * @param value true is enabled, false otherwise
     */
    public void setClientAcknowledgements(boolean value) {
        clientAcknowledgements = value;
    }

    /**
     * Getter for client acknowledgements
     * @return true if enabled, false otherwise
     */
    public boolean isClientAcknowledgements() {
        return clientAcknowledgements;
    }

    /**
     * Setter for the highest version of the BOSH protocol that the connection manager supports, or the version
     * specified by the client in its request, whichever is lower.
     * @param version the BOSH 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;
        }
    }

    /**
     * Getter for the BOSH protocol version
     * @return the BOSH version
     */
    public String getBoshVersion() {
        return boshVersion;
    }

    /**
     * Getter for the BOSH 'inactivity' parameter, the longest allowable inactivity period (in seconds).
     * @return the BOSH 'inactivity' parameter
     */
    public int getInactivity() {
        return inactivitySeconds;
    }

    /**
     * Getter for the BOSH 'polling' parameter, the shortest allowable polling interval (in seconds).
     * @return the BOSH 'polling' parameter
     */
    public int getPolling() {
        return pollingSeconds;
    }

    /**
     * Getter for the BOSH 'requests' parameter, the limit number of simultaneous requests the client makes.
     * @return the BOSH 'requests' parameter
     */
    public int getRequests() {
        return parallelRequestsCount;
    }

    /*
     * A request expires when it stays enqueued in the requestsWindow longer than the allowed 'wait' time.
     * The synchronization on the session object ensures that there will be no concurrent writes or other concurrent
     * expirations for the BOSH client while the current request expires.
     */
    private void requestExpired(final AsyncContext context) {
        final BoshRequest req = (BoshRequest) context.getRequest().getAttribute(BOSH_REQUEST_ATTRIBUTE);
        if (req == null) {
            LOGGER.warn("SID = " + getSessionId() + " - Continuation expired without having "
                    + "an associated request!");
            return;
        }
        LOGGER.debug("SID = " + getSessionId() + " - rid = {} - BOSH request expired", req.getRid());
        while (!requestsWindow.isEmpty() && requestsWindow.firstRid() <= req.getRid()) {
            writeBoshResponse(BoshStanzaUtils.EMPTY_BOSH_RESPONSE);
        }
    }

    /**
     * Suspends and enqueues an HTTP request to be used later when an asynchronous message needs to be sent from
     * the connection manager to the BOSH client.
     * 
     * @param req the HTTP request
     */
    public void insertRequest(final BoshRequest br) {

        final Stanza boshOuterBody = br.getBody();
        final Long rid = br.getRid();
        LOGGER.debug("SID = " + getSessionId() + " - rid = {} - inserting new BOSH request", rid);

        // reset the inactivity
        currentInactivitySeconds = inactivitySeconds;

        final HttpServletRequest request = br.getHttpServletRequest();
        request.setAttribute(BOSH_REQUEST_ATTRIBUTE, br);
        final AsyncContext context = request.startAsync();
        addContinuationExpirationListener(context);
        context.setTimeout(this.wait * 1000);

        // allow two more parallel request, be generous in what you receive
        final int maxToleratedParallelRequests = parallelRequestsCount + 2;
        synchronized (requestsWindow) {

            // only allow 'parallelRequestsCount' request to be queued
            final long highestContinuousRid = requestsWindow.getHighestContinuousRid();
            if (highestContinuousRid != -1 && rid > highestContinuousRid + maxToleratedParallelRequests) {
                LOGGER.warn(
                        "SID = " + getSessionId()
                                + " - rid = {} - received RID >= the permitted window of concurrent requests ({})",
                        rid, highestContinuousRid);
                // don't queue // queueRequest(br);
                sendError(br, "item-not-found");
                return;
            }

            // resend missed responses
            final boolean resend = rid <= requestsWindow.getCurrentProcessingRequest();
            if (resend) {
                // OLD: if (highestContinuousRid != null && rid <= highestContinuousRid) {                
                synchronized (sentResponses) {
                    if (LOGGER.isInfoEnabled()) {
                        final String pendingRids = requestsWindow.logRequestWindow();
                        final String sentRids = logSentResponsesBuffer();
                        LOGGER.info("SID = " + getSessionId()
                                + " - rid = {} - resend request. sent buffer: {} - req.win.: " + pendingRids, rid,
                                sentRids);
                    }
                    if (sentResponses.containsKey(rid)) {
                        LOGGER.info("SID = " + getSessionId() + " - rid = {} (re-sending)", rid);
                        // Resending the old response
                        resendResponse(br);
                    } else {
                        // not in sent responses, try alternatives: backlog and requestWindow

                        final BoshResponse response = sentResponsesBacklog.lookup(rid);
                        if (response != null) {
                            LOGGER.warn(
                                    "SID = " + getSessionId()
                                            + " - rid = {} - BOSH response retrieved from sentResponsesBacklog",
                                    rid);
                            resendResponse(br, rid, response);
                            return; // no error
                        }

                        // rid not in sent responses, nor backlog. check to see if rid is still in requests window
                        boolean inRequestsWindow = requestsWindow.containsRid(rid);
                        if (!inRequestsWindow) {
                            if (LOGGER.isWarnEnabled()) {
                                final String sentRids = logSentResponsesBuffer();
                                LOGGER.warn(
                                        "SID = " + getSessionId()
                                                + " - rid = {} - BOSH response not in buffer error - " + sentRids,
                                        rid);
                            }
                        } else {
                            if (LOGGER.isWarnEnabled()) {
                                final String sentRids = logSentResponsesBuffer();
                                LOGGER.warn("SID = " + getSessionId()
                                        + " - rid = {} - BOSH response still in requests window - " + sentRids,
                                        rid);
                            }
                        }
                        sendError(br, "item-not-found");
                    }
                }
                return;
            }
            // check for too many parallel requests
            final boolean terminate = "terminate".equals(boshOuterBody.getAttributeValue("type"));
            final boolean pause = boshOuterBody.getAttributeValue("pause") != null;
            final boolean bodyIsEmpty = boshOuterBody.getInnerElements().isEmpty();
            final int distinctRIDs = requestsWindow.getDistinctRIDs();

            if (distinctRIDs >= maxToleratedParallelRequests && !terminate && !pause) {
                LOGGER.warn("SID = " + getSessionId()
                        + " - rid = {} - BOSH Overactivity: Too many simultaneous requests, max = {} "
                        + logRIDSequence(), rid, maxToleratedParallelRequests);
                sendError(br, "policy-violation");
                return;
            }
            // check for new request comes early
            if (distinctRIDs + 1 == maxToleratedParallelRequests && !terminate && !pause && bodyIsEmpty) {
                final long millisSinceLastCalls = Math
                        .abs(br.getTimestamp() - requestsWindow.getLatestAddionTimestamp());
                if (millisSinceLastCalls < pollingSeconds * 1000 && !rid.equals(requestsWindow.getLatestRID())) {
                    LOGGER.warn("SID = " + getSessionId()
                            + " - rid = {} - BOSH Overactivity: Too frequent requests, millis since requests = {}, "
                            + logRIDSequence(), rid, millisSinceLastCalls);
                    sendError(br, "policy-violation");
                    return;
                }
            }
            // check 
            if ((wait == 0 || hold == 0) && bodyIsEmpty) {
                final long millisBetweenEmptyReqs = Math
                        .abs(br.getTimestamp() - latestEmptyPollingRequestTimestamp);
                if (millisBetweenEmptyReqs < pollingSeconds * 1000 && !rid.equals(requestsWindow.getLatestRID())) {
                    LOGGER.warn("SID = " + getSessionId()
                            + " - rid = {} - BOSH Overactivity for polling: Too frequent requests, millis since requests = {}, "
                            + logRIDSequence(), rid, millisBetweenEmptyReqs);
                    sendError(br, "policy-violation");
                    return;
                }
                latestEmptyPollingRequestTimestamp = br.getTimestamp();
            }

            queueRequest(br);
        }

        if (isClientAcknowledgements()) {
            synchronized (sentResponses) {
                if (boshOuterBody.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
                    sentResponsesBacklog.addAll(sentResponses);
                    sentResponses.clear();
                } else if (!sentResponses.isEmpty()) {
                    // After receiving a request with an 'ack' value less than the 'rid' of the last request that it has already responded to,
                    // 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(boshOuterBody.getAttributeValue("ack"));
                    if (ack < sentResponses.lastKey() && sentResponses.containsKey(ack + 1)) {
                        long delta = System.currentTimeMillis() - sentResponses.get(ack + 1).getTimestamp();
                        if (delta >= brokenConnectionReportTimeoutMillis) {
                            sendBrokenConnectionReport(ack + 1, delta);
                            return;
                        }
                    }
                }
            }
        }

        // we cannot pause if there are missing requests, this is tested with
        // br.getRid().equals(requestsWindow.lastKey()) && highestContinuousRid.equals(br.getRid())
        synchronized (requestsWindow) {
            final String pauseAttribute = boshOuterBody.getAttributeValue("pause");
            if (pauseAttribute != null && rid.equals(requestsWindow.getLatestRID())
                    && rid.equals(requestsWindow.getHighestContinuousRid())) {
                int pause;
                try {
                    pause = Integer.parseInt(pauseAttribute);
                } catch (NumberFormatException e) {
                    queueRequest(br);
                    sendError("bad-request");
                    return;
                }
                pause = Math.max(0, pause);
                pause = Math.min(pause, maxpauseSeconds);
                respondToPause(pause);
                return;
            }
        }

        // 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;
        ArrayList<Stanza> mergeCandidates = null; // do not create until there is a delayed response
        while ((delayedResponse = delayedResponseQueue.poll()) != null) {
            if (mergeCandidates == null)
                mergeCandidates = new ArrayList<Stanza>();
            mergeCandidates.add(delayedResponse);
        }
        Stanza mergedResponse = BoshStanzaUtils.mergeResponses(mergeCandidates);
        if (mergedResponse != null) {
            LOGGER.debug("SID = " + getSessionId() + " - writing merged response. stanzas merged = "
                    + mergeCandidates.size());
            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(BoshStanzaUtils.EMPTY_BOSH_RESPONSE);
        }
    }

    public String logSentResponsesBuffer() {
        final StringBuffer logMsg = new StringBuffer("sent = [");
        for (Iterator<Long> iterator = sentResponses.keySet().iterator(); iterator.hasNext();) {
            Long sentRid = iterator.next();
            logMsg.append(sentRid);
            if (iterator.hasNext())
                logMsg.append(", ");
        }
        logMsg.append("]");
        return logMsg.toString();
    }

    protected void respondToPause(int pause) {
        LOGGER.debug("SID = " + getSessionId() + " - Setting inactivity period to {}", pause);
        currentInactivitySeconds = pause;
        while (!requestsWindow.isEmpty()) {
            writeBoshResponse(BoshStanzaUtils.EMPTY_BOSH_RESPONSE);
        }
    }

    protected void sendBrokenConnectionReport(long report, long delta) {
        StanzaBuilder stanzaBuilder = BoshStanzaUtils.createBrokenSessionReport(report, delta);
        writeBoshResponse(stanzaBuilder.build());
    }

    protected void addContinuationExpirationListener(final AsyncContext context) {
        // listen the continuation to be notified when the request expires
        context.addListener(new AsyncListener() {

            public void onTimeout(AsyncEvent event) throws IOException {
                requestExpired(context);
            }

            public void onStartAsync(AsyncEvent event) throws IOException {
                // ignore
            }

            public void onError(AsyncEvent event) throws IOException {
                handleAsyncEventError(event);
            }

            public void onComplete(AsyncEvent event) throws IOException {
                // ignore
            }
        });
    }

    protected void handleAsyncEventError(AsyncEvent event) {
        final ServletRequest suppliedRequest = event.getSuppliedRequest();
        final ServletResponse suppliedResponse = event.getSuppliedResponse();
        BoshRequest boshRequest = (BoshRequest) suppliedRequest.getAttribute(BOSH_REQUEST_ATTRIBUTE);
        BoshResponse boshResponse = (BoshResponse) suppliedRequest.getAttribute(BOSH_RESPONSE_ATTRIBUTE);

        // works at least for jetty:
        final Exception exceptionObject = (Exception) suppliedRequest.getAttribute("javax.servlet.error.exception");
        final Throwable throwable = event.getThrowable() != null ? event.getThrowable() : exceptionObject;

        final Long rid = boshRequest == null ? null : boshRequest.getRid();
        LOGGER.warn("SID = " + getSessionId() + " - JID = " + getInitiatingEntity() + " - RID = " + rid
                + " - async error on event ", event.getClass(), throwable);
    }

    protected void resendResponse(BoshRequest br) {
        final Long rid = br.getRid();
        BoshResponse boshResponse = sentResponses.get(rid);
        resendResponse(br, rid, boshResponse);
    }

    protected void resendResponse(BoshRequest br, Long rid, BoshResponse boshResponse) {
        if (boshResponse == null) {
            LOGGER.debug("SID = " + getSessionId()
                    + " - rid = {} - BOSH response could not (no longer) be retrieved for resending.", rid);
            return;
        }
        if (LOGGER.isDebugEnabled()) {
            LOGGER.debug("SID = " + getSessionId() + " - rid = {} - BOSH writing response (resending): {}", rid,
                    new String(boshResponse.getContent()));
        }

        try {
            final AsyncContext asyncContext = saveResponse(br, boshResponse);
            asyncContext.dispatch();
        } catch (Exception e) {
            LOGGER.warn("SID = " + getSessionId() + " - exception in async processing", e);
        }
        setLatestWriteTimestamp();
        updateInactivityChecker();
    }

    protected BoshResponse getBoshResponse(Stanza stanza, Long ack) {
        if (ack != null) {
            stanza = BoshStanzaUtils.addAttribute(stanza, "ack", ack.toString());
        }
        byte[] content = new Renderer(stanza).getComplete().getBytes(UTF8_CHARSET);
        return new BoshResponse(contentType, content);
    }

    protected void queueRequest(BoshRequest br) {
        requestsWindow.queueRequest(br);
        updateInactivityChecker();
    }

    protected AsyncContext saveResponse(final BoshRequest boshRequest, final BoshResponse boshResponse) {
        if (boshResponse == null)
            throw new IllegalArgumentException("boshResponse cannot be null");
        final HttpServletRequest request = boshRequest.getHttpServletRequest();
        final AsyncContext asyncContext = request.getAsyncContext();
        request.setAttribute(BOSH_RESPONSE_ATTRIBUTE, boshResponse);
        return asyncContext;
    }
}