Package flex.messaging.endpoints

Source Code of flex.messaging.endpoints.BaseStreamingHTTPEndpoint

/*************************************************************************
*
* ADOBE CONFIDENTIAL
* __________________
*
*  [2002] - [2007] Adobe Systems Incorporated
*  All Rights Reserved.
*
* NOTICE:  All information contained herein is, and remains
* the property of Adobe Systems Incorporated and its suppliers,
* if any.  The intellectual and technical concepts contained
* herein are proprietary to Adobe Systems Incorporated
* and its suppliers and may be covered by U.S. and Foreign Patents,
* patents in process, and are protected by trade secret or copyright law.
* Dissemination of this information or reproduction of this material
* is strictly forbidden unless prior written permission is obtained
* from Adobe Systems Incorporated.
**************************************************************************/
package flex.messaging.endpoints;

import flex.messaging.FlexContext;
import flex.messaging.FlexSession;
import flex.messaging.MessageException;
import flex.messaging.client.EndpointPushNotifier;
import flex.messaging.client.FlexClient;
import flex.messaging.client.FlushResult;
import flex.messaging.client.UserAgentSettings;
import flex.messaging.config.ConfigMap;
import flex.messaging.log.Log;
import flex.messaging.messages.AcknowledgeMessage;
import flex.messaging.messages.CommandMessage;
import flex.messaging.messages.Message;
import flex.messaging.messages.MessagePerformanceInfo;
import flex.messaging.messages.MessagePerformanceUtils;
import flex.messaging.util.TimeoutManager;
import flex.messaging.util.UserAgentManager;

import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;

/**
* Base for HTTP-based endpoints that support streaming HTTP connections to
* connected clients.
* Each streaming connection managed by this endpoint consumes one of the request
* handler threads provided by the servlet container, so it is not highly scalable
* but offers performance advantages over client polling for clients receiving
* a steady, rapid stream of pushed messages.
* This endpoint does not support polling clients and will fault any poll requests
* that are received. To support polling clients use subclasses of
* BaseHTTPEndpoint instead.
*/
public abstract class BaseStreamingHTTPEndpoint extends BaseHTTPEndpoint
{
    //--------------------------------------------------------------------------
    //
    // Private Static Constants
    //
    //--------------------------------------------------------------------------

    /**
     * This token is used in chunked HTTP responses frequently so initialize it statically for general use.
     */
    private static final byte[] CRLF_BYTES = {(byte)13, (byte)10};

    /**
     * This token is used for the terminal chunk within a chunked response.
     */
    private static final byte ZERO_BYTE = (byte)48;

    /**
     * This token is used to signal that a chunk of data should be skipped by the client.
     */
    private static final byte NULL_BYTE = (byte)0;

    /**
     * Parameter name for 'command' passed in a request for a new streaming connection.
     */
    private static final String COMMAND_PARAM_NAME = "command";

    /**
     * This is the token at the end of the HTTP request line that indicates that it's
     * a stream connection that we should hold open to push data back to the client over
     * as opposed to a regular request-response message.
     */
    private static final String OPEN_COMMAND = "open";

    /**
     * This is the token at the end of the HTTP request line that indicates that it's
     * a stream connection that we should close.
     */
    private static final String CLOSE_COMMAND = "close";

    /**
     *  Parameter name for the stream id; passed with commands for an existing streaming connection.
     */
    private static final String STREAM_ID_PARAM_NAME = "streamId";

    /**
     * Parameter name for 'version' passed in a request for a new streaming connection.
     */
    private static final String VERSION_PARAM_NAME = "version";

    /**
     * Constant for HTTP/1.0.
     */
    private static final String HTTP_1_0 = "HTTP/1.0";

    /**
     * Thread name suffix for request threads that are servicing a pinned open streaming connection.
     */
    private static final String STREAMING_THREAD_NAME_EXTENSION = "-in-streaming-mode";

    /**
     * Configuration constants.
     */
    private static final String IDLE_TIMEOUT_MINUTES = "idle-timeout-minutes";
    private static final String MAX_STREAMING_CLIENTS = "max-streaming-clients";
    private static final String SERVER_TO_CLIENT_HEARTBEAT_MILLIS = "server-to-client-heartbeat-millis";

    /**
     * Defaults.
     */
    private static final int DEFAULT_SERVER_TO_CLIENT_HEARTBEAT_MILLIS = 5000;
    private static final int DEFAULT_IDLE_TIMEOUT_MINUTES = 0;
    private static final int DEFAULT_MAX_STREAMING_CLIENTS = 10;

    /**
     * Errors.
     */
    public static final String POLL_NOT_SUPPORTED_CODE = "Server.PollNotSupported";
    public static final int POLL_NOT_SUPPORTED_MESSAGE = 10034;


