com.sonicle.webtop.core.xmpp.XMPPClient.java Source code

Java tutorial

Introduction

Here is the source code for com.sonicle.webtop.core.xmpp.XMPPClient.java

Source

/*
 * WebTop Services is a Web Application framework developed by Sonicle S.r.l.
 * Copyright (C) 2014 Sonicle S.r.l.
 *
 * This program is free software; you can redistribute it and/or modify it under
 * the terms of the GNU Affero General Public License version 3 as published by
 * the Free Software Foundation with the addition of the following permission
 * added to Section 15 as permitted in Section 7(a): FOR ANY PART OF THE COVERED
 * WORK IN WHICH THE COPYRIGHT IS OWNED BY SONICLE, SONICLE DISCLAIMS THE
 * WARRANTY OF NON INFRINGEMENT OF THIRD PARTY RIGHTS.
 *
 * This program is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
 * details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program; if not, see http://www.gnu.org/licenses or write to
 * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
 * MA 02110-1301 USA.
 *
 * You can contact Sonicle S.r.l. at email address sonicle@sonicle.com
 *
 * The interactive user interfaces in modified source and object code versions
 * of this program must display Appropriate Legal Notices, as required under
 * Section 5 of the GNU Affero General Public License version 3.
 *
 * In accordance with Section 7(b) of the GNU Affero General Public License
 * version 3, these Appropriate Legal Notices must retain the display of the
 * Sonicle logo and Sonicle copyright notice. If the display of the logo is not
 * reasonably feasible for technical reasons, the Appropriate Legal Notices must
 * display the words "Copyright (C) 2014 Sonicle S.r.l.".
 */
package com.sonicle.webtop.core.xmpp;

import com.sonicle.commons.EnumUtils;
import com.sonicle.commons.IdentifierUtils;
import com.sonicle.webtop.core.xmpp.packet.OutOfBandData;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicBoolean;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.lang3.StringUtils;
import org.jivesoftware.smack.AbstractXMPPConnection;
import org.jivesoftware.smack.MessageListener;
import org.jivesoftware.smack.SmackConfiguration;
import org.jivesoftware.smack.SmackException;
import org.jivesoftware.smack.XMPPConnection;
import org.jivesoftware.smack.XMPPException;
import org.jivesoftware.smack.chat2.Chat;
import org.jivesoftware.smack.chat2.ChatManager;
import org.jivesoftware.smack.chat2.IncomingChatMessageListener;
import org.jivesoftware.smack.packet.Message;
import org.jivesoftware.smack.packet.Presence;
import org.jivesoftware.smack.packet.XMPPError;
import org.jivesoftware.smack.provider.ProviderManager;
import org.jivesoftware.smack.roster.Roster;
import org.jivesoftware.smack.roster.RosterEntry;
import org.jivesoftware.smack.roster.RosterListener;
import org.jivesoftware.smack.tcp.XMPPTCPConnection;
import org.jivesoftware.smack.tcp.XMPPTCPConnectionConfiguration;
import org.jivesoftware.smackx.delay.packet.DelayInformation;
import org.jivesoftware.smackx.iqlast.LastActivityManager;
import org.jivesoftware.smackx.iqlast.packet.LastActivity;
import org.jivesoftware.smackx.muc.Affiliate;
import org.jivesoftware.smackx.muc.DiscussionHistory;
import org.jivesoftware.smackx.muc.InvitationListener;
import org.jivesoftware.smackx.muc.MultiUserChat;
import org.jivesoftware.smackx.muc.MultiUserChatManager;
import org.jivesoftware.smackx.muc.Occupant;
import org.jivesoftware.smackx.muc.ParticipantStatusListener;
import org.jivesoftware.smackx.muc.RoomInfo;
import org.jivesoftware.smackx.muc.SubjectUpdatedListener;
import org.jivesoftware.smackx.muc.UserStatusListener;
import org.jivesoftware.smackx.muc.packet.MUCUser;
import org.jivesoftware.smackx.xdata.Form;
import org.jivesoftware.smackx.xdata.FormField;
import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
import org.joda.time.Seconds;
import org.jxmpp.jid.BareJid;
import org.jxmpp.jid.EntityBareJid;
import org.jxmpp.jid.EntityFullJid;
import org.jxmpp.jid.EntityJid;
import org.jxmpp.jid.Jid;
import org.jxmpp.jid.impl.JidCreate;
import org.jxmpp.jid.parts.Resourcepart;
import org.jxmpp.jid.util.JidUtil;
import org.jxmpp.stringprep.XmppStringprepException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 *
 * https://github.com/igniterealtime/jxmpp/tree/master/jxmpp-jid/src/main/java/org/jxmpp/jid
 * 
 * EntityBareJid -> user@xmpp.org
 * EntityFullJid -> user@xmpp.org/resource
 * DomainBareJid -> xmpp.org
 * DomainFullJid -> xmpp.org/resource
 * FullJid -> localpart@domain.part/resourcepart
 * 
 * @author malbinola
 */
public class XMPPClient {
    final static Logger logger = (Logger) LoggerFactory.getLogger(XMPPClient.class);
    public static final String CHAT_DOMAIN_PREFIX = "instant";

    public static final String MUC_ROOMCONFIG_ROOMNAME = "muc#roomconfig_roomname";
    public static final String MUC_ROOMCONFIG_ROOMDESC = "muc#roomconfig_roomdesc";
    public static final String MUC_ROOMCONFIG_ENABLELOGGING = "muc#roomconfig_enablelogging";
    public static final String MUC_ROOMCONFIG_CHANGESUBJECT = "muc#roomconfig_changesubject";
    public static final String MUC_ROOMCONFIG_ALLOWINVITES = "muc#roomconfig_allowinvites";
    public static final String MUC_ROOMCONFIG_ALLOWPM = "muc#roomconfig_allowpm";
    public static final String MUC_ROOMCONFIG_MAXUSERS = "muc#roomconfig_maxusers";
    public static final String MUC_ROOMCONFIG_PRESENCEBROADCAST = "muc#roomconfig_presencebroadcast";
    public static final String MUC_ROOMCONFIG_GETMEMBERLIST = "muc#roomconfig_getmemberlist";
    public static final String MUC_ROOMCONFIG_PUBLICROOM = "muc#roomconfig_publicroom";
    public static final String MUC_ROOMCONFIG_PERSISTENTROOM = "muc#roomconfig_persistentroom";
    public static final String MUC_ROOMCONFIG_MODERATEDROOM = "muc#roomconfig_moderatedroom";
    public static final String MUC_ROOMCONFIG_MEMBERSONLY = "muc#roomconfig_membersonly";
    public static final String MUC_ROOMCONFIG_PASSWORDPROTECTEDROOM = "muc#roomconfig_passwordprotectedroom";
    public static final String MUC_ROOMCONFIG_ROOMSECRET = "muc#roomconfig_roomsecret";
    public static final String MUC_ROOMCONFIG_WHOIS = "muc#roomconfig_whois";
    public static final String MUC_ROOMCONFIG_MAXHISTORYFETCH = "muc#maxhistoryfetch";
    public static final String MUC_ROOMCONFIG_ROOMADMINS = "muc#roomconfig_roomadmins";
    public static final String MUC_ROOMCONFIG_ROOMOWNERS = "muc#roomconfig_roomowners";

