/** * * Copyright 2003-2007 Jive Software, 2020 Florian Schmaus. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.jivesoftware.smackx.offline; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.WeakHashMap; import java.util.logging.Level; import java.util.logging.Logger; import org.jivesoftware.smack.Manager; import org.jivesoftware.smack.SmackException.NoResponseException; import org.jivesoftware.smack.SmackException.NotConnectedException; import org.jivesoftware.smack.StanzaCollector; import org.jivesoftware.smack.XMPPConnection; import org.jivesoftware.smack.XMPPException.XMPPErrorException; import org.jivesoftware.smack.filter.AndFilter; import org.jivesoftware.smack.filter.StanzaExtensionFilter; import org.jivesoftware.smack.filter.StanzaFilter; import org.jivesoftware.smack.filter.StanzaTypeFilter; import org.jivesoftware.smack.packet.IQ; import org.jivesoftware.smack.packet.Message; import org.jivesoftware.smack.packet.Stanza; import org.jivesoftware.smackx.disco.ServiceDiscoveryManager; import org.jivesoftware.smackx.disco.packet.DiscoverInfo; import org.jivesoftware.smackx.disco.packet.DiscoverItems; import org.jivesoftware.smackx.offline.packet.OfflineMessageInfo; import org.jivesoftware.smackx.offline.packet.OfflineMessageRequest; import org.jivesoftware.smackx.xdata.packet.DataForm; /** * The OfflineMessageManager helps manage offline messages even before the user has sent an * available presence. When a user asks for his offline messages before sending an available * presence then the server will not send a flood with all the offline messages when the user * becomes online. The server will not send a flood with all the offline messages to the session * that made the offline messages request or to any other session used by the user that becomes * online.

* * Once the session that made the offline messages request has been closed and the user becomes * offline in all the resources then the server will resume storing the messages offline and will * send all the offline messages to the user when he becomes online. Therefore, the server will * flood the user when he becomes online unless the user uses this class to manage his offline * messages. * * @author Gaston Dombiak */ public final class OfflineMessageManager extends Manager { private static final Logger LOGGER = Logger.getLogger(OfflineMessageManager.class.getName()); public static final String NAMESPACE = "http://jabber.org/protocol/offline"; private static final Map INSTANCES = new WeakHashMap<>(); private static final StanzaFilter PACKET_FILTER = new AndFilter(new StanzaExtensionFilter( new OfflineMessageInfo()), StanzaTypeFilter.MESSAGE); private ServiceDiscoveryManager serviceDiscoveryManager; private OfflineMessageManager(XMPPConnection connection) { super(connection); this.serviceDiscoveryManager = ServiceDiscoveryManager.getInstanceFor(connection); } public static synchronized OfflineMessageManager getInstanceFor(XMPPConnection connection) { OfflineMessageManager manager = INSTANCES.get(connection); if (manager == null) { manager = new OfflineMessageManager(connection); INSTANCES.put(connection, manager); } return manager; } /** * Returns true if the server supports Flexible Offline Message Retrieval. When the server * supports Flexible Offline Message Retrieval it is possible to get the header of the offline * messages, get specific messages, delete specific messages, etc. * * @return a boolean indicating if the server supports Flexible Offline Message Retrieval. * @throws XMPPErrorException If the user is not allowed to make this request. * @throws NoResponseException if there was no response from the server. * @throws NotConnectedException if the XMPP connection is not connected. * @throws InterruptedException if the calling thread was interrupted. */ public boolean supportsFlexibleRetrieval() throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { return serviceDiscoveryManager.serverSupportsFeature(NAMESPACE); } /** * Returns the number of offline messages for the user of the connection. * * @return the number of offline messages for the user of the connection. * @throws XMPPErrorException If the user is not allowed to make this request or the server does * not support offline message retrieval. * @throws NoResponseException if there was no response from the server. * @throws NotConnectedException if the XMPP connection is not connected. * @throws InterruptedException if the calling thread was interrupted. */ public int getMessageCount() throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { DiscoverInfo info = serviceDiscoveryManager.discoverInfo(null, NAMESPACE); DataForm dataForm = DataForm.from(info, NAMESPACE); if (dataForm == null) { return 0; } String numberOfMessagesString = dataForm.getField("number_of_messages").getFirstValue(); return Integer.parseInt(numberOfMessagesString); } /** * Returns a List of OfflineMessageHeader that keep information about the * offline message. The OfflineMessageHeader includes a stamp that could be used to retrieve * the complete message or delete the specific message. * * @return a List of OfflineMessageHeader that keep information about the offline * message. * @throws XMPPErrorException If the user is not allowed to make this request or the server does * not support offline message retrieval. * @throws NoResponseException if there was no response from the server. * @throws NotConnectedException if the XMPP connection is not connected. * @throws InterruptedException if the calling thread was interrupted. */ public List getHeaders() throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { List answer = new ArrayList<>(); DiscoverItems items = serviceDiscoveryManager.discoverItems(null, NAMESPACE); for (DiscoverItems.Item item : items.getItems()) { answer.add(new OfflineMessageHeader(item)); } return answer; } /** * Returns a List of the offline Messages whose stamp matches the specified * request. The request will include the list of stamps that uniquely identifies * the offline messages to retrieve. The returned offline messages will not be deleted * from the server. Use {@link #deleteMessages(java.util.List)} to delete the messages. * * @param nodes the list of stamps that uniquely identifies offline message. * @return a List with the offline Messages that were received as part of * this request. * @throws XMPPErrorException If the user is not allowed to make this request or the server does * not support offline message retrieval. * @throws NoResponseException if there was no response from the server. * @throws NotConnectedException if the XMPP connection is not connected. * @throws InterruptedException if the calling thread was interrupted. */ public List getMessages(final List nodes) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { List messages = new ArrayList<>(nodes.size()); OfflineMessageRequest request = new OfflineMessageRequest(); for (String node : nodes) { OfflineMessageRequest.Item item = new OfflineMessageRequest.Item(node); item.setAction("view"); request.addItem(item); } // Filter offline messages that were requested by this request StanzaFilter messageFilter = new AndFilter(PACKET_FILTER, new StanzaFilter() { @Override public boolean accept(Stanza packet) { OfflineMessageInfo info = packet.getExtension(OfflineMessageInfo.class); return nodes.contains(info.getNode()); } }); int pendingNodes = nodes.size(); try (StanzaCollector messageCollector = connection().createStanzaCollector(messageFilter)) { connection().createStanzaCollectorAndSend(request).nextResultOrThrow(); // Collect the received offline messages Message message; do { message = messageCollector.nextResult(); if (message != null) { messages.add(message); pendingNodes--; } else if (message == null && pendingNodes > 0) { LOGGER.log(Level.WARNING, "Did not receive all expected offline messages. " + pendingNodes + " are missing."); } } while (message != null && pendingNodes > 0); } return messages; } /** * Returns a List of Messages with all the offline Messages of the user. The returned offline * messages will not be deleted from the server. Use {@link #deleteMessages(java.util.List)} * to delete the messages. * * @return a List with all the offline Messages of the user. * @throws XMPPErrorException If the user is not allowed to make this request or the server does * not support offline message retrieval. * @throws NoResponseException if there was no response from the server. * @throws NotConnectedException if the XMPP connection is not connected. * @throws InterruptedException if the calling thread was interrupted. */ public List getMessages() throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { OfflineMessageRequest request = new OfflineMessageRequest(); request.setFetch(true); StanzaCollector resultCollector = connection().createStanzaCollectorAndSend(request); StanzaCollector.Configuration messageCollectorConfiguration = StanzaCollector.newConfiguration().setStanzaFilter(PACKET_FILTER).setCollectorToReset(resultCollector); List messages; try (StanzaCollector messageCollector = connection().createStanzaCollector(messageCollectorConfiguration)) { resultCollector.nextResultOrThrow(); // Be extra safe, cancel the message collector right here so that it does not collector // other messages that eventually match (although I've no idea how this could happen in // case of XEP-13). messageCollector.cancel(); messages = new ArrayList<>(messageCollector.getCollectedCount()); Message message; while ((message = messageCollector.pollResult()) != null) { messages.add(message); } } return messages; } /** * Deletes the specified list of offline messages. The request will include the list of * stamps that uniquely identifies the offline messages to delete. * * @param nodes the list of stamps that uniquely identifies offline message. * @throws XMPPErrorException If the user is not allowed to make this request or the server does * not support offline message retrieval. * @throws NoResponseException if there was no response from the server. * @throws NotConnectedException if the XMPP connection is not connected. * @throws InterruptedException if the calling thread was interrupted. */ public void deleteMessages(List nodes) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { OfflineMessageRequest request = new OfflineMessageRequest(); request.setType(IQ.Type.set); for (String node : nodes) { OfflineMessageRequest.Item item = new OfflineMessageRequest.Item(node); item.setAction("remove"); request.addItem(item); } connection().createStanzaCollectorAndSend(request).nextResultOrThrow(); } /** * Deletes all offline messages of the user. * * @throws XMPPErrorException If the user is not allowed to make this request or the server does * not support offline message retrieval. * @throws NoResponseException if there was no response from the server. * @throws NotConnectedException if the XMPP connection is not connected. * @throws InterruptedException if the calling thread was interrupted. */ public void deleteMessages() throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { OfflineMessageRequest request = new OfflineMessageRequest(); request.setType(IQ.Type.set); request.setPurge(true); connection().createStanzaCollectorAndSend(request).nextResultOrThrow(); } }