    //--------------------------------------------------------------------------
    //
    // Constructor
    //
    //--------------------------------------------------------------------------

    /**
     * Constructs an unmanaged <code>BaseStreamingHTTPEndpoint</code>.
     */
    public BaseStreamingHTTPEndpoint()
    {
        this(false);
    }

    /**
     * Constructs an <code>BaseStreamingHTTPEndpoint</code> with the indicated management.
     *
     * @param enableManagement <code>true</code> if the <code>BaseStreamingHTTPEndpoint</code>
     * is manageable; otherwise <code>false</code>.
     */
    public BaseStreamingHTTPEndpoint(boolean enableManagement)
    {
        super(enableManagement);
        setIdleTimeoutMinutes(idleTimeoutMinutes);
    }

    //--------------------------------------------------------------------------
    //
    // Initialize, validate, start, and stop methods.
    //
    //--------------------------------------------------------------------------

    /**
     * Initializes the <code>Endpoint</code> with the properties.
     * If subclasses override, they must call <code>super.initialize()</code>.
     *
     * @param id Id of the <code>Endpoint</code>.
     * @param properties Properties for the <code>Endpoint</code>.
     */
    public void initialize(String id, ConfigMap properties)
    {
        super.initialize(id, properties);

        if (properties == null || properties.size() == 0)
            return;

        // The interval that the server will check if the client is still available.
        serverToClientHeartbeatMillis = properties.getPropertyAsLong(SERVER_TO_CLIENT_HEARTBEAT_MILLIS, DEFAULT_SERVER_TO_CLIENT_HEARTBEAT_MILLIS);
        setServerToClientHeartbeatMillis(serverToClientHeartbeatMillis);

        // Number of minutes a client can remain idle before the server times the connection out.
        int idleTimeoutMinutes = properties.getPropertyAsInt(IDLE_TIMEOUT_MINUTES, DEFAULT_IDLE_TIMEOUT_MINUTES);
        setIdleTimeoutMinutes(idleTimeoutMinutes);

        // User-agent configuration for kick-start bytes and max streaming connections per session.
        UserAgentManager.setupUserAgentManager(properties, userAgentManager);

        // Maximum number of clients allowed to have streaming HTTP connections with the endpoint.
        maxStreamingClients = properties.getPropertyAsInt(MAX_STREAMING_CLIENTS, DEFAULT_MAX_STREAMING_CLIENTS);

        // Set initial state for the canWait flag based on whether we allow waits or not.
        canStream =  (maxStreamingClients > 0);
    }


    public void start()
    {
        if (isStarted())
            return;

        super.start();

        if (idleTimeoutMinutes > 0)
            pushNotifierTimeoutManager = new TimeoutManager();

        currentStreamingRequests = new ConcurrentHashMap();
    }

    /**
     * @see flex.messaging.endpoints.AbstractEndpoint#stop()
     */
    public void stop()
    {
        if (!isStarted())
            return;

        // Shutdown the timeout manager for streaming connections cleanly.
        if (pushNotifierTimeoutManager != null)
        {
            pushNotifierTimeoutManager.shutdown();
            pushNotifierTimeoutManager = null;
        }

        // Shutdown any currently open streaming connections.
        for (Iterator iter = currentStreamingRequests.values().iterator(); iter.hasNext(); )
        {
            EndpointPushNotifier notifier = (EndpointPushNotifier)iter.next();
            notifier.close();
        }
        currentStreamingRequests = null;

        super.stop();
    }

    //--------------------------------------------------------------------------
    //
    // Variables
    //
    //--------------------------------------------------------------------------

    /**
     * Used to synchronize sets and gets to the number of streaming clients.
     */
    protected final Object lock = new Object();

    /**
     * Used to keep track of the mapping between user agent match strings and
     * the bytes needed to kickstart their streaming connections.
     */
    protected UserAgentManager userAgentManager = new UserAgentManager();

    /**
     * This flag is volatile to allow for consistent reads across thread without
     * needing to pay the cost for a synchronized lock for each read.
     */
    private volatile boolean canStream = true;

    /**
     * Manages timing out EndpointPushNotifier instances.
     */
    private volatile TimeoutManager pushNotifierTimeoutManager;

    /**
     * A Map(EndpointPushNotifier, Boolean.TRUE) containing all currently open streaming notifiers
     * for this endpoint.
     * Used for clean shutdown.
     */
    private ConcurrentHashMap currentStreamingRequests;

    //--------------------------------------------------------------------------
    //
    // Properties
    //
    //--------------------------------------------------------------------------

    //----------------------------------
    //  serverToClientHeartbeatMillis
    //----------------------------------

    private long serverToClientHeartbeatMillis = DEFAULT_SERVER_TO_CLIENT_HEARTBEAT_MILLIS;