    private XMPPTCPConnectionConfiguration config;
    private final String mucSubDomain;
    private final EntityFullJid userJid;
    private Resourcepart userNickname = null;
    private final AbstractXMPPConnection con;
    private final XMPPClientListener listener;
    private final ConversationHistory history;
    private final XmppsRosterListener rosterListener;
    private final XmppsICIncomingMessageListener icIncomingMessageListener;
    private final XmppsMUCInvitationListener mucInvitationListener;
    private final Map<EntityBareJid, IChat> instantChats = new HashMap<>();
    private final Map<EntityBareJid, GChat> groupChats = new HashMap<>();
    private int loginCount = 0;
    private final AtomicBoolean isDisconnecting = new AtomicBoolean(false);
    private Presence lastPresence = null;
    private ConcurrentHashMap<String, EntityBareJid> cacheInstantChatToFriend = new ConcurrentHashMap<>();

    static {
        ProviderManager.addExtensionProvider(OutOfBandData.ELEMENT_NAME, OutOfBandData.NAMESPACE,
                new OutOfBandData.Provider());
    }

    public XMPPClient(XMPPTCPConnectionConfiguration.Builder builder, String mucSubdomain, String nickname,
            XMPPClientListener listener) {
        this(builder, mucSubdomain, nickname, listener, null);
    }

    public XMPPClient(XMPPTCPConnectionConfiguration.Builder builder, String mucSubdomain, String nickname,
            XMPPClientListener listener, ConversationHistory history) {
        builder.setSendPresence(false);

        this.config = builder.build();
        this.mucSubDomain = mucSubdomain;
        this.userJid = JidCreate.entityFullFrom(XMPPHelper.asLocalpart(config.getUsername()),
                config.getXMPPServiceDomain(), config.getResource());
        this.userNickname = XMPPHelper.asResourcepart(nickname);
        this.con = new XMPPTCPConnection(config);
        this.listener = listener;
        this.history = history;
        this.rosterListener = new XmppsRosterListener();
        this.icIncomingMessageListener = new XmppsICIncomingMessageListener();
        this.mucInvitationListener = new XmppsMUCInvitationListener();
    }

    public boolean isConnected() {
        synchronized (con) {
            return con.isConnected();
        }
    }

    public boolean isAuthenticated() {
        synchronized (con) {
            return con.isAuthenticated();
        }
    }

    public void disconnect() throws XMPPClientException {
        synchronized (con) {
            if (!isConnected() && (loginCount == 0))
                return;
            checkConnection();
            try {
                isDisconnecting.set(true);
                con.sendStanza(createOfflinePresence());
                internalLogout();
                con.disconnect();
                isDisconnecting.set(false);
            } catch (SmackException | InterruptedException ex) {
                throw new XMPPClientException(ex);
            }
        }
    }

    public EntityFullJid getUserJid() {
        return this.userJid;
    }

    public String getUserNickame() {
        return this.userNickname.toString();
    }

    public void updatePresence(PresenceStatus presenceStatus, String statusText) throws XMPPClientException {
        checkAuthentication();

        Presence presence = new Presence(PresenceStatus.presenceType(presenceStatus));
        //presence.setPriority(24);
        Presence.Mode mode = PresenceStatus.presenceMode(presenceStatus);
        if (mode != null)
            presence.setMode(mode);
        if (statusText != null)
            presence.setStatus(statusText);

        try {
            internalSendPresence(presence);

        } catch (SmackException | InterruptedException ex) {
            throw new XMPPClientException(ex);
        }
    }

    public List<Friend> listFriends() throws XMPPClientException {
        checkAuthentication();

        try {
            final Roster roster = getRoster();

            checkRosterLoaded(roster);
            ArrayList<Friend> friends = new ArrayList<>();
            for (RosterEntry entry : roster.getEntries()) {
                final EntityBareJid friendJid = entry.getJid().asEntityBareJidOrThrow();
                final String instantChat = generateChatBareJid(friendJid).toString();
                cacheInstantChatToFriend.putIfAbsent(instantChat, friendJid);
                FriendPresence presence = getFriendPresence(friendJid);
                friends.add(new Friend(entry, presence, instantChat));
            }
            return friends;

        } catch (SmackException | InterruptedException ex) {
            throw new XMPPClientException(ex);
        }
    }

    public FriendPresence getFriendPresence(String friendBareJid) throws XMPPClientException {
        return getFriendPresence(XMPPHelper.asEntityBareJid(friendBareJid));
    }

    public FriendPresence getFriendPresence(EntityBareJid friend) throws XMPPClientException {
        checkAuthentication();

        Roster roster = Roster.getInstanceFor(con);
        Presence presence = roster.getPresence(friend.asBareJid());
        return (presence != null) ? new FriendPresence(presence, generateChatBareJid(friend).toString()) : null;
    }

    public String getFriendNickname(EntityBareJid friend) throws XMPPClientException {
        return getFriendNickname(friend, true);
    }

    public String getFriendNickname(EntityBareJid friend, boolean guessIfNull) throws XMPPClientException {
        checkAuthentication();

        Roster roster = Roster.getInstanceFor(con);
        return getRosterEntryNickname(roster, friend, guessIfNull);
    }

    public LastActivity getLastActivity(Jid friend) throws XMPPClientException {
        checkAuthentication();

        try {
            final LastActivityManager lastActivityManager = LastActivityManager.getInstanceFor(con);
            return lastActivityManager.getLastActivity(friend);

        } catch (SmackException | XMPPException | InterruptedException ex) {
            throw new XMPPClientException(ex);
        }
    }

    public EntityBareJid generateChatBareJid(String withUserBareJid) {
        return createInstantChatJid(withUserBareJid);
    }

    public EntityBareJid generateChatBareJid(EntityBareJid withUser) {
        return createInstantChatJid(withUser);
    }

    public List<ChatRoom> listChats() throws XMPPClientException {
        checkAuthentication();

        ArrayList<ChatRoom> chats = new ArrayList<>();
        synchronized (instantChats) {
            for (IChat chatObj : instantChats.values()) {
                chats.add(chatObj.getChatRoom());
            }
        }
        synchronized (groupChats) {
            for (GChat chatObj : groupChats.values()) {
                chats.add(chatObj.getChatRoom());
            }
        }
        return chats;
    }

    public EntityBareJid newInstantChat(EntityBareJid withFriend) throws XMPPClientException {
        checkAuthentication();

        try {
            final Roster roster = getRoster();
            checkRosterLoaded(roster);

            final EntityBareJid chatJid = createInstantChatJid(withFriend);
            final String withUserNick = getRosterEntryNickname(roster, withFriend, true);

            synchronized (instantChats) {
                IChat chatObj = instantChats.get(chatJid);
                if (chatObj == null) {
                    final ChatManager chatMgr = getChatManager();
                    final Chat chat = chatMgr.chatWith(withFriend);
                    chatObj = doAddChat(false, true, chatJid, userJid.asEntityBareJid(), withUserNick, null,
                            withFriend, chat);
                }
            }
            return chatJid;

        } catch (SmackException | InterruptedException ex) {
            throw new XMPPClientException(ex);
        }
    }

    public EntityBareJid newGroupChat(String name, List<EntityBareJid> withFriends) throws XMPPClientException {
        checkAuthentication();

        final EntityBareJid myJid = userJid.asEntityBareJid();
        final EntityBareJid chatJid = createGroupChatJid();

        synchronized (groupChats) {
            GChat chatObj = groupChats.get(chatJid);
            if (chatObj == null) {
                try {
                    final MultiUserChatManager muChatMgr = getMUChatManager();
                    final MultiUserChat muc = muChatMgr.getMultiUserChat(chatJid);

                    muc.create(XMPPHelper.asResourcepart(name));
                    Form form = muc.getConfigurationForm();
                    Form answerForm = form.createAnswerForm();
                    configureMucForm(answerForm, chatJid, name, myJid);
                    muc.sendConfigurationForm(answerForm);
                    muc.changeSubject(name);
                    muc.join(userNickname);
                    chatObj = doAddGroupChat(false, true, chatJid, myJid, name, null, muc);

                    for (EntityBareJid withFriend : withFriends) {
                        muc.invite(withFriend, name);
                    }

                } catch (SmackException | XMPPException | InterruptedException ex) {
                    throw new XMPPClientException(ex);
                }
            }
        }

        return chatJid;
    }

    public ChatRoom getChat(String chatJid) throws XMPPClientException {
        return getChat(XMPPHelper.asEntityBareJid(chatJid));
    }

    public ChatRoom getChat(EntityBareJid chatJid) throws XMPPClientException {
        checkAuthentication();

        if (isInstantChat(chatJid)) {
            IChat chatObj = instantChats.get(chatJid);
            return (chatObj != null) ? chatObj.getChatRoom() : null;
        } else {
            GChat chatObj = groupChats.get(chatJid);
            return (chatObj != null) ? chatObj.getChatRoom() : null;
        }
    }

    public void existChat(String chatJid) throws XMPPClientException {
        existChat(XMPPHelper.asEntityBareJid(chatJid));
    }

    public boolean existChat(EntityBareJid chatJid) throws XMPPClientException {
        checkAuthentication();

        if (isInstantChat(chatJid)) {
            return instantChats.containsKey(chatJid);
        } else {
            return groupChats.containsKey(chatJid);
        }
    }

    public void forgetChat(String chatJid) throws XMPPClientException {
        forgetChat(XMPPHelper.asEntityBareJid(chatJid));
    }

    public void forgetChat(EntityBareJid chatJid) throws XMPPClientException {
        checkAuthentication();

        if (isInstantChat(chatJid)) {
            synchronized (instantChats) {
                if (instantChats.containsKey(chatJid))
                    doRemoveChat(chatJid);
            }

        } else {
            try {
                synchronized (groupChats) {
                    if (groupChats.containsKey(chatJid))
                        doRemoveGroupChat(chatJid);
                }
            } catch (XMPPException | SmackException | InterruptedException ex) {
                throw new XMPPClientException(ex);
            }
        }
    }

    public List<ChatMember> getChatMembers(String chatJid) throws XMPPClientException {
        return getChatMembers(XMPPHelper.asEntityBareJid(chatJid));
    }

    public List<ChatMember> getChatMembers(EntityBareJid chatJid) throws XMPPClientException {
        ArrayList<ChatMember> members = new ArrayList<>();

        checkAuthentication();

        final EntityBareJid myJid = userJid.asEntityBareJid();

        if (isInstantChat(chatJid)) {
            synchronized (instantChats) {
                IChat chatObj = instantChats.get(chatJid);
                if (chatObj == null)
                    return null;

                try {
                    final Roster roster = getRoster();
                    checkRosterLoaded(roster);

                    final String withNickname = getRosterEntryNickname(roster, chatObj.getChatRoom().getWithJid(),
                            true);
                    if (chatObj.getChatRoom().isOwner(myJid)) {
                        members.add(new ChatMember(myJid, MemberRole.OWNER, userNickname.toString()));
                        members.add(new ChatMember(chatObj.getChatRoom().getWithJid(), MemberRole.PARTECIPANT,
                                withNickname));
                    } else {
                        members.add(
                                new ChatMember(chatObj.getChatRoom().getWithJid(), MemberRole.OWNER, withNickname));
                        members.add(new ChatMember(myJid, MemberRole.PARTECIPANT, userNickname.toString()));
                    }

                } catch (SmackException | InterruptedException ex) {
                    throw new XMPPClientException(ex);
                }
                return members;
            }
        } else {
            synchronized (groupChats) {
                GChat chatObj = groupChats.get(chatJid);
                if (chatObj == null)
                    return null;

                try {
                    final MultiUserChat muc = chatObj.getRawChat();
                    final Roster roster = getRoster();
                    checkRosterLoaded(roster);

                    for (Affiliate aff : muc.getOwners()) {
                        final EntityBareJid jid = aff.getJid().asEntityBareJidOrThrow();
                        final String nick = getEntryNickname(roster, jid, true);
                        members.add(new ChatMember(jid, MemberRole.OWNER, nick));
                    }
                    for (Affiliate aff : muc.getMembers()) {
                        final EntityBareJid jid = aff.getJid().asEntityBareJidOrThrow();
                        final String nick = getEntryNickname(roster, jid, true);
                        members.add(new ChatMember(jid, MemberRole.PARTECIPANT, nick));
                    }
                    return members;

                } catch (XMPPException | SmackException | InterruptedException ex) {
                    throw new XMPPClientException(ex);
                }
            }
        }
    }

    public FriendPresence getChatPresence(String chatJid) throws XMPPClientException {
        return getChatPresence(XMPPHelper.asEntityBareJid(chatJid));
    }

    public FriendPresence getChatPresence(EntityBareJid chatJid) throws XMPPClientException {
        checkAuthentication();

        if (isInstantChat(chatJid)) {
            synchronized (instantChats) {
                EntityBareJid presenceUser = null;
                IChat chatObj = instantChats.get(chatJid);
                if (chatObj == null) {
                    presenceUser = cacheInstantChatToFriend.get(chatJid.toString());
                    if (presenceUser == null)
                        return null;
                } else {
                    presenceUser = chatObj.getChatRoom().getWithJid();
                }
                return getFriendPresence(presenceUser);
            }
        } else {
            throw new UnsupportedOperationException("Feature not available for a groupChat");
        }
    }

    public ChatMessage sendTextMessage(String chatBareJid, String text) throws XMPPClientException {
        return sendTextMessage(XMPPHelper.asEntityBareJid(chatBareJid), text);
    }

    public ChatMessage sendTextMessage(EntityBareJid chatJid, String text) throws XMPPClientException {
        Message message = new Message();
        message.setBody(text);
        return internalSendMessage(chatJid, message);
    }

    public ChatMessage sendFileMessage(String chatBareJid, String name, String url, String mediaType, long size)
            throws XMPPClientException {
        return sendFileMessage(XMPPHelper.asEntityBareJid(chatBareJid), name, url, mediaType, size);
    }

    public ChatMessage sendFileMessage(EntityBareJid chatJid, String name, String url, String mediaType, long size)
            throws XMPPClientException {
        Message message = new Message();
        message.setBody(name);
        OutOfBandData oobData = new OutOfBandData(url, mediaType, size);
        message.addExtension(oobData);
        return internalSendMessage(chatJid, message);
    }

    private ChatMessage internalSendMessage(EntityBareJid chatJid, Message message) throws XMPPClientException {
        checkAuthentication();

        final EntityBareJid myJid = userJid.asEntityBareJid();
        final String myResource = XMPPHelper.asResourcepartString(userJid.getResourceOrNull());

        if (isInstantChat(chatJid)) {
            synchronized (instantChats) {
                IChat chatObj = instantChats.get(chatJid);
                if (chatObj == null) {
                    EntityBareJid withUser = cacheInstantChatToFriend.get(chatJid.toString());
                    if (withUser != null) {
                        newInstantChat(withUser);
                        chatObj = instantChats.get(chatJid);
                    }
                }
                if (chatObj != null) {
                    try {
                        final DateTime now = ChatMessage.nowTimestamp();
                        chatObj.getRawChat().send(message);
                        if (logger.isTraceEnabled()) {
                            logger.trace("Message sent [{}, {}]", now,
                                    StringUtils.abbreviate(message.getBody(), 20));
                        }
                        ChatMessage chatMessage = new ChatMessage(chatJid, myJid, myResource,
                                userNickname.toString(), now, now, message);

                        // TODO: threadify this?
                        try {
                            listener.onChatRoomMessageSent(chatObj.getChatRoom(), chatMessage);
                        } catch (Throwable t) {
                            logger.error("Listener error", t);
                        }

                        return chatMessage;

                    } catch (SmackException | InterruptedException ex) {
                        throw new XMPPClientException(ex);
                    }
                }
            }

        } else {
            synchronized (groupChats) {
                GChat chatObj = groupChats.get(chatJid);
                if (chatObj != null) {
                    try {
                        final DateTime now = ChatMessage.nowTimestamp();
                        chatObj.getRawChat().sendMessage(message);
                        if (logger.isTraceEnabled()) {
                            logger.trace("Message sent [{}, {}]", now,
                                    StringUtils.abbreviate(message.getBody(), 20));
                        }
                        ChatMessage chatMessage = new ChatMessage(chatJid, myJid, myResource,
                                userNickname.toString(), now, now, message);

                        // TODO: threadify this?
                        try {
                            listener.onChatRoomMessageSent(chatObj.getChatRoom(), chatMessage);
                        } catch (Throwable t) {
                            logger.error("Listener error", t);
                        }

                        return chatMessage;

                    } catch (SmackException | InterruptedException ex) {
                        throw new XMPPClientException(ex);
                    }
                }
            }
        }

        throw new XMPPClientException("Chat not found. Please create a chat before try to send messages in it!");
    }

    private IChat doAddChat(boolean skipListeners, boolean self, EntityBareJid chatJid, EntityBareJid ownerJid,
            String name, DateTime lastSeenActivity, EntityBareJid withJid, Chat chat) {
        return doAddChat(skipListeners, self,
                new InstantChatRoom(chatJid, ownerJid, name, lastSeenActivity, withJid), chat);
    }

    private IChat doAddChat(boolean skipListeners, boolean self, InstantChatRoom chatRoom, Chat chat) {
        logger.debug("Adding instant chat [{}]", chatRoom.getChatJid().toString());
        IChat chatObj = new IChat(chatRoom, chat);
        instantChats.put(chatRoom.getChatJid(), chatObj);

        if (!skipListeners) {
            try {
                listener.onChatRoomAdded(chatObj.getChatRoom(), getChatOwnerNick(chatRoom), self);
            } catch (Throwable t) {
                logger.error("Listener error", t);
            }
        }

        return chatObj;
    }

    private String getChatOwnerNick(ChatRoom chatRoom) throws XMPPClientException {
        if (XMPPHelper.jidEquals(chatRoom.getOwnerJid(), getUserJid())) {
            return getUserNickame();
        } else {
            return getFriendNickname(chatRoom.getOwnerJid(), true);
        }
    }

    private void doRemoveChat(EntityBareJid chatJid) {
        logger.debug("Removing instant chat [{}]", chatJid.toString());
        IChat chatObj = instantChats.remove(chatJid);

        try {
            final ChatRoom chatRoom = chatObj.getChatRoom();
            listener.onChatRoomRemoved(chatObj.getChatRoom().getChatJid(), chatRoom.getName(),
                    chatRoom.getOwnerJid(), getChatOwnerNick(chatRoom));
        } catch (Throwable t) {
            logger.error("Listener error", t);
        }
    }

    private GChat doAddGroupChat(boolean skipListeners, boolean self, EntityBareJid chatJid, EntityBareJid ownerJid,
            String name, DateTime lastSeenActivity, MultiUserChat chat) {
        return doAddGroupChat(skipListeners, self, new GroupChatRoom(chatJid, ownerJid, name, lastSeenActivity),
                chat);
    }

    private GChat doAddGroupChat(boolean skipListeners, boolean self, GroupChatRoom chatRoom, MultiUserChat chat) {
        logger.debug("Creating group chat [{}]", chatRoom.getChatJid().toString());
        GChat chatObj = new GChat(chatRoom);
        groupChats.put(chatRoom.getChatJid(), chatObj);

        if (!skipListeners) {
            try {
                listener.onChatRoomAdded(chatObj.getChatRoom(), getChatOwnerNick(chatRoom), self);
            } catch (Throwable t) {
                logger.error("Listener error", t);
            }
        }

        chat.addSubjectUpdatedListener(chatObj);
        chat.addMessageListener(chatObj);
        chat.addUserStatusListener(chatObj);
        chat.addParticipantStatusListener(chatObj);

        return chatObj;
    }

    private void doRemoveGroupChat(EntityBareJid chatJid)
            throws XMPPException, SmackException, InterruptedException {
        logger.debug("Removing group chat [{}]", chatJid.toString());
        GChat chatObj = groupChats.remove(chatJid);

        MultiUserChat chat = chatObj.getRawChat();
        chat.removeSubjectUpdatedListener(chatObj);
        chat.removeMessageListener(chatObj);
        chat.removeUserStatusListener(chatObj);
        chat.removeParticipantStatusListener(chatObj);
        if (XMPPHelper.jidEquals(chatObj.getChatRoom().getOwnerJid(), getUserJid().asEntityBareJid())) {
            chat.destroy("Cleanup", null);
        }

        try {
            final ChatRoom chatRoom = chatObj.getChatRoom();
            listener.onChatRoomRemoved(chatObj.getChatRoom().getChatJid(), chatRoom.getName(),
                    chatRoom.getOwnerJid(), getChatOwnerNick(chatRoom));
        } catch (Throwable t) {
            logger.error("Listener error", t);
        }
    }

    private Presence createOfflinePresence() {
        Presence presence = null;
        if (this.lastPresence != null) {
            presence = this.lastPresence.clone();
            presence.setType(Presence.Type.unavailable);
        } else {
            presence = new Presence(Presence.Type.unavailable);
        }
        presence.setMode(Presence.Mode.away);
        //presence.setPriority(24);
        return presence;
    }

    private void checkRosterLoaded(final Roster roster) throws SmackException, InterruptedException {
        if (!roster.isLoaded())
            roster.reloadAndWait();
    }

    private String chatDomain(String subname) {
        return subname + "." + userJid.getDomain().toString();
    }

    private EntityBareJid createInstantChatJid(EntityBareJid friend) {
        return (friend == null) ? null : createInstantChatJid(friend.toString());
    }

    private EntityBareJid createInstantChatJid(String friendBareJid) {
        return buildChatJid(DigestUtils.md5Hex(friendBareJid), chatDomain(CHAT_DOMAIN_PREFIX));
    }

    private EntityBareJid createGroupChatJid() {
        return buildChatJid(IdentifierUtils.getUUIDTimeBased(true), chatDomain(mucSubDomain));
    }

    private EntityBareJid buildChatJid(String local, String domain) {
        final String s = local + "@" + domain;
        try {
            return JidCreate.bareFrom(s).asEntityBareJidIfPossible();
        } catch (XmppStringprepException ex) {
            logger.error("Error building MUC jid [{}]", ex, s);
            return null;
        }
    }

    private EntityBareJid createOccupantJid(String nick) {
        return JidCreate.entityBareFrom(XMPPHelper.asLocalpart(nick), userJid.getDomain());
    }

    private Roster getRoster() {
        return Roster.getInstanceFor(con);
    }

    private ChatManager getChatManager() {
        return ChatManager.getInstanceFor(con);
    }

    private MultiUserChatManager getMUChatManager() {
        return MultiUserChatManager.getInstanceFor(con);
    }

    private String getEntryNickname(Roster roster, BareJid jid, boolean guessIfNull) {
        if (XMPPHelper.jidBareEquals(jid, userJid)) {
            return userNickname.toString();
        } else {
            return getRosterEntryNickname(roster, jid, guessIfNull);
        }
    }

    private String getRosterEntryNickname(Roster roster, BareJid jid, boolean guessIfNull) {
        RosterEntry entry = roster.getEntry(jid);
        String nick = (entry != null) ? entry.getName() : null;
        if (StringUtils.isBlank(nick)) {
            return guessIfNull ? XMPPHelper.buildGuessedString(jid.toString()) : null;
        } else {
            return nick;
        }
    }

    private void configureMucForm(Form answerForm, EntityBareJid chatJid, String chatName, EntityBareJid ownerJid) {
        // Apply defaults for all form fields
        for (FormField field : answerForm.getFields()) {
            if (field.getType() == FormField.Type.hidden || StringUtils.isBlank(field.getVariable())) {
                continue;
            }
            answerForm.setDefaultAnswer(field.getVariable());
        }

        // Natural-Language Room Name
        answerForm.setAnswer(MUC_ROOMCONFIG_ROOMNAME, chatName);
        // Short Description of Room
        answerForm.setAnswer(MUC_ROOMCONFIG_ROOMDESC, "");
        // Enable Public Logging?
        if (answerForm.hasField(MUC_ROOMCONFIG_ENABLELOGGING)) {
            answerForm.setAnswer(MUC_ROOMCONFIG_ENABLELOGGING, false);
        }
        // Allow Occupants to Change Subject?
        if (answerForm.hasField(MUC_ROOMCONFIG_CHANGESUBJECT)) {
            answerForm.setAnswer(MUC_ROOMCONFIG_CHANGESUBJECT, false);
        }
        // Allow Occupants to Invite Others?
        if (answerForm.hasField(MUC_ROOMCONFIG_ALLOWINVITES)) {
            answerForm.setAnswer(MUC_ROOMCONFIG_ALLOWINVITES, false);
        }
        // Who Can Send Private Messages? (anyone, participants, moderators, none)
        if (answerForm.hasField(MUC_ROOMCONFIG_ALLOWPM)) {
            answerForm.setAnswer(MUC_ROOMCONFIG_ALLOWPM, XMPPHelper.asFormListSingleType("none"));
        }
        // Maximum Number of Occupants (10, 20, 30, 50, 100, none)
        if (answerForm.hasField(MUC_ROOMCONFIG_MAXUSERS)) {
            answerForm.setAnswer(MUC_ROOMCONFIG_MAXUSERS, XMPPHelper.asFormListSingleType("50"));
        }
        // Roles for which Presence is Broadcasted (moderator, participant, visitor)
        /*
        if (answerForm.hasField(MUC_ROOMCONFIG_PRESENCEBROADCAST)) {
           answerForm.setAnswer(MUC_ROOMCONFIG_PRESENCEBROADCAST, XMPPHelper.asFormListMultiType("participant"));
        }
        */
        // Roles and Affiliations that May Retrieve Member List (moderator, participant, visitor)
        if (answerForm.hasField(MUC_ROOMCONFIG_GETMEMBERLIST)) {
            answerForm.setAnswer(MUC_ROOMCONFIG_GETMEMBERLIST, XMPPHelper.asFormListMultiType("participant"));
        }
        // Make Room Publicly Searchable?
        if (answerForm.hasField(MUC_ROOMCONFIG_PUBLICROOM)) {
            answerForm.setAnswer(MUC_ROOMCONFIG_PUBLICROOM, false);
        }
        // Make Room Persistent?
        if (answerForm.hasField(MUC_ROOMCONFIG_PERSISTENTROOM)) {
            answerForm.setAnswer(MUC_ROOMCONFIG_PERSISTENTROOM, true);
        }
        // Make Room Moderated?
        if (answerForm.hasField(MUC_ROOMCONFIG_MODERATEDROOM)) {
            answerForm.setAnswer(MUC_ROOMCONFIG_MODERATEDROOM, false);
        }
        // Make Room Members Only?
        if (answerForm.hasField(MUC_ROOMCONFIG_MEMBERSONLY)) {
            answerForm.setAnswer(MUC_ROOMCONFIG_MEMBERSONLY, true);
        }
        // Password Required for Entry?
        if (answerForm.hasField(MUC_ROOMCONFIG_PASSWORDPROTECTEDROOM)) {
            answerForm.setAnswer(MUC_ROOMCONFIG_PASSWORDPROTECTEDROOM, false);
        }
        // Who May Discover Real JIDs? (moderators, anyone)
        if (answerForm.hasField(MUC_ROOMCONFIG_WHOIS)) {
            answerForm.setAnswer(MUC_ROOMCONFIG_WHOIS, XMPPHelper.asFormListSingleType("anyone"));
        }
        // Maximum Number of History Messages Returned by Room
        if (answerForm.hasField(MUC_ROOMCONFIG_MAXHISTORYFETCH)) {
            answerForm.setAnswer(MUC_ROOMCONFIG_MAXHISTORYFETCH, "50");
        }
        // Additional Room Admins
        // Room Owners
        if (answerForm.hasField(MUC_ROOMCONFIG_ROOMOWNERS)) {
            Set<EntityBareJid> owners = new HashSet<>();
            owners.add(ownerJid);
            answerForm.setAnswer(MUC_ROOMCONFIG_ROOMOWNERS, JidUtil.toStringList(owners));
        }

        if (logger.isTraceEnabled()) {
            logger.trace("Dumping MUC configuration...");
            for (FormField field : answerForm.getFields()) {
                logger.trace("{}: {}", field.getVariable(), field.getValues());
            }
        }
    }

    private boolean joinMuc(final MultiUserChat muc, final DateTime lastSeenActivity) {
        try {
            logger.debug("Joining group chat [{}, {}]", muc.getRoom().toString(), lastSeenActivity);
            DiscussionHistory mucHistory = new DiscussionHistory();
            if (lastSeenActivity != null) {
                DateTime now = new DateTime(DateTimeZone.UTC);
                Seconds seconds = Seconds.secondsBetween(lastSeenActivity.withZone(DateTimeZone.UTC), now);
                mucHistory.setSeconds(Math.abs(seconds.getSeconds()));
            }
            muc.join(userNickname, null, mucHistory, SmackConfiguration.getDefaultReplyTimeout());
            return true;

        } catch (SmackException | XMPPException | InterruptedException ex) {
            logger.error("Error joining group chat", ex);
            return false;
        }
    }

    private GuessMucFromResult guessMucFrom(Roster roster, MultiUserChat muc, EntityFullJid rawFrom) {
        EntityFullJid fromJid = null;
        String fromNick = null;

        Occupant occupant = muc.getOccupant(rawFrom);
        if (occupant != null) {
            fromJid = occupant.getJid().asEntityFullJidIfPossible();
            fromNick = getRosterEntryNickname(roster, occupant.getJid().asBareJid(), false);
            if (StringUtils.isBlank(fromNick)) {
                fromNick = XMPPHelper.buildGuessedString(occupant.getJid().asBareJid());
            }

        } else {
            // Due to occupant is not available, we assume that nick in message
            // sender is reconducible to a domain user...
            final EntityBareJid occupantBareJid = createOccupantJid(XMPPHelper.asResourcepartString(rawFrom));
            fromJid = JidCreate.entityFullFrom(occupantBareJid,
                    XMPPHelper.asResourcepart(rawFrom.asEntityBareJidString()));
            fromNick = getRosterEntryNickname(roster, occupantBareJid, false);
            if (StringUtils.isBlank(fromNick)) {
                fromNick = XMPPHelper
                        .buildGuessedString(XMPPHelper.asResourcepartString(rawFrom.getResourceOrNull()));
            }
        }
        return new GuessMucFromResult(fromJid, fromNick);
    }

    private DateTime retrieveDelayInformation(Message message) {
        DelayInformation dinfo = DelayInformation.from(message);
        if (dinfo != null) {
            return new DateTime(dinfo.getStamp(), DateTimeZone.UTC);
        } else {
            return null;
        }
    }

    private void internalLogin() throws SmackException, XMPPException, InterruptedException, IOException {
        final boolean restoreHistory = (loginCount == 0);
        loginCount++;

        final Roster roster = getRoster();
        roster.addRosterListener(rosterListener);
        final ChatManager chatMgr = getChatManager();
        chatMgr.addIncomingListener(icIncomingMessageListener);
        final MultiUserChatManager muChatMgr = getMUChatManager();
        muChatMgr.addInvitationListener(mucInvitationListener);

        // Restore instant (one-to-one) chat rooms from history
        if (restoreHistory && (history != null)) {
            for (ChatRoom chat : history.getChats()) {
                if (chat instanceof InstantChatRoom) {
                    final InstantChatRoom chatObj = (InstantChatRoom) chat;
                    doAddChat(true, true, chatObj, null);
                }
            }
        }

        con.login();
        checkRosterLoaded(roster);

        // Restore previous multi chat rooms from history
        if (restoreHistory && (history != null)) {
            for (ChatRoom chat : history.getChats()) {
                if (chat instanceof GroupChatRoom) {
                    final GroupChatRoom chatRoom = (GroupChatRoom) chat;

                    RoomInfo info = getRoomInfo(muChatMgr, chat.getChatJid());
                    if (info != null) {
                        final MultiUserChat muc = muChatMgr.getMultiUserChat(chat.getChatJid());
                        doAddGroupChat(true, true, chatRoom, muc);
                    } else {
                        logger.debug("Group chat unavailable [{}, {}]", chatRoom.getChatJid(), chatRoom.getName());
                        try {
                            listener.onChatRoomUnavailable(chatRoom, getChatOwnerNick(chatRoom));
                        } catch (Throwable t) {
                            logger.error("Listener error", t);
                        }
                    }
                }
            }
        }

        for (IChat chatObj : instantChats.values()) {
            final EntityBareJid withJid = chatObj.getChatRoom().getWithJid().asEntityBareJid();
            String nick = chatObj.getChatRoom().getName();
            String friendNick = getRosterEntryNickname(roster, withJid, false);
            if (friendNick != null)
                nick = friendNick;
            if (StringUtils.isBlank(nick))
                nick = XMPPHelper.buildGuessedString(withJid.toString());
            if (!StringUtils.equals(chatObj.getChatRoom().getName(), nick)) {
                // Nick updated
            }
        }

        for (GChat chatObj : groupChats.values()) {
            final MultiUserChat muc = chatObj.getRawChat();
            if (!muc.isJoined()) {
                boolean joined = joinMuc(muc, chatObj.getChatRoom().getLastSeenActivity());
                if (joined)
                    logger.debug("Group chat re-joined [{}]", muc.getNickname());
            }
        }

        if (this.lastPresence != null) {
            internalSendPresence(this.lastPresence);
        }
    }

    private void internalSendPresence(Presence presence) throws SmackException, InterruptedException {
        this.lastPresence = presence;
        con.sendStanza(presence);
    }

    private RoomInfo getRoomInfo(MultiUserChatManager mucManager, EntityBareJid chatJid)
            throws SmackException, XMPPException, InterruptedException, IOException {
        try {
            return mucManager.getRoomInfo(chatJid);
        } catch (XMPPException.XMPPErrorException ex) {
            if (ex.getXMPPError().getCondition() == XMPPError.Condition.item_not_found) {
                return null;
            } else {
                throw ex;
            }
        } catch (SmackException | XMPPException | InterruptedException ex) {
            throw ex;
        }
    }

    private void internalLogout() {
        final MultiUserChatManager muChatMgr = getMUChatManager();
        muChatMgr.removeInvitationListener(mucInvitationListener);
        final ChatManager chatMgr = getChatManager();
        chatMgr.removeListener(icIncomingMessageListener);
        final Roster roster = getRoster();
        roster.removeRosterListener(rosterListener);
    }

    private void checkAuthentication() throws XMPPClientException {
        synchronized (con) {
            checkConnection();

            try {
                if (!con.isAuthenticated())
                    internalLogin();
            } catch (SmackException | XMPPException | InterruptedException | IOException ex) {
                logger.error("Unable to login", ex);
                throw new XMPPClientException(ex);
            }
        }
    }

    private void checkConnection() throws XMPPClientException {
        synchronized (con) {
            try {
                if (!con.isConnected())
                    con.connect();
            } catch (SmackException | XMPPException | InterruptedException | IOException ex) {
                logger.error("Unable to establish a connection", ex);
                throw new XMPPClientException(ex);
            }
        }
    }

    private class XmppsRosterListener implements RosterListener {

        @Override
        public void entriesAdded(Collection<Jid> clctn) {
            if (isDisconnecting.get())
                return;
            try {
                logger.debug("Entry added [{}]", clctn);
                listener.onFriendsAdded(clctn);
            } catch (Throwable t) {
                logger.error("Listener error", t);
            }
        }

        @Override
        public void entriesUpdated(Collection<Jid> clctn) {
            if (isDisconnecting.get())
                return;
            try {
                logger.debug("Entry updated [{}]", clctn);
                listener.onFriendsUpdated(clctn);
            } catch (Throwable t) {
                logger.error("Listener error", t);
            }
        }

        @Override
        public void entriesDeleted(Collection<Jid> clctn) {
            if (isDisconnecting.get())
                return;
            try {
                logger.debug("Entry deleted [{}]", clctn);
                listener.onFriendsDeleted(clctn);
            } catch (Throwable t) {
                logger.error("Listener error", t);
            }
        }

        @Override
        public void presenceChanged(Presence prsnc) {
            if (isDisconnecting.get())
                return;
            final EntityBareJid friendJid = prsnc.getFrom().asEntityBareJidIfPossible();
            final FriendPresence presence = new FriendPresence(prsnc, generateChatBareJid(friendJid).toString());
            logger.debug("Presence changed [{}, {}]", EnumUtils.toSerializedName(presence.getPresenceStatus()),
                    presence.getFriendFullJid());
            try {
                listener.onFriendPresenceChanged(prsnc.getFrom(), presence, getFriendPresence(friendJid));
            } catch (Throwable t) {
                logger.error("Listener error", t);
            }
        }
    }

    private class XmppsICIncomingMessageListener implements IncomingChatMessageListener {

        @Override
        public void newIncomingMessage(EntityBareJid from, Message message, Chat chat) {
            if (isDisconnecting.get())
                return;

            try {
                final Roster roster = getRoster();
                checkRosterLoaded(roster);

                final EntityBareJid chatJid = createInstantChatJid(from);
                final String fromNick = getRosterEntryNickname(roster, from, true);

                IChat chatObj = null;
                synchronized (instantChats) {
                    chatObj = instantChats.get(chatJid);
                    if (chatObj == null) {
                        chatObj = doAddChat(false, false, chatJid, from, fromNick, ChatMessage.nowTimestamp(), from,
                                chat);
                    }
                }

                final DateTime delay = retrieveDelayInformation(message);
                final DateTime now = ChatMessage.nowTimestamp();
                chatObj.getChatRoom().setLastSeenActivity(now);

                DateTime msgTs, delivTs;
                if (delay != null) {
                    msgTs = delay;
                    delivTs = now;
                } else {
                    msgTs = now;
                    delivTs = now;
                }

                if (logger.isTraceEnabled()) {
                    logger.trace("Message received [{}, {}, {}]", msgTs, delivTs,
                            StringUtils.abbreviate(message.getBody(), 20));
                }

                try {
                    ChatMessage chatMessage = new ChatMessage(chatJid, message.getFrom(), fromNick, msgTs, delivTs,
                            message);
                    listener.onChatRoomMessageReceived(chatObj.getChatRoom(), chatMessage);
                } catch (Throwable t) {
                    logger.error("Listener error", t);
                }

            } catch (SmackException | InterruptedException ex) {
                logger.error("IncomingChatMessageListener error", ex);
            }
        }
    }

    private class XmppsMUCInvitationListener implements InvitationListener {

        @Override
        public void invitationReceived(XMPPConnection xmppc, MultiUserChat multiUserChat, EntityJid inviterJid,
                String reason, String password, Message message, MUCUser.Invite invitation) {
            if (isDisconnecting.get())
                return;
            final MultiUserChatManager muChatMgr = getMUChatManager();
            final EntityBareJid chatJid = multiUserChat.getRoom();
            final String name = reason;

            logger.debug("Group chat invitation received [{}, {}]", chatJid.toString(), inviterJid.toString());
            MultiUserChat muc = muChatMgr.getMultiUserChat(chatJid);

            GChat chatObj = null;
            synchronized (groupChats) {
                chatObj = groupChats.get(chatJid);
                if (chatObj == null) {
                    chatObj = doAddGroupChat(false, false, chatJid, inviterJid.asEntityBareJid(), name, null, muc);
                }

                if (!muc.isJoined()) {
                    boolean joined = joinMuc(muc, null);
                    if (joined)
                        logger.debug("Group chat re-joined [{}]", muc.getNickname());
                } else {
                    logger.warn("Unexpected! Group chat already joined [{}]", multiUserChat.getNickname());
                }
            }
        }
    }

    private class IChat {
        private final InstantChatRoom chatRoom;
        private Chat rawChat;

        public IChat(InstantChatRoom chatRoom) {
            this(chatRoom, null);
        }

        public IChat(InstantChatRoom chatRoom, Chat rawChat) {
            this.chatRoom = chatRoom;
            this.rawChat = rawChat;
        }

        public InstantChatRoom getChatRoom() {
            return chatRoom;
        }

        public synchronized Chat getRawChat() {
            if (rawChat == null) {
                rawChat = getChatManager().chatWith(chatRoom.getWithJid());
            }
            return rawChat;
        }
    }

    private class GChat
            implements SubjectUpdatedListener, MessageListener, UserStatusListener, ParticipantStatusListener {
        private final GroupChatRoom chatRoom;

        public GChat(GroupChatRoom chatRoom) {
            this.chatRoom = chatRoom;
        }

        public GroupChatRoom getChatRoom() {
            return chatRoom;
        }

        public MultiUserChat getRawChat() {
            return getMUChatManager().getMultiUserChat(chatRoom.getChatJid());
        }

        @Override
        public void subjectUpdated(String subject, EntityFullJid from) {
            chatRoom.setName(subject);
            try {
                listener.onChatRoomUpdated(chatRoom, false);
            } catch (Throwable t) {
                logger.error("Listener error", t);
            }
        }

        @Override
        public void processMessage(Message message) {
            final Roster roster = getRoster();

            if (message.getSubjects().size() > 0) {
                logger.warn("Subject update {}", message);

            } else if (!StringUtils.isBlank(message.getBody())) {
                boolean skipListener = false;

                // In groupchat the sender jid is not directly reconducible to
                // an internal roster jid: it is like to "chat@conference.domain.tld/nick"
                // Instead of above, we want fromJid like this: "user@domain.tld/resource"
                // NB: Now we assume that the nick provided is reconducible to a real user!
                final Jid rawFromJid = message.getFrom();
                final GuessMucFromResult guessedFrom = guessMucFrom(roster, getRawChat(),
                        rawFromJid.asEntityFullJidOrThrow());

                // Ignore messages coming from myself
                if (XMPPHelper.jidEquals(guessedFrom.jid, getUserJid()))
                    skipListener = true;

                final DateTime delay = retrieveDelayInformation(message);
                if (!skipListener) {
                    if (delay != null && history != null) {
                        final HashSet<String> idsInHistory = history.getStanzaIds(getChatRoom().getChatJid());
                        if (idsInHistory != null) {
                            skipListener = idsInHistory.contains(message.getStanzaId());
                        }
                    }
                }

                final DateTime now = ChatMessage.nowTimestamp();
                chatRoom.setLastSeenActivity(now);

                DateTime msgTs, delivTs;
                if (delay != null) {
                    msgTs = delay;
                    delivTs = now;
                } else {
                    msgTs = now;
                    delivTs = now;
                }

                if (logger.isTraceEnabled()) {
                    logger.trace("Message received [{}, {}, {}]", msgTs, delivTs,
                            StringUtils.abbreviate(message.getBody(), 20));
                }

                if (!skipListener) {
                    try {
                        ChatMessage chatMessage = new ChatMessage(chatRoom.getChatJid(),
                                guessedFrom.jid.asEntityBareJidIfPossible(),
                                XMPPHelper.asResourcepartString(guessedFrom.jid.getResourceOrNull()),
                                guessedFrom.nick, msgTs, delivTs, message);
                        listener.onChatRoomMessageReceived(chatRoom, chatMessage);
                    } catch (Throwable t) {
                        logger.error("Listener error", t);
                    }
                }

                // In groupchat the sender jid is not directly reconducible to
                // an internal roster jid: it is like to "chat@conference.domain.top/nick"
                // Instead of above, we want fromJid like this: "user@domain.top/resource"
                //final Jid rawFromJid = message.getFrom();
                // We need to get firstly the occupant and then guess the real jid
                // and its nickname from internal roster.
                //final Occupant occupant = getRawChat().getOccupant(rawFromJid.asEntityFullJidOrThrow());
                //final EntityFullJid fromJid = occupant.getJid().asEntityFullJidIfPossible();
                //final String fromNick = guessOccupantNick(roster, occupant);
                //final EntityFullJid fromJid = guessOccupantJid(getRawChat(), rawFromJid.asEntityFullJidOrThrow());
                //final String fromNick = guessOccupantNick(roster, getRawChat(), rawFromJid.asEntityFullJidOrThrow());
                //final Jid fromJid = message.getFrom();
                //final String fromNick = getRosterEntryNickname(roster, fromJid.asBareJid(), true);
                /*
                if (!XMPPHelper.jidEquals(fromJid, getUserJid())) {
                   try {
                      ChatMessage chatMessage = new ChatMessage(chatRoom.getChatJid(), fromJid.asEntityBareJidIfPossible(), XMPPHelper.asResourcepartString(fromJid.getResourceOrNull()), fromNick, ts, message);
                      listener.onChatRoomMessageReceived(chatRoom, chatMessage);
                   } catch(Throwable t) {
                      logger.error("Listener error", t);
                   }
                }
                */

            } else {
                logger.warn("Empty message {}", message);
            }
        }

        @Override
        public void kicked(Jid actor, String reason) {
        }

        @Override
        public void voiceGranted() {
        }

        @Override
        public void voiceRevoked() {
        }

        @Override
        public void banned(Jid actor, String reason) {
        }

        @Override
        public void membershipGranted() {
        }

        @Override
        public void membershipRevoked() {
        }

        @Override
        public void moderatorGranted() {
        }

        @Override
        public void moderatorRevoked() {
        }

        @Override
        public void ownershipGranted() {
        }

        @Override
        public void ownershipRevoked() {
        }

        @Override
        public void adminGranted() {
        }

        @Override
        public void adminRevoked() {
        }

        @Override
        public void roomDestroyed(MultiUserChat alternateMuc, String reason) {
            /*
            if (isDisconnecting.get()) return;
            final EntityBareJid chatJid = getChatRoom().getChatJid();
                
            logger.debug("Group chat destroyed [{}, {}]", chatJid.toString(), reason);
            try {
               synchronized(groupChats) {
                  if (groupChats.containsKey(chatJid)) doRemoveGroupChat(false, chatJid);
               }
            } catch(XMPPException | SmackException | InterruptedException ex) {
               logger.error("UserStatusListener error", ex);
            }
            */
        }

        @Override
        public void joined(EntityFullJid participant) {
            try {
                listener.onChatRoomParticipantJoined(chatRoom, participant);
            } catch (Throwable t) {
                logger.error("Listener error", t);
            }
        }

        @Override
        public void left(EntityFullJid participant) {
            try {
                listener.onChatRoomParticipantLeft(chatRoom, participant, false);
            } catch (Throwable t) {
                logger.error("Listener error", t);
            }
        }

        @Override
        public void kicked(EntityFullJid participant, Jid actor, String string) {
            try {
                listener.onChatRoomParticipantLeft(chatRoom, participant, true);
            } catch (Throwable t) {
                logger.error("Listener error", t);
            }
        }

        @Override
        public void voiceGranted(EntityFullJid efj) {
        }

        @Override
        public void voiceRevoked(EntityFullJid efj) {
        }

        @Override
        public void banned(EntityFullJid efj, Jid jid, String string) {
        }

        @Override
        public void membershipGranted(EntityFullJid efj) {
            logger.debug("{}", efj.toString());
        }

        @Override
        public void membershipRevoked(EntityFullJid efj) {
            logger.debug("{}", efj.toString());
        }

        @Override
        public void moderatorGranted(EntityFullJid efj) {
            logger.debug("{}", efj.toString());
        }

        @Override
        public void moderatorRevoked(EntityFullJid efj) {
            logger.debug("{}", efj.toString());
        }

        @Override
        public void ownershipGranted(EntityFullJid efj) {
            logger.debug("{}", efj.toString());
        }

        @Override
        public void ownershipRevoked(EntityFullJid efj) {
            logger.debug("{}", efj.toString());
        }

        @Override
        public void adminGranted(EntityFullJid efj) {
            logger.debug("{}", efj.toString());
        }

        @Override
        public void adminRevoked(EntityFullJid efj) {
            logger.debug("{}", efj.toString());
        }

        @Override
        public void nicknameChanged(EntityFullJid efj, Resourcepart r) {
            logger.debug("{}", efj.toString());
        }
    }

    public static boolean isGroupChat(EntityBareJid chatJid) {
        return isGroupChat(chatJid.toString());
    }

    public static boolean isGroupChat(String chatEntityBareJid) {
        return !isInstantChat(chatEntityBareJid);
    }

    public static boolean isInstantChat(EntityBareJid chatJid) {
        return isInstantChat(chatJid.toString());
    }

    public static boolean isInstantChat(String chatEntityBareJid) {
        return StringUtils.contains(chatEntityBareJid, "@" + CHAT_DOMAIN_PREFIX + ".");
    }

    private final class GuessMucAffiliateResult {
        public EntityBareJid jid;
        public String nick;

        public GuessMucAffiliateResult(EntityBareJid jid, String nick) {
            this.jid = jid;
            this.nick = nick;
        }
    }

    private final class GuessMucFromResult {
        public EntityFullJid jid;
        public String nick;

        public GuessMucFromResult(EntityFullJid jid, String nick) {
            this.jid = jid;
            this.nick = nick;
        }
    }

    /*
        
    private EntityFullJid guessOccupantJid(MultiUserChat muc, EntityFullJid messageFrom) {
       Occupant occupant = muc.getOccupant(messageFrom);
       return (occupant != null) ? occupant.getJid().asEntityFullJidIfPossible() : messageFrom;
    }
        
    private String guessOccupantNick(Roster roster, MultiUserChat muc, EntityFullJid messageFrom) {
       Occupant occupant = muc.getOccupant(messageFrom);
       if (occupant != null) {
     String nick = getRosterEntryNickname(roster, occupant.getJid().asBareJid(), false);
     if (!StringUtils.isBlank(nick)) return nick;
        
     nick = XMPPHelper.asResourcepartString(occupant.getNick());
     if (!StringUtils.isBlank(nick)) return nick;
        
     return XMPPHelper.buildGuessedString(occupant.getJid().asBareJid());
       } else {
     return XMPPHelper.asResourcepartString(messageFrom.getResourceOrNull());
       }
    }
        
    private String guessOccupantNick(Roster roster, Occupant occupant) {
       String nick = getRosterEntryNickname(roster, occupant.getJid().asBareJid(), false);
       if (!StringUtils.isBlank(nick)) return nick;
        
       nick = XMPPHelper.asResourcepartString(occupant.getNick());
       if (!StringUtils.isBlank(nick)) return nick;
        
       return XMPPHelper.buildGuessedString(occupant.getJid().asBareJid());
    }
        
        
    public void login() throws XMPPClientException {
       checkConnection();
           
       try {
     final Roster roster = getRoster();
     roster.addRosterListener(rosterListener);
     final ChatManager chatMgr = getChatManager();
     chatMgr.addIncomingListener(dcIncomingMessageListener);
     final MultiUserChatManager muChatMgr = getMUChatManager();
     muChatMgr.addInvitationListener(mucInvitationListener);
     this.nickname = XMPPHelper.asResourcepart(nickname);
         
     // Restore previous  direct (one-to-one) chat rooms
     if (history != null) {
        synchronized(directChats) {
           for(ChatRoom chat : history.getChats()) {
              if (chat instanceof DirectChatRoom) {
                 directChats.put(chat.getChatJid(), new DChat((DirectChatRoom)chat));
              }
           }
        }
     }
        
     con.login();
     ensureRosterLoaded(roster);
         
     // Checks for updated nicks...
     synchronized(directChats) {
        for(DChat dchat : directChats.values()) {
           final EntityBareJid withJid = dchat.getChatRoom().getWithJid().asEntityBareJid();
           String nick = dchat.getChatRoom().getName();
           String friendNick = getRosterEntryNickname(roster, withJid, false);
           if (friendNick != null) nick = friendNick;
           if (StringUtils.isBlank(nick)) nick = XMPPHelper.buildGuessedNickname(withJid.toString());
           if (!StringUtils.equals(dchat.getChatRoom().getName(), nick)) {
              // Nick updated
           }
        }
     }
         
       } catch(SmackException | XMPPException | InterruptedException | IOException ex) {
     throw new XMPPClientException(ex);
       }
    }
    */

    /*
    private synchronized void ensureAuthentication() throws XMPPClientException {
       checkConnection();
       if (!con.isAuthenticated()) throw new XMPPClientException("No logged user");
    }
    */
}