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.PacketCollector;
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     */
081    public boolean supportsFlexibleRetrieval() throws NoResponseException, XMPPErrorException, NotConnectedException {
082        return ServiceDiscoveryManager.getInstanceFor(connection).serverSupportsFeature(namespace);
083    }
084
085    /**
086     * Returns the number of offline messages for the user of the connection.
087     *
088     * @return the number of offline messages for the user of the connection.
089     * @throws XMPPErrorException If the user is not allowed to make this request or the server does
090     *                       not support offline message retrieval.
091     * @throws NoResponseException if there was no response from the server.
092     * @throws NotConnectedException 
093     */
094    public int getMessageCount() throws NoResponseException, XMPPErrorException, NotConnectedException {
095        DiscoverInfo info = ServiceDiscoveryManager.getInstanceFor(connection).discoverInfo(null,
096                namespace);
097        Form extendedInfo = Form.getFormFrom(info);
098        if (extendedInfo != null) {
099            String value = extendedInfo.getField("number_of_messages").getValues().get(0);
100            return Integer.parseInt(value);
101        }
102        return 0;
103    }
104
105    /**
106     * Returns a List of <tt>OfflineMessageHeader</tt> that keep information about the
107     * offline message. The OfflineMessageHeader includes a stamp that could be used to retrieve
108     * the complete message or delete the specific message.
109     *
110     * @return a List of <tt>OfflineMessageHeader</tt> that keep information about the offline
111     *         message.
112     * @throws XMPPErrorException If the user is not allowed to make this request or the server does
113     *                       not support offline message retrieval.
114     * @throws NoResponseException if there was no response from the server.
115     * @throws NotConnectedException 
116     */
117    public List<OfflineMessageHeader> getHeaders() throws NoResponseException, XMPPErrorException, NotConnectedException {
118        List<OfflineMessageHeader> answer = new ArrayList<OfflineMessageHeader>();
119        DiscoverItems items = ServiceDiscoveryManager.getInstanceFor(connection).discoverItems(
120                null, namespace);
121        for (DiscoverItems.Item item : items.getItems()) {
122            answer.add(new OfflineMessageHeader(item));
123        }
124        return answer;
125    }
126
127    /**
128     * Returns a List of the offline <tt>Messages</tt> whose stamp matches the specified
129     * request. The request will include the list of stamps that uniquely identifies
130     * the offline messages to retrieve. The returned offline messages will not be deleted
131     * from the server. Use {@link #deleteMessages(java.util.List)} to delete the messages.
132     *
133     * @param nodes the list of stamps that uniquely identifies offline message.
134     * @return a List with the offline <tt>Messages</tt> that were received as part of
135     *         this request.
136     * @throws XMPPErrorException If the user is not allowed to make this request or the server does
137     *                       not support offline message retrieval.
138     * @throws NoResponseException if there was no response from the server.
139     * @throws NotConnectedException 
140     */
141    public List<Message> getMessages(final List<String> nodes) throws NoResponseException, XMPPErrorException, NotConnectedException {
142        List<Message> messages = new ArrayList<Message>();
143        OfflineMessageRequest request = new OfflineMessageRequest();
144        for (String node : nodes) {
145            OfflineMessageRequest.Item item = new OfflineMessageRequest.Item(node);
146            item.setAction("view");
147            request.addItem(item);
148        }
149        // Filter offline messages that were requested by this request
150        StanzaFilter messageFilter = new AndFilter(PACKET_FILTER, new StanzaFilter() {
151            public boolean accept(Stanza packet) {
152                OfflineMessageInfo info = (OfflineMessageInfo) packet.getExtension("offline",
153                        namespace);
154                return nodes.contains(info.getNode());
155            }
156        });
157        PacketCollector messageCollector = connection.createPacketCollector(messageFilter);
158        try {
159            connection.createPacketCollectorAndSend(request).nextResultOrThrow();
160            // Collect the received offline messages
161            Message message = messageCollector.nextResult();
162            while (message != null) {
163                messages.add(message);
164                message = messageCollector.nextResult();
165            }
166        }
167        finally {
168            // Stop queuing offline messages
169            messageCollector.cancel();
170        }
171        return messages;
172    }
173
174    /**
175     * Returns a List of Messages with all the offline <tt>Messages</tt> of the user. The returned offline
176     * messages will not be deleted from the server. Use {@link #deleteMessages(java.util.List)}
177     * to delete the messages.
178     *
179     * @return a List with all the offline <tt>Messages</tt> of the user.
180     * @throws XMPPErrorException If the user is not allowed to make this request or the server does
181     *                       not support offline message retrieval.
182     * @throws NoResponseException if there was no response from the server.
183     * @throws NotConnectedException 
184     */
185    public List<Message> getMessages() throws NoResponseException, XMPPErrorException, NotConnectedException {
186        OfflineMessageRequest request = new OfflineMessageRequest();
187        request.setFetch(true);
188
189        PacketCollector resultCollector = connection.createPacketCollectorAndSend(request);
190        PacketCollector.Configuration messageCollectorConfiguration = PacketCollector.newConfiguration().setStanzaFilter(PACKET_FILTER).setCollectorToReset(resultCollector);
191        PacketCollector messageCollector = connection.createPacketCollector(messageCollectorConfiguration);
192
193        List<Message> messages = null;
194        try {
195            resultCollector.nextResultOrThrow();
196            // Be extra safe, cancel the message collector right here so that it does not collector
197            // other messages that eventually match (although I've no idea how this could happen in
198            // case of XEP-13).
199            messageCollector.cancel();
200            messages = new ArrayList<>(messageCollector.getCollectedCount());
201            Message message;
202            while ((message = messageCollector.pollResult()) != null) {
203                messages.add(message);
204            }
205        }
206        finally {
207            // Ensure that the message collector is canceled even if nextResultOrThrow threw. It
208            // doesn't matter if we cancel the message collector twice
209            messageCollector.cancel();
210        }
211        return messages;
212    }
213
214    /**
215     * Deletes the specified list of offline messages. The request will include the list of
216     * stamps that uniquely identifies the offline messages to delete.
217     *
218     * @param nodes the list of stamps that uniquely identifies offline message.
219     * @throws XMPPErrorException If the user is not allowed to make this request or the server does
220     *                       not support offline message retrieval.
221     * @throws NoResponseException if there was no response from the server.
222     * @throws NotConnectedException 
223     */
224    public void deleteMessages(List<String> nodes) throws NoResponseException, XMPPErrorException, NotConnectedException {
225        OfflineMessageRequest request = new OfflineMessageRequest();
226        request.setType(IQ.Type.set);
227        for (String node : nodes) {
228            OfflineMessageRequest.Item item = new OfflineMessageRequest.Item(node);
229            item.setAction("remove");
230            request.addItem(item);
231        }
232        connection.createPacketCollectorAndSend(request).nextResultOrThrow();
233    }
234
235    /**
236     * Deletes all offline messages of the user.
237     *
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 
242     */
243    public void deleteMessages() throws NoResponseException, XMPPErrorException, NotConnectedException {
244        OfflineMessageRequest request = new OfflineMessageRequest();
245        request.setType(IQ.Type.set);
246        request.setPurge(true);
247        connection.createPacketCollectorAndSend(request).nextResultOrThrow();
248    }
249}