    /**
     * Returns the number of milliseconds the server will wait before writing a
     * single null byte to the streaming connection to make sure the client is
     * still available.
     */
    public long getServerToClientHeartbeatMillis()
    {
        return serverToClientHeartbeatMillis;
    }

    /**
     * Returns the number of milliseconds the server will wait before writing a
     * single null byte to the streaming connection to make sure the client is
     * still available when there are no new messages for the client.
     * A non-positive value means server will wait forever for new messages and
     * it will not write the null byte to determine if the client is available.
     */
    public void setServerToClientHeartbeatMillis(long serverToClientHeartbeatMillis)
    {
        if (serverToClientHeartbeatMillis < 0)
            serverToClientHeartbeatMillis = 0;
        this.serverToClientHeartbeatMillis = serverToClientHeartbeatMillis;
    }

    //----------------------------------
    //  idleTimeoutMinutes
    //----------------------------------

    private int idleTimeoutMinutes = DEFAULT_IDLE_TIMEOUT_MINUTES;

    /**
     * Returns the number of minutes a client can remain idle before the server
     * times the connection out.
     *
     * @return The number of minutes a client can remain idle before the server
     * times the connection out.
     */
    public int getIdleTimeoutMinutes()
    {
        return idleTimeoutMinutes;
    }

    /**
     * Sets the number of minutes a client can remain idle before the server
     * times the connection out. A value of 0 or below indicates that
     * connections will not be timed out.
     *
     * @param idleTimeoutMinutes The number of minutes a client can remain idle
     * before the server times the connection out.
     */
    public void setIdleTimeoutMinutes(int idleTimeoutMinutes)
    {
        this.idleTimeoutMinutes = idleTimeoutMinutes;
    }

    //----------------------------------
    //  maxStreamingClients
    //----------------------------------

    private int maxStreamingClients = DEFAULT_MAX_STREAMING_CLIENTS;

    /**
     * Returns the maximum number of clients that will be allowed to establish
     * a streaming HTTP connection with the endpoint.
     *
     * @return The maximum number of clients that will be allowed to establish
     * a streaming HTTP connection with the endpoint.
     */
    public int getMaxStreamingClients()
    {
        return maxStreamingClients;
    }

    /**
     * Sets the maximum number of clients that will be allowed to establish
     * a streaming HTTP connection with the server.
     *
     * @param maxStreamingClients The maximum number of clients that will be allowed
     * to establish a streaming HTTP connection with the server.
     */
    public void setMaxStreamingClients(int maxStreamingClients)
    {
        this.maxStreamingClients = maxStreamingClients;
        canStream = (streamingClientsCount < maxStreamingClients);
    }

    //----------------------------------
    //  streamingClientsCount
    //----------------------------------

    protected int streamingClientsCount;

    /**
     * Returns the the number of clients that are currently in the streaming state.
     *
     * @return The number of clients that are currently in the streaming state.
     */
    public int getStreamingClientsCount()
    {
        return streamingClientsCount;
    }

    //--------------------------------------------------------------------------
    //
    // Public Methods
    //
    //--------------------------------------------------------------------------

    /**
     * @exclude
     * Returns a <code>ConfigMap</code> of endpoint properties that the client
     * needs. This includes properties from <code>super.describeEndpoint</code>
     * and additional <code>BaseHTTPEndpoint</code> specific properties under
     * "properties" key.
     */
    public ConfigMap describeEndpoint()
    {
        // Any future properties that will be needed by the client should be
        // added here.
        return super.describeEndpoint();
    }

    /**
     * Handles HTTP requests targetting this endpoint.
     * Two types or requests are supported. If the request is a regular request-response AMF/AMFX
     * message it is handled by the base logic in BaseHTTPEndpoint.service. However, if it is a
     * request to open a streaming HTTP connection to the client this endpoint performs some
     * validation checks and then holds the connection open to stream data back to the client
     * over.
     *
     * @param req The original servlet request
     * @param res The active servlet response
     */
    public void service(HttpServletRequest req, HttpServletResponse res)
    {
        String command = req.getParameter(COMMAND_PARAM_NAME);
        if (command != null)
            serviceStreamingRequest(req, res);
        else // Let BaseHTTPEndpoint logic handle regular request-response messaging.
            super.service(req, res);
    }

    //--------------------------------------------------------------------------
    //
    // Protected Methods
    //
    //--------------------------------------------------------------------------

