Package org.cometd.client

Source Code of org.cometd.client.BayeuxClient$PublishTransportListener

/*
* Copyright (c) 2008-2014 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.cometd.client;

import java.net.CookieManager;
import java.net.CookieStore;
import java.net.HttpCookie;
import java.net.URI;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executors;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;

import org.cometd.bayeux.Bayeux;
import org.cometd.bayeux.Channel;
import org.cometd.bayeux.ChannelId;
import org.cometd.bayeux.Message;
import org.cometd.bayeux.client.ClientSession;
import org.cometd.bayeux.client.ClientSessionChannel;
import org.cometd.client.transport.ClientTransport;
import org.cometd.client.transport.HttpClientTransport;
import org.cometd.client.transport.LongPollingTransport;
import org.cometd.client.transport.MessageClientTransport;
import org.cometd.client.transport.TransportListener;
import org.cometd.client.transport.TransportRegistry;
import org.cometd.common.AbstractClientSession;
import org.cometd.common.HashMapMessage;
import org.cometd.common.TransportException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
* <p>{@link BayeuxClient} is the implementation of a client for the Bayeux protocol.</p>
* <p> A {@link BayeuxClient} can receive/publish messages from/to a Bayeux server, and
* it is the counterpart in Java of the JavaScript library used in browsers (and as such
* it is ideal for Swing applications, load testing tools, etc.).</p>
* <p>A {@link BayeuxClient} handshakes with a Bayeux server
* and then subscribes {@link ClientSessionChannel.MessageListener} to channels in order
* to receive messages, and may also publish messages to the Bayeux server.</p>
* <p>{@link BayeuxClient} relies on pluggable transports for communication with the Bayeux
* server, and the most common transport is {@link LongPollingTransport}, which uses
* HTTP to transport Bayeux messages and it is based on
* <a href="http://wiki.eclipse.org/Jetty/Feature/HttpClient">Jetty's HTTP client</a>.</p>
* <p>When the communication with the server is finished, the {@link BayeuxClient} can be
* disconnected from the Bayeux server.</p>
* <p>Typical usage:</p>
* <pre>
* // Handshake
* String url = "http://localhost:8080/cometd";
* BayeuxClient client = new BayeuxClient(url, LongPollingTransport.create(null));
* client.handshake();
* client.waitFor(1000, BayeuxClient.State.CONNECTED);
*
* // Subscription to channels
* ClientSessionChannel channel = client.getChannel("/foo");
* channel.subscribe(new ClientSessionChannel.MessageListener()
* {
*     public void onMessage(ClientSessionChannel channel, Message message)
*     {
*         // Handle the message
*     }
* });
*
* // Publishing to channels
* Map&lt;String, Object&gt; data = new HashMap&lt;String, Object&gt;();
* data.put("bar", "baz");
* channel.publish(data);
*
* // Disconnecting
* client.disconnect();
* client.waitFor(1000, BayeuxClient.State.DISCONNECTED);
* </pre>
*/
public class BayeuxClient extends AbstractClientSession implements Bayeux
{
    public static final String BACKOFF_INCREMENT_OPTION = "backoffIncrement";
    public static final String MAX_BACKOFF_OPTION = "maxBackoff";
    public static final String BAYEUX_VERSION = "1.0";

    protected final Logger logger = LoggerFactory.getLogger(getClass().getName() + "." + Integer.toHexString(System.identityHashCode(this)));
    private final TransportRegistry transportRegistry = new TransportRegistry();
    private final Map<String, Object> options = new ConcurrentHashMap<>();
    private final AtomicReference<BayeuxClientState> bayeuxClientState = new AtomicReference<>();
    private final List<Message.Mutable> messageQueue = new ArrayList<>(32);
    private final CookieStore cookieStore = new CookieManager().getCookieStore();
    private final TransportListener handshakeListener = new HandshakeTransportListener();
    private final TransportListener connectListener = new ConnectTransportListener();
    private final TransportListener disconnectListener = new DisconnectTransportListener();
    private final TransportListener publishListener = new PublishTransportListener();
    private final String url;
    private volatile ScheduledExecutorService scheduler;
    private volatile boolean shutdownScheduler;
    private volatile long backoffIncrement;
    private volatile long maxBackoff;
    private int stateUpdaters;

    /**
     * <p>Creates a {@link BayeuxClient} that will connect to the Bayeux server at the given URL
     * and with the given transport(s).</p>
     * <p>This constructor allocates a new {@link ScheduledExecutorService scheduler}; it is recommended that
     * when creating a large number of {@link BayeuxClient}s a shared scheduler is used.</p>
     *
     * @param url        the Bayeux server URL to connect to
     * @param transport  the default (mandatory) transport to use
     * @param transports additional optional transports to use in case the default transport cannot be used
     * @see #BayeuxClient(String, ScheduledExecutorService, ClientTransport, ClientTransport...)
     */
    public BayeuxClient(String url, ClientTransport transport, ClientTransport... transports)
    {
        this(url, null, transport, transports);
    }

    /**
     * <p>Creates a {@link BayeuxClient} that will connect to the Bayeux server at the given URL,
     * with the given scheduler and with the given transport(s).</p>
     *
     * @param url        the Bayeux server URL to connect to
     * @param scheduler  the scheduler to use for scheduling timed operations
     * @param transport  the default (mandatory) transport to use
     * @param transports additional optional transports to use in case the default transport cannot be used
     */
    public BayeuxClient(String url, ScheduledExecutorService scheduler, ClientTransport transport, ClientTransport... transports)
    {
        this.url = Objects.requireNonNull(url);
        this.scheduler = scheduler;

        transport = Objects.requireNonNull(transport);
        transportRegistry.add(transport);
        for (ClientTransport t : transports)
            transportRegistry.add(t);

        for (String transportName : transportRegistry.getKnownTransports())
        {
            ClientTransport clientTransport = transportRegistry.getTransport(transportName);
            if (clientTransport instanceof MessageClientTransport)
            {
                ((MessageClientTransport)clientTransport).setMessageTransportListener(publishListener);
            }
            if (clientTransport instanceof HttpClientTransport)
            {
                HttpClientTransport httpTransport = (HttpClientTransport)clientTransport;
                httpTransport.setURL(url);
                httpTransport.setCookieStore(cookieStore);
            }
        }

        bayeuxClientState.set(new DisconnectedState(null));
    }

