com.kixeye.kixmpp.client.KixmppClient.java Source code

Java tutorial

Introduction

Here is the source code for com.kixeye.kixmpp.client.KixmppClient.java

Source

package com.kixeye.kixmpp.client;

/*
 * #%L
 * KIXMPP
 * %%
 * Copyright (C) 2014 KIXEYE, Inc
 * %%
 * 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.
 * #L%
 */

import io.netty.bootstrap.Bootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.channel.Channel;
import io.netty.channel.ChannelDuplexHandler;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.ChannelPromise;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.channel.epoll.EpollEventLoopGroup;
import io.netty.channel.epoll.EpollSocketChannel;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.base64.Base64;
import io.netty.handler.codec.http.DefaultHttpHeaders;
import io.netty.handler.codec.http.FullHttpResponse;
import io.netty.handler.codec.http.HttpClientCodec;
import io.netty.handler.codec.http.HttpObjectAggregator;
import io.netty.handler.codec.http.websocketx.WebSocketClientHandshaker;
import io.netty.handler.codec.http.websocketx.WebSocketClientHandshakerFactory;
import io.netty.handler.codec.http.websocketx.WebSocketVersion;
import io.netty.handler.ssl.SslContext;
import io.netty.handler.ssl.SslHandler;
import io.netty.util.CharsetUtil;
import io.netty.util.concurrent.Future;
import io.netty.util.concurrent.GenericFutureListener;

import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Collections;
import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;

import org.jdom2.Attribute;
import org.jdom2.Element;
import org.jdom2.Namespace;
import org.jdom2.output.XMLOutputter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.SettableFuture;
import com.kixeye.kixmpp.KixmppAuthException;
import com.kixeye.kixmpp.KixmppCodec;
import com.kixeye.kixmpp.KixmppException;
import com.kixeye.kixmpp.KixmppJid;
import com.kixeye.kixmpp.KixmppStanzaRejectedException;
import com.kixeye.kixmpp.KixmppStreamEnd;
import com.kixeye.kixmpp.KixmppStreamStart;
import com.kixeye.kixmpp.KixmppWebSocketCodec;
import com.kixeye.kixmpp.client.module.KixmppClientModule;
import com.kixeye.kixmpp.client.module.chat.MessageKixmppClientModule;
import com.kixeye.kixmpp.client.module.error.ErrorKixmppClientModule;
import com.kixeye.kixmpp.client.module.muc.MucKixmppClientModule;
import com.kixeye.kixmpp.client.module.presence.PresenceKixmppClientModule;
import com.kixeye.kixmpp.handler.KixmppEventEngine;
import com.kixeye.kixmpp.handler.KixmppStanzaHandler;
import com.kixeye.kixmpp.interceptor.KixmppStanzaInterceptor;

/**
 * A XMPP client.
 * 
 * @author ebahtijaragic
 */
public class KixmppClient implements AutoCloseable {
    private static final Logger logger = LoggerFactory.getLogger(KixmppClient.class);

    private static String OS = System.getProperty("os.name").toLowerCase();

    public enum Type {
        TCP, WEBSOCKET
    }

    private final ConcurrentHashMap<KixmppClientOption<?>, Object> clientOptions = new ConcurrentHashMap<KixmppClientOption<?>, Object>();
    private final Bootstrap bootstrap;

    private final KixmppEventEngine eventEngine;

    private final SslContext sslContext;

    private final Set<KixmppStanzaInterceptor> interceptors = Collections
            .newSetFromMap(new ConcurrentHashMap<KixmppStanzaInterceptor, Boolean>());

    private final Set<String> modulesToRegister = Collections
            .newSetFromMap(new ConcurrentHashMap<String, Boolean>());
    private final ConcurrentHashMap<String, KixmppClientModule> modules = new ConcurrentHashMap<>();

    private final Type type;

    private WebSocketClientHandshaker handshaker;

    private SettableFuture<KixmppClient> deferredLogin;
    private SettableFuture<KixmppClient> deferredDisconnect;

    private KixmppJid jid;
    private String password;

    private AtomicReference<Channel> channel = new AtomicReference<>(null);
    private AtomicReference<GenericFutureListener<Future<? super Void>>> connectListener = new AtomicReference<>();

    private AtomicReference<State> state = new AtomicReference<>(State.DISCONNECTED);

    private static enum State {
        CONNECTING, CONNECTED,

        LOGGING_IN, SECURING, LOGGED_IN,

        DISCONNECTING, DISCONNECTED
    }

    /**
     * Creates a new {@link KixmppClient}.
     */
    public KixmppClient() {
        this(null);
    }

    /**
     * Creates a new {@link KixmppClient} with the given ssl engine.
     * 
     * @param sslContext
     */
    public KixmppClient(SslContext sslContext) {
        this(sslContext, Type.TCP);
    }

    /**
     * Creates a new {@link KixmppClient} with the given ssl engine with a type.
     * 
     * @param sslContext
     * @param type
     */
    public KixmppClient(SslContext sslContext, Type type) {
        this(null, new KixmppEventEngine(), sslContext, type);
    }

    /**
     * Creates a new {@link KixmppClient}.
     * 
     * @param eventLoopGroup
     * @param eventEngine
     * @param sslContext
     */
    public KixmppClient(EventLoopGroup eventLoopGroup, KixmppEventEngine eventEngine, SslContext sslContext) {
        this(eventLoopGroup, new KixmppEventEngine(), sslContext, Type.TCP);
    }

    /**
     * Creates a new {@link KixmppClient}.
     * 
     * @param eventLoopGroup
     * @param eventEngine
     * @param sslContext
     * @param type
     */
    public KixmppClient(EventLoopGroup eventLoopGroup, KixmppEventEngine eventEngine, SslContext sslContext,
            Type type) {
        if (sslContext != null) {
            assert sslContext.isClient() : "The given SslContext must be a client context.";
        }

        if (eventLoopGroup == null) {
            if (OS.indexOf("nux") >= 0) {
                eventLoopGroup = new EpollEventLoopGroup();
            } else {
                eventLoopGroup = new NioEventLoopGroup();
            }
        }

        this.type = type;

        this.sslContext = sslContext;
        this.eventEngine = eventEngine;

        // set modules to be registered
        this.modulesToRegister.add(MucKixmppClientModule.class.getName());
        this.modulesToRegister.add(PresenceKixmppClientModule.class.getName());
        this.modulesToRegister.add(MessageKixmppClientModule.class.getName());
        this.modulesToRegister.add(ErrorKixmppClientModule.class.getName());

        if (eventLoopGroup instanceof EpollEventLoopGroup) {
            this.bootstrap = new Bootstrap().group(eventLoopGroup).channel(EpollSocketChannel.class)
                    .option(ChannelOption.TCP_NODELAY, false).option(ChannelOption.SO_KEEPALIVE, true);
        } else {
            this.bootstrap = new Bootstrap().group(eventLoopGroup).channel(NioSocketChannel.class)
                    .option(ChannelOption.TCP_NODELAY, false).option(ChannelOption.SO_KEEPALIVE, true);
        }

        switch (type) {
        case TCP:
            bootstrap.handler(new KixmppClientChannelInitializer());
            break;
        case WEBSOCKET:
            bootstrap.handler(new KixmppClientWebSocketChannelInitializer());
            break;
        }
    }

    /**
     * Connects to the hostname and port.
     * 
     * @param hostname
     * @param port
     */
    public ListenableFuture<KixmppClient> connect(String hostname, int port, String domain) {
        checkAndSetState(State.CONNECTING, State.DISCONNECTED);

        this.jid = new KixmppJid(domain);
        try {
            this.handshaker = WebSocketClientHandshakerFactory.newHandshaker(
                    new URI("ws://" + hostname + ":" + port), WebSocketVersion.V13, null, false,
                    new DefaultHttpHeaders());
        } catch (Exception e) {
            throw new RuntimeException("Unable to set up handshaker.", e);
        }

        setUp();

        // set this in case we get disconnected
        deferredDisconnect = SettableFuture.create();
        deferredLogin = SettableFuture.create();

        final SettableFuture<KixmppClient> responseFuture = SettableFuture.create();

        connectListener.set(new GenericFutureListener<Future<? super Void>>() {
            public void operationComplete(Future<? super Void> future) throws Exception {
                if (future.isSuccess()) {
                    if (state.compareAndSet(State.CONNECTING, State.CONNECTED)) {
                        logger.info("Kixmpp Client connected to [{}]",
                                ((ChannelFuture) future).channel().remoteAddress());

                        channel.set(((ChannelFuture) future).channel());
                        responseFuture.set(KixmppClient.this);
                    }
                } else {
                    state.set(State.DISCONNECTED);
                    responseFuture.setException(future.cause());
                }
            }
        });

        ChannelFuture future = bootstrap.connect(hostname, port);

        switch (type) {
        case TCP:
            future.addListener(connectListener.get());
            break;
        case WEBSOCKET:
            future.addListener(new GenericFutureListener<Future<? super Void>>() {
                public void operationComplete(Future<? super Void> future) throws Exception {
                    if (!future.isSuccess()) {
                        state.set(State.DISCONNECTED);
                        responseFuture.setException(future.cause());
                    }
                }
            });
            break;
        }

        return responseFuture;
    }

    /**
     * Logs the user into the XMPP server.
     * 
     * @param username
     * @param password
     * @param resource
     * @throws InterruptedException 
     */
    public ListenableFuture<KixmppClient> login(String username, String password, String resource)
            throws InterruptedException {
        checkAndSetState(State.LOGGING_IN, State.CONNECTED);

        this.jid = this.jid.withNode(username).withResource(resource);
        this.password = password;

        channel.get().writeAndFlush(new KixmppStreamStart(null, new KixmppJid(jid.getDomain()), true));

        return deferredLogin;
    }

    /**
     * Disconnects from the current server.
     */
    public ListenableFuture<KixmppClient> disconnect() {
        State currentState = compareAndSetState(State.DISCONNECTING, State.CONNECTED, State.LOGGED_IN,
                State.LOGGING_IN);

        if (currentState == State.DISCONNECTED) {
            final SettableFuture<KixmppClient> deferred = SettableFuture.create();
            deferred.set(this);

            return deferred;
        } else if (currentState == null) {
            return deferredDisconnect;
        }

        final Channel currentChannel = channel.get();

        if (currentChannel != null) {
            channel.get().writeAndFlush(new KixmppStreamEnd());

            // do a disconnect timeout in case server doesn't close stream.
            bootstrap.group().schedule(new DisconnectTimeoutTask(), 2, TimeUnit.SECONDS);
        } else {
            deferredDisconnect.setException(new KixmppException("No channel available to close."));
        }

        return deferredDisconnect;
    }

    /**
     * @see java.lang.AutoCloseable#close()
     */
    public void close() throws Exception {
        disconnect();
    }

    /**
     * Sets the client's {@link KixmppClientOption}s.
     * 
     * @param option
     * @param value
     * @return
     */
    public <T> KixmppClient clientOption(KixmppClientOption<T> option, T value) {
        if (value == null) {
            clientOptions.remove(option);
        } else {
            clientOptions.put(option, value);
        }
        return this;
    }

    /**
     * Sets Netty {@link ChannelOption}s.
     * 
     * @param option
     * @param value
     * @return
     */
    public <T> KixmppClient channelOption(ChannelOption<T> option, T value) {
        bootstrap.option(option, value);
        return this;
    }

    /**
     * Adds a stanza interceptor.
     * 
     * @param interceptor
     */
    public boolean addInterceptor(KixmppStanzaInterceptor interceptor) {
        return interceptors.add(interceptor);
    }

    /**
     * Removes a stanza interceptor.
     * 
     * @param interceptor
     */
    public boolean removeInterceptor(KixmppStanzaInterceptor interceptor) {
        return interceptors.remove(interceptor);
    }

    /**
     * Gets the event engine.
     * 
     * @return
     */
    public KixmppEventEngine getEventEngine() {
        return eventEngine;
    }

    /**
     * @param moduleClass
     * @return true if module is installed
     */
    public boolean hasActiveModule(Class<?> moduleClass) {
        return modules.containsKey(moduleClass.getName());
    }

    /**
     * Returns true if the client is connected.
     * 
     * @return
     */
    public boolean isConnected() {
        return state.get() != State.DISCONNECTED;
    }

    /**
     * Gets or installs a module.
     * 
     * @param moduleClass
     * @return
     */
    @SuppressWarnings("unchecked")
    public <T extends KixmppClientModule> T module(Class<T> moduleClass) {
        if (!(state.get() == State.CONNECTED || state.get() == State.LOGGING_IN || state.get() == State.SECURING
                || state.get() == State.LOGGED_IN)) {
            throw new IllegalStateException(
                    String.format("The current state is [%s] but must be [CONNECTED or LOGGED_IN]", state.get()));
        }

        T module = (T) modules.get(moduleClass.getName());

        if (module == null) {
            module = (T) installModule(moduleClass.getName());
        }

        return module;
    }

    /**
     * Writes a stanza to the channel.
     * 
     * @param element
     */
    public void sendStanza(Element element) {
        channel.get().writeAndFlush(element);
    }

    /**
     * Gets the jid of the user.
     * 
     * @return
     */
    public KixmppJid getJid() {
        return jid;
    }

    /**
     * Gets the type of client.
     * 
     * @return
     */
    public Type getType() {
        return type;
    }

    /**
     * Cas for state.
     * 
     * @param update
     * @param expectedStates
     * @return
     */
    private State compareAndSetState(State update, State... expectedStates) {
        State setState = null;

        if (expectedStates != null) {
            for (State expectedState : expectedStates) {
                if (state.compareAndSet(expectedState, update)) {
                    setState = update;
                    break;
                }
            }

            if (setState == null) {
                if (update != state.get()) {
                    setState = state.get();
                }
            }
        } else {
            if (state.compareAndSet(null, update)) {
                setState = update;
            } else {
                setState = state.get();
            }
        }

        return setState;
    }

    /**
     * Checks the state and sets it.
     * 
     * @param update
     * @param expectedStates
     * @throws IllegalStateException
     */
    private void checkAndSetState(State update, State... expectedStates) throws IllegalStateException {
        if (compareAndSetState(update, expectedStates) != update) {
            throw new IllegalStateException(String.format("The current state is [%s] but must be %s", state.get(),
                    expectedStates == null ? "null" : Arrays.toString(expectedStates)));
        }
    }

    /**
     * Registers all the consumers and modules.
     */
    private void setUp() {
        if (state.get() == State.CONNECTING) {
            // this client deals with the following stanzas
            eventEngine.registerGlobalStanzaHandler("stream:features", streamFeaturesHandler);

            eventEngine.registerGlobalStanzaHandler("proceed", tlsResponseHandler);

            eventEngine.registerGlobalStanzaHandler("success", authResultHandler);
            eventEngine.registerGlobalStanzaHandler("failure", authResultHandler);

            eventEngine.registerGlobalStanzaHandler("iq", iqResultHandler);

            // register all modules
            for (String moduleClassName : modulesToRegister) {
                installModule(moduleClassName);
            }
        }
    }

    /**
     * Unregisters all consumers and modules.
     */
    private void cleanUp() {
        if (state.get() == State.DISCONNECTING) {
            for (Entry<String, KixmppClientModule> entry : modules.entrySet()) {
                entry.getValue().uninstall(this);
            }

            eventEngine.unregisterAll();
        }
    }

    /**
     * Tries to install module.
     * 
     * @param moduleClassName
     * @throws Exception
     */
    private KixmppClientModule installModule(String moduleClassName) {
        KixmppClientModule module = null;

        try {
            module = (KixmppClientModule) Class.forName(moduleClassName).newInstance();
            module.install(this);

            modules.put(moduleClassName, module);
        } catch (Exception e) {
            logger.error("Error while installing module", e);
        }

        return module;
    }

    /**
     * Performs auth.
     */
    private void performAuth() {
        byte[] authToken = ("\0" + jid.getNode() + "\0" + password).getBytes(StandardCharsets.UTF_8);

        Element auth = new Element("auth", "urn:ietf:params:xml:ns:xmpp-sasl");
        auth.setAttribute("mechanism", "PLAIN");

        ByteBuf rawCredentials = channel.get().alloc().buffer().writeBytes(authToken);
        ByteBuf encodedCredentials = Base64.encode(rawCredentials);
        String encodedCredentialsString = encodedCredentials.toString(StandardCharsets.UTF_8);
        encodedCredentials.release();
        rawCredentials.release();

        auth.setText(encodedCredentialsString);

        channel.get().writeAndFlush(auth);
    }

    /**
     * Handles stream features
     */
    private final KixmppStanzaHandler streamFeaturesHandler = new KixmppStanzaHandler() {
        public void handle(Channel channel, Element streamFeatures) {
            Element startTls = streamFeatures.getChild("starttls",
                    Namespace.getNamespace("urn:ietf:params:xml:ns:xmpp-tls"));

            Object enableTls = clientOptions.get(KixmppClientOption.ENABLE_TLS);

            if ((enableTls != null && (boolean) enableTls)
                    || (startTls != null && startTls.getChild("required", startTls.getNamespace()) != null)) {
                // if its required, always do tls
                startTls = new Element("starttls", "tls", "urn:ietf:params:xml:ns:xmpp-tls");

                KixmppClient.this.channel.get().writeAndFlush(startTls);
            } else {
                performAuth();
            }
        }
    };

    /**
      * Handles stream features
      */
    private final KixmppStanzaHandler tlsResponseHandler = new KixmppStanzaHandler() {
        public void handle(Channel channel, Element tlsResponse) {
            if (state.compareAndSet(State.LOGGING_IN, State.SECURING)) {
                SslHandler handler = sslContext.newHandler(KixmppClient.this.channel.get().alloc());
                handler.handshakeFuture().addListener(new GenericFutureListener<Future<? super Channel>>() {
                    public void operationComplete(Future<? super Channel> future) throws Exception {
                        if (future.isSuccess()) {
                            KixmppClient.this.channel.get().pipeline().replace(KixmppCodec.class, "kixmppCodec",
                                    new KixmppCodec());

                            KixmppClient.this.channel.get().writeAndFlush(
                                    new KixmppStreamStart(null, new KixmppJid(jid.getDomain()), true));
                        } else {
                            deferredLogin.setException(new KixmppAuthException("tls failed"));
                        }
                    }
                });

                KixmppClient.this.channel.get().pipeline().addFirst("sslHandler", handler);
            }
        }
    };

    /**
      * Handles auth success
      */
    private final KixmppStanzaHandler authResultHandler = new KixmppStanzaHandler() {
        public void handle(Channel channel, Element authResult) {
            switch (authResult.getName()) {
            case "success":
                // send bind
                Element bindRequest = new Element("iq");
                bindRequest.setAttribute("type", "set");
                bindRequest.setAttribute("id", "bind");

                Element bind = new Element("bind", "urn:ietf:params:xml:ns:xmpp-bind");

                if (KixmppClient.this.jid.getResource() != null) {
                    Element resource = new Element("resource", null, "urn:ietf:params:xml:ns:xmpp-bind");
                    resource.setText(KixmppClient.this.jid.getResource());
                    bind.addContent(resource);
                }

                bindRequest.addContent(bind);

                KixmppClient.this.channel.get().writeAndFlush(bindRequest);
                break;
            default:
                // fail
                deferredLogin.setException(new KixmppAuthException(new XMLOutputter().outputString(authResult)));
                break;
            }
        }
    };

    /**
      * Handles iq stanzas
      */
    private final KixmppStanzaHandler iqResultHandler = new KixmppStanzaHandler() {
        public void handle(Channel channel, Element iqResult) {
            Attribute idAttribute = iqResult.getAttribute("id");
            Attribute typeAttribute = iqResult.getAttribute("type");

            if (idAttribute != null) {
                switch (idAttribute.getValue()) {
                case "bind":
                    if (typeAttribute != null && "result".equals(typeAttribute.getValue())) {
                        Element bind = iqResult.getChild("bind",
                                Namespace.getNamespace("urn:ietf:params:xml:ns:xmpp-bind"));

                        if (bind != null) {
                            jid = KixmppJid.fromRawJid(bind.getChildText("jid", bind.getNamespace()));
                        }

                        // start the session
                        Element startSession = new Element("iq");
                        startSession.setAttribute("to", jid.getDomain());
                        startSession.setAttribute("type", "set");
                        startSession.setAttribute("id", "session");

                        Element session = new Element("session", "urn:ietf:params:xml:ns:xmpp-session");
                        startSession.addContent(session);

                        KixmppClient.this.channel.get().writeAndFlush(startSession);
                    } else {
                        // fail
                        deferredLogin
                                .setException(new KixmppAuthException(new XMLOutputter().outputString(iqResult)));
                    }
                    break;
                case "session":
                    if (typeAttribute != null && "result".equals(typeAttribute.getValue())) {
                        deferredLogin.set(KixmppClient.this);
                        state.set(State.LOGGED_IN);

                        logger.debug("Logged in as: " + jid);
                    } else {
                        // fail
                        deferredLogin
                                .setException(new KixmppAuthException(new XMLOutputter().outputString(iqResult)));
                    }
                    break;
                default:
                    logger.warn("Unsupported IQ stanza: " + new XMLOutputter().outputString(iqResult));
                    break;
                }
            }
        }
    };

    /**
     * Channel initializer for the {@link KixmppClient}.
     * 
     * @author ebahtijaragic
     */
    private final class KixmppClientChannelInitializer extends ChannelInitializer<SocketChannel> {
        /**
         * @see io.netty.channel.ChannelInitializer#initChannel(io.netty.channel.Channel)
         */
        protected void initChannel(SocketChannel ch) throws Exception {
            // initially only add the codec and out client handler
            ch.pipeline().addLast("kixmppCodec", new KixmppCodec());
            ch.pipeline().addLast("kixmppClientMessageHandler", new KixmppClientMessageHandler());
        }
    }

    /**
      * Channel initializer for the {@link KixmppClient} using WebSocket.
      * 
      * @author ebahtijaragic
      */
    private final class KixmppClientWebSocketChannelInitializer extends ChannelInitializer<SocketChannel> {
        /**
         * @see io.netty.channel.ChannelInitializer#initChannel(io.netty.channel.Channel)
         */
        protected void initChannel(SocketChannel ch) throws Exception {
            // initially only add the codec and out client handler
            ch.pipeline().addLast(new HttpClientCodec());
            ch.pipeline().addLast(new HttpObjectAggregator(65536));
            ch.pipeline().addLast(new WebSocketClientHandler());
            ch.pipeline().addLast("kixmppCodec", new KixmppWebSocketCodec());
            ch.pipeline().addLast("kixmppClientMessageHandler", new KixmppClientMessageHandler());
        }
    }

    /**
     * Message handler for the {@link KixmppClient}
     * 
     * @author ebahtijaragic
     */
    private final class KixmppClientMessageHandler extends ChannelDuplexHandler {
        @Override
        public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
            if (msg instanceof Element) {
                Element stanza = (Element) msg;

                boolean rejected = false;

                for (KixmppStanzaInterceptor interceptor : interceptors) {
                    try {
                        interceptor.interceptIncoming(ctx.channel(), (Element) msg);
                    } catch (KixmppStanzaRejectedException e) {
                        rejected = true;

                        logger.debug("Incoming stanza interceptor [{}] threw an rejected exception.", interceptor,
                                e);
                    } catch (Exception e) {
                        logger.error("Incoming stanza interceptor [{}] threw an exception.", interceptor, e);
                    }
                }

                if (!rejected) {
                    eventEngine.publishStanza(ctx.channel(), stanza);
                }
            } else if (msg instanceof KixmppStreamStart) {
                eventEngine.publishStreamStart(ctx.channel(), (KixmppStreamStart) msg);
            } else if (msg instanceof KixmppStreamEnd) {
                eventEngine.publishStreamEnd(ctx.channel(), (KixmppStreamEnd) msg);
            }
        }

        @Override
        public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
            boolean rejected = false;

            if (msg instanceof Element) {
                for (KixmppStanzaInterceptor interceptor : interceptors) {
                    try {
                        interceptor.interceptOutgoing(ctx.channel(), (Element) msg);
                    } catch (KixmppStanzaRejectedException e) {
                        rejected = true;

                        logger.debug("Outgoing stanza interceptor [{}] threw an rejected exception.", interceptor,
                                e);
                    } catch (Exception e) {
                        logger.error("Outgoing stanza interceptor [{}] threw an exception.", interceptor, e);
                    }
                }
            }

            if (!rejected) {
                super.write(ctx, msg, promise);
            }
        }

        @Override
        public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
            logger.error("Unexpected error.", cause);

            KixmppClient.this.disconnect();
        }

        @Override
        public void channelActive(ChannelHandlerContext ctx) throws Exception {
            eventEngine.publishConnected(ctx.channel());
        }

        @Override
        public void channelInactive(ChannelHandlerContext ctx) throws Exception {
            eventEngine.publishDisconnected(ctx.channel());

            cleanUp();

            deferredDisconnect.set(KixmppClient.this);
            state.set(State.DISCONNECTED);

            if (KixmppClient.this.isConnected()) {
                KixmppClient.this.disconnect();
            }
        }
    }

    public class WebSocketClientHandler extends SimpleChannelInboundHandler<Object> {
        private ChannelPromise handshakeFuture;

        public ChannelFuture handshakeFuture() {
            return handshakeFuture;
        }

        @Override
        public void handlerAdded(ChannelHandlerContext ctx) {
            handshakeFuture = ctx.newPromise();
        }

        @Override
        public void channelActive(ChannelHandlerContext ctx) {
            handshaker.handshake(ctx.channel());

            ctx.fireChannelActive();
        }

        @Override
        public void channelInactive(ChannelHandlerContext ctx) throws Exception {
            ctx.fireChannelInactive();
        }

        @Override
        public void channelRead0(ChannelHandlerContext ctx, Object msg) throws Exception {
            Channel ch = ctx.channel();
            if (!handshaker.isHandshakeComplete()) {
                handshaker.finishHandshake(ch, (FullHttpResponse) msg);
                handshakeFuture.setSuccess().addListener(connectListener.get());
                return;
            }

            if (msg instanceof FullHttpResponse) {
                FullHttpResponse response = (FullHttpResponse) msg;
                throw new IllegalStateException("Unexpected FullHttpResponse (getStatus=" + response.getStatus()
                        + ", content=" + response.content().toString(CharsetUtil.UTF_8) + ')');
            }

            ctx.fireChannelRead(msg);
        }
    }

    private class DisconnectTimeoutTask implements Runnable {
        public void run() {
            Channel channel = KixmppClient.this.channel.get();

            if (channel != null) {
                channel.close();
            }
        }
    }
}