    /**
     * If the message has MPI enabled, this method adds all the needed performance
     * headers to the message.
     *
     * @param message Message to add performance headers to.
     */
    protected void addPerformanceInfo(Message message)
    {
        // If MPI is not enabled, simply return.
        MessagePerformanceInfo mpiOriginal = MessagePerformanceUtils.getMPII(message);
        if (mpiOriginal == null)
            return;

        // Otherwise, move the MPII object of the queued message to be
        // the MPIP object of the outgoing message.
        MessagePerformanceInfo mpip;
        mpip = (MessagePerformanceInfo)mpiOriginal.clone();
        try
        {
            // Set the original message info as the pushed causer info.
            MessagePerformanceUtils.setMPIP(message, mpip);
            MessagePerformanceUtils.setMPII(message, null);
        }
        catch (Exception e)
        {
            if (Log.isDebug())
                log.debug("MPI exception while streaming the message: " + e.toString());
        }

        // Overhead only used when MPI is enabled for sizing
        long serializationOverhead;
        MessagePerformanceInfo mpio;
        mpio = new MessagePerformanceInfo();
        if (mpip.recordMessageTimes)
        {
            mpio.sendTime = System.currentTimeMillis();
            mpio.infoType = "OUT";
        }
        mpio.pushedFlag = true;
        MessagePerformanceUtils.setMPIO(message, mpio);

        // If MPI sizing information is enabled serialize again so that we know size
        if (mpip.recordMessageSizes)
        {
            try
            {
                // Each subclass serializes the message in their own format to
                // get the message size for the MPIO.
                serializationOverhead = System.currentTimeMillis();
                mpio.messageSize = getMessageSizeForPerformanceInfo(message);

                // Set serialization overhead to the time calculated during serialization above
                if (mpip.recordMessageTimes)
                {
                    serializationOverhead = System.currentTimeMillis() - serializationOverhead;
                    mpip.addToOverhead(serializationOverhead);
                    mpiOriginal.addToOverhead(serializationOverhead);
                    mpio.sendTime = System.currentTimeMillis();
                }
            }
            catch(Exception e)
            {
                log.debug("MPI exception while streaming the message: " + e.toString());
            }
        }
    }

    /**
     * Used internally for performance information gathering; not intended for
     * public use. The default implementation of this method returns zero.
     * Subclasses should overwrite if they want to accurately report message
     * size information in performance information gathering.
     *
     * @param message Message to get the size for.
     *
     * @return The size of the message after message is serialized.
     */
    protected long getMessageSizeForPerformanceInfo(Message message)
    {
        return 0;
    }

    /**
     * This streaming endpoint does not support polling clients.
     *
     * @param flexClient The FlexClient that issued the poll request.
     * @param pollCommand The poll command from the client.
     * @return The flush info used to build the poll response.
     */
    protected FlushResult handleFlexClientPoll(FlexClient flexClient, CommandMessage pollCommand)
    {
        MessageException me = new MessageException();
        me.setMessage(POLL_NOT_SUPPORTED_MESSAGE);
        me.setDetails(POLL_NOT_SUPPORTED_MESSAGE);
        me.setCode(POLL_NOT_SUPPORTED_CODE);
        throw me;
    }

