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