001/** 002 * 003 * Copyright 2003-2007 Jive Software, 2020 Florian Schmaus. 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.offline; 019 020import java.util.ArrayList; 021import java.util.List; 022import java.util.Map; 023import java.util.WeakHashMap; 024import java.util.logging.Level; 025import java.util.logging.Logger; 026 027import org.jivesoftware.smack.Manager; 028import org.jivesoftware.smack.SmackException.NoResponseException; 029import org.jivesoftware.smack.SmackException.NotConnectedException; 030import org.jivesoftware.smack.StanzaCollector; 031import org.jivesoftware.smack.XMPPConnection; 032import org.jivesoftware.smack.XMPPException.XMPPErrorException; 033import org.jivesoftware.smack.filter.AndFilter; 034import org.jivesoftware.smack.filter.StanzaExtensionFilter; 035import org.jivesoftware.smack.filter.StanzaFilter; 036import org.jivesoftware.smack.filter.StanzaTypeFilter; 037import org.jivesoftware.smack.packet.IQ; 038import org.jivesoftware.smack.packet.Message; 039import org.jivesoftware.smack.packet.Stanza; 040 041import org.jivesoftware.smackx.disco.ServiceDiscoveryManager; 042import org.jivesoftware.smackx.disco.packet.DiscoverInfo; 043import org.jivesoftware.smackx.disco.packet.DiscoverItems; 044import org.jivesoftware.smackx.offline.packet.OfflineMessageInfo; 045import org.jivesoftware.smackx.offline.packet.OfflineMessageRequest; 046import org.jivesoftware.smackx.xdata.packet.DataForm; 047 048/** 049 * The OfflineMessageManager helps manage offline messages even before the user has sent an 050 * available presence. When a user asks for his offline messages before sending an available 051 * presence then the server will not send a flood with all the offline messages when the user 052 * becomes online. The server will not send a flood with all the offline messages to the session 053 * that made the offline messages request or to any other session used by the user that becomes 054 * online.<p> 055 * 056 * Once the session that made the offline messages request has been closed and the user becomes 057 * offline in all the resources then the server will resume storing the messages offline and will 058 * send all the offline messages to the user when he becomes online. Therefore, the server will 059 * flood the user when he becomes online unless the user uses this class to manage his offline 060 * messages. 061 * 062 * @author Gaston Dombiak 063 */ 064public final class OfflineMessageManager extends Manager { 065 066 private static final Logger LOGGER = Logger.getLogger(OfflineMessageManager.class.getName()); 067 068 private static final String namespace = "http://jabber.org/protocol/offline"; 069 070 private static final Map<XMPPConnection, OfflineMessageManager> INSTANCES = new WeakHashMap<>(); 071 072 private static final StanzaFilter PACKET_FILTER = new AndFilter(new StanzaExtensionFilter( 073 new OfflineMessageInfo()), StanzaTypeFilter.MESSAGE); 074 075 private ServiceDiscoveryManager serviceDiscoveryManager; 076 077 private OfflineMessageManager(XMPPConnection connection) { 078 super(connection); 079 this.serviceDiscoveryManager = ServiceDiscoveryManager.getInstanceFor(connection); 080 } 081 082 public static synchronized OfflineMessageManager getInstanceFor(XMPPConnection connection) { 083 OfflineMessageManager manager = INSTANCES.get(connection); 084 if (manager == null) { 085 manager = new OfflineMessageManager(connection); 086 INSTANCES.put(connection, manager); 087 } 088 return manager; 089 } 090 091 /** 092 * Returns true if the server supports Flexible Offline Message Retrieval. When the server 093 * supports Flexible Offline Message Retrieval it is possible to get the header of the offline 094 * messages, get specific messages, delete specific messages, etc. 095 * 096 * @return a boolean indicating if the server supports Flexible Offline Message Retrieval. 097 * @throws XMPPErrorException If the user is not allowed to make this request. 098 * @throws NoResponseException if there was no response from the server. 099 * @throws NotConnectedException if the XMPP connection is not connected. 100 * @throws InterruptedException if the calling thread was interrupted. 101 */ 102 public boolean supportsFlexibleRetrieval() throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { 103 return serviceDiscoveryManager.serverSupportsFeature(namespace); 104 } 105 106 /** 107 * Returns the number of offline messages for the user of the connection. 108 * 109 * @return the number of offline messages for the user of the connection. 110 * @throws XMPPErrorException If the user is not allowed to make this request or the server does 111 * not support offline message retrieval. 112 * @throws NoResponseException if there was no response from the server. 113 * @throws NotConnectedException if the XMPP connection is not connected. 114 * @throws InterruptedException if the calling thread was interrupted. 115 */ 116 public int getMessageCount() throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { 117 DiscoverInfo info = serviceDiscoveryManager.discoverInfo(null, namespace); 118 DataForm dataForm = DataForm.from(info, namespace); 119 if (dataForm == null) { 120 return 0; 121 } 122 String numberOfMessagesString = dataForm.getField("number_of_messages").getFirstValue(); 123 return Integer.parseInt(numberOfMessagesString); 124 } 125 126 /** 127 * Returns a List of <code>OfflineMessageHeader</code> that keep information about the 128 * offline message. The OfflineMessageHeader includes a stamp that could be used to retrieve 129 * the complete message or delete the specific message. 130 * 131 * @return a List of <code>OfflineMessageHeader</code> that keep information about the offline 132 * message. 133 * @throws XMPPErrorException If the user is not allowed to make this request or the server does 134 * not support offline message retrieval. 135 * @throws NoResponseException if there was no response from the server. 136 * @throws NotConnectedException if the XMPP connection is not connected. 137 * @throws InterruptedException if the calling thread was interrupted. 138 */ 139 public List<OfflineMessageHeader> getHeaders() throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { 140 List<OfflineMessageHeader> answer = new ArrayList<>(); 141 DiscoverItems items = serviceDiscoveryManager.discoverItems(null, namespace); 142 for (DiscoverItems.Item item : items.getItems()) { 143 answer.add(new OfflineMessageHeader(item)); 144 } 145 return answer; 146 } 147 148 /** 149 * Returns a List of the offline <code>Messages</code> whose stamp matches the specified 150 * request. The request will include the list of stamps that uniquely identifies 151 * the offline messages to retrieve. The returned offline messages will not be deleted 152 * from the server. Use {@link #deleteMessages(java.util.List)} to delete the messages. 153 * 154 * @param nodes the list of stamps that uniquely identifies offline message. 155 * @return a List with the offline <code>Messages</code> that were received as part of 156 * this request. 157 * @throws XMPPErrorException If the user is not allowed to make this request or the server does 158 * not support offline message retrieval. 159 * @throws NoResponseException if there was no response from the server. 160 * @throws NotConnectedException if the XMPP connection is not connected. 161 * @throws InterruptedException if the calling thread was interrupted. 162 */ 163 public List<Message> getMessages(final List<String> nodes) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { 164 List<Message> messages = new ArrayList<>(nodes.size()); 165 OfflineMessageRequest request = new OfflineMessageRequest(); 166 for (String node : nodes) { 167 OfflineMessageRequest.Item item = new OfflineMessageRequest.Item(node); 168 item.setAction("view"); 169 request.addItem(item); 170 } 171 // Filter offline messages that were requested by this request 172 StanzaFilter messageFilter = new AndFilter(PACKET_FILTER, new StanzaFilter() { 173 @Override 174 public boolean accept(Stanza packet) { 175 OfflineMessageInfo info = packet.getExtension(OfflineMessageInfo.class); 176 return nodes.contains(info.getNode()); 177 } 178 }); 179 int pendingNodes = nodes.size(); 180 try (StanzaCollector messageCollector = connection().createStanzaCollector(messageFilter)) { 181 connection().createStanzaCollectorAndSend(request).nextResultOrThrow(); 182 // Collect the received offline messages 183 Message message; 184 do { 185 message = messageCollector.nextResult(); 186 if (message != null) { 187 messages.add(message); 188 pendingNodes--; 189 } else if (message == null && pendingNodes > 0) { 190 LOGGER.log(Level.WARNING, 191 "Did not receive all expected offline messages. " + pendingNodes + " are missing."); 192 } 193 } while (message != null && pendingNodes > 0); 194 } 195 return messages; 196 } 197 198 /** 199 * Returns a List of Messages with all the offline <code>Messages</code> of the user. The returned offline 200 * messages will not be deleted from the server. Use {@link #deleteMessages(java.util.List)} 201 * to delete the messages. 202 * 203 * @return a List with all the offline <code>Messages</code> of the user. 204 * @throws XMPPErrorException If the user is not allowed to make this request or the server does 205 * not support offline message retrieval. 206 * @throws NoResponseException if there was no response from the server. 207 * @throws NotConnectedException if the XMPP connection is not connected. 208 * @throws InterruptedException if the calling thread was interrupted. 209 */ 210 public List<Message> getMessages() throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { 211 OfflineMessageRequest request = new OfflineMessageRequest(); 212 request.setFetch(true); 213 214 StanzaCollector resultCollector = connection().createStanzaCollectorAndSend(request); 215 StanzaCollector.Configuration messageCollectorConfiguration = StanzaCollector.newConfiguration().setStanzaFilter(PACKET_FILTER).setCollectorToReset(resultCollector); 216 217 List<Message> messages; 218 try (StanzaCollector messageCollector = connection().createStanzaCollector(messageCollectorConfiguration)) { 219 resultCollector.nextResultOrThrow(); 220 // Be extra safe, cancel the message collector right here so that it does not collector 221 // other messages that eventually match (although I've no idea how this could happen in 222 // case of XEP-13). 223 messageCollector.cancel(); 224 messages = new ArrayList<>(messageCollector.getCollectedCount()); 225 Message message; 226 while ((message = messageCollector.pollResult()) != null) { 227 messages.add(message); 228 } 229 } 230 return messages; 231 } 232 233 /** 234 * Deletes the specified list of offline messages. The request will include the list of 235 * stamps that uniquely identifies the offline messages to delete. 236 * 237 * @param nodes the list of stamps that uniquely identifies offline message. 238 * @throws XMPPErrorException If the user is not allowed to make this request or the server does 239 * not support offline message retrieval. 240 * @throws NoResponseException if there was no response from the server. 241 * @throws NotConnectedException if the XMPP connection is not connected. 242 * @throws InterruptedException if the calling thread was interrupted. 243 */ 244 public void deleteMessages(List<String> nodes) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { 245 OfflineMessageRequest request = new OfflineMessageRequest(); 246 request.setType(IQ.Type.set); 247 for (String node : nodes) { 248 OfflineMessageRequest.Item item = new OfflineMessageRequest.Item(node); 249 item.setAction("remove"); 250 request.addItem(item); 251 } 252 connection().createStanzaCollectorAndSend(request).nextResultOrThrow(); 253 } 254 255 /** 256 * Deletes all offline messages of the user. 257 * 258 * @throws XMPPErrorException If the user is not allowed to make this request or the server does 259 * not support offline message retrieval. 260 * @throws NoResponseException if there was no response from the server. 261 * @throws NotConnectedException if the XMPP connection is not connected. 262 * @throws InterruptedException if the calling thread was interrupted. 263 */ 264 public void deleteMessages() throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { 265 OfflineMessageRequest request = new OfflineMessageRequest(); 266 request.setType(IQ.Type.set); 267 request.setPurge(true); 268 connection().createStanzaCollectorAndSend(request).nextResultOrThrow(); 269 } 270}