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