001/**
002 *
003 * Copyright 2003-2007 Jive Software.
004 *
005 * Licensed under the Apache License, Version 2.0 (the "License");
006 * you may not use this file except in compliance with the License.
007 * You may obtain a copy of the License at
008 *
009 *     http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017
018package org.jivesoftware.smackx.chatstates;
019
020import java.util.Map;
021import java.util.WeakHashMap;
022
023import org.jivesoftware.smack.MessageListener;
024import org.jivesoftware.smack.SmackException.NotConnectedException;
025import org.jivesoftware.smack.XMPPConnection;
026import org.jivesoftware.smack.Manager;
027import org.jivesoftware.smack.chat.Chat;
028import org.jivesoftware.smack.chat.ChatManager;
029import org.jivesoftware.smack.chat.ChatManagerListener;
030import org.jivesoftware.smack.chat.ChatMessageListener;
031import org.jivesoftware.smack.filter.NotFilter;
032import org.jivesoftware.smack.filter.StanzaExtensionFilter;
033import org.jivesoftware.smack.filter.StanzaFilter;
034import org.jivesoftware.smack.packet.Message;
035import org.jivesoftware.smack.packet.ExtensionElement;
036import org.jivesoftware.smackx.chatstates.packet.ChatStateExtension;
037import org.jivesoftware.smackx.disco.ServiceDiscoveryManager;
038
039/**
040 * Handles chat state for all chats on a particular XMPPConnection. This class manages both the
041 * stanza(/packet) extensions and the disco response necessary for compliance with
042 * <a href="http://www.xmpp.org/extensions/xep-0085.html">XEP-0085</a>.
043 *
044 * NOTE: {@link org.jivesoftware.smackx.chatstates.ChatStateManager#getInstance(org.jivesoftware.smack.XMPPConnection)}
045 * needs to be called in order for the listeners to be registered appropriately with the connection.
046 * If this does not occur you will not receive the update notifications.
047 *
048 * @author Alexander Wenckus
049 * @see org.jivesoftware.smackx.chatstates.ChatState
050 * @see org.jivesoftware.smackx.chatstates.packet.ChatStateExtension
051 */
052public class ChatStateManager extends Manager {
053    public static final String NAMESPACE = "http://jabber.org/protocol/chatstates";
054
055    private static final Map<XMPPConnection, ChatStateManager> INSTANCES =
056            new WeakHashMap<XMPPConnection, ChatStateManager>();
057
058    private static final StanzaFilter filter = new NotFilter(new StanzaExtensionFilter(NAMESPACE));
059
060    /**
061     * Returns the ChatStateManager related to the XMPPConnection and it will create one if it does
062     * not yet exist.
063     *
064     * @param connection the connection to return the ChatStateManager
065     * @return the ChatStateManager related the the connection.
066     */
067    public static synchronized ChatStateManager getInstance(final XMPPConnection connection) {
068            ChatStateManager manager = INSTANCES.get(connection);
069            if (manager == null) {
070                manager = new ChatStateManager(connection);
071            }
072            return manager;
073    }
074
075    private final OutgoingMessageInterceptor outgoingInterceptor = new OutgoingMessageInterceptor();
076
077    private final IncomingMessageInterceptor incomingInterceptor = new IncomingMessageInterceptor();
078
079    /**
080     * Maps chat to last chat state.
081     */
082    private final Map<Chat, ChatState> chatStates = new WeakHashMap<Chat, ChatState>();
083
084    private final ChatManager chatManager;
085
086    private ChatStateManager(XMPPConnection connection) {
087        super(connection);
088        chatManager = ChatManager.getInstanceFor(connection);
089        chatManager.addOutgoingMessageInterceptor(outgoingInterceptor, filter);
090        chatManager.addChatListener(incomingInterceptor);
091
092        ServiceDiscoveryManager.getInstanceFor(connection).addFeature(NAMESPACE);
093        INSTANCES.put(connection, this);
094    }
095
096
097    /**
098     * Sets the current state of the provided chat. This method will send an empty bodied Message
099     * stanza(/packet) with the state attached as a {@link org.jivesoftware.smack.packet.ExtensionElement}, if
100     * and only if the new chat state is different than the last state.
101     *
102     * @param newState the new state of the chat
103     * @param chat the chat.
104     * @throws NotConnectedException 
105     */
106    public void setCurrentState(ChatState newState, Chat chat) throws NotConnectedException {
107        if(chat == null || newState == null) {
108            throw new IllegalArgumentException("Arguments cannot be null.");
109        }
110        if(!updateChatState(chat, newState)) {
111            return;
112        }
113        Message message = new Message();
114        ChatStateExtension extension = new ChatStateExtension(newState);
115        message.addExtension(extension);
116
117        chat.sendMessage(message);
118    }
119
120
121    public boolean equals(Object o) {
122        if (this == o) return true;
123        if (o == null || getClass() != o.getClass()) return false;
124
125        ChatStateManager that = (ChatStateManager) o;
126
127        return connection().equals(that.connection());
128
129    }
130
131    public int hashCode() {
132        return connection().hashCode();
133    }
134
135    private synchronized boolean updateChatState(Chat chat, ChatState newState) {
136        ChatState lastChatState = chatStates.get(chat);
137        if (lastChatState != newState) {
138            chatStates.put(chat, newState);
139            return true;
140        }
141        return false;
142    }
143
144    private void fireNewChatState(Chat chat, ChatState state) {
145        for (ChatMessageListener listener : chat.getListeners()) {
146            if (listener instanceof ChatStateListener) {
147                ((ChatStateListener) listener).stateChanged(chat, state);
148            }
149        }
150    }
151
152    private class OutgoingMessageInterceptor implements MessageListener {
153
154        @Override
155        public void processMessage(Message message) {
156            Chat chat = chatManager.getThreadChat(message.getThread());
157            if (chat == null) {
158                return;
159            }
160            if (updateChatState(chat, ChatState.active)) {
161                message.addExtension(new ChatStateExtension(ChatState.active));
162            }
163        }
164    }
165
166    private class IncomingMessageInterceptor implements ChatManagerListener, ChatMessageListener {
167
168        public void chatCreated(final Chat chat, boolean createdLocally) {
169            chat.addMessageListener(this);
170        }
171
172        public void processMessage(Chat chat, Message message) {
173            ExtensionElement extension = message.getExtension(NAMESPACE);
174            if (extension == null) {
175                return;
176            }
177
178            ChatState state;
179            try {
180                state = ChatState.valueOf(extension.getElementName());
181            }
182            catch (Exception ex) {
183                return;
184            }
185
186            fireNewChatState(chat, state);
187        }
188    }
189}