Package org.atmosphere.cometd

Source Code of org.atmosphere.cometd.WebSocketTransport$LongPollScheduler

/*
* Copyright 2014 Jeanfrancois Arcand
*
* Licensed 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.
*/

// This class was hightly inspired by its Cometd implementation
/*
* Copyright (c) 2010 the original author or authors.
*
* Licensed 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.atmosphere.cometd;

import org.atmosphere.cpr.HeaderConfig;
import org.cometd.bayeux.Channel;
import org.cometd.bayeux.Message;
import org.cometd.bayeux.server.ServerMessage;
import org.cometd.server.AbstractServerTransport;
import org.cometd.server.BayeuxServerImpl;
import org.cometd.server.ServerSessionImpl;
import org.cometd.server.transport.LongPollingTransport;
import org.eclipse.jetty.continuation.Continuation;
import org.eclipse.jetty.continuation.ContinuationListener;
import org.eclipse.jetty.continuation.ContinuationSupport;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.text.ParseException;
import java.util.List;
import java.util.Map;


public class WebSocketTransport extends LongPollingTransport {
    private final Logger logger = LoggerFactory.getLogger(getClass());

    public final static String PREFIX = "long-polling.ws";
    public final static String NAME = "websocket";
    public final static String MIME_TYPE_OPTION = "mimeType";
    public final static String CALLBACK_PARAMETER_OPTION = "callbackParameter";

    private String _mimeType = "text/javascript;charset=UTF-8";
    private String _callbackParam = "jsonp";
    private boolean _autoBatch = true;
    private boolean _allowMultiSessionsNoBrowser = false;
    private long _multiSessionInterval = 2000;

    public WebSocketTransport(BayeuxServerImpl bayeux) {
        super(bayeux, NAME);
        setOptionPrefix(PREFIX);
    }

    /**
     * @see org.cometd.server.transport.LongPollingTransport#isAlwaysFlushingAfterHandle()
     */
    @Override
    protected boolean isAlwaysFlushingAfterHandle() {
        return true;
    }

    /**
     * @see org.cometd.server.transport.JSONTransport#init()
     */
    @Override
    protected void init() {
        super.init();
        _callbackParam = getOption(CALLBACK_PARAMETER_OPTION, _callbackParam);
        _mimeType = getOption(MIME_TYPE_OPTION, _mimeType);
    }

    @Override
    public boolean accept(HttpServletRequest request) {
        return request.getHeader(HeaderConfig.X_ATMOSPHERE_TRANSPORT) == HeaderConfig.WEBSOCKET_TRANSPORT;
    }

    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
        // Is this a resumed connect?
        LongPollScheduler scheduler = (LongPollScheduler) request.getAttribute(LongPollScheduler.ATTRIBUTE);
        if (scheduler == null) {
            // No - process messages

            // Remember if we start a batch
            boolean batch = false;

            // Don't know the session until first message or handshake response.
            ServerSessionImpl session = null;
            boolean connect = false;

            try {
                ServerMessage.Mutable[] messages = parseMessages(request);
                if (messages == null)
                    return;

                PrintWriter writer = null;
                for (ServerMessage.Mutable message : messages) {
                    // Is this a connect?
                    connect = Channel.META_CONNECT.equals(message.getChannel());

                    // Get the session from the message
                    String client_id = message.getClientId();
                    if (session == null || client_id != null && !client_id.equals(session.getId())) {
                        session = (ServerSessionImpl) getBayeux().getSession(client_id);
                        if (_autoBatch && !batch && session != null && !connect && !message.isMeta()) {
                            // start a batch to group all resulting messages into a single response.
                            batch = true;
                            session.startBatch();
                        }
                    } else if (!session.isHandshook()) {
                        batch = false;
                        session = null;
                    }

                    if (connect && session != null) {
                        // cancel previous scheduler to cancel any prior waiting long poll
                        // this should also dec the browser ID
                        session.setScheduler(null);
                    }

                    boolean wasConnected = session != null && session.isConnected();

                    // Forward handling of the message.
                    // The actual reply is return from the call, but other messages may
                    // also be queued on the session.
                    ServerMessage.Mutable reply = bayeuxServerHandle(session, message);

                    // Do we have a reply ?
                    if (reply != null) {
                        if (session == null) {
                            // This must be a handshake, extract a session from the reply
                            session = (ServerSessionImpl) getBayeux().getSession(reply.getClientId());

                            // Get the user agent while we are at it, and add the browser ID cookie
                            if (session != null) {
                                String userAgent = request.getHeader("User-Agent");
                                session.setUserAgent(userAgent);

                                String browserId = findBrowserId(request);
                                if (browserId == null)
                                    setBrowserId(request, response);
                            }
                        } else {
                            // Special handling for connect
                            if (connect) {
                                try {
                                    writer = sendQueue(request, response, session, writer);

                                    // If the writer is non null, we have already started sending a response, so we should not suspend
                                    if (writer == null && reply.isSuccessful() && session.isQueueEmpty()) {
                                        // Detect if we have multiple sessions from the same browser
                                        // Note that CORS requests do not send cookies, so we need to handle them specially
                                        // CORS requests always have the Origin header

                                        String browserId = findBrowserId(request);
                                        boolean allowSuspendConnect;
                                        if (browserId != null)
                                            allowSuspendConnect = incBrowserId(browserId);
                                        else
                                            allowSuspendConnect = _allowMultiSessionsNoBrowser;

                                        if (allowSuspendConnect) {
                                            long timeout = session.calculateTimeout(getTimeout());

                                            // Support old clients that do not send advice:{timeout:0} on the first connect
                                            if (timeout > 0 && wasConnected && session.isConnected()) {
                                                // Suspend and wait for messages
                                                Continuation continuation = ContinuationSupport.getContinuation(request);
                                                continuation.setTimeout(timeout);
                                                continuation.suspend(response);
                                                scheduler = new LongPollScheduler(session, continuation, reply, browserId);
                                                session.setScheduler(scheduler);
                                                request.setAttribute(LongPollScheduler.ATTRIBUTE, scheduler);
                                                reply = null;
                                                metaConnectSuspended(request, session, timeout);
                                            } else {
                                                decBrowserId(browserId);
                                            }
                                        } else {
                                            // There are multiple sessions from the same browser
                                            Map<String, Object> advice = reply.getAdvice(true);

                                            if (browserId != null)
                                                advice.put("multiple-clients", true);

                                            if (_multiSessionInterval > 0) {
                                                advice.put(Message.RECONNECT_FIELD, Message.RECONNECT_RETRY_VALUE);
                                                advice.put(Message.INTERVAL_FIELD, _multiSessionInterval);
                                            } else {
                                                advice.put(Message.RECONNECT_FIELD, Message.RECONNECT_NONE_VALUE);
                                                reply.setSuccessful(false);
                                            }
                                            session.reAdvise();
                                        }
                                    }
                                } finally {
                                    if (reply != null && session.isConnected())
                                        session.startIntervalTimeout(getInterval());
                                }
                            } else {
                                if (!isMetaConnectDeliveryOnly() && !session.isMetaConnectDeliveryOnly()) {
                                    writer = sendQueue(request, response, session, writer);
                                }
                            }
                        }

                        // If the reply has not been otherwise handled, send it
                        if (reply != null) {
                            if (connect && session != null && !session.isConnected())
                                reply.getAdvice(true).put(Message.RECONNECT_FIELD, Message.RECONNECT_NONE_VALUE);

                            reply = getBayeux().extendReply(session, session, reply);

                            if (reply != null) {
                                getBayeux().freeze(reply);
                                writer = send(request, response, writer, reply);
                            }
                        }
                    }

                    // Disassociate the reply
                    message.setAssociated(null);
                }
                if (writer != null)
                    complete(writer);
            } catch (ParseException x) {
                handleJSONParseException(request, response, x.getMessage(), x.getCause());
            } finally {
                // If we started a batch, end it now
                if (batch) {
                    boolean ended = session.endBatch();

                    // Flush session if not done by the batch, since some browser order <script> requests
                    if (!ended && isAlwaysFlushingAfterHandle())
                        session.flush();
                } else if (session != null && !connect && isAlwaysFlushingAfterHandle()) {
                    session.flush();
                }
            }
        } else {
            // Get the resumed session
            ServerSessionImpl session = scheduler.getSession();
            metaConnectResumed(request, session);

            PrintWriter writer;
            try {
                // Send the message queue
                writer = sendQueue(request, response, session, null);
            } finally {
                // We need to start the interval timeout before the connect reply
                // otherwise we open up a race condition where the client receives
                // the connect reply and sends a new connect request before we start
                // the interval timeout, which will be wrong.
                // We need to put this into a finally block in case sending the queue
                // throws an exception (for example because the client is gone), so that
                // we start the interval timeout that is important to sweep the session
                if (session.isConnected())
                    session.startIntervalTimeout(getInterval());
            }

            // Send the connect reply
            ServerMessage.Mutable reply = scheduler.getReply();

            if (!session.isConnected())
                reply.getAdvice(true).put(Message.RECONNECT_FIELD, Message.RECONNECT_NONE_VALUE);

            reply = getBayeux().extendReply(session, session, reply);

            if (reply != null) {
                getBayeux().freeze(reply);
                writer = send(request, response, writer, reply);
            }

            complete(writer);
        }
    }

    private PrintWriter sendQueue(HttpServletRequest request, HttpServletResponse response, ServerSessionImpl session, PrintWriter writer)
            throws IOException {
        final List<ServerMessage> queue = session.takeQueue();
        for (ServerMessage m : queue)
            writer = send(request, response, writer, m);
        return writer;
    }

    @Override
    protected ServerMessage.Mutable[] parseMessages(HttpServletRequest request) throws IOException, ParseException {
        return super.parseMessages(request.getReader(), true);
    }

    @Override
    protected PrintWriter send(HttpServletRequest request, HttpServletResponse response, PrintWriter writer, ServerMessage message) throws IOException {
        StringBuilder builder = new StringBuilder(message.size() * 32);
        if (writer == null) {
            response.setContentType(_mimeType);
            writer = response.getWriter();
        }
        builder.append("[").append(message.getJSON()).append("]");
        writer.append(builder.toString());
        return writer;
    }

    @Override
    protected void complete(PrintWriter writer) throws IOException {
    }

    private class LongPollScheduler implements AbstractServerTransport.OneTimeScheduler, ContinuationListener {
        private static final String ATTRIBUTE = "org.cometd.scheduler";

        private final ServerSessionImpl _session;
        private final Continuation _continuation;
        private final ServerMessage.Mutable _reply;
        private String _browserId;

        public LongPollScheduler(ServerSessionImpl session, Continuation continuation, ServerMessage.Mutable reply, String browserId) {
            _session = session;
            _continuation = continuation;
            _continuation.addContinuationListener(this);
            _reply = reply;
            _browserId = browserId;
        }

        public void cancel() {
            if (_continuation != null && _continuation.isSuspended() && !_continuation.isExpired()) {
                try {
                    decBrowserId();
                    ((HttpServletResponse) _continuation.getServletResponse()).sendError(HttpServletResponse.SC_REQUEST_TIMEOUT);
                } catch (IOException x) {
                    logger.trace("", x);
                }

                try {
                    _continuation.complete();
                } catch (Exception x) {
                    logger.trace("", x);
                }
            }
        }

        public void schedule() {
            decBrowserId();
            _continuation.resume();
        }

        public ServerSessionImpl getSession() {
            return _session;
        }

        public ServerMessage.Mutable getReply() {
            Map<String, Object> advice = _session.takeAdvice();
            if (advice != null)
                _reply.put(Message.ADVICE_FIELD, advice);
            return _reply;
        }

        public void onComplete(Continuation continuation) {
            decBrowserId();
        }

        public void onTimeout(Continuation continuation) {
            _session.setScheduler(null);
        }

        private void decBrowserId() {
            WebSocketTransport.this.decBrowserId(_browserId);
            _browserId = null;
        }
    }
}
TOP

Related Classes of org.atmosphere.cometd.WebSocketTransport$LongPollScheduler

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.