    /**
     * Handles streaming connection open command sent by the FlexClient.
     *
     * @param req The <code>HttpServletRequest</code> to service.
     * @param res The <code>HttpServletResponse</code> to be used in case an error
     * has to be sent back.
     * @param flexClient FlexClient that requested the streaming connection.
     */
    protected void handleFlexClientStreamingOpenRequest(HttpServletRequest req, HttpServletResponse res, FlexClient flexClient)
    {
        FlexSession session = FlexContext.getFlexSession();
        if (canStream && session.canStream)
        {
            // If canStream/session.canStream is true it means we currently have
            // less than the max number of allowed streaming threads, per endpoint/session.

            // We need to protect writes/reads to the stream count with the endpoint's lock.
            // Also, we have to be careful to handle the case where two threads get to this point when only
            // one streaming spot remains; one thread will win and the other needs to fault.
            boolean thisThreadCanStream;
            synchronized (lock)
            {
                ++streamingClientsCount;
                if (streamingClientsCount == maxStreamingClients)
                {
                    thisThreadCanStream = true; // This thread got the last spot.
                    canStream = false;
                }
                else if (streamingClientsCount > maxStreamingClients)
                {
                    thisThreadCanStream = false; // This thread was beaten out for the last spot.
                    --streamingClientsCount; // Decrement the count because we're not going to grant the streaming right to the client.
                }
                else
                {
                    // We haven't hit the limit yet, allow this thread to stream.
                    thisThreadCanStream = true;
                }
            }

            // If the thread cannot wait due to endpoint streaming connection
            // limit, inform the client and return.
            if (!thisThreadCanStream)
            {
                if (Log.isError())
                    log.error("Endpoint with id '" + getId() + "' cannot grant streaming connection to FlexClient with id '"
                            + flexClient.getId() + "' because " + MAX_STREAMING_CLIENTS + " limit of '"
                            + maxStreamingClients + "' has been reached.");
                try
                {
                    // Return an HTTP status code 400.
                    res.sendError(HttpServletResponse.SC_BAD_REQUEST);
                }
                catch (IOException ignore)
                {}
                return;
            }

            // Setup for specific user agents.
            byte[] kickStartBytesToStream = null;
            String userAgentValue = req.getHeader(UserAgentManager.USER_AGENT_HEADER_NAME);
            UserAgentSettings agentSettings = userAgentManager.match(userAgentValue);
            if (agentSettings != null)
            {
                synchronized (session)
                {
                    session.maxConnectionsPerSession = agentSettings.getMaxStreamingConnectionsPerSession();
                }
                int kickStartBytes = agentSettings.getKickstartBytes();
                if (kickStartBytes > 0)
                {
                    // Determine the minimum number of actual bytes that need to be sent to
                    // kickstart, taking into account transfer-encoding overhead.
                    try
                    {
                        int chunkLengthHeaderSize = Integer.toHexString(kickStartBytes).getBytes("ASCII").length;
                        int chunkOverhead = chunkLengthHeaderSize + 4; // 4 for the 2 wrapping CRLF tokens.
                        int minimumKickstartBytes = kickStartBytes - chunkOverhead;
                        kickStartBytesToStream = new byte[(minimumKickstartBytes > 0) ? minimumKickstartBytes :
                                kickStartBytes];
                    }
                    catch (UnsupportedEncodingException ignore)
                    {
                        kickStartBytesToStream = new byte[kickStartBytes];
                    }
                    Arrays.fill(kickStartBytesToStream, NULL_BYTE);
                }
            }

            // Now, check with the session before granting the streaming connection.
            synchronized(session)
            {
                ++session.streamingConnectionsCount;
                if (session.streamingConnectionsCount == session.maxConnectionsPerSession)
                {
                    thisThreadCanStream = true; // This thread got the last spot in the session.
                    session.canStream = false;
                }
                else if (session.streamingConnectionsCount > session.maxConnectionsPerSession)
                {
                    thisThreadCanStream = false; // This thread was beaten out for the last spot.
                    --session.streamingConnectionsCount;
                    synchronized(lock)
                    {
                        // Decrement the endpoint count because we're not going to grant the streaming right to the client.
                        --streamingClientsCount;
                    }
                }
                else
                {
                    // We haven't hit the limit yet, allow this thread to stream.
                    thisThreadCanStream = true;
                }
            }

            // If the thread cannot wait due to session streaming connection
            // limit, inform the client and return.
            if (!thisThreadCanStream)
            {
                if (Log.isInfo())
                    log.info("Endpoint with id '" + getId() + "' cannot grant streaming connection to FlexClient with id '"
                            + flexClient.getId() + "' because " + UserAgentManager.MAX_STREAMING_CONNECTIONS_PER_SESSION + " limit of '" + session.maxConnectionsPerSession
                            + ((agentSettings != null) ? "' for user-agent '" + agentSettings.getMatchOn() + "'" : "") " has been reached." );
                try
                {
                 // Return an HTTP status code 400.
                    res.sendError(HttpServletResponse.SC_BAD_REQUEST);
                }
                catch (IOException ignore)
                {
                    // NOWARN
                }
                return;
            }

            Thread currentThread = Thread.currentThread();
            String threadName = currentThread.getName();
            EndpointPushNotifier notifier = null;
            boolean suppressIOExceptionLogging = false; // Used to suppress logging for IO exception.
            try
            {
                currentThread.setName(threadName + STREAMING_THREAD_NAME_EXTENSION);

                // Open and commit response headers and get output stream.
                if (addNoCacheHeaders)
                    addNoCacheHeaders(req, res);
                res.setContentType(getResponseContentType());
                res.setHeader("Connection", "close");
                res.setHeader("Transfer-Encoding", "chunked");
                ServletOutputStream os = res.getOutputStream();
                res.flushBuffer();

                // If kickstart-bytes are specified, stream them.
                if (kickStartBytesToStream != null)
                {
                    if (Log.isDebug())
                        log.debug("Endpoint with id '" + getId() + "' is streaming " + kickStartBytesToStream.length
                                + " bytes (not counting chunk encoding overhead) to kick-start the streaming connection for FlexClient with id '"
                                + flexClient.getId() + "'.");

                    streamChunk(kickStartBytesToStream, os, res);
                }

                // Setup serialization and type marshalling contexts
                setThreadLocals();

                // Activate streaming helper for this connection.
                // Watch out for duplicate stream issues.
                try
                {
                    notifier = new EndpointPushNotifier(this, flexClient);
                }
                catch (MessageException me)
                {
                    if (me.getNumber() == 10033) // It's a duplicate stream request from the same FlexClient. Leave the current stream in place and fault this.
                    {
                        if (Log.isWarn())
                            log.warn("Endpoint with id '" + getId() + "' received a duplicate streaming connection request from, FlexClient with id '"
                                    + flexClient.getId() + "'. Faulting request.");

                        // Rollback counters and send an error response.
                        synchronized (lock)
                        {
                            --streamingClientsCount;
                            canStream = (streamingClientsCount < maxStreamingClients);
                            synchronized (session)
                            {
                                --session.streamingConnectionsCount;
                                session.canStream = (session.streamingConnectionsCount < session.maxConnectionsPerSession);
                            }
                        }
                        try
                        {
                            res.sendError(HttpServletResponse.SC_BAD_REQUEST);
                        }
                        catch (IOException ignore)
                        {
                            // NOWARN
                        }
                        return; // Exit early.
                    }
                }
                notifier.setIdleTimeoutMinutes(idleTimeoutMinutes);
                notifier.setLogCategory(getLogCategory());
                monitorTimeout(notifier);
                currentStreamingRequests.put(notifier.getNotifierId(), notifier);

                // Push down an acknowledgement for the 'connect' request containing the unique id for this specific stream.
                AcknowledgeMessage connectAck = new AcknowledgeMessage();
                connectAck.setBody(notifier.getNotifierId());
                connectAck.setCorrelationId(BaseStreamingHTTPEndpoint.OPEN_COMMAND);
                ArrayList toPush = new ArrayList(1);
                toPush.add(connectAck);
                streamMessages(toPush, os, res);

                // Output session level streaming count.
                if (Log.isDebug())
                    Log.getLogger(FlexSession.FLEX_SESSION_LOG_CATEGORY).info("Number of streaming clients for FlexSession with id '"+ session.getId() +"' is " + session.streamingConnectionsCount + ".");

                // Output endpoint level streaming count.
                if (Log.isDebug())
                    log.debug("Number of streaming clients for endpoint with id '"+ getId() +"' is " + streamingClientsCount + ".");

                // And cycle in a wait-notify loop with the aid of the helper until it
                // is closed, we're interrupted or the act of streaming data to the client fails.
                while (!notifier.isClosed())
                {
                    // Synchronize on pushNeeded which is our condition variable.
                    synchronized (notifier.pushNeeded)
                    {
                        try
                        {
                            // Drain any messages that might have been accumulated
                            // while the previous drain was being processed.
                            streamMessages(notifier.drainMessages(), os, res);

                            notifier.pushNeeded.wait(serverToClientHeartbeatMillis);

                            List messages = notifier.drainMessages();
                            // If there are no messages to send to the client, send an null
                            // byte as a heartbeat to make sure the client is still valid.
                            if (messages == null && serverToClientHeartbeatMillis > 0)
                            {
                                try
                                {
                                    os.write(NULL_BYTE);
                                    res.flushBuffer();
                                }
                                catch (IOException e)
                                {
                                    if (Log.isWarn())
                                        log.warn("Endpoint with id '" + getId() + "' is closing the streaming connection to FlexClient with id '"
                                                + flexClient.getId() + "' because endpoint encountered a socket write error" +
                                        ", possibly due to an unresponsive FlexClient.");
                                    break; // Exit the wait loop.
                                }
                            }
                            // Otherwise stream the messages to the client.
                            else
                            {
                                // Update the last time notifier was used to drain messages.
                                // Important for idle timeout detection.
                                notifier.updateLastUse();

                                streamMessages(messages, os, res);
                            }
                        }
                        catch (InterruptedException e)
                        {
                            if (Log.isWarn())
                                log.warn("Streaming thread '" + threadName + "' for endpoint with id '" + getId() + "' has been interrupted and the streaming connection will be closed.");
                            os.close();
                            break; // Exit the wait loop.
                        }
                    }
                    // Update the FlexClient last use time to prevent FlexClient from
                    // timing out when the client is still subscribed. It is important
                    // to do this outside synchronized(notifier.pushNeeded) to avoid
                    // thread deadlock!
                    flexClient.updateLastUse();
                }
                if (Log.isDebug())
                    log.debug("Streaming thread '" + threadName + "' for endpoint with id '" + getId() + "' is releasing connection and returning to the request handler pool.");
                suppressIOExceptionLogging = true;
                // Terminate the response.
                streamChunk(null, os, res);
            }
            catch (IOException e)
            {
                if (Log.isWarn() && !suppressIOExceptionLogging)
                    log.warn("Streaming thread '" + threadName + "' for endpoint with id '" + getId() + "' is closing connection due to an IO error.", e);
            }
            finally
            {
                currentThread.setName(threadName);

                // We're done so decrement the counts for streaming threads,
                // and update the canStream flag if necessary.
                synchronized (lock)
                {
                    --streamingClientsCount;
                    canStream = (streamingClientsCount < maxStreamingClients);
                    synchronized (session)
                    {
                        --session.streamingConnectionsCount;
                        session.canStream = (session.streamingConnectionsCount < session.maxConnectionsPerSession);
                    }
                }

                if (notifier != null && currentStreamingRequests != null)
                {
                    currentStreamingRequests.remove(notifier.getNotifierId());
                    notifier.close();
                }

                // Output session level streaming count.
                if (Log.isDebug())
                    Log.getLogger(FlexSession.FLEX_SESSION_LOG_CATEGORY).info("Number of streaming clients for FlexSession with id '"+ session.getId() +"' is " + session.streamingConnectionsCount + ".");

                // Output endpoint level streaming count.
                if (Log.isDebug())
                    log.debug("Number of streaming clients for endpoint with id '"+ getId() +"' is " + streamingClientsCount + ".");
            }
        }
        // Otherwise, client's streaming connection open request could not be granted.
        else
        {
            if (Log.isError())
            {
                String logString = null;
                if (!canStream)
                {
                    logString = "Endpoint with id '" + getId() + "' cannot grant streaming connection to FlexClient with id '"
                    + flexClient.getId() + "' because " + MAX_STREAMING_CLIENTS + " limit of '"
                    + maxStreamingClients + "' has been reached.";
                }
                else if (!session.canStream)
                {
                    logString = "Endpoint with id '" + getId() + "' cannot grant streaming connection to FlexClient with id '"
                    + flexClient.getId() + "' because " + UserAgentManager.MAX_STREAMING_CONNECTIONS_PER_SESSION + " limit of '"
                    + session.maxConnectionsPerSession + "' has been reached.";
                }
                if (logString != null)
                    log.error(logString);
            }

            try
            {
                // Return an HTTP status code 400 to indicate that client request can't be processed.
                res.sendError(HttpServletResponse.SC_BAD_REQUEST);
            }
            catch (IOException ignore)
            {}
        }
    }

