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