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}