    /**
     * Handles streaming connection close command sent by the FlexClient.
     *
     * @param req The <code>HttpServletRequest</code> to service.
     * @param res The <code>HttpServletResponse</code> to be used in case an error
     * has to be sent back.
     * @param flexClient FlexClient that requested the streaming connection.
     * @param streamId The id for the stream to close.
     */
    protected void handleFlexClientStreamingCloseRequest(HttpServletRequest req, HttpServletResponse res, FlexClient flexClient, String streamId)
    {
        if (streamId != null)
        {
            EndpointPushNotifier notifier = (EndpointPushNotifier)flexClient.getEndpointPushHandler(getId());
            if ((notifier != null) && notifier.getNotifierId().equals(streamId))
                notifier.close();
        }
    }

    /**
     * Service streaming connection commands.
     *
     * @param req The <code>HttpServletRequest</code> to service.
     * @param res The <code>HttpServletResponse</code> to be used in case an error
     * has to be sent back.
     */
    protected void serviceStreamingRequest(HttpServletRequest req, HttpServletResponse res)
    {
        // If this is a request for a streaming connection, make sure it's for a valid FlexClient
        // and that the FlexSession doesn't already have a streaming connection open.
        // Streaming requests are POSTs (to help prevent the possibility of caching) that carry the
        // following parameters:
        // command - Indicating a custom command for the endpoint; currently 'open' to request a new
        //           streaming connection be opened, and 'close' to request the streaming connection
        //           to close.
        // version - Indicates the streaming connection 'version' to use; it's here for backward comp. support
        //           if we need to change how commands are handled in a future product release.
        // DSId - The FlexClient id value that uniquely identifies the swf making the request.
        String command = req.getParameter(COMMAND_PARAM_NAME);

        // Only HTTP 1.1 is supported, disallow HTTP 1.0.
        if (req.getProtocol().equals(HTTP_1_0))
        {
            if (Log.isError())
                log.error("Endpoint with id '" + getId() + "' cannot service the streaming request made with " +
                " HTTP 1.0. Only HTTP 1.1 is supported.");

            try
            {
                // Return an HTTP status code 400 to indicate that the client's request was syntactically invalid (bad command).
                res.sendError(HttpServletResponse.SC_BAD_REQUEST);
            }
            catch (IOException ignore)
            {}
            return; // Abort further server processing.
        }

        if (!(command.equals(OPEN_COMMAND) || command.equals(CLOSE_COMMAND)))
        {
            if (Log.isError())
                log.error("Endpoint with id '" + getId() + "' cannot service the streaming request as the supplied command '"
                        + command + "' is invalid.");

            try
            {
                // Return an HTTP status code 400 to indicate that the client's request was syntactically invalid (bad command).
                res.sendError(HttpServletResponse.SC_BAD_REQUEST);
            }
            catch (IOException ignore)
            {}
            return; // Abort further server processing.
        }

        String flexClientId = req.getParameter(Message.FLEX_CLIENT_ID_HEADER);
        if (flexClientId == null)
        {
            if (Log.isError())
                log.error("Endpoint with id '" + getId() + "' cannot service the streaming request as no FlexClient id"
                        + " has been supplied in the request.");

            try
            {
                // Return an HTTP status code 400 to indicate that the client's request was syntactically invalid (missing id).
                res.sendError(HttpServletResponse.SC_BAD_REQUEST);
            }
            catch (IOException ignore)
            {}
            return; // Abort further server processing.
        }

        // Validate that the provided FlexClient id exists and is associated with the current session.
        // We don't do this validation with CLOSE_COMMAND because CLOSE_COMMAND can come in on a
        // different session. For example, when the session expires due to timeout, the streaming client
        // using that session sends a CLOSE_COMMAND on a new session to let the server know to clean client's
        // corresponding server constructs. In that case, server already knows that session has expired so
        // we can simply omit this validation.
        FlexClient flexClient = null;
        List flexClients = FlexContext.getFlexSession().getFlexClients();
        boolean validFlexClientId = false;
        for (Iterator iter = flexClients.iterator(); iter.hasNext();)
        {
            flexClient = (FlexClient)iter.next();
            if (flexClient.getId().equals(flexClientId) && flexClient.isValid())
            {
                validFlexClientId = true;
                break;
            }
        }
        if (!command.equals(CLOSE_COMMAND) && !validFlexClientId)
        {
            if (Log.isError())
                log.error("Endpoint with id '" + getId() + "' cannot service the streaming request as either the supplied"
                        + " FlexClient id '" + flexClientId + " is not valid, or the FlexClient with that id is not valid.");

            try
            {
                // Return an HTTP status code 400 to indicate that the client's request was syntactically invalid (invalid id).
                res.sendError(HttpServletResponse.SC_BAD_REQUEST);
            }
            catch (IOException ignore)
            {}
            return; // Abort further server processing.
        }

        if (flexClient != null)
        {
            if (command.equals(OPEN_COMMAND))
                handleFlexClientStreamingOpenRequest(req, res, flexClient);
            else if (command.equals(CLOSE_COMMAND))
                handleFlexClientStreamingCloseRequest(req, res, flexClient, req.getParameter(STREAM_ID_PARAM_NAME));
        }
    }

