OfflineMessageManager.java

  1. /**
  2.  *
  3.  * Copyright 2003-2007 Jive Software, 2020 Florian Schmaus.
  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.smackx.offline;

  18. import java.util.ArrayList;
  19. import java.util.List;
  20. import java.util.Map;
  21. import java.util.WeakHashMap;
  22. import java.util.logging.Level;
  23. import java.util.logging.Logger;

  24. import org.jivesoftware.smack.Manager;
  25. import org.jivesoftware.smack.SmackException.NoResponseException;
  26. import org.jivesoftware.smack.SmackException.NotConnectedException;
  27. import org.jivesoftware.smack.StanzaCollector;
  28. import org.jivesoftware.smack.XMPPConnection;
  29. import org.jivesoftware.smack.XMPPException.XMPPErrorException;
  30. import org.jivesoftware.smack.filter.AndFilter;
  31. import org.jivesoftware.smack.filter.StanzaExtensionFilter;
  32. import org.jivesoftware.smack.filter.StanzaFilter;
  33. import org.jivesoftware.smack.filter.StanzaTypeFilter;
  34. import org.jivesoftware.smack.packet.IQ;
  35. import org.jivesoftware.smack.packet.Message;
  36. import org.jivesoftware.smack.packet.Stanza;

  37. import org.jivesoftware.smackx.disco.ServiceDiscoveryManager;
  38. import org.jivesoftware.smackx.disco.packet.DiscoverInfo;
  39. import org.jivesoftware.smackx.disco.packet.DiscoverItems;
  40. import org.jivesoftware.smackx.offline.packet.OfflineMessageInfo;
  41. import org.jivesoftware.smackx.offline.packet.OfflineMessageRequest;
  42. import org.jivesoftware.smackx.xdata.packet.DataForm;

  43. /**
  44.  * The OfflineMessageManager helps manage offline messages even before the user has sent an
  45.  * available presence. When a user asks for his offline messages before sending an available
  46.  * presence then the server will not send a flood with all the offline messages when the user
  47.  * becomes online. The server will not send a flood with all the offline messages to the session
  48.  * that made the offline messages request or to any other session used by the user that becomes
  49.  * online.<p>
  50.  *
  51.  * Once the session that made the offline messages request has been closed and the user becomes
  52.  * offline in all the resources then the server will resume storing the messages offline and will
  53.  * send all the offline messages to the user when he becomes online. Therefore, the server will
  54.  * flood the user when he becomes online unless the user uses this class to manage his offline
  55.  * messages.
  56.  *
  57.  * @author Gaston Dombiak
  58.  */
  59. public final class OfflineMessageManager extends Manager {

  60.     private static final Logger LOGGER = Logger.getLogger(OfflineMessageManager.class.getName());

  61.     public static final String NAMESPACE = "http://jabber.org/protocol/offline";

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

  63.     private static final StanzaFilter PACKET_FILTER = new AndFilter(new StanzaExtensionFilter(
  64.                     new OfflineMessageInfo()), StanzaTypeFilter.MESSAGE);

  65.     private ServiceDiscoveryManager serviceDiscoveryManager;

  66.     private OfflineMessageManager(XMPPConnection connection) {
  67.         super(connection);
  68.         this.serviceDiscoveryManager = ServiceDiscoveryManager.getInstanceFor(connection);
  69.     }

  70.     public static synchronized OfflineMessageManager getInstanceFor(XMPPConnection connection) {
  71.         OfflineMessageManager manager = INSTANCES.get(connection);
  72.         if (manager == null) {
  73.             manager = new OfflineMessageManager(connection);
  74.             INSTANCES.put(connection, manager);
  75.         }
  76.         return manager;
  77.     }

  78.     /**
  79.      * Returns true if the server supports Flexible Offline Message Retrieval. When the server
  80.      * supports Flexible Offline Message Retrieval it is possible to get the header of the offline
  81.      * messages, get specific messages, delete specific messages, etc.
  82.      *
  83.      * @return a boolean indicating if the server supports Flexible Offline Message Retrieval.
  84.      * @throws XMPPErrorException If the user is not allowed to make this request.
  85.      * @throws NoResponseException if there was no response from the server.
  86.      * @throws NotConnectedException if the XMPP connection is not connected.
  87.      * @throws InterruptedException if the calling thread was interrupted.
  88.      */
  89.     public boolean supportsFlexibleRetrieval() throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
  90.         return serviceDiscoveryManager.serverSupportsFeature(NAMESPACE);
  91.     }

  92.     /**
  93.      * Returns the number of offline messages for the user of the connection.
  94.      *
  95.      * @return the number of offline messages for the user of the connection.
  96.      * @throws XMPPErrorException If the user is not allowed to make this request or the server does
  97.      *                       not support offline message retrieval.
  98.      * @throws NoResponseException if there was no response from the server.
  99.      * @throws NotConnectedException if the XMPP connection is not connected.
  100.      * @throws InterruptedException if the calling thread was interrupted.
  101.      */
  102.     public int getMessageCount() throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
  103.         DiscoverInfo info = serviceDiscoveryManager.discoverInfo(null, NAMESPACE);
  104.         DataForm dataForm = DataForm.from(info, NAMESPACE);
  105.         if (dataForm == null) {
  106.             return 0;
  107.         }
  108.         String numberOfMessagesString = dataForm.getField("number_of_messages").getFirstValue();
  109.         return Integer.parseInt(numberOfMessagesString);
  110.     }

  111.     /**
  112.      * Returns a List of <code>OfflineMessageHeader</code> that keep information about the
  113.      * offline message. The OfflineMessageHeader includes a stamp that could be used to retrieve
  114.      * the complete message or delete the specific message.
  115.      *
  116.      * @return a List of <code>OfflineMessageHeader</code> that keep information about the offline
  117.      *         message.
  118.      * @throws XMPPErrorException If the user is not allowed to make this request or the server does
  119.      *                       not support offline message retrieval.
  120.      * @throws NoResponseException if there was no response from the server.
  121.      * @throws NotConnectedException if the XMPP connection is not connected.
  122.      * @throws InterruptedException if the calling thread was interrupted.
  123.      */
  124.     public List<OfflineMessageHeader> getHeaders() throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
  125.         List<OfflineMessageHeader> answer = new ArrayList<>();
  126.         DiscoverItems items = serviceDiscoveryManager.discoverItems(null, NAMESPACE);
  127.         for (DiscoverItems.Item item : items.getItems()) {
  128.             answer.add(new OfflineMessageHeader(item));
  129.         }
  130.         return answer;
  131.     }

  132.     /**
  133.      * Returns a List of the offline <code>Messages</code> whose stamp matches the specified
  134.      * request. The request will include the list of stamps that uniquely identifies
  135.      * the offline messages to retrieve. The returned offline messages will not be deleted
  136.      * from the server. Use {@link #deleteMessages(java.util.List)} to delete the messages.
  137.      *
  138.      * @param nodes the list of stamps that uniquely identifies offline message.
  139.      * @return a List with the offline <code>Messages</code> that were received as part of
  140.      *         this request.
  141.      * @throws XMPPErrorException If the user is not allowed to make this request or the server does
  142.      *                       not support offline message retrieval.
  143.      * @throws NoResponseException if there was no response from the server.
  144.      * @throws NotConnectedException if the XMPP connection is not connected.
  145.      * @throws InterruptedException if the calling thread was interrupted.
  146.      */
  147.     public List<Message> getMessages(final List<String> nodes) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
  148.         List<Message> messages = new ArrayList<>(nodes.size());
  149.         OfflineMessageRequest request = new OfflineMessageRequest();
  150.         for (String node : nodes) {
  151.             OfflineMessageRequest.Item item = new OfflineMessageRequest.Item(node);
  152.             item.setAction("view");
  153.             request.addItem(item);
  154.         }
  155.         // Filter offline messages that were requested by this request
  156.         StanzaFilter messageFilter = new AndFilter(PACKET_FILTER, new StanzaFilter() {
  157.             @Override
  158.             public boolean accept(Stanza packet) {
  159.                 OfflineMessageInfo info = packet.getExtension(OfflineMessageInfo.class);
  160.                 return nodes.contains(info.getNode());
  161.             }
  162.         });
  163.         int pendingNodes = nodes.size();
  164.         try (StanzaCollector messageCollector = connection().createStanzaCollector(messageFilter)) {
  165.             connection().sendIqRequestAndWaitForResponse(request);
  166.             // Collect the received offline messages
  167.             Message message;
  168.             do {
  169.                 message = messageCollector.nextResult();
  170.                 if (message != null) {
  171.                     messages.add(message);
  172.                     pendingNodes--;
  173.                 } else if (message == null && pendingNodes > 0) {
  174.                     LOGGER.log(Level.WARNING,
  175.                                     "Did not receive all expected offline messages. " + pendingNodes + " are missing.");
  176.                 }
  177.             } while (message != null && pendingNodes > 0);
  178.         }
  179.         return messages;
  180.     }

  181.     /**
  182.      * Returns a List of Messages with all the offline <code>Messages</code> of the user. The returned offline
  183.      * messages will not be deleted from the server. Use {@link #deleteMessages(java.util.List)}
  184.      * to delete the messages.
  185.      *
  186.      * @return a List with all the offline <code>Messages</code> of the user.
  187.      * @throws XMPPErrorException If the user is not allowed to make this request or the server does
  188.      *                       not support offline message retrieval.
  189.      * @throws NoResponseException if there was no response from the server.
  190.      * @throws NotConnectedException if the XMPP connection is not connected.
  191.      * @throws InterruptedException if the calling thread was interrupted.
  192.      */
  193.     public List<Message> getMessages() throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
  194.         OfflineMessageRequest request = new OfflineMessageRequest();
  195.         request.setFetch(true);

  196.         StanzaCollector resultCollector = connection().createStanzaCollectorAndSend(request);
  197.         StanzaCollector.Configuration messageCollectorConfiguration = StanzaCollector.newConfiguration().setStanzaFilter(PACKET_FILTER).setCollectorToReset(resultCollector);

  198.         List<Message> messages;
  199.         try (StanzaCollector messageCollector = connection().createStanzaCollector(messageCollectorConfiguration)) {
  200.             resultCollector.nextResultOrThrow();
  201.             // Be extra safe, cancel the message collector right here so that it does not collector
  202.             // other messages that eventually match (although I've no idea how this could happen in
  203.             // case of XEP-13).
  204.             messageCollector.cancel();
  205.             messages = new ArrayList<>(messageCollector.getCollectedCount());
  206.             Message message;
  207.             while ((message = messageCollector.pollResult()) != null) {
  208.                 messages.add(message);
  209.             }
  210.         }
  211.         return messages;
  212.     }

  213.     /**
  214.      * Deletes the specified list of offline messages. The request will include the list of
  215.      * stamps that uniquely identifies the offline messages to delete.
  216.      *
  217.      * @param nodes the list of stamps that uniquely identifies offline message.
  218.      * @throws XMPPErrorException If the user is not allowed to make this request or the server does
  219.      *                       not support offline message retrieval.
  220.      * @throws NoResponseException if there was no response from the server.
  221.      * @throws NotConnectedException if the XMPP connection is not connected.
  222.      * @throws InterruptedException if the calling thread was interrupted.
  223.      */
  224.     public void deleteMessages(List<String> nodes) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
  225.         OfflineMessageRequest request = new OfflineMessageRequest();
  226.         request.setType(IQ.Type.set);
  227.         for (String node : nodes) {
  228.             OfflineMessageRequest.Item item = new OfflineMessageRequest.Item(node);
  229.             item.setAction("remove");
  230.             request.addItem(item);
  231.         }
  232.         connection().sendIqRequestAndWaitForResponse(request);
  233.     }

  234.     /**
  235.      * Deletes all offline messages of the user.
  236.      *
  237.      * @throws XMPPErrorException If the user is not allowed to make this request or the server does
  238.      *                       not support offline message retrieval.
  239.      * @throws NoResponseException if there was no response from the server.
  240.      * @throws NotConnectedException if the XMPP connection is not connected.
  241.      * @throws InterruptedException if the calling thread was interrupted.
  242.      */
  243.     public void deleteMessages() throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
  244.         OfflineMessageRequest request = new OfflineMessageRequest();
  245.         request.setType(IQ.Type.set);
  246.         request.setPurge(true);
  247.         connection().sendIqRequestAndWaitForResponse(request);
  248.     }
  249. }