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