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.WeakHashMap;
  22. import java.util.concurrent.ConcurrentHashMap;
  23. import java.util.concurrent.CopyOnWriteArraySet;
  24. import java.util.logging.Logger;

  25. import org.jivesoftware.smack.Manager;
  26. import org.jivesoftware.smack.MessageListener;
  27. import org.jivesoftware.smack.SmackException.NotConnectedException;
  28. import org.jivesoftware.smack.StanzaCollector;
  29. import org.jivesoftware.smack.StanzaListener;
  30. import org.jivesoftware.smack.XMPPConnection;
  31. import org.jivesoftware.smack.filter.AndFilter;
  32. import org.jivesoftware.smack.filter.FlexibleStanzaTypeFilter;
  33. import org.jivesoftware.smack.filter.FromMatchesFilter;
  34. import org.jivesoftware.smack.filter.MessageTypeFilter;
  35. import org.jivesoftware.smack.filter.OrFilter;
  36. import org.jivesoftware.smack.filter.StanzaFilter;
  37. import org.jivesoftware.smack.filter.ThreadFilter;
  38. import org.jivesoftware.smack.packet.Message;
  39. import org.jivesoftware.smack.packet.Stanza;
  40. import org.jivesoftware.smack.util.StringUtils;

  41. import org.jxmpp.jid.EntityBareJid;
  42. import org.jxmpp.jid.EntityJid;
  43. import org.jxmpp.jid.Jid;

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

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

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

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

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

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

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

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

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

  101.     });

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

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

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

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

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

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

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

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

  126.         // Add a listener for all message packets so that we can deliver
  127.         // messages to the best Chat instance available.
  128.         connection.addSyncStanzaListener(new StanzaListener() {
  129.             @Override
  130.             public void processStanza(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.                 // TODO: Use AsyncButOrdered (with Chat as Key?)
  146.                 deliverMessage(chat, message);
  147.             }
  148.         }, packetFilter);
  149.         INSTANCES.put(connection, this);
  150.     }

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

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

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

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

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

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

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

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

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

  232.         return chat;
  233.     }

  234.     void closeChat(Chat chat) {
  235.         threadChats.remove(chat.getThreadID());
  236.         EntityJid userJID = chat.getParticipant();
  237.         jidChats.remove(userJID);
  238.         baseJidChats.remove(userJID.asEntityBareJid());
  239.     }

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

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

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

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

  284.         if (match == null && (matchMode == MatchMode.BARE_JID)) {
  285.             EntityBareJid entityBareJid = userJID.asEntityBareJidIfPossible();
  286.             if (entityBareJid != null) {
  287.                 match = baseJidChats.get(entityBareJid);
  288.             }
  289.         }
  290.         return match;
  291.     }

  292.     public Chat getThreadChat(String thread) {
  293.         return threadChats.get(thread);
  294.     }

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

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

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

  321.     private static void deliverMessage(Chat chat, Message message) {
  322.         // Here we will run any interceptors
  323.         chat.deliver(message);
  324.     }

  325.     void sendMessage(Chat chat, Message message) throws NotConnectedException, InterruptedException {
  326.         for (Map.Entry<MessageListener, StanzaFilter> interceptor : interceptors.entrySet()) {
  327.             StanzaFilter filter = interceptor.getValue();
  328.             if (filter != null && filter.accept(message)) {
  329.                 interceptor.getKey().processMessage(message);
  330.             }
  331.         }
  332.         connection().sendStanza(message);
  333.     }

  334.     StanzaCollector createStanzaCollector(Chat chat) {
  335.         return connection().createStanzaCollector(new AndFilter(new ThreadFilter(chat.getThreadID()),
  336.                         FromMatchesFilter.create(chat.getParticipant())));
  337.     }

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

  346.     public void addOutgoingMessageInterceptor(MessageListener messageInterceptor, StanzaFilter filter) {
  347.         if (messageInterceptor == null) {
  348.             return;
  349.         }
  350.         interceptors.put(messageInterceptor, filter);
  351.     }

  352.     /**
  353.      * Returns a unique id.
  354.      *
  355.      * @return the next id.
  356.      */
  357.     private static String nextID() {
  358.         return StringUtils.secureUniqueRandomString();
  359.     }

  360.     public static void setDefaultMatchMode(MatchMode mode) {
  361.         defaultMatchMode = mode;
  362.     }

  363.     public static void setDefaultIsNormalIncluded(boolean allowNormal) {
  364.         defaultIsNormalInclude = allowNormal;
  365.     }
  366. }