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