    /**
     * Helper method to write a chunk of bytes to the output stream in an HTTP
     * "Transfer-Encoding: chunked" format.
     * If the bytes array is null or empty, a terminal chunk will be written to
     * signal the end of the response.
     * Once the chunk is written to the output stream, the stream will be flushed immediately (no buffering).
     *
     * @param bytes The array of bytes to write as a chunk in the response; or if null, the signal to write the final chunk to complete the response.
     * @param os The output stream the chunk will be written to.
     * @param response The HttpServletResponse, used to flush the chunk to the client.
     *
     * @throws IOException if writing the chunk to the output stream fails.
     */
    protected void streamChunk(byte[] bytes, ServletOutputStream os, HttpServletResponse response) throws IOException
    {
        if ((bytes != null) && (bytes.length > 0))
        {
            byte[] chunkLength = Integer.toHexString(bytes.length).getBytes("ASCII");
            os.write(chunkLength);
            os.write(CRLF_BYTES);
            os.write(bytes);
            os.write(CRLF_BYTES);
            response.flushBuffer();
        }
        else // Send final 'EOF' chunk for the response.
        {
            os.write(ZERO_BYTE);
            os.write(CRLF_BYTES);
            response.flushBuffer();
        }
    }

    /**
     * Helper method invoked by the endpoint request handler thread cycling in wait-notify.
     * Serializes messages and streams each to the client as a response chunk using streamChunk().
     *
     * @param messages The messages to serialize and push to the client.
     * @param os The output stream the chunk will be written to.
     * @param response The HttpServletResponse, used to flush the chunk to the client.
     */
    protected abstract void streamMessages(List messages, ServletOutputStream os, HttpServletResponse response) throws IOException;

    //--------------------------------------------------------------------------
    //
    // Private methods.
    //
    //--------------------------------------------------------------------------

    /**
     * Utility method used at EndpointPushNotifier construction to monitor it for timeout.
     *
     * @param notifier The EndpointPushNotifier to monitor.
     */
    private void monitorTimeout(EndpointPushNotifier notifier)
    {
        if (pushNotifierTimeoutManager != null)
            pushNotifierTimeoutManager.scheduleTimeout(notifier);
    }
}
TOP

Related Classes of flex.messaging.endpoints.BaseStreamingHTTPEndpoint

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.