ChatManager.java

  1. /**
  2.  *
  3.  * Copyright 2003-2007 Jive Software.
  4.  *
  5.  * Licensed under the Apache License, Version 2.0 (the "License");
  6.  * you may not use this file except in compliance with the License.
  7.  * You may obtain a copy of the License at
  8.  *
  9.  *     http://www.apache.org/licenses/LICENSE-2.0
  10.  *
  11.  * Unless required by applicable law or agreed to in writing, software
  12.  * distributed under the License is distributed on an "AS IS" BASIS,
  13.  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14.  * See the License for the specific language governing permissions and
  15.  * limitations under the License.
  16.  */

  17. package org.jivesoftware.smack.chat;

  18. import java.util.Collections;
  19. import java.util.Map;
  20. import java.util.Set;
  21. import java.util.UUID;
  22. import java.util.WeakHashMap;
  23. import java.util.concurrent.ConcurrentHashMap;
  24. import java.util.concurrent.CopyOnWriteArraySet;
  25. import java.util.logging.Logger;

  26. import org.jivesoftware.smack.Manager;
  27. import org.jivesoftware.smack.MessageListener;
  28. import org.jivesoftware.smack.PacketCollector;
  29. import org.jivesoftware.smack.StanzaListener;
  30. import org.jivesoftware.smack.XMPPConnection;
  31. import org.jivesoftware.smack.SmackException.NotConnectedException;
  32. import org.jivesoftware.smack.filter.AndFilter;
  33. import org.jivesoftware.smack.filter.FlexibleStanzaTypeFilter;
  34. import org.jivesoftware.smack.filter.FromMatchesFilter;
  35. import org.jivesoftware.smack.filter.MessageTypeFilter;
  36. import org.jivesoftware.smack.filter.OrFilter;
  37. import org.jivesoftware.smack.filter.StanzaFilter;
  38. import org.jivesoftware.smack.filter.ThreadFilter;
  39. import org.jivesoftware.smack.packet.Message;
  40. import org.jivesoftware.smack.packet.Message.Type;
  41. import org.jivesoftware.smack.packet.Stanza;
  42. import org.jxmpp.jid.BareJid;
  43. import org.jxmpp.jid.Jid;
  44. import org.jxmpp.jid.JidWithLocalpart;

  45. /**
  46.  * The chat manager keeps track of references to all current chats. It will not hold any references
  47.  * in memory on its own so it is necessary to keep a reference to the chat object itself. To be
  48.  * made aware of new chats, register a listener by calling {@link #addChatListener(ChatManagerListener)}.
  49.  *
  50.  * @author Alexander Wenckus
  51.  */
  52. public class ChatManager extends Manager{

  53.     private static final Logger LOGGER = Logger.getLogger(ChatManager.class.getName());

  54.     private static final Map<XMPPConnection, ChatManager> INSTANCES = new WeakHashMap<XMPPConnection, ChatManager>();

  55.     /**
  56.      * Sets the default behaviour for allowing 'normal' messages to be used in chats. As some clients don't set
  57.      * the message type to chat, the type normal has to be accepted to allow chats with these clients.
  58.      */
  59.     private static boolean defaultIsNormalInclude = true;

  60.     /**
  61.      * Sets the default behaviour for how to match chats when there is NO thread id in the incoming message.
  62.      */
  63.     private static MatchMode defaultMatchMode = MatchMode.BARE_JID;

  64.     /**
  65.      * Returns the ChatManager instance associated with a given XMPPConnection.
  66.      *
  67.      * @param connection the connection used to look for the proper ServiceDiscoveryManager.
  68.      * @return the ChatManager associated with a given XMPPConnection.
  69.      */
  70.     public static synchronized ChatManager getInstanceFor(XMPPConnection connection) {
  71.         ChatManager manager = INSTANCES.get(connection);
  72.         if (manager == null)
  73.             manager = new ChatManager(connection);
  74.         return manager;
  75.     }

  76.     /**
  77.      * Defines the different modes under which a match will be attempted with an existing chat when
  78.      * the incoming message does not have a thread id.
  79.      */
  80.     public enum MatchMode {
  81.         /**
  82.          * Will not attempt to match, always creates a new chat.
  83.          */
  84.         NONE,
  85.         /**
  86.          * Will match on the JID in the from field of the message.
  87.          */
  88.         SUPPLIED_JID,
  89.         /**
  90.          * Will attempt to match on the JID in the from field, and then attempt the base JID if no match was found.
  91.          * This is the most lenient matching.
  92.          */
  93.         BARE_JID;
  94.     }

  95.     private final StanzaFilter packetFilter = new OrFilter(MessageTypeFilter.CHAT, new FlexibleStanzaTypeFilter<Message>() {

  96.         @Override
  97.         protected boolean acceptSpecific(Message message) {
  98.             return normalIncluded ? message.getType() == Type.normal : false;
  99.         }

  100.     });

  101.     /**
  102.      * Determines whether incoming messages of type normal can create chats.
  103.      */
  104.     private boolean normalIncluded = defaultIsNormalInclude;

  105.     /**
  106.      * Determines how incoming message with no thread will be matched to existing chats.
  107.      */
  108.     private MatchMode matchMode = defaultMatchMode;

  109.     /**
  110.      * Maps thread ID to chat.
  111.      */
  112.     private Map<String, Chat> threadChats = new ConcurrentHashMap<>();

  113.     /**
  114.      * Maps jids to chats
  115.      */
  116.     private Map<Jid, Chat> jidChats = new ConcurrentHashMap<>();

  117.     /**
  118.      * Maps base jids to chats
  119.      */
  120.     private Map<BareJid, Chat> baseJidChats = new ConcurrentHashMap<>();

  121.     private Set<ChatManagerListener> chatManagerListeners
  122.             = new CopyOnWriteArraySet<ChatManagerListener>();

  123.     private Map<MessageListener, StanzaFilter> interceptors
  124.             = new WeakHashMap<MessageListener, StanzaFilter>();

  125.     private ChatManager(XMPPConnection connection) {
  126.         super(connection);

  127.         // Add a listener for all message packets so that we can deliver
  128.         // messages to the best Chat instance available.
  129.         connection.addSyncStanzaListener(new StanzaListener() {
  130.             public void processPacket(Stanza packet) {
  131.                 Message message = (Message) packet;
  132.                 Chat chat;
  133.                 if (message.getThread() == null) {
  134.                     chat = getUserChat(message.getFrom());
  135.                 }
  136.                 else {
  137.                     chat = getThreadChat(message.getThread());
  138.                 }

  139.                 if(chat == null) {
  140.                     chat = createChat(message);
  141.                 }
  142.                 // The chat could not be created, abort here
  143.                 if (chat == null)
  144.                     return;
  145.                 deliverMessage(chat, message);
  146.             }
  147.         }, packetFilter);
  148.         INSTANCES.put(connection, this);
  149.     }

  150.     /**
  151.      * Determines whether incoming messages of type <i>normal</i> will be used for creating new chats or matching
  152.      * a message to existing ones.
  153.      *
  154.      * @return true if normal is allowed, false otherwise.
  155.      */
  156.     public boolean isNormalIncluded() {
  157.         return normalIncluded;
  158.     }

  159.     /**
  160.      * Sets whether to allow incoming messages of type <i>normal</i> to be used for creating new chats or matching
  161.      * a message to an existing one.
  162.      *
  163.      * @param normalIncluded true to allow normal, false otherwise.
  164.      */
  165.     public void setNormalIncluded(boolean normalIncluded) {
  166.         this.normalIncluded = normalIncluded;
  167.     }

  168.     /**
  169.      * Gets the current mode for matching messages with <b>NO</b> thread id to existing chats.
  170.      *
  171.      * @return The current mode.
  172.      */
  173.     public MatchMode getMatchMode() {
  174.         return matchMode;
  175.     }

  176.     /**
  177.      * Sets the mode for matching messages with <b>NO</b> thread id to existing chats.
  178.      *
  179.      * @param matchMode The mode to set.
  180.      */
  181.     public void setMatchMode(MatchMode matchMode) {
  182.         this.matchMode = matchMode;
  183.     }

  184.     /**
  185.      * Creates a new chat and returns it.
  186.      *
  187.      * @param userJID the user this chat is with.
  188.      * @return the created chat.
  189.      */
  190.     public Chat createChat(JidWithLocalpart userJID) {
  191.         return createChat(userJID, null);
  192.     }

  193.     /**
  194.      * Creates a new chat and returns it.
  195.      *
  196.      * @param userJID the user this chat is with.
  197.      * @param listener the optional listener which will listen for new messages from this chat.
  198.      * @return the created chat.
  199.      */
  200.     public Chat createChat(JidWithLocalpart userJID, ChatMessageListener listener) {
  201.         return createChat(userJID, null, listener);
  202.     }

  203.     /**
  204.      * Creates a new chat using the specified thread ID, then returns it.
  205.      *
  206.      * @param userJID the jid of the user this chat is with
  207.      * @param thread the thread of the created chat.
  208.      * @param listener the optional listener to add to the chat
  209.      * @return the created chat.
  210.      */
  211.     public Chat createChat(JidWithLocalpart userJID, String thread, ChatMessageListener listener) {
  212.         if (thread == null) {
  213.             thread = nextID();
  214.         }
  215.         Chat chat = threadChats.get(thread);
  216.         if(chat != null) {
  217.             throw new IllegalArgumentException("ThreadID is already used");
  218.         }
  219.         chat = createChat(userJID, thread, true);
  220.         chat.addMessageListener(listener);
  221.         return chat;
  222.     }

  223.     private Chat createChat(JidWithLocalpart userJID, String threadID, boolean createdLocally) {
  224.         Chat chat = new Chat(this, userJID, threadID);
  225.         threadChats.put(threadID, chat);
  226.         jidChats.put(userJID, chat);
  227.         baseJidChats.put(userJID.asBareJid(), chat);

  228.         for(ChatManagerListener listener : chatManagerListeners) {
  229.             listener.chatCreated(chat, createdLocally);
  230.         }

  231.         return chat;
  232.     }

  233.     void closeChat(Chat chat) {
  234.         threadChats.remove(chat.getThreadID());
  235.         JidWithLocalpart userJID = chat.getParticipant();
  236.         jidChats.remove(userJID);
  237.         baseJidChats.remove(userJID.withoutResource());
  238.     }

  239.     /**
  240.      * Creates a new {@link Chat} based on the message. May returns null if no chat could be
  241.      * created, e.g. because the message comes without from.
  242.      *
  243.      * @param message
  244.      * @return a Chat or null if none can be created
  245.      */
  246.     private Chat createChat(Message message) {
  247.         Jid from = message.getFrom();
  248.         // According to RFC6120 8.1.2.1 4. messages without a 'from' attribute are valid, but they
  249.         // are of no use in this case for ChatManager
  250.         if (from == null) {
  251.             return null;
  252.         }

  253.         JidWithLocalpart userJID = from.asJidWithLocalpartIfPossible();
  254.         if (userJID == null) {
  255.             LOGGER.warning("Message from JID without localpart: '" +message.toXML() + "'");
  256.             return null;
  257.         }
  258.         String threadID = message.getThread();
  259.         if(threadID == null) {
  260.             threadID = nextID();
  261.         }

  262.         return createChat(userJID, threadID, false);
  263.     }

  264.     /**
  265.      * Try to get a matching chat for the given user JID, based on the {@link MatchMode}.
  266.      * <li>NONE - return null
  267.      * <li>SUPPLIED_JID - match the jid in the from field of the message exactly.
  268.      * <li>BARE_JID - if not match for from field, try the bare jid.
  269.      *
  270.      * @param userJID jid in the from field of message.
  271.      * @return Matching chat, or null if no match found.
  272.      */
  273.     private Chat getUserChat(Jid userJID) {
  274.         if (matchMode == MatchMode.NONE) {
  275.             return null;
  276.         }
  277.         // According to RFC6120 8.1.2.1 4. messages without a 'from' attribute are valid, but they
  278.         // are of no use in this case for ChatManager
  279.         if (userJID == null) {
  280.             return null;
  281.         }
  282.         Chat match = jidChats.get(userJID);
  283.    
  284.         if (match == null && (matchMode == MatchMode.BARE_JID)) {
  285.             match = baseJidChats.get(userJID.asBareJidIfPossible());
  286.         }
  287.         return match;
  288.     }

  289.     public Chat getThreadChat(String thread) {
  290.         return threadChats.get(thread);
  291.     }

  292.     /**
  293.      * Register a new listener with the ChatManager to recieve events related to chats.
  294.      *
  295.      * @param listener the listener.
  296.      */
  297.     public void addChatListener(ChatManagerListener listener) {
  298.         chatManagerListeners.add(listener);
  299.     }

  300.     /**
  301.      * Removes a listener, it will no longer be notified of new events related to chats.
  302.      *
  303.      * @param listener the listener that is being removed
  304.      */
  305.     public void removeChatListener(ChatManagerListener listener) {
  306.         chatManagerListeners.remove(listener);
  307.     }

  308.     /**
  309.      * Returns an unmodifiable set of all chat listeners currently registered with this
  310.      * manager.
  311.      *
  312.      * @return an unmodifiable collection of all chat listeners currently registered with this
  313.      * manager.
  314.      */
  315.     public Set<ChatManagerListener> getChatListeners() {
  316.         return Collections.unmodifiableSet(chatManagerListeners);
  317.     }

  318.     private void deliverMessage(Chat chat, Message message) {
  319.         // Here we will run any interceptors
  320.         chat.deliver(message);
  321.     }

  322.     void sendMessage(Chat chat, Message message) throws NotConnectedException, InterruptedException {
  323.         for(Map.Entry<MessageListener, StanzaFilter> interceptor : interceptors.entrySet()) {
  324.             StanzaFilter filter = interceptor.getValue();
  325.             if(filter != null && filter.accept(message)) {
  326.                 interceptor.getKey().processMessage(message);
  327.             }
  328.         }
  329.         // Ensure that messages being sent have a proper FROM value
  330.         if (message.getFrom() == null) {
  331.             message.setFrom(connection().getUser());
  332.         }
  333.         connection().sendStanza(message);
  334.     }

  335.     PacketCollector createPacketCollector(Chat chat) {
  336.         return connection().createPacketCollector(new AndFilter(new ThreadFilter(chat.getThreadID()),
  337.                         FromMatchesFilter.create(chat.getParticipant())));
  338.     }

  339.     /**
  340.      * Adds an interceptor which intercepts any messages sent through chats.
  341.      *
  342.      * @param messageInterceptor the interceptor.
  343.      */
  344.     public void addOutgoingMessageInterceptor(MessageListener messageInterceptor) {
  345.         addOutgoingMessageInterceptor(messageInterceptor, null);
  346.     }

  347.     public void addOutgoingMessageInterceptor(MessageListener messageInterceptor, StanzaFilter filter) {
  348.         if (messageInterceptor == null) {
  349.             return;
  350.         }
  351.         interceptors.put(messageInterceptor, filter);
  352.     }
  353.    
  354.     /**
  355.      * Returns a unique id.
  356.      *
  357.      * @return the next id.
  358.      */
  359.     private static String nextID() {
  360.         return UUID.randomUUID().toString();
  361.     }
  362.    
  363.     public static void setDefaultMatchMode(MatchMode mode) {
  364.         defaultMatchMode = mode;
  365.     }
  366.    
  367.     public static void setDefaultIsNormalIncluded(boolean allowNormal) {
  368.         defaultIsNormalInclude = allowNormal;
  369.     }
  370. }