    /**
     * @return the URL passed when constructing this instance
     */
    public String getURL()
    {
        return url;
    }

    /**
     * @return the period of time that increments the pause to wait before trying to reconnect
     *         after each failed attempt to connect to the Bayeux server
     * @see #getMaxBackoff()
     */
    public long getBackoffIncrement()
    {
        return backoffIncrement;
    }

    /**
     * @return the maximum pause to wait before trying to reconnect after each failed attempt
     *         to connect to the Bayeux server
     * @see #getBackoffIncrement()
     */
    public long getMaxBackoff()
    {
        return maxBackoff;
    }

    public CookieStore getCookieStore()
    {
        return cookieStore;
    }

    /**
     * <p>Retrieves the first cookie with the given name, if available.</p>
     * <p>Note that currently only HTTP transports support cookies.</p>
     *
     * @param name the cookie name
     * @return the cookie, or null if no such cookie is found
     * @see #putCookie(HttpCookie)
     */
    public HttpCookie getCookie(String name)
    {
        for (HttpCookie cookie : getCookieStore().get(URI.create(getURL())))
        {
            if (name.equals(cookie.getName()))
                return cookie;
        }
        return null;
    }

    public void putCookie(HttpCookie cookie)
    {
        URI uri = URI.create(getURL());
        if (cookie.getPath() == null)
        {
            String path = uri.getPath();
            if (path == null || !path.contains("/"))
                path = "/";
            else
                path = path.substring(0, path.lastIndexOf("/") + 1);
            cookie.setPath(path);
        }
        if (cookie.getDomain() == null)
            cookie.setDomain(uri.getHost());
        getCookieStore().add(uri, cookie);
    }

    public String getId()
    {
        return bayeuxClientState.get().clientId;
    }

    public boolean isHandshook()
    {
        return isHandshook(bayeuxClientState.get());
    }

    private boolean isHandshook(BayeuxClientState bayeuxClientState)
    {
        return bayeuxClientState.type == State.CONNECTING ||
                bayeuxClientState.type == State.CONNECTED ||
                bayeuxClientState.type == State.UNCONNECTED;
    }

    private boolean isHandshaking(BayeuxClientState bayeuxClientState)
    {
        return bayeuxClientState.type == State.HANDSHAKING ||
                bayeuxClientState.type == State.REHANDSHAKING;
    }

    private boolean isConnecting(BayeuxClientState bayeuxClientState)
    {
        return bayeuxClientState.type == State.CONNECTING;
    }

    public boolean isConnected()
    {
        return isConnected(bayeuxClientState.get());
    }

    private boolean isConnected(BayeuxClientState bayeuxClientState)
    {
        return bayeuxClientState.type == State.CONNECTED;
    }

    private boolean isDisconnecting(BayeuxClientState bayeuxClientState)
    {
        return bayeuxClientState.type == State.DISCONNECTING;
    }

    private boolean isDisconnected(BayeuxClientState bayeuxClientState)
    {
        return bayeuxClientState.type == State.DISCONNECTED;
    }

    /**
     * @return whether this {@link BayeuxClient} is disconnecting or disconnected
     */
    public boolean isDisconnected()
    {
        BayeuxClientState bayeuxClientState = this.bayeuxClientState.get();
        return isDisconnecting(bayeuxClientState) || isDisconnected(bayeuxClientState);
    }

    /**
     * @return the current state of this {@link BayeuxClient}
     */
    protected State getState()
    {
        return bayeuxClientState.get().type;
    }

    public void handshake()
    {
        handshake(null, null);
    }

    public void handshake(final Map<String, Object> handshakeFields)
    {
        handshake(handshakeFields, null);
    }

    public void handshake(ClientSessionChannel.MessageListener callback)
    {
        handshake(null, callback);
    }

    public void handshake(final Map<String, Object> template, final ClientSessionChannel.MessageListener callback)
    {
        initialize();

        List<String> allowedTransports = getAllowedTransports();
        // Pick the first transport for the handshake, it will renegotiate if not right
        final ClientTransport initialTransport = transportRegistry.negotiate(allowedTransports.toArray(), BAYEUX_VERSION).get(0);
        prepareTransport(null, initialTransport);
        if (logger.isDebugEnabled())
            logger.debug("Using initial transport {} from {}", initialTransport.getName(), allowedTransports);

        updateBayeuxClientState(new BayeuxClientStateUpdater()
        {
            public BayeuxClientState create(BayeuxClientState oldState)
            {
                return new HandshakingState(template, callback, initialTransport);
            }
        });
    }

    /**
     * <p>Performs the handshake and waits at most the given time for the handshake to complete.</p>
     * <p>When this method returns, the handshake may have failed (for example because the Bayeux
     * server denied it), so it is important to check the return value to know whether the handshake
     * completed or not.</p>
     *
     * @param waitMs the time to wait for the handshake to complete
     * @return the state of this {@link BayeuxClient}
     * @see #handshake(Map, long)
     */
    public State handshake(long waitMs)
    {
        return handshake(null, waitMs);
    }

    /**
     * <p>Performs the handshake with the given template and waits at most the given time for the handshake to complete.</p>
     * <p>When this method returns, the handshake may have failed (for example because the Bayeux
     * server denied it), so it is important to check the return value to know whether the handshake
     * completed or not.</p>
     *
     * @param template the template object to be merged with the handshake message
     * @param waitMs   the time to wait for the handshake to complete
     * @return the state of this {@link BayeuxClient}
     * @see #handshake(long)
     */
    public State handshake(Map<String, Object> template, long waitMs)
    {
        handshake(template);
        waitFor(waitMs, State.CONNECTING, State.CONNECTED, State.DISCONNECTED);
        return getState();
    }

    protected boolean sendHandshake()
    {
        BayeuxClientState bayeuxClientState = this.bayeuxClientState.get();
        if (isHandshaking(bayeuxClientState))
        {
            Message.Mutable message = newMessage();
            if (bayeuxClientState.handshakeFields != null)
                message.putAll(bayeuxClientState.handshakeFields);
            message.setChannel(Channel.META_HANDSHAKE);
            List<ClientTransport> transports = transportRegistry.negotiate(getAllowedTransports().toArray(), BAYEUX_VERSION);
            List<String> transportNames = new ArrayList<>(transports.size());
            for (ClientTransport transport : transports)
                transportNames.add(transport.getName());
            message.put(Message.SUPPORTED_CONNECTION_TYPES_FIELD, transportNames);
            message.put(Message.VERSION_FIELD, BayeuxClient.BAYEUX_VERSION);
            if (bayeuxClientState.callback != null)
                message.put(CALLBACK_KEY, bayeuxClientState.callback);

            if (logger.isDebugEnabled())
                logger.debug("Handshaking on transport {}: {}", bayeuxClientState.transport, message);
            List<Message.Mutable> messages = new ArrayList<>(1);
            messages.add(message);
            return bayeuxClientState.send(handshakeListener, messages);
        }
        return false;
    }

    /**
     * <p>Waits for this {@link BayeuxClient} to reach the given state(s) within the given time.</p>
     *
     * @param waitMs the time to wait to reach the given state(s)
     * @param state  the state to reach
     * @param states additional states to reach in alternative
     * @return true if one of the state(s) has been reached within the given time, false otherwise
     */
    public boolean waitFor(long waitMs, State state, State... states)
    {
        long start = System.nanoTime();
        List<State> waitForStates = new ArrayList<>();
        waitForStates.add(state);
        waitForStates.addAll(Arrays.asList(states));
        synchronized (this)
        {
            long elapsed = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start);
            while (elapsed < waitMs)
            {
                // This check is needed to avoid that we return from waitFor() too early,
                // when the state has been set, but its effects (like notifying listeners)
                // are not completed yet (COMETD-212).
                // Transient states (like CONNECTING or DISCONNECTING) may "miss" the
                // wake up in this way:
                // * T1 goes in wait - releases lock
                // * T2 finishes update to CONNECTING - notifies lock
                // * T3 starts a state update to CONNECTED - releases lock
                // * T1 wakes up, takes lock, but sees update in progress, waits - releases lock
                // * T3 finishes update to CONNECTED - notifies lock
                // * T1 wakes up, takes lock, sees status == CONNECTED - CONNECTING has been "missed"
                // To avoid this, we use State.implies()
                if (stateUpdaters == 0)
                {
                    State currentState = getState();
                    for (State s : waitForStates)
                    {
                        if (currentState.implies(s))
                            return true;
                    }
                }

                try
                {
                    wait(waitMs - elapsed);
                }
                catch (InterruptedException x)
                {
                    Thread.currentThread().interrupt();
                    break;
                }

                elapsed = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start);
            }
            return false;
        }
    }

    protected boolean sendConnect()
    {
        BayeuxClientState bayeuxClientState = this.bayeuxClientState.get();
        if (isHandshook(bayeuxClientState))
        {
            Message.Mutable message = newMessage();
            message.setChannel(Channel.META_CONNECT);
            message.put(Message.CONNECTION_TYPE_FIELD, bayeuxClientState.transport.getName());
            if (isConnecting(bayeuxClientState) || bayeuxClientState.type == State.UNCONNECTED)
            {
                // First connect after handshake or after failure, add advice
                message.getAdvice(true).put("timeout", 0);
            }
            if (logger.isDebugEnabled())
                logger.debug("Connecting, transport {}", bayeuxClientState.transport);
            List<Message.Mutable> messages = new ArrayList<>(1);
            messages.add(message);
            return bayeuxClientState.send(connectListener, messages);
        }
        return false;
    }

    protected ChannelId newChannelId(String channelId)
    {
        // Save some parsing by checking if there is already one
        AbstractSessionChannel channel = getChannels().get(channelId);
        return channel == null ? new ChannelId(channelId) : channel.getChannelId();
    }

    protected AbstractSessionChannel newChannel(ChannelId channelId)
    {
        return new BayeuxClientChannel(channelId);
    }

    protected void sendBatch()
    {
        if (canSend())
        {
            List<Message.Mutable> messages = takeMessages();
            if (!messages.isEmpty())
                sendMessages(messages);
        }
    }

    protected boolean sendMessages(List<Message.Mutable> messages)
    {
        return bayeuxClientState.get().send(publishListener, messages);
    }

    private List<Message.Mutable> takeMessages()
    {
        // Multiple threads can call this method concurrently (for example
        // a batched publish() is executed exactly when a message arrives
        // and a listener also performs a batched publish() in response to
        // the message).
        // The queue must be drained atomically, otherwise we risk that the
        // same message is drained twice.

        List<Message.Mutable> messages;
        synchronized (messageQueue)
        {
            messages = new ArrayList<>(messageQueue);
            messageQueue.clear();
        }
        return messages;
    }

    /**
     * @see #disconnect(long)
     */
    public void disconnect()
    {
        disconnect(null);
    }

    public void disconnect(final ClientSessionChannel.MessageListener callback)
    {
        updateBayeuxClientState(new BayeuxClientStateUpdater()
        {
            public BayeuxClientState create(BayeuxClientState oldState)
            {
                if (isConnecting(oldState) || isConnected(oldState))
                    return new DisconnectingState(callback, oldState.transport, oldState.clientId);
                else if (isDisconnecting(oldState))
                    return new DisconnectingState(callback, oldState.transport, oldState.clientId);
                else
                    return new DisconnectedState(oldState.transport);
            }
        });
    }

    /**
     * <p>Performs a {@link #disconnect() disconnect} and uses the given {@code timeout}
     * to wait for the disconnect to complete.</p>
     * <p>When a disconnect is sent to the server, the server also wakes up the long
     * poll that may be outstanding, so that a connect reply message may arrive to
     * the client later than the disconnect reply message.</p>
     * <p>This method waits for the given {@code timeout} for the disconnect reply, but also
     * waits the same timeout for the last connect reply; in the worst case the
     * maximum time waited will therefore be twice the given {@code timeout} parameter.</p>
     * <p>This method returns true if the disconnect reply message arrived within the
     * given {@code timeout} parameter, no matter if the connect reply message arrived or not.</p>
     *
     * @param timeout the timeout to wait for the disconnect to complete
     * @return true if the disconnect completed within the given timeout
     */
    public boolean disconnect(long timeout)
    {
        if (isDisconnected(bayeuxClientState.get()))
            return true;

        final CountDownLatch latch = new CountDownLatch(1);
        ClientSessionChannel.MessageListener lastConnectListener = new ClientSessionChannel.MessageListener()
        {
            public void onMessage(ClientSessionChannel channel, Message message)
            {
                final Map<String, Object> advice = message.getAdvice();
                if (!message.isSuccessful() ||
                        advice != null && Message.RECONNECT_NONE_VALUE.equals(advice.get(Message.RECONNECT_FIELD)))
                    latch.countDown();
            }
        };
        getChannel(Channel.META_CONNECT).addListener(lastConnectListener);

        disconnect();
        boolean disconnected = waitFor(timeout, BayeuxClient.State.DISCONNECTED);

        // There is a possibility that we are in the window where the server
        // has returned the long poll and the client has not issued it again,
        // so wait for the timeout, but do not complain if the latch does not trigger.
        try
        {
            latch.await(timeout, TimeUnit.MILLISECONDS);
        }
        catch (InterruptedException x)
        {
            Thread.currentThread().interrupt();
        }

        getChannel(Channel.META_CONNECT).removeListener(lastConnectListener);

        // Force to DISCONNECTED state
        updateBayeuxClientState(new BayeuxClientStateUpdater()
        {
            @Override
            public BayeuxClientState create(BayeuxClientState oldState)
            {
                return new DisconnectedState(oldState.transport);
            }
        });

        return disconnected;
    }

    /**
     * <p>Interrupts abruptly the communication with the Bayeux server.</p>
     * <p>This method may be useful to simulate network failures.</p>
     *
     * @see #disconnect()
     */
    public void abort()
    {
        updateBayeuxClientState(new BayeuxClientStateUpdater()
        {
            public BayeuxClientState create(BayeuxClientState oldState)
            {
                return new AbortedState(oldState.transport);
            }
        });
    }

    protected void processHandshake(final Message.Mutable handshake)
    {
        if (logger.isDebugEnabled())
            logger.debug("Processing meta handshake {}", handshake);
        if (handshake.isSuccessful())
        {
            Object field = handshake.get(Message.SUPPORTED_CONNECTION_TYPES_FIELD);
            Object[] serverTransports = field instanceof List ? ((List)field).toArray() : (Object[])field;
            List<ClientTransport> negotiatedTransports = transportRegistry.negotiate(serverTransports, BAYEUX_VERSION);
            if (negotiatedTransports.isEmpty())
            {
                // Signal the failure
                String error = "405:c" +
                        getAllowedTransports() +
                        ",s" +
                        Arrays.toString(serverTransports) +
                        ":no transport";

                handshake.setSuccessful(false);
                handshake.put(Message.ERROR_FIELD, error);

                updateBayeuxClientState(new BayeuxClientStateUpdater()
                {
                    public BayeuxClientState create(BayeuxClientState oldState)
                    {
                        onTransportFailure(oldState.transport.getName(), null, new TransportException(null));
                        return new DisconnectedState(oldState.transport);
                    }

                    @Override
                    public void postCreate()
                    {
                        receive(handshake);
                    }
                });
            }
            else
            {
                final ClientTransport newTransport = negotiatedTransports.get(0);
                updateBayeuxClientState(new BayeuxClientStateUpdater()
                {
                    public BayeuxClientState create(BayeuxClientState oldState)
                    {
                        if (newTransport != oldState.transport)
                            prepareTransport(oldState.transport, newTransport);

                        String action = getAdviceAction(handshake.getAdvice(), Message.RECONNECT_RETRY_VALUE);
                        if (Message.RECONNECT_RETRY_VALUE.equals(action))
                            return new ConnectingState(oldState.handshakeFields, oldState.callback, handshake.getAdvice(), newTransport, handshake.getClientId());
                        else if (Message.RECONNECT_NONE_VALUE.equals(action))
                            return new DisconnectedState(oldState.transport);
                        return null;
                    }

                    @Override
                    public void postCreate()
                    {
                        receive(handshake);
                    }
                });
            }
        }
        else
        {
            updateBayeuxClientState(new BayeuxClientStateUpdater()
            {
                public BayeuxClientState create(BayeuxClientState oldState)
                {
                    String action = getAdviceAction(handshake.getAdvice(), Message.RECONNECT_HANDSHAKE_VALUE);
                    if (Message.RECONNECT_HANDSHAKE_VALUE.equals(action) || Message.RECONNECT_RETRY_VALUE.equals(action))
                        return new RehandshakingState(oldState.handshakeFields, oldState.callback, oldState.transport, oldState.nextBackoff());
                    else if (Message.RECONNECT_NONE_VALUE.equals(action))
                        return new DisconnectedState(oldState.transport);
                    return null;
                }

                @Override
                public void postCreate()
                {
                    receive(handshake);
                }
            });
        }
    }

    protected void processConnect(final Message.Mutable connect)
    {
        // TODO: Split "connected" state into connectSent+connectReceived ?
        // It may happen that the server replies to the meta connect with a delay
        // that exceeds the maxNetworkTimeout (for example because the server is
        // busy and the meta connect reply thread is starved).
        // In this case, it is possible that we issue 2 concurrent connects, one
        // for the response arrived late, and one from the unconnected state.
        // We should avoid this, although it is a very rare case.

        if (logger.isDebugEnabled())
            logger.debug("Processing meta connect {}", connect);
        updateBayeuxClientState(new BayeuxClientStateUpdater()
        {
            public BayeuxClientState create(BayeuxClientState oldState)
            {
                Map<String, Object> advice = connect.getAdvice();
                if (advice == null)
                    advice = oldState.advice;

                String action = getAdviceAction(advice, Message.RECONNECT_RETRY_VALUE);
                if (connect.isSuccessful())
                {
                    if (Message.RECONNECT_RETRY_VALUE.equals(action))
                        return new ConnectedState(oldState.handshakeFields, oldState.callback, advice, oldState.transport, oldState.clientId);
                    else if (Message.RECONNECT_NONE_VALUE.equals(action))
                        // This case happens when the connect reply arrives after a disconnect
                        // We do not go into a disconnected state to allow normal processing of the disconnect reply
                        return new DisconnectingState(null, oldState.transport, oldState.clientId);
                }
                else
                {
                    if (Message.RECONNECT_HANDSHAKE_VALUE.equals(action))
                        return new RehandshakingState(oldState.handshakeFields, oldState.callback, oldState.transport, 0);
                    else if (Message.RECONNECT_RETRY_VALUE.equals(action))
                        return new UnconnectedState(oldState.handshakeFields, oldState.callback, advice, oldState.transport, oldState.clientId, oldState.nextBackoff());
                    else if (Message.RECONNECT_NONE_VALUE.equals(action))
                        return new DisconnectedState(oldState.transport);
                }
                return null;
            }

            @Override
            public void postCreate()
            {
                receive(connect);
            }
        });
    }

    protected void processDisconnect(final Message.Mutable disconnect)
    {
        if (logger.isDebugEnabled())
            logger.debug("Processing meta disconnect {}", disconnect);

        updateBayeuxClientState(new BayeuxClientStateUpdater()
        {
            public BayeuxClientState create(BayeuxClientState oldState)
            {
                return new DisconnectedState(oldState.transport);
            }

            @Override
            public void postCreate()
            {
                receive(disconnect);
            }
        });
    }

    protected void processMessage(Message.Mutable message)
    {
        if (logger.isDebugEnabled())
            logger.debug("Processing message {}", message);
        receive(message);
    }

    private String getAdviceAction(Map<String, Object> advice, String defaultResult)
    {
        String action = defaultResult;
        if (advice != null && advice.containsKey(Message.RECONNECT_FIELD))
            action = (String)advice.get(Message.RECONNECT_FIELD);
        return action;
    }

    protected boolean scheduleHandshake(long interval, long backoff)
    {
        return scheduleAction(new Runnable()
        {
            public void run()
            {
                sendHandshake();
            }
        }, interval, backoff);
    }

    protected boolean scheduleConnect(long interval, long backoff)
    {
        return scheduleAction(new Runnable()
        {
            public void run()
            {
                sendConnect();
            }
        }, interval, backoff);
    }

    private boolean scheduleAction(Runnable action, long interval, long backoff)
    {
        // Prevent NPE in case of concurrent disconnect
        ScheduledExecutorService scheduler = this.scheduler;
        if (scheduler != null)
        {
            try
            {
                scheduler.schedule(action, interval + backoff, TimeUnit.MILLISECONDS);
                return true;
            }
            catch (RejectedExecutionException x)
            {
                // It has been shut down
                logger.trace("", x);
            }
        }
        if (logger.isDebugEnabled())
            logger.debug("Could not schedule action {} to scheduler {}", action, scheduler);
        return false;
    }

    public List<String> getAllowedTransports()
    {
        return transportRegistry.getAllowedTransports();
    }

    public Set<String> getKnownTransportNames()
    {
        return transportRegistry.getKnownTransports();
    }

    public ClientTransport getTransport(String transport)
    {
        return transportRegistry.getTransport(transport);
    }

    public ClientTransport getTransport()
    {
        BayeuxClientState bayeuxClientState = this.bayeuxClientState.get();
        return bayeuxClientState == null ? null : bayeuxClientState.transport;
    }

    protected void initialize()
    {
        Number value = (Number)getOption(BACKOFF_INCREMENT_OPTION);
        long backoffIncrement = value == null ? -1 : value.longValue();
        if (backoffIncrement < 0)
            backoffIncrement = 1000L;
        this.backoffIncrement = backoffIncrement;

        value = (Number)getOption(MAX_BACKOFF_OPTION);
        long maxBackoff = value == null ? -1 : value.longValue();
        if (maxBackoff <= 0)
            maxBackoff = 30000L;
        this.maxBackoff = maxBackoff;

        if (scheduler == null)
        {
            scheduler = Executors.newSingleThreadScheduledExecutor();
            shutdownScheduler = true;
        }
    }

    protected void terminate()
    {
        List<Message.Mutable> messages = takeMessages();
        failMessages(null, messages);

        cookieStore.removeAll();

        if (shutdownScheduler)
        {
            shutdownScheduler = false;
            scheduler.shutdownNow();
            scheduler = null;
        }
    }

    public Object getOption(String qualifiedName)
    {
        return options.get(qualifiedName);
    }

    public void setOption(String qualifiedName, Object value)
    {
        options.put(qualifiedName, value);
        // Forward the option to the transports.
        for (String name : transportRegistry.getKnownTransports())
        {
            ClientTransport transport = transportRegistry.getTransport(name);
            transport.setOption(qualifiedName, value);
        }
    }

    public Set<String> getOptionNames()
    {
        return options.keySet();
    }

    /**
     * @return the options that configure with {@link BayeuxClient}
     */
    public Map<String, Object> getOptions()
    {
        return Collections.unmodifiableMap(options);
    }

    protected Message.Mutable newMessage()
    {
        return new HashMapMessage();
    }

    protected void enqueueSend(Message.Mutable message)
    {
        if (canSend())
        {
            List<Message.Mutable> messages = new ArrayList<>(1);
            messages.add(message);
            boolean sent = sendMessages(messages);
            if (logger.isDebugEnabled())
                logger.debug("{} message {}", sent ? "Sent" : "Failed", message);
        }
        else
        {
            synchronized (messageQueue)
            {
                messageQueue.add(message);
            }
            if (logger.isDebugEnabled())
                logger.debug("Enqueued message {} (batching: {})", message, isBatching());
        }
    }

    private boolean canSend()
    {
        return !isBatching() && !isHandshaking(bayeuxClientState.get());
    }

    protected void failMessages(Throwable x, List<? extends Message> messages)
    {
        for (Message message : messages)
            failMessage(message, x);
    }

    protected void failMessage(Message message, Throwable x)
    {
        Message.Mutable failed = newMessage();
        failed.setId(message.getId());
        failed.setSuccessful(false);
        failed.setChannel(message.getChannel());
        failed.put(Message.SUBSCRIPTION_FIELD, message.get(Message.SUBSCRIPTION_FIELD));
        failed.put(CALLBACK_KEY, message.remove(CALLBACK_KEY));

        Map<String, Object> failure = new HashMap<>();
        failed.put("failure", failure);
        failure.put("message", message);
        if (x != null)
            failure.put("exception", x);
        if (x instanceof TransportException)
        {
            Map<String, Object> fields = ((TransportException)x).getFields();
            if (fields != null)
                failure.putAll(fields);
        }
        failure.put(Message.CONNECTION_TYPE_FIELD, getTransport().getName());

        receive(failed);
    }

    @Override
    protected void notifyListeners(Message.Mutable message)
    {
        ClientSessionChannel.MessageListener callback = (ClientSessionChannel.MessageListener)message.remove(CALLBACK_KEY);
        if (message.isMeta() || message.isPublishReply())
        {
            String messageId = message.getId();
            callback = messageId == null ? callback : unregisterCallback(messageId);
            if (callback != null)
                notifyListener(callback, message);
        }
        super.notifyListeners(message);
    }

    /**
     * <p>Callback method invoked when the given messages have hit the network towards the Bayeux server.</p>
     * <p>The messages may not be modified, and any modification will be useless because the message have
     * already been sent.</p>
     *
     * @param messages the messages sent
     */
    public void onSending(List<? extends Message> messages)
    {
    }

    /**
     * <p>Callback method invoke when the given messages have just arrived from the Bayeux server.</p>
     * <p>The messages may be modified, but it's suggested to use {@link Extension}s instead.</p>
     * <p>Extensions will be processed after the invocation of this method.</p>
     *
     * @param messages the messages arrived
     */
    public void onMessages(List<Message.Mutable> messages)
    {
    }

    /**
     * <p>Callback method invoked when the given messages have failed to be sent.</p>
     * <p>The default implementation logs the failure at DEBUG level.</p>
     *
     * @param failure        the exception that caused the failure
     * @param messages the messages being sent
     */
    public void onFailure(Throwable failure, List<? extends Message> messages)
    {
        if (logger.isDebugEnabled())
            logger.debug("Messages failed " + messages, failure);
    }

    private void updateBayeuxClientState(BayeuxClientStateUpdater updater)
    {
        // Increase how many threads are updating the state.
        // This is needed so that in waitFor() we can check
        // the state being sure that nobody is updating it.
        synchronized (this)
        {
            ++stateUpdaters;
        }

        // State update is non-blocking
        try
        {
            BayeuxClientState newState = null;
            BayeuxClientState oldState = bayeuxClientState.get();
            boolean updated = false;
            while (!updated)
            {
                newState = updater.create(oldState);
                if (newState == null)
                    throw new IllegalStateException();

                if (!oldState.isUpdateableTo(newState))
                {
                    if (logger.isDebugEnabled())
                        logger.debug("State not updateable: {} -> {}", oldState, newState);
                    break;
                }

                updated = bayeuxClientState.compareAndSet(oldState, newState);
                if (logger.isDebugEnabled())
                    logger.debug("State update: {} -> {}{}", oldState, newState, updated ? "" : " failed (concurrent update)");
                if (!updated)
                    oldState = bayeuxClientState.get();
            }

            updater.postCreate();

            if (updated)
            {
                if (!oldState.getType().equals(newState.getType()))
                    newState.enter(oldState.getType());

                newState.execute();
            }
        }
        finally
        {
            // Notify threads waiting in waitFor()
            synchronized (this)
            {
                --stateUpdaters;
                if (stateUpdaters == 0)
                    notifyAll();
            }
        }
    }

    public String dump()
    {
        StringBuilder b = new StringBuilder();
        dump(b, "");
        return b.toString();
    }

    private void prepareTransport(ClientTransport oldTransport, ClientTransport newTransport)
    {
        if (oldTransport != null)
            oldTransport.terminate();
        newTransport.init();
    }

    protected void onTransportFailure(String oldTransportName, String newTransportName, Throwable failure)
    {
    }

    /**
     * The states that a {@link BayeuxClient} may assume
     */
    public enum State
    {
        /**
         * State assumed after the handshake when the connection is broken
         */
        UNCONNECTED,
        /**
         * State assumed when the handshake is being sent
         */
        HANDSHAKING,
        /**
         * State assumed when a first handshake failed and the handshake is retried,
         * or when the Bayeux server requests a re-handshake
         */
        REHANDSHAKING,
        /**
         * State assumed when the connect is being sent for the first time
         */
        CONNECTING(HANDSHAKING),
        /**
         * State assumed when this {@link BayeuxClient} is connected to the Bayeux server
         */
        CONNECTED(HANDSHAKING, CONNECTING),
        /**
         * State assumed when the disconnect is being sent
         */
        DISCONNECTING,
        /**
         * State assumed before the handshake and when the disconnect is completed
         */
        DISCONNECTED(DISCONNECTING);

        private final State[] implieds;

        private State(State... implieds)
        {
            this.implieds = implieds;
        }

        private boolean implies(State state)
        {
            if (state == this)
                return true;
            for (State implied : implieds)
            {
                if (state == implied)
                    return true;
            }
            return false;
        }
    }

    private class PublishTransportListener implements TransportListener
    {
        public void onSending(List<? extends Message> messages)
        {
            BayeuxClient.this.onSending(messages);
        }

        public void onMessages(List<Message.Mutable> messages)
        {
            BayeuxClient.this.onMessages(messages);
            for (Message.Mutable message : messages)
                processMessage(message);
        }

        protected void processMessage(Message.Mutable message)
        {
            BayeuxClient.this.processMessage(message);
        }

        public void onFailure(Throwable failure, List<? extends Message> messages)
        {
            BayeuxClient.this.onFailure(failure, messages);
            failMessages(failure, messages);
        }
    }

    private class HandshakeTransportListener extends PublishTransportListener
    {
        public void onFailure(final Throwable failure, List<? extends Message> messages)
        {
            if (logger.isDebugEnabled())
                logger.debug("Handshake failed: " + messages, failure);

            List<ClientTransport> transports = transportRegistry.negotiate(getAllowedTransports().toArray(), BAYEUX_VERSION);
            if (transports.isEmpty())
            {
                updateBayeuxClientState(new BayeuxClientStateUpdater()
                {
                    @Override
                    public BayeuxClientState create(BayeuxClientState oldState)
                    {
                        onTransportFailure(oldState.transport.getName(), null, failure);
                        return new DisconnectedState(oldState.transport);
                    }
                });
            }
            else
            {
                final ClientTransport newTransport = transports.get(0);
                updateBayeuxClientState(new BayeuxClientStateUpdater()
                {
                    @Override
                    public BayeuxClientState create(BayeuxClientState oldState)
                    {
                        if (newTransport != oldState.transport)
                            prepareTransport(oldState.transport, newTransport);
                        onTransportFailure(oldState.transport.getName(), newTransport.getName(), failure);
                        return new RehandshakingState(oldState.handshakeFields, oldState.callback, newTransport, oldState.nextBackoff());
                    }
                });
            }
            super.onFailure(failure, messages);
        }

        @Override
        protected void processMessage(Message.Mutable message)
        {
            if (Channel.META_HANDSHAKE.equals(message.getChannel()))
                processHandshake(message);
            else
                super.processMessage(message);
        }
    }

    private class ConnectTransportListener extends PublishTransportListener
    {
        @Override
        public void onFailure(Throwable failure, List<? extends Message> messages)
        {
            updateBayeuxClientState(new BayeuxClientStateUpdater()
            {
                public BayeuxClientState create(BayeuxClientState oldState)
                {
                    return new UnconnectedState(oldState.handshakeFields, oldState.callback, oldState.advice, oldState.transport, oldState.clientId, oldState.nextBackoff());
                }
            });
            super.onFailure(failure, messages);
        }

        @Override
        protected void processMessage(Message.Mutable message)
        {
            if (Channel.META_CONNECT.equals(message.getChannel()))
                processConnect(message);
            else
                super.processMessage(message);
        }
    }

    private class DisconnectTransportListener extends PublishTransportListener
    {
        @Override
        public void onFailure(Throwable failure, List<? extends Message> messages)
        {
            updateBayeuxClientState(new BayeuxClientStateUpdater()
            {
                public BayeuxClientState create(BayeuxClientState oldState)
                {
                    return new DisconnectedState(oldState.transport);
                }
            });
            super.onFailure(failure, messages);
        }

        @Override
        protected void processMessage(Message.Mutable message)
        {
            if (Channel.META_DISCONNECT.equals(message.getChannel()))
                processDisconnect(message);
            else
                super.processMessage(message);
        }
    }

    protected class BayeuxClientChannel extends AbstractSessionChannel
    {
        protected BayeuxClientChannel(ChannelId channelId)
        {
            super(channelId);
        }

        public ClientSession getSession()
        {
            throwIfReleased();
            return BayeuxClient.this;
        }

        public void publish(Object data, MessageListener callback)
        {
            throwIfReleased();
            Message.Mutable message = newMessage();
            message.setChannel(getId());
            message.setData(data);
            if (callback != null)
                message.put(CALLBACK_KEY, callback);
            enqueueSend(message);
        }

        protected void sendSubscribe(MessageListener listener, MessageListener callback)
        {
            Message.Mutable message = newMessage();
            message.setChannel(Channel.META_SUBSCRIBE);
            message.put(Message.SUBSCRIPTION_FIELD, getId());
            if (listener != null)
                message.put(SUBSCRIBER_KEY, listener);
            if (callback != null)
                message.put(CALLBACK_KEY, callback);
            enqueueSend(message);
        }

        protected void sendUnSubscribe(MessageListener callback)
        {
            Message.Mutable message = newMessage();
            message.setChannel(Channel.META_UNSUBSCRIBE);
            message.put(Message.SUBSCRIPTION_FIELD, getId());
            if (callback != null)
                message.put(CALLBACK_KEY, callback);
            enqueueSend(message);
        }
    }

    private abstract class BayeuxClientStateUpdater
    {
        public abstract BayeuxClientState create(BayeuxClientState oldState);

        public void postCreate()
        {
        }
    }

    private abstract class BayeuxClientState
    {
        protected final State type;
        protected final Map<String, Object> handshakeFields;
        protected final ClientSessionChannel.MessageListener callback;
        protected final Map<String, Object> advice;
        protected final ClientTransport transport;
        protected final String clientId;
        protected final long backoff;

        private BayeuxClientState(State type,
                                  Map<String, Object> handshakeFields,
                                  ClientSessionChannel.MessageListener callback,
                                  Map<String, Object> advice,
                                  ClientTransport transport,
                                  String clientId,
                                  long backoff)
        {
            this.type = type;
            this.handshakeFields = handshakeFields;
            this.callback = callback;
            this.advice = advice;
            this.transport = transport;
            this.clientId = clientId;
            this.backoff = backoff;
        }

        protected long getInterval()
        {
            long result = 0;
            if (advice != null && advice.containsKey(Message.INTERVAL_FIELD))
                result = ((Number)advice.get(Message.INTERVAL_FIELD)).longValue();
            return result;
        }

        protected boolean send(TransportListener listener, List<Message.Mutable> messages)
        {
            for (Iterator<Message.Mutable> iterator = messages.iterator(); iterator.hasNext();)
            {
                Message.Mutable message = iterator.next();

                String messageId = newMessageId();
                message.setId(messageId);

                if (clientId != null)
                    message.setClientId(clientId);

                // Remove the synthetic fields before calling the extensions
                ClientSessionChannel.MessageListener subscriber = (ClientSessionChannel.MessageListener)message.remove(SUBSCRIBER_KEY);
                ClientSessionChannel.MessageListener callback = (ClientSessionChannel.MessageListener)message.remove(CALLBACK_KEY);

                if (extendSend(message))
                {
                    // Extensions may have modified the messageId, but we need to own
                    // the messageId in case of meta messages to link request/response
                    // in non request/response transports such as websocket
                    message.setId(messageId);

                    registerSubscriber(messageId, subscriber);
                    registerCallback(messageId, callback);
                }
                else
                {
                    iterator.remove();
                }
            }
            if (messages.isEmpty())
                return false;
            if (logger.isDebugEnabled())
                logger.debug("Sending messages {}", messages);
            return transportSend(listener, messages);
        }

        protected boolean transportSend(TransportListener listener, List<Message.Mutable> messages)
        {
            transport.send(listener, messages);
            return true;
        }

        private long nextBackoff()
        {
            return Math.min(backoff + getBackoffIncrement(), getMaxBackoff());
        }

        protected abstract boolean isUpdateableTo(BayeuxClientState newState);

        /**
         * <p>Callback invoked when the state changed from the given {@code oldState}
         * to this state (and only when the two states are different).</p>
         *
         * @param oldState the previous state
         * @see #execute()
         */
        protected void enter(State oldState)
        {
        }

        /**
         * <p>Callback invoked when this state becomes the new state, even if the
         * previous state was equal to this state.</p>
         *
         * @see #enter(State)
         */
        protected abstract void execute();

        public State getType()
        {
            return type;
        }

        @Override
        public String toString()
        {
            return type.toString();
        }
    }

    private class DisconnectedState extends BayeuxClientState
    {
        private DisconnectedState(ClientTransport transport)
        {
            super(State.DISCONNECTED, null, null, null, transport, null, 0);
        }

        @Override
        protected boolean transportSend(TransportListener listener, List<Message.Mutable> messages)
        {
            failMessages(new TransportException(null), messages);
            return false;
        }

        @Override
        protected boolean isUpdateableTo(BayeuxClientState newState)
        {
            return newState.type == State.HANDSHAKING;
        }

        @Override
        protected void execute()
        {
            transport.terminate();
            terminate();
        }
    }

    private class AbortedState extends DisconnectedState
    {
        private AbortedState(ClientTransport transport)
        {
            super(transport);
        }

        @Override
        protected void execute()
        {
            transport.abort();
            terminate();
        }
    }

    private class HandshakingState extends BayeuxClientState
    {
        private HandshakingState(Map<String, Object> handshakeFields, ClientSessionChannel.MessageListener callback, ClientTransport transport)
        {
            super(State.HANDSHAKING, handshakeFields, callback, null, transport, null, 0);
        }

        @Override
        protected boolean isUpdateableTo(BayeuxClientState newState)
        {
            return newState.type == State.CONNECTING ||
                    newState.type == State.REHANDSHAKING ||
                    newState.type == State.DISCONNECTED;
        }

        @Override
        protected void enter(State oldState)
        {
            // Always reset the subscriptions when a handshake has been requested.
            resetSubscriptions();
        }

        @Override
        protected void execute()
        {
            // The state could change between now and when sendHandshake() runs;
            // in this case the handshake message will not be sent and will not
            // be failed, because most probably the client has been disconnected.
            sendHandshake();
        }
    }

    private class RehandshakingState extends BayeuxClientState
    {
        public RehandshakingState(Map<String, Object> handshakeFields, ClientSessionChannel.MessageListener callback, ClientTransport transport, long backoff)
        {
            super(State.REHANDSHAKING, handshakeFields, callback, null, transport, null, backoff);
        }

        @Override
        protected boolean isUpdateableTo(BayeuxClientState newState)
        {
            return newState.type == State.CONNECTING ||
                    newState.type == State.REHANDSHAKING ||
                    newState.type == State.DISCONNECTED;
        }

        @Override
        protected void enter(State oldState)
        {
            // Reset the subscriptions if this is not a failure from a requested handshake.
            // Subscriptions may be queued after requested handshakes.
            if (oldState != State.HANDSHAKING)
                resetSubscriptions();
        }

        @Override
        protected void execute()
        {
            scheduleHandshake(getInterval(), backoff);
        }
    }

    private class ConnectingState extends BayeuxClientState
    {
        private ConnectingState(Map<String, Object> handshakeFields, ClientSessionChannel.MessageListener callback, Map<String, Object> advice, ClientTransport transport, String clientId)
        {
            super(State.CONNECTING, handshakeFields, callback, advice, transport, clientId, 0);
        }

        @Override
        protected boolean isUpdateableTo(BayeuxClientState newState)
        {
            return newState.type == State.CONNECTED ||
                    newState.type == State.UNCONNECTED ||
                    newState.type == State.REHANDSHAKING ||
                    newState.type == State.DISCONNECTING ||
                    newState.type == State.DISCONNECTED;
        }

        @Override
        protected void execute()
        {
            // Send the messages that may have queued up before the handshake completed
            sendBatch();
            scheduleConnect(getInterval(), backoff);
        }
    }

    private class ConnectedState extends BayeuxClientState
    {
        private ConnectedState(Map<String, Object> handshakeFields, ClientSessionChannel.MessageListener callback, Map<String, Object> advice, ClientTransport transport, String clientId)
        {
            super(State.CONNECTED, handshakeFields, callback, advice, transport, clientId, 0);
        }

        @Override
        protected boolean isUpdateableTo(BayeuxClientState newState)
        {
            return newState.type == State.CONNECTED ||
                    newState.type == State.UNCONNECTED ||
                    newState.type == State.REHANDSHAKING ||
                    newState.type == State.DISCONNECTING ||
                    newState.type == State.DISCONNECTED;
        }

        @Override
        protected void execute()
        {
            scheduleConnect(getInterval(), backoff);
        }
    }

    private class UnconnectedState extends BayeuxClientState
    {
        private UnconnectedState(Map<String, Object> handshakeFields, ClientSessionChannel.MessageListener callback, Map<String, Object> advice, ClientTransport transport, String clientId, long backoff)
        {
            super(State.UNCONNECTED, handshakeFields, callback, advice, transport, clientId, backoff);
        }

        @Override
        protected boolean isUpdateableTo(BayeuxClientState newState)
        {
            return newState.type == State.CONNECTED ||
                    newState.type == State.UNCONNECTED ||
                    newState.type == State.REHANDSHAKING ||
                    newState.type == State.DISCONNECTED;
        }

        @Override
        protected void execute()
        {
            scheduleConnect(getInterval(), backoff);
        }
    }

    private class DisconnectingState extends BayeuxClientState
    {
        private DisconnectingState(ClientSessionChannel.MessageListener callback, ClientTransport transport, String clientId)
        {
            super(State.DISCONNECTING, null, callback, null, transport, clientId, 0);
        }

        @Override
        protected boolean isUpdateableTo(BayeuxClientState newState)
        {
            return newState.type == State.DISCONNECTED;
        }

        @Override
        protected void execute()
        {
            Message.Mutable message = newMessage();
            message.setChannel(Channel.META_DISCONNECT);
            if (callback != null)
                message.put(CALLBACK_KEY, callback);
            List<Message.Mutable> messages = new ArrayList<>(1);
            messages.add(message);
            send(disconnectListener, messages);
        }
    }
}
TOP

Related Classes of org.cometd.client.BayeuxClient$PublishTransportListener

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.