public class KixmppAuthException extends KixmppException {
	private static final long serialVersionUID = -2495670401789500578L;

	/**
	 * Creates an auth response.
	 * 
	 * @param response
	 */
	public KixmppAuthException(String response) {
		super(response);
	}
}
 public class KixmppCodec extends ByteToMessageCodec<Object> {
	private static final int STANZA_ELEMENT_DEPTH = 2;

	private static final Logger logger  = LoggerFactory.getLogger(KixmppCodec.class);

	private StAXElementBuilder elementBuilder = null;
	
	private InputFactoryImpl inputFactory = new InputFactoryImpl();
	private AsyncXMLStreamReader streamReader = inputFactory.createAsyncXMLStreamReader();
	private AsyncInputFeeder asyncInputFeeder = streamReader.getInputFeeder();
	
	public enum XMLStreamReaderConfiguration {
		SPEED,
		LOW_MEMORY_USAGE,
		ROUND_TRIPPING,
		CONVENIENCE,
		XML_CONFORMANCE
	}
	
	/**
	 * Creates a new codec and optimizes the parser for speed.
	 */
	public KixmppCodec() {
		this(XMLStreamReaderConfiguration.SPEED);
	}
	
	/**
	 * Creates a new codec and optimizes the parser based on the configuration flag.
	 * 
	 * @param configuration tells the codec how to optimize the XMLStreamReader
	 */
	public KixmppCodec(XMLStreamReaderConfiguration configuration) {
		switch (configuration) {
			case CONVENIENCE:
				inputFactory.configureForConvenience();
				break;
			case LOW_MEMORY_USAGE:
				inputFactory.configureForLowMemUsage();
				break;
			case ROUND_TRIPPING:
				inputFactory.configureForRoundTripping();
				break;
			case SPEED:
				inputFactory.configureForSpeed();
				break;
			case XML_CONFORMANCE:
				inputFactory.configureForXmlConformance();
				break;
		}
	}
	
	/**
	 * @see io.netty.handler.codec.ByteToMessageCodec#decode(io.netty.channel.ChannelHandlerContext, io.netty.buffer.ByteBuf, java.util.List)
	 */
	@Override
	protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
		if (logger.isDebugEnabled()) {
			logger.debug("Received: [{}]", in.toString(StandardCharsets.UTF_8));
		}
		
		int retryCount = 0;
		Exception thrownException = null;
		
		// feed the data into the async xml input feeded
		byte[] data = new byte[in.readableBytes()];
		in.readBytes(data);
		
		if (streamReader != null) {
			while (retryCount < 2) {
				try {
					asyncInputFeeder.feedInput(data, 0, data.length);
					
					int event = -1;
					
					while (isValidEvent(event = streamReader.next())) {
						// handle stream start/end
						if (streamReader.getDepth() == STANZA_ELEMENT_DEPTH - 1) {
							if (event == XMLStreamConstants.END_ELEMENT) {
								out.add(new KixmppStreamEnd());

								asyncInputFeeder.endOfInput();
								streamReader.close();
								
								streamReader = null;
								asyncInputFeeder = null;
								
								break;
							} else if (event == XMLStreamConstants.START_ELEMENT) {
								StAXElementBuilder streamElementBuilder = new StAXElementBuilder(true);
								
								streamElementBuilder.process(streamReader);
			
								out.add(new KixmppStreamStart(null, true));
							}
						// only handle events that have element depth of 2 and above (everything under <stream:stream>..)
						} else if (streamReader.getDepth() >= STANZA_ELEMENT_DEPTH) {
							// if this is the beginning of the element and this is at stanza depth
							if (event == XMLStreamConstants.START_ELEMENT && streamReader.getDepth() == STANZA_ELEMENT_DEPTH) {
								elementBuilder = new StAXElementBuilder(true);
								elementBuilder.process(streamReader);
	
								// get the constructed element
								Element element = elementBuilder.getElement();
								
								if ("stream:stream".equals(element.getQualifiedName())) {
									throw new RuntimeException("Starting a new stream.");
								}
							// if this is the ending of the element and this is at stanza depth
						    } else if (event == XMLStreamConstants.END_ELEMENT && streamReader.getDepth() == STANZA_ELEMENT_DEPTH) {
								elementBuilder.process(streamReader);
	
								// get the constructed element
								Element element = elementBuilder.getElement();
								
					    		out.add(element);
					    
					    	// just process the event
						    } else {
								elementBuilder.process(streamReader);
						    }
						}
					}
					
					break;
				} catch (Exception e) {
					retryCount++;
					
					logger.info("Attempting to recover from impropper XML: " + e.getMessage());
					
					thrownException = e;
					
					try {
						streamReader.close();
					} finally {
						streamReader = inputFactory.createAsyncXMLStreamReader();
						asyncInputFeeder = streamReader.getInputFeeder();
					}
				}
			}
			
			if (retryCount > 1) {
				throw thrownException;
			}
		}
	}
	
	/**
	 * @param event the event id
	 * @return <b>true</b> if this event is not the end of a document event and it is not an event incomplete event
	 */
	private boolean isValidEvent(int event) {
		return (event != XMLStreamConstants.END_DOCUMENT && event != AsyncXMLStreamReader.EVENT_INCOMPLETE);
	}
	
	/**
	 * @see io.netty.handler.codec.ByteToMessageCodec#encode(io.netty.channel.ChannelHandlerContext, java.lang.Object, io.netty.buffer.ByteBuf)
	 */
	@Override
	protected void encode(ChannelHandlerContext ctx, Object msg, ByteBuf out) throws Exception {
		if (msg instanceof Element) {
			new XMLOutputter().output((Element)msg, new ByteBufOutputStream(out));
		} else if (msg instanceof KixmppStreamStart) {
			KixmppStreamStart streamStart = (KixmppStreamStart)msg;
			if (streamStart.doesIncludeXmlHeader()) {
				out.writeBytes("<?xml version='1.0' encoding='UTF-8'?>".getBytes(StandardCharsets.UTF_8));
			}
			out.writeBytes("<stream:stream ".getBytes(StandardCharsets.UTF_8));
			if (streamStart.getId() != null) {
				out.writeBytes(String.format("id=\"%s\" ", streamStart.getId()).getBytes(StandardCharsets.UTF_8));
			}
			if (streamStart.getFrom() != null) {
				out.writeBytes(String.format("from=\"%s\" ", streamStart.getFrom().getFullJid()).getBytes(StandardCharsets.UTF_8));
			}
			if (streamStart.getTo() != null) {
				out.writeBytes(String.format("to=\"%s\" ", streamStart.getTo().getFullJid()).getBytes(StandardCharsets.UTF_8));
			}
			out.writeBytes("version=\"1.0\" xmlns=\"jabber:client\" xmlns:stream=\"http://etherx.jabber.org/streams\">".getBytes(StandardCharsets.UTF_8));
		} else if (msg instanceof KixmppStreamEnd) {
			out.writeBytes("</stream:stream>".getBytes(StandardCharsets.UTF_8));
		} else if (msg instanceof String) {
			out.writeBytes(((String)msg).getBytes(StandardCharsets.UTF_8));
		} else if (msg instanceof ByteBuf) {
			ByteBuf buf = (ByteBuf)msg;
			
			out.writeBytes(buf, 0, buf.readableBytes());
		}
		
		if (logger.isDebugEnabled()) {
			logger.debug("Sending: [{}]", out.toString(StandardCharsets.UTF_8));
		}
	}
} public class KixmppException extends RuntimeException {
	private static final long serialVersionUID = 9000769692935976910L;
	
	/** Constructs a new exception with {@code null} as its
     * detail message.  The cause is not initialized, and may subsequently be
     * initialized by a call to {@link #initCause}.
     */
    public KixmppException() {
        super();
    }

    /** Constructs a new exception with the specified detail message.
     * The cause is not initialized, and may subsequently be initialized by a
     * call to {@link #initCause}.
     *
     * @param   message   the detail message. The detail message is saved for
     *          later retrieval by the {@link #getMessage()} method.
     */
    public KixmppException(String message) {
        super(message);
    }

    /**
     * Constructs a new exception with the specified detail message and
     * cause.  <p>Note that the detail message associated with
     * {@code cause} is <i>not</i> automatically incorporated in
     * this runtime exception's detail message.
     *
     * @param  message the detail message (which is saved for later retrieval
     *         by the {@link #getMessage()} method).
     * @param  cause the cause (which is saved for later retrieval by the
     *         {@link #getCause()} method).  (A <tt>null</tt> value is
     *         permitted, and indicates that the cause is nonexistent or
     *         unknown.)
     * @since  1.4
     */
    public KixmppException(String message, Throwable cause) {
        super(message, cause);
    }
}
 public class KixmppJid {
	private final String node;
	private final String domain;
	private final String resource;
	
	/**
	 * @param domain
	 */
	public KixmppJid(String domain) {
		this(null, domain, null);
	}
	
	/**
	 * @param node
	 * @param domain
	 */
	public KixmppJid(String node, String domain) {
		this(node, domain, null);
	}

	/**
	 * @param node
	 * @param domain
	 * @param resource
	 */
	public KixmppJid(String node, String domain, String resource) {
		assert domain != null && !domain.isEmpty() : "Argument 'domain' cannot be null or empty";

		this.node = node;
		this.domain = domain.toLowerCase();
		this.resource = resource;
	}
	
	/**
	 * Creates a {@link KixmppJid} from a raw jid.
	 * 
	 * @param jid
	 * @return
	 */
	public static KixmppJid fromRawJid(String jid) {
		String[] jidSplit = jid.split("/", 2);
		String[] domainSplit = jidSplit[0].split("@", 2);
		
		if (domainSplit.length == 1) {
			return new KixmppJid(null, domainSplit[0], jidSplit.length == 2 ? jidSplit[1] : null);
		} else {
			return new KixmppJid(domainSplit[0], domainSplit[1], jidSplit.length == 2 ? jidSplit[1] : null);
		}
	}

	/**
	 * Returns a clone of this JID with a different node.
	 * 
	 * @param node
	 * @return
	 */
	public KixmppJid withNode(String node) {
		return new KixmppJid(node, domain, resource);
	}

	/**
	 * Returns a clone of this JID with a different domain.
	 * 
	 * @param domain
	 * @return
	 */
	public KixmppJid withDomain(String domain) {
		return new KixmppJid(node, domain, resource);
	}

	/**
	 * Returns a clone of this JID with a different resource.
	 * 
	 * @param resource
	 * @return
	 */
	public KixmppJid withResource(String resource) {
		return new KixmppJid(node, domain, resource);
	}
	
	/**
	 * Returns a clone of this JID without the resource.
	 * 
	 * @return
	 */
	public KixmppJid withoutResource() {
		return new KixmppJid(node, domain);
	}

	/**
	 * @return the node
	 */
	public String getNode() {
		return node;
	}

	/**
	 * @return the domain
	 */
	public String getDomain() {
		return domain;
	}

	/**
	 * @return the resource
	 */
	public String getResource() {
		return resource;
	}
	
	/**
	 * Gets the base JID (node)@(domain).
	 * 
	 * @return
	 */
	public String getBaseJid() {
		if (node == null) {
			return domain;
		} else {
			return node + "@" + domain;
		}
	}
	
	/**
	 * Gets the full JID (node)@(domain)[/(resource)].
	 * 
	 * @return
	 */
	public String getFullJid() {
		if (resource == null) {
			return getBaseJid();
		} else {
			return node + "@" + domain + "/" + resource;
		}
	}
	
	/**
	 * @see java.lang.Object#toString()
	 */
	public String toString() {
		return getFullJid();
	}

	/**
	 * @see java.lang.Object#hashCode()
	 */
	public int hashCode() {
		final int prime = 31;
		int result = 1;
		result = prime * result + ((domain == null) ? 0 : domain.hashCode());
		result = prime * result + ((node == null) ? 0 : node.hashCode());
		result = prime * result
				+ ((resource == null) ? 0 : resource.hashCode());
		return result;
	}

	/**
	 * @see java.lang.Object#equals(java.lang.Object)
	 */
	public boolean equals(Object obj) {
		if (this == obj)
			return true;
		if (obj == null)
			return false;
		if (getClass() != obj.getClass())
			return false;
		KixmppJid other = (KixmppJid) obj;
		if (domain == null) {
			if (other.domain != null)
				return false;
		} else if (!domain.equals(other.domain))
			return false;
		if (node == null) {
			if (other.node != null)
				return false;
		} else if (!node.equals(other.node))
			return false;
		if (resource == null) {
			if (other.resource != null)
				return false;
		} else if (!resource.equals(other.resource))
			return false;
		return true;
	}
} public class KixmppStanzaRejectedException extends KixmppException {
	private static final long serialVersionUID = -2495670401789500578L;

	/**
	 * A rejection exception with a reason.
	 * 
	 * @param reason
	 */
	public KixmppStanzaRejectedException(String reason) {
		super(reason);
	}
	
	/**
	 * A rejection exception with a reason and a cause.
	 * 
	 * @param reason
	 * @param cause
	 */
	public KixmppStanzaRejectedException(String reason, Throwable cause) {
		super(reason, cause);
	}
}
 public class KixmppStreamEnd {

}
 public class KixmppStreamStart {
	private final boolean includeXmlHeader;
	private final KixmppJid from;
	private final KixmppJid to;
	private final String id;
	
	/**
	 * @param includeXmlHeader
	 * @param from
	 * @param to
	 */
	public KixmppStreamStart(KixmppJid from, KixmppJid to, boolean includeXmlHeader, String id) {
		this.from = from;
		this.to = to;
		this.includeXmlHeader = includeXmlHeader;
		this.id = id;
	}
	
	/**
	 * @param includeXmlHeader
	 * @param from
	 * @param to
	 */
	public KixmppStreamStart(KixmppJid from, KixmppJid to, boolean includeXmlHeader) {
		this(from, to, includeXmlHeader, null);
	}

	/**
	 * Creates a stream start.
	 * 
	 * @param element
	 */
	public KixmppStreamStart(Element element, boolean includeXmlHeader) {
		this.includeXmlHeader = includeXmlHeader;
		
		if (element != null) {
			String from = element.getAttributeValue("from");
			String to = element.getAttributeValue("to");
			
			if (from != null) {
				this.from = KixmppJid.fromRawJid(from);
			} else {
				this.from = null;
			}
			
			if (to != null) {
				this.to = KixmppJid.fromRawJid(to);
			} else {
				this.to = null;
			}
			
			this.id = element.getAttributeValue("id");
		} else {
			this.from = null;
			this.to = null;
			this.id = null;
		}
	}

	/**
	 * @return the from
	 */
	public KixmppJid getFrom() {
		return from;
	}

	/**
	 * @return the to
	 */
	public KixmppJid getTo() {
		return to;
	}

	/**
	 * @return the includeXmlHeader
	 */
	public boolean doesIncludeXmlHeader() {
		return includeXmlHeader;
	}

	/**
	 * @return the id
	 */
	public String getId() {
		return id;
	}
}
 public class KixmppWebSocketCodec extends MessageToMessageCodec<Object, Object> {
	private static final Logger logger  = LoggerFactory.getLogger(KixmppWebSocketCodec.class);

	private XMLReaderSAX2Factory readerFactory = new XMLReaderSAX2Factory(false);

	@Override
	public boolean acceptInboundMessage(Object msg) throws Exception {
		return msg instanceof WebSocketFrame;
	}

	@Override
	public boolean acceptOutboundMessage(Object msg) throws Exception {
		return msg instanceof Element || msg instanceof KixmppStreamStart || 
				msg instanceof KixmppStreamEnd || msg instanceof String || 
				msg instanceof ByteBuf;
	}

	@Override
	protected void encode(ChannelHandlerContext ctx, Object msg, List<Object> out) throws Exception {
		WebSocketFrame frame = null;

		if (msg instanceof Element) {
			Element element = (Element)msg;

			if (element.getNamespace() == null || element.getNamespace() == Namespace.NO_NAMESPACE) {
				if ("stream".equals(element.getNamespacePrefix())) {
					element.setNamespace(Namespace.getNamespace("http://etherx.jabber.org/streams"));
				} else {
					element.setNamespace(Namespace.getNamespace("jabber:client"));

					IteratorIterable<Content> descendants = element.getDescendants();

					while (descendants.hasNext()) {
						Content content = descendants.next();

						if (content instanceof Element) {
							Element descendantElement = (Element)content;
							if (descendantElement.getNamespace() == null || descendantElement.getNamespace() == Namespace.NO_NAMESPACE) {
								descendantElement.setNamespace(element.getNamespace());
							}
						}
					}
				}
			}

			ByteBuf binaryData = ctx.alloc().buffer();
			new XMLOutputter().output((Element)msg, new ByteBufOutputStream(binaryData));

			frame = new TextWebSocketFrame(binaryData);
		} else if (msg instanceof KixmppStreamStart) {
			KixmppStreamStart streamStart = (KixmppStreamStart)msg;

			StringWriter writer = new StringWriter();

			if (streamStart.doesIncludeXmlHeader()) {
				writer.append("<?xml version='1.0' encoding='UTF-8'?>");
			}
			writer.append("<stream:stream ");
			if (streamStart.getId() != null) {
				writer.append(String.format("id=\"%s\" ", streamStart.getId()));
			}
			if (streamStart.getFrom() != null) {
				writer.append(String.format("from=\"%s\" ", streamStart.getFrom().getFullJid()));
			}
			if (streamStart.getTo() != null) {
				writer.append(String.format("to=\"%s\" ", streamStart.getTo()));
			}
			writer.append("version=\"1.0\" xmlns=\"jabber:client\" xmlns:stream=\"http://etherx.jabber.org/streams\">");

			frame = new TextWebSocketFrame(writer.toString());
		} else if (msg instanceof KixmppStreamEnd) {
			frame = new TextWebSocketFrame("</stream:stream>");
		} else if (msg instanceof String) {
			frame = new TextWebSocketFrame((String)msg);
		} else if (msg instanceof ByteBuf) {
			frame = new TextWebSocketFrame((ByteBuf)msg);
		}

		if (frame != null) {
			if (logger.isDebugEnabled()) {
				logger.debug("Sending: [{}]", frame.content().toString(StandardCharsets.UTF_8));
			}

			out.add(frame);
		}
	}

	@Override
	protected void decode(ChannelHandlerContext ctx, Object msg, List<Object> out) throws Exception {
		WebSocketFrame frame = (WebSocketFrame)msg;

		ByteBuf content = frame.retain().content();
		String frameString = content.toString(StandardCharsets.UTF_8);

		if (logger.isDebugEnabled()) {
			logger.debug("Received: [{}]", frameString);
		}

		if (frameString.startsWith("<?xml")) {
			frameString = frameString.replaceFirst("<\\?xml.*?\\?>", "");
		}

		if (frameString.startsWith("<stream:stream")) {
			out.add(new KixmppStreamStart(null, true));
		} else if (frameString.startsWith("</stream:stream")) {
			out.add(new KixmppStreamEnd());
		} else {

			SAXBuilder saxBuilder = new SAXBuilder(readerFactory);
			Document document = saxBuilder.build(new ByteBufInputStream(content));

			Element element = document.getRootElement();

			out.add(element);
		}

	}
} public interface KixmppClientModule extends KixmppModule<KixmppClient> {
	
} public class MessageKixmppClientModule implements KixmppClientModule {
	private static final Logger logger = LoggerFactory.getLogger(MessageKixmppClientModule.class);
	
	private Set<MessageListener> messageListeners = Collections.newSetFromMap(new ConcurrentHashMap<MessageListener, Boolean>());

	private KixmppClient client = null;
	
	/**
	 * @param listener the listener to add
	 */
	public void addMessageListener(MessageListener listener) {
		messageListeners.add(listener);
	}

	/**
	 * @param listener the listener to remove
	 */
	public void removeMessageListener(MessageListener listener) {
		messageListeners.remove(listener);
	}

	/**
	 * @see com.kixeye.kixmpp.client.module.KixmppClientModule#install(com.kixeye.kixmpp.client.KixmppClient)
	 */
	public void install(KixmppClient client) {
		this.client = client;

		client.getEventEngine().registerGlobalStanzaHandler("message", messageHandler);
	}

	/**
	 * @see com.kixeye.kixmpp.client.module.KixmppClientModule#uninstall(com.kixeye.kixmpp.client.KixmppClient)
	 */
	public void uninstall(KixmppClient client) {
		client.getEventEngine().unregisterGlobalStanzaHandler("message", messageHandler);
	}

	/**
	 * Sends a private message.
	 * 
	 * @param toJid
	 * @param body
	 */
    public void sendMessage(KixmppJid toJid, String body) {
    	Element messageElement = new Element("message");
    	messageElement.setAttribute("type", "chat");
    	messageElement.setAttribute("from", client.getJid().getFullJid());
    	messageElement.setAttribute("to", toJid.getBaseJid());
    	
    	Element bodyElement = new Element("body");
    	bodyElement.setText(body);
    	
    	messageElement.addContent(bodyElement);
    	
		client.sendStanza(messageElement);
    }

	private KixmppStanzaHandler messageHandler = new KixmppStanzaHandler() {
		public void handle(Channel channel, Element stanza) {
			if ("chat".equals(stanza.getAttributeValue("type"))) {
				Message message = new Message(KixmppJid.fromRawJid(stanza.getAttributeValue("from")), 
						KixmppJid.fromRawJid(stanza.getAttributeValue("to")), 
						stanza.getChildText("body", stanza.getNamespace()));
				
				for (MessageListener listener : messageListeners) {
					try {
						listener.handle(message);
					} catch (Exception e) {
						logger.error("Exception thrown while executing message listener", e);
					}
				}
			}
		}
	};
} public class ErrorKixmppClientModule implements KixmppClientModule {
	private static final Logger logger = LoggerFactory.getLogger(ErrorKixmppClientModule.class);
	
	private Set<ErrorListener> errorListeners = Collections.newSetFromMap(new ConcurrentHashMap<ErrorListener, Boolean>());

	/**
	 * @param listener the listener to add
	 */
	public void addErrorListener(ErrorListener listener) {
		errorListeners.add(listener);
	}

	/**
	 * @param listener the listener to remove
	 */
	public void removeErrorListener(ErrorListener listener) {
		errorListeners.remove(listener);
	}
	
	@Override
	public void install(KixmppClient client) {
		client.getEventEngine().registerGlobalStanzaHandler(errorHandler);
	}

	@Override
	public void uninstall(KixmppClient client) {
		client.getEventEngine().registerGlobalStanzaHandler(errorHandler);
	}

	private KixmppStanzaHandler errorHandler = new KixmppStanzaHandler() {
		public void handle(Channel channel, Element stanza) {
			if ("error".equals(stanza.getAttributeValue("type"))) {
				KixmppJid by = null;
				String type = null;
				Integer code = null;
				Element conditionElement = null;
				
				Element errorElement = stanza.getChild("error", stanza.getNamespace());
				
				if (errorElement != null) {
					String byString = errorElement.getAttributeValue("by");
					if (byString != null) {
						by = KixmppJid.fromRawJid(byString);
					}
					
					type = errorElement.getAttributeValue("type");
					
					String codeString = errorElement.getAttributeValue("code");
					if (codeString != null) {
						code = Integer.parseInt(codeString);
					}
					
					if (!errorElement.getChildren().isEmpty()) {
						conditionElement = errorElement.getChildren().get(0);
					}
				}
				
				Error error = new Error(stanza, by, type, code, conditionElement);
				
				for (ErrorListener listener : errorListeners) {
					try {
						listener.handle(error);
					} catch (Exception e) {
						logger.error("Exception thrown while executing error listener", e);
					}
				}
			}
		}
	};
} public class MucKixmppClientModule implements KixmppClientModule {
	private static final Logger logger = LoggerFactory.getLogger(MucKixmppClientModule.class);
	
	private Set<MucListener<MucMessage>> messageListeners = Collections.newSetFromMap(new ConcurrentHashMap<MucListener<MucMessage>, Boolean>());
	private Set<MucListener<MucInvite>> invitationListeners = Collections.newSetFromMap(new ConcurrentHashMap<MucListener<MucInvite>, Boolean>());
	private Set<MucListener<MucJoin>> joinListeners = Collections.newSetFromMap(new ConcurrentHashMap<MucListener<MucJoin>, Boolean>());
	
	private KixmppClient client = null;
	
	/**
	 * @param listener the listener to add
	 */
	public void addMessageListener(MucListener<MucMessage> listener) {
		messageListeners.add(listener);
	}

	/**
	 * @param listener the listener to add
	 */
	public void removeMessageListener(MucListener<MucMessage> listener) {
		messageListeners.remove(listener);
	}

	/**
	 * @param listener the listener to add
	 */
	public void addInviteListener(MucListener<MucInvite> listener) {
		invitationListeners.add(listener);
	}

	/**
	 * @param listener the listener to add
	 */
	public void removeInviteListener(MucListener<MucInvite> listener) {
		invitationListeners.remove(listener);
	}
	
	/**
	 * @param listener the listener to add
	 */
	public void addJoinListener(MucListener<MucJoin> listener) {
		joinListeners.add(listener);
	}
	
	/**
	 * @param listener the listener to add
	 */
	public void removeJoinListener(MucListener<MucJoin> listener) {
		joinListeners.remove(listener);
	}
	
	/**
	 * Joins a room.
	 * 
	 * @param roomJid
	 * @param nickname
	 */
	public void joinRoom(KixmppJid roomJid, String nickname, Integer maxStanzas, Integer maxChars, Integer seconds, DateTime since) {
		Element presence = new Element("presence");
		presence.setAttribute("from", client.getJid().getFullJid());
		presence.setAttribute("to", roomJid + "/" + nickname);

		Element x = new Element("x", "http://jabber.org/protocol/muc");
		presence.addContent(x);

		Element history = new Element("history", "http://jabber.org/protocol/muc");
		if (maxStanzas != null) {
			history.setAttribute("maxstanzas", maxStanzas.toString());
		}
		if (maxChars != null) {
			history.setAttribute("maxchars", maxChars.toString());
		}
		if (seconds != null) {
			history.setAttribute("seconds", seconds.toString());
		}
		if (since != null) {
			history.setAttribute("since", XmppDateUtils.format(since));
		}
		x.addContent(history);
		
		client.sendStanza(presence);
	}
	
	/**
	 * Joins a room.
	 * 
	 * @param roomJid
	 * @param nickname
	 */
	public void joinRoom(KixmppJid roomJid, String nickname) {
		joinRoom(roomJid, nickname, null, null, null, null);
	}
	
	/**
	 * Joins a room with an invitation.
	 * 
	 * @param invitation
	 * @param invitation
	 */
	public void joinRoom(MucInvite invitation, String nickname) {
		joinRoom(invitation.getRoomJid(), nickname);
	}
	
	/**
	 * Sends a room message to a room.
	 * 
	 * @param roomJid
	 * @param roomMessage
	 */
	public void sendRoomMessage(KixmppJid roomJid, String roomMessage, String nickname) {
		Element message = new Element("message");
		message.setAttribute("from", client.getJid().toString());
		message.setAttribute("to", roomJid.toString());
		message.setAttribute("type", "groupchat");
		
		Element bodyElement = new Element("body", message.getNamespace());
		bodyElement.setText(roomMessage);
		message.addContent(bodyElement);

		client.sendStanza(message);
	}
	
	/**
	 * @see com.kixeye.kixmpp.client.module.KixmppClientModule#install(com.kixeye.kixmpp.client.KixmppClient)
	 */
	public void install(KixmppClient client) {
		this.client = client;
		
		client.getEventEngine().registerGlobalStanzaHandler("message", mucMessageHandler);
		client.getEventEngine().registerGlobalStanzaHandler("presence", mucPresenceHandler);
	}

	/**
	 * @see com.kixeye.kixmpp.client.module.KixmppClientModule#uninstall(com.kixeye.kixmpp.client.KixmppClient)
	 */
	public void uninstall(KixmppClient client) {
		client.getEventEngine().unregisterGlobalStanzaHandler("message", mucMessageHandler);
		client.getEventEngine().unregisterGlobalStanzaHandler("presence", mucPresenceHandler);
	}
	
	private KixmppStanzaHandler mucPresenceHandler = new KixmppStanzaHandler() {
		@Override
		public void handle(Channel channel, Element stanza) {
			Element inX  = stanza.getChild("x", Namespace.getNamespace("http://jabber.org/protocol/muc#user"));
			
			if (inX != null) {
				Element inItem = inX.getChild("item", Namespace.getNamespace("http://jabber.org/protocol/muc#user"));
				
				if (inItem != null) {
					KixmppJid from = KixmppJid.fromRawJid(stanza.getAttributeValue("from"));
					
					MucJoin message = new MucJoin(from.withoutResource(), 
							from, 
							inItem.getAttributeValue("affiliation"), 
							inItem.getAttributeValue("role"));
					
					for (MucListener<MucJoin> listener : joinListeners) {
						try {
							listener.handle(message);
						} catch (Exception e) {
							logger.error("Exception thrown while executing MucJoin listener", e);
						}
					}
				}
			}
		}
	};
	
	private KixmppStanzaHandler mucMessageHandler = new KixmppStanzaHandler() {
		public void handle(Channel channel, Element stanza) {
			String type = stanza.getAttributeValue("type");
			
			if (type == null) {
				type = "";
			}
			
			switch (type) {
				case "chat":
					// ignore
					break;
				case "groupchat":
					String language = null;
					String bodyMessage = null;
					
					Element body = stanza.getChild("body", stanza.getNamespace());
					
					if (body != null) {
						bodyMessage = body.getText();
						language = body.getAttributeValue("xml:lang");

						MucMessage message = new MucMessage(KixmppJid.fromRawJid(stanza.getAttributeValue("from")), KixmppJid.fromRawJid(stanza.getAttributeValue("to")), language, bodyMessage);
						
						for (MucListener<MucMessage> listener : messageListeners) {
							try {
								listener.handle(message);
							} catch (Exception e) {
								logger.error("Exception thrown while executing MucInvite listener", e);
							}
						}
					}
					
					break;
				case "":
					// check if invite
					for (Element invitation : stanza.getChildren("x", Namespace.getNamespace("jabber:x:conference"))) {
						KixmppJid roomJid = KixmppJid.fromRawJid(invitation.getAttributeValue("jid"));
                        KixmppJid toJid = KixmppJid.fromRawJid(stanza.getAttributeValue("to"));
                        KixmppJid fromJid = KixmppJid.fromRawJid(stanza.getAttributeValue("from"));
						MucInvite invite = new MucInvite(fromJid, toJid, roomJid);
						for (MucListener<MucInvite> listener : invitationListeners) {
							try {
								listener.handle(invite);
							} catch (Exception e) {
								logger.error("Exception thrown while executing MucInvite listener", e);
							}
						}
					}
                    for (Element x : stanza.getChildren("x",  Namespace.getNamespace("http://jabber.org/protocol/muc#user"))) {
                        for (Element i : x.getChildren("invite")) {
                            KixmppJid roomJid = KixmppJid.fromRawJid( stanza.getAttributeValue("from"));
                            KixmppJid toJid =  KixmppJid.fromRawJid( stanza.getAttributeValue("to"));
                            KixmppJid fromJid = KixmppJid.fromRawJid( i.getAttributeValue("from"));
                            MucInvite invite = new MucInvite(fromJid,toJid,roomJid);
                            for (MucListener<MucInvite> listener : invitationListeners) {
                                try {
                                    listener.handle(invite);
                                } catch (Exception e) {
                                    logger.error("Exception thrown while executing MucInvite listener", e);
                                }
                            }
                        }
                    }
					break;
			}
		}
	};
} public class PresenceKixmppClientModule implements KixmppClientModule {
	private static final Logger logger = LoggerFactory.getLogger(PresenceKixmppClientModule.class);
	
	private Set<PresenceListener> presenceListeners = Collections.newSetFromMap(new ConcurrentHashMap<PresenceListener, Boolean>());
	
	private KixmppClient client = null;
	
	/**
	 * @param listener the listener to add
	 */
	public void addPresenceListener(PresenceListener listener) {
		presenceListeners.add(listener);
	}

	/**
	 * @param listener the listener to remove
	 */
	public void removePresenceListener(PresenceListener listener) {
		presenceListeners.remove(listener);
	}

	/**
	 * @see com.kixeye.kixmpp.client.module.KixmppClientModule#install(com.kixeye.kixmpp.client.KixmppClient)
	 */
	public void install(KixmppClient client) {
		this.client = client;

		client.getEventEngine().registerGlobalStanzaHandler("presence", presenceHandler);
	}

	/**
	 * @see com.kixeye.kixmpp.client.module.KixmppClientModule#uninstall(com.kixeye.kixmpp.client.KixmppClient)
	 */
	public void uninstall(KixmppClient client) {
		client.getEventEngine().unregisterGlobalStanzaHandler("presence", presenceHandler);
	}

	/**
	 * Updates the current user's presence.
	 *     
	 * @param presence
	 */
    public void updatePresence(Presence presence) {
    	Element presenceElement = new Element("presence");
    	
    	if (presence.getType() != null) {
    		presenceElement.setAttribute("type", presence.getType());
    	}
    	
    	if (presence.getStatus() != null) {
    		Element statusElement = new Element("status");
    		statusElement.setText(presence.getStatus());
    		
    		presenceElement.addContent(statusElement);
    	}
    	
    	if (presence.getShow() != null) {
    		Element showElement = new Element("show");
    		showElement.setText(presence.getShow());
    		
    		presenceElement.addContent(showElement);
    	}
		
		client.sendStanza(presenceElement);
    }

	private KixmppStanzaHandler presenceHandler = new KixmppStanzaHandler() {
		public void handle(Channel channel, Element stanza) {
			Presence presence = new Presence(KixmppJid.fromRawJid(stanza.getAttributeValue("from")), 
					KixmppJid.fromRawJid(stanza.getAttributeValue("to")), 
					stanza.getAttributeValue("type"), 
					stanza.getChildText("status", stanza.getNamespace()), 
					stanza.getChildText("show", stanza.getNamespace()));
			
			for (PresenceListener listener : presenceListeners) {
				try {
					listener.handle(presence);
				} catch (Exception e) {
					logger.error("Exception thrown while executing Presence listener", e);
				}
			}
		}
	};
} public class KixmppEventEngine {
	private static final String HANDLER_WILDCARD = "*";
	
	private final ConcurrentHashMap<Tuple, Set<KixmppStanzaHandler>> stanzaHandlers = new ConcurrentHashMap<>();
	private final Set<KixmppConnectionHandler> connectionHandlers = Collections.newSetFromMap(new ConcurrentHashMap<KixmppConnectionHandler, Boolean>());
	private final Set<KixmppStreamHandler> streamHandlers = Collections.newSetFromMap(new ConcurrentHashMap<KixmppStreamHandler, Boolean>());
	private final Set<KixmppSessionHandler> sessionHandlers = Collections.newSetFromMap(new ConcurrentHashMap<KixmppSessionHandler, Boolean>());

	private final LoadingCache<String, DispatchQueue> queues = CacheBuilder.newBuilder()
			.expireAfterAccess(30, TimeUnit.SECONDS)
			.build(new CacheLoader<String, DispatchQueue>() {
				public DispatchQueue load(String key) throws Exception {
					return Dispatch.createQueue(key);
				}
			});
	
	/**
	 * Publishes a stanza.
	 * 
	 * @param channel
	 * @param stanza
	 */
	public void publishStanza(Channel channel, Element stanza) {
		String to = stanza.getAttributeValue("to");
		
		DispatchQueue queue;
		
		try {
			if (to != null) {
				queue = queues.get("address:" + to);
			} else {
				queue = queues.get("channel:" + channel.hashCode());
			}
		} catch (ExecutionException e) {
			throw new RuntimeException(e);
		}
		
		if (to != null) {
			Set<KixmppStanzaHandler> recipientHandlers = stanzaHandlers.get(Tuple.from(stanza.getQualifiedName(), KixmppJid.fromRawJid(to)));
			
			if (recipientHandlers != null) {
				for (KixmppStanzaHandler handler : recipientHandlers) {
					queue.execute(new ExecuteStanzaHandler(handler, channel, stanza));
				}
			}
			
			recipientHandlers = stanzaHandlers.get(Tuple.from(HANDLER_WILDCARD, KixmppJid.fromRawJid(to)));
			
			if (recipientHandlers != null) {
				for (KixmppStanzaHandler handler : recipientHandlers) {
					queue.execute(new ExecuteStanzaHandler(handler, channel, stanza));
				}
			}
		}
		
		Set<KixmppStanzaHandler> globalHandlers = stanzaHandlers.get(Tuple.from(stanza.getQualifiedName()));
		
		if (globalHandlers != null) {
			for (KixmppStanzaHandler handler : globalHandlers) {
				queue.execute(new ExecuteStanzaHandler(handler, channel, stanza));
			}
		}
		
		globalHandlers = stanzaHandlers.get(Tuple.from(HANDLER_WILDCARD));
		
		if (globalHandlers != null) {
			for (KixmppStanzaHandler handler : globalHandlers) {
				queue.execute(new ExecuteStanzaHandler(handler, channel, stanza));
			}
		}
	}
	
	/**
	 * Published an arbitrary task for serial execution.
	 * 
	 * @param jid
	 * @param task
	 */
	public void publishTask(KixmppJid jid, Task task) {
		DispatchQueue queue;
		
		try {
            if (jid != null) {
                queue = queues.get("address:" + jid.getFullJid());
            } else {
                queue = queues.get("default");
            }
		} catch (ExecutionException e) {
			throw new RuntimeException(e);
		}
		
		queue.execute(task);
	}
	
	/**
	 * Published an arbitrary task for serial execution.
	 * 
	 * @param channel
	 * @param task
	 */
	public void publishTask(Channel channel, Task task) {
		DispatchQueue queue;
		
		try {
			queue = queues.get("channel:" + channel.hashCode());
		} catch (ExecutionException e) {
			throw new RuntimeException(e);
		}

		queue.execute(task);
	}
	
	/**
	 * Publishes that a channel has connected.
	 * 
	 * @param channel
	 */
	public void publishConnected(Channel channel) {
		DispatchQueue queue;
		
		try {
			queue = queues.get("channel:" + channel.hashCode());
		} catch (ExecutionException e) {
			throw new RuntimeException(e);
		}
		
		for (KixmppConnectionHandler handler : connectionHandlers) {
			queue.execute(new ExecuteConnectionConnectedHandler(handler, channel));
		}
	}
	
	/**
	 * Publishes that a channel has disconnected.
	 * 
	 * @param channel
	 */
	public void publishDisconnected(Channel channel) {
		DispatchQueue queue;
		
		try {
			queue = queues.get("channel:" + channel.hashCode());
		} catch (ExecutionException e) {
			throw new RuntimeException(e);
		}
		
		for (KixmppConnectionHandler handler : connectionHandlers) {
			queue.execute(new ExecuteConnectionDisconnectedHandler(handler, channel));
		}
	}
	
	/**
	 * Publishes a stream start event.
	 * 
	 * @param channel
	 * @param streamStart
	 */
	public void publishStreamStart(Channel channel, KixmppStreamStart streamStart) {
		DispatchQueue queue;
		
		try {
			queue = queues.get("channel:" + channel.hashCode());
		} catch (ExecutionException e) {
			throw new RuntimeException(e);
		}
		
		for (KixmppStreamHandler handler : streamHandlers) {
			queue.execute(new ExecuteStreamStartHandler(handler, channel, streamStart));
		}
	}
	
	/**
	 * Publishes a stream end event.
	 * 
	 * @param channel
	 * @param streamEnd
	 */
	public void publishStreamEnd(Channel channel, KixmppStreamEnd streamEnd) {
		DispatchQueue queue;
		
		try {
			queue = queues.get("channel:" + channel.hashCode());
		} catch (ExecutionException e) {
			throw new RuntimeException(e);
		}
		
		for (KixmppStreamHandler handler : streamHandlers) {
			queue.execute(new ExecuteStreamEndHandler(handler, channel, streamEnd));
		}
	}

	/**
	 * Publishes a session start event.
	 *
	 * @param channel
	 */
	public void publishSessionStart(Channel channel) {
		DispatchQueue queue;

		try {
			queue = queues.get("channel:" + channel.hashCode());
		} catch (ExecutionException e) {
			throw new RuntimeException(e);
		}

		for (KixmppSessionHandler handler : sessionHandlers) {
			queue.execute(new ExecuteSessionStartHandler(handler, channel));
		}
	}

	/**
	 * Registers a handler to listen to connection events.
	 * 
	 * @param handler
	 */
	public void registerConnectionHandler(KixmppConnectionHandler handler) {
		connectionHandlers.add(handler);
	}

	/**
	 * Unregisters a connection handler.
	 * 
	 * @param handler
	 */
	public void unregisterConnectionHandler(KixmppConnectionHandler handler) {
		connectionHandlers.remove(handler);
	}

	/**
	 * Registers a stream handler.
	 *
	 * @param handler
	 */
	public void registerStreamHandler(KixmppStreamHandler handler) {
		streamHandlers.add(handler);
	}

	/**
	 * Unregisters a stream handler.
	 *
	 * @param handler
	 */
	public void unregisterStreamHandler(KixmppStreamHandler handler) {
		streamHandlers.remove(handler);
	}

	/**
	 * Registers a session handler.
	 *
	 * @param handler
	 */
	public void registerSessionHandler(KixmppSessionHandler handler) {
		sessionHandlers.add(handler);
	}

	/**
	 * Unregisters a session handler.
	 *
	 * @param handler
	 */
	public void unregisterSessionHandler(KixmppSessionHandler handler) {
		sessionHandlers.remove(handler);
	}

	/**
	 * Registers a stanza handler.
	 * 
	 * @param qualifiedName
	 * @param jid
	 * @param handler
	 */
	public void registerStanzaHandler(KixmppJid jid, String qualifiedName, KixmppStanzaHandler handler) {
		Tuple key = Tuple.from(qualifiedName, jid);
		
		Set<KixmppStanzaHandler> handlers = stanzaHandlers.get(key);
		
		if (handlers == null) {
			Set<KixmppStanzaHandler> newHandlers = Collections.newSetFromMap(new ConcurrentHashMap<KixmppStanzaHandler, Boolean>());
			
			handlers = stanzaHandlers.putIfAbsent(key, newHandlers);
			
			if (handlers == null) {
				handlers = newHandlers;
			}
        }
		
		handlers.add(handler);
	}
	
	/**
	 * Registers a stanza handler.
	 * 
	 * @param jid
	 * @param handler
	 */
	public void registerStanzaHandler(KixmppJid jid, KixmppStanzaHandler handler) {
		registerStanzaHandler(jid, HANDLER_WILDCARD, handler);
	}
	
	/**
	 * Registers a stanza handler.
	 * 
	 * @param handler
	 */
	public void registerGlobalStanzaHandler(String qualifiedName, KixmppStanzaHandler handler) {
		Tuple key = Tuple.from(qualifiedName);
		
		Set<KixmppStanzaHandler> handlers = stanzaHandlers.get(key);
		
		if (handlers == null) {
			Set<KixmppStanzaHandler> newHandlers = Collections.newSetFromMap(new ConcurrentHashMap<KixmppStanzaHandler, Boolean>());
			
			handlers = stanzaHandlers.putIfAbsent(key, newHandlers);
			
			if (handlers == null) {
				handlers = newHandlers;
			}
        }
		
		handlers.add(handler);
	}
	
	/**
	 * Registers a stanza handler.
	 * 
	 * @param handler
	 */
	public void registerGlobalStanzaHandler(KixmppStanzaHandler handler) {
		registerGlobalStanzaHandler(HANDLER_WILDCARD, handler);
	}

	/**
	 * Unregisters a stanza handler.
	 * 
	 * @param jid
	 * @param qualifiedName
	 * @param handler
	 */
	public void unregisterStanzaHandler(KixmppJid jid, String qualifiedName, KixmppStanzaHandler handler) {
		Set<KixmppStanzaHandler> handlers = stanzaHandlers.get(Tuple.from(qualifiedName, jid));
		
		if (handlers != null) {
			handlers.remove(handler);
		}
	}
	
	/**
	 * Unregisters a stanza handler.
	 * 
	 * @param jid
	 * @param handler
	 */
	public void unregisterStanzaHandler(KixmppJid jid, KixmppStanzaHandler handler) {
		unregisterStanzaHandler(jid, HANDLER_WILDCARD, handler);
	}
	
	/**
	 * Unregisters a stanza handler.
	 * 
	 * @param qualifiedName
	 * @param handler
	 */
	public void unregisterGlobalStanzaHandler(String qualifiedName, KixmppStanzaHandler handler) {
		Set<KixmppStanzaHandler> handlers = stanzaHandlers.get(Tuple.from(qualifiedName));
		
		if (handlers != null) {
			handlers.remove(handler);
		}
	}
	
	/**
	 * Unregisters a stanza handler.
	 * 
	 * @param handler
	 */
	public void unregisterGlobalStanzaHandler(KixmppStanzaHandler handler) {
		unregisterGlobalStanzaHandler(HANDLER_WILDCARD, handler);
	}
	
	/**
	 * Unregisters all the handlers.
	 */
	public void unregisterAll() {
		stanzaHandlers.clear();
		connectionHandlers.clear();
		streamHandlers.clear();
		sessionHandlers.clear();
	}
	
	private static class ExecuteStanzaHandler extends Task {
		private final KixmppStanzaHandler handler;
		private final Channel channel;
		private final Element stanza;
		
		public ExecuteStanzaHandler(KixmppStanzaHandler handler, Channel channel, Element stanza) {
			this.handler = handler;
			this.channel = channel;
			this.stanza = stanza;
		}

		public void run() {
			handler.handle(channel, stanza);
		}
	}
	
	private static class ExecuteConnectionConnectedHandler extends Task {
		private final KixmppConnectionHandler handler;
		private final Channel channel;
		
		public ExecuteConnectionConnectedHandler(KixmppConnectionHandler handler, Channel channel) {
			this.handler = handler;
			this.channel = channel;
		}

		public void run() {
			handler.handleConnected(channel);
		}
	}
	
	private static class ExecuteConnectionDisconnectedHandler extends Task {
		private final KixmppConnectionHandler handler;
		private final Channel channel;
		
		public ExecuteConnectionDisconnectedHandler(KixmppConnectionHandler handler, Channel channel) {
			this.handler = handler;
			this.channel = channel;
		}

		public void run() {
			handler.handleDisconnected(channel);
		}
	}
	
	private static class ExecuteStreamStartHandler extends Task {
		private final KixmppStreamHandler handler;
		private final Channel channel;
		private final KixmppStreamStart start;
		
		public ExecuteStreamStartHandler(KixmppStreamHandler handler, Channel channel, KixmppStreamStart start) {
			this.handler = handler;
			this.channel = channel;
			this.start = start;
		}

		public void run() {
			handler.handleStreamStart(channel, start);
		}
	}
	
	private static class ExecuteStreamEndHandler extends Task {
		private final KixmppStreamHandler handler;
		private final Channel channel;
		private final KixmppStreamEnd end;
		
		public ExecuteStreamEndHandler(KixmppStreamHandler handler, Channel channel, KixmppStreamEnd end) {
			this.handler = handler;
			this.channel = channel;
			this.end = end;
		}

		public void run() {
			handler.handleStreamEnd(channel, end);
		}
	}

	private static class ExecuteSessionStartHandler extends Task {
		private final KixmppSessionHandler handler;
		private final Channel channel;

		public ExecuteSessionStartHandler(KixmppSessionHandler handler, Channel channel) {
			this.handler = handler;
			this.channel = channel;
		}

		public void run() {
			handler.handleSessionStart(channel);
		}
	}
} public interface KixmppStanzaHandler {
	/**
	 * Handles a stanza.
	 * 
	 * @param channel
	 * @param stanza
	 */
	public void handle(Channel channel, Element stanza);
} public interface KixmppStanzaInterceptor {
	/**
	 * Intercepts an incoming stanza.
	 * 
	 * @param channel
	 * @param stanza
	 */
	public void interceptIncoming(Channel channel, Element stanza) throws KixmppStanzaRejectedException;

	/**
	 * Intercepts an outgoing stanza.
	 * 
	 * @param channel
	 * @param stanza
	 */
	public void interceptOutgoing(Channel channel, Element stanza) throws KixmppStanzaRejectedException;
}
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;
     * @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"));