001/**
002 *
003 * Copyright © 2016-2017 Florian Schmaus, Fernando Ramirez
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 */
017package org.jivesoftware.smackx.mam;
018
019import java.util.ArrayList;
020import java.util.Date;
021import java.util.HashMap;
022import java.util.List;
023import java.util.Map;
024import java.util.UUID;
025import java.util.WeakHashMap;
026
027import org.jivesoftware.smack.ConnectionCreationListener;
028import org.jivesoftware.smack.Manager;
029import org.jivesoftware.smack.SmackException.NoResponseException;
030import org.jivesoftware.smack.SmackException.NotConnectedException;
031import org.jivesoftware.smack.SmackException.NotLoggedInException;
032import org.jivesoftware.smack.StanzaCollector;
033import org.jivesoftware.smack.XMPPConnection;
034import org.jivesoftware.smack.XMPPConnectionRegistry;
035import org.jivesoftware.smack.XMPPException.XMPPErrorException;
036import org.jivesoftware.smack.filter.IQReplyFilter;
037import org.jivesoftware.smack.packet.IQ;
038import org.jivesoftware.smack.packet.Message;
039import org.jivesoftware.smack.util.Objects;
040
041import org.jivesoftware.smackx.disco.ServiceDiscoveryManager;
042import org.jivesoftware.smackx.forward.packet.Forwarded;
043import org.jivesoftware.smackx.mam.element.MamElements;
044import org.jivesoftware.smackx.mam.element.MamFinIQ;
045import org.jivesoftware.smackx.mam.element.MamPrefsIQ;
046import org.jivesoftware.smackx.mam.element.MamPrefsIQ.DefaultBehavior;
047import org.jivesoftware.smackx.mam.element.MamQueryIQ;
048import org.jivesoftware.smackx.mam.filter.MamResultFilter;
049import org.jivesoftware.smackx.rsm.packet.RSMSet;
050import org.jivesoftware.smackx.xdata.FormField;
051import org.jivesoftware.smackx.xdata.packet.DataForm;
052
053import org.jxmpp.jid.BareJid;
054import org.jxmpp.jid.EntityBareJid;
055import org.jxmpp.jid.EntityFullJid;
056import org.jxmpp.jid.Jid;
057import org.jxmpp.util.XmppDateTime;
058
059/**
060 * A Manager for Message Archive Management (XEP-0313).
061 * 
062 * @see <a href="http://xmpp.org/extensions/xep-0313.html">XEP-0313: Message
063 *      Archive Management</a>
064 * @author Florian Schmaus
065 * @author Fernando Ramirez
066 * 
067 */
068public final class MamManager extends Manager {
069
070    static {
071        XMPPConnectionRegistry.addConnectionCreationListener(new ConnectionCreationListener() {
072            @Override
073            public void connectionCreated(XMPPConnection connection) {
074                getInstanceFor(connection);
075            }
076        });
077    }
078
079    private static final Map<XMPPConnection, Map<Jid, MamManager>> INSTANCES = new WeakHashMap<>();
080
081    /**
082     * Get the singleton instance of MamManager.
083     * 
084     * @param connection
085     * @return the instance of MamManager
086     */
087    public static MamManager getInstanceFor(XMPPConnection connection) {
088        return getInstanceFor(connection, null);
089    }
090
091    public static synchronized MamManager getInstanceFor(XMPPConnection connection, Jid archiveAddress) {
092        Map<Jid, MamManager> managers = INSTANCES.get(connection);
093        if (managers == null) {
094            managers = new HashMap<>();
095            INSTANCES.put(connection, managers);
096        }
097        MamManager mamManager = managers.get(archiveAddress);
098        if (mamManager == null) {
099            mamManager = new MamManager(connection, archiveAddress);
100            managers.put(archiveAddress, mamManager);
101        }
102        return mamManager;
103    }
104
105    private final Jid archiveAddress;
106
107    private final ServiceDiscoveryManager serviceDiscoveryManager;
108
109    private MamManager(XMPPConnection connection, Jid archiveAddress) {
110        super(connection);
111        this.archiveAddress = archiveAddress;
112        serviceDiscoveryManager = ServiceDiscoveryManager.getInstanceFor(connection);
113    }
114
115    /**
116     * Query archive with a maximum amount of results.
117     * 
118     * @param max
119     * @return the MAM query result
120     * @throws NoResponseException
121     * @throws XMPPErrorException
122     * @throws NotConnectedException
123     * @throws InterruptedException
124     * @throws NotLoggedInException
125     */
126    public MamQueryResult queryArchive(Integer max) throws NoResponseException, XMPPErrorException,
127            NotConnectedException, InterruptedException, NotLoggedInException {
128        return queryArchive(null, max, null, null, null, null);
129    }
130
131    /**
132     * Query archive with a JID (only messages from/to the JID).
133     * 
134     * @param withJid
135     * @return the MAM query result
136     * @throws NoResponseException
137     * @throws XMPPErrorException
138     * @throws NotConnectedException
139     * @throws InterruptedException
140     * @throws NotLoggedInException
141     */
142    public MamQueryResult queryArchive(Jid withJid) throws NoResponseException, XMPPErrorException,
143            NotConnectedException, InterruptedException, NotLoggedInException {
144        return queryArchive(null, null, null, null, withJid, null);
145    }
146
147    /**
148     * Query archive filtering by start and/or end date. If start == null, the
149     * value of 'start' will be equal to the date/time of the earliest message
150     * stored in the archive. If end == null, the value of 'end' will be equal
151     * to the date/time of the most recent message stored in the archive.
152     * 
153     * @param start
154     * @param end
155     * @return the MAM query result
156     * @throws NoResponseException
157     * @throws XMPPErrorException
158     * @throws NotConnectedException
159     * @throws InterruptedException
160     * @throws NotLoggedInException
161     */
162    public MamQueryResult queryArchive(Date start, Date end) throws NoResponseException, XMPPErrorException,
163            NotConnectedException, InterruptedException, NotLoggedInException {
164        return queryArchive(null, null, start, end, null, null);
165    }
166
167    /**
168     * Query Archive adding filters with additional fields.
169     * 
170     * @param additionalFields
171     * @return the MAM query result
172     * @throws NoResponseException
173     * @throws XMPPErrorException
174     * @throws NotConnectedException
175     * @throws InterruptedException
176     * @throws NotLoggedInException
177     */
178    public MamQueryResult queryArchive(List<FormField> additionalFields) throws NoResponseException, XMPPErrorException,
179            NotConnectedException, InterruptedException, NotLoggedInException {
180        return queryArchive(null, null, null, null, null, additionalFields);
181    }
182
183    /**
184     * Query archive filtering by start date. The value of 'end' will be equal
185     * to the date/time of the most recent message stored in the archive.
186     * 
187     * @param start
188     * @return the MAM query result
189     * @throws NoResponseException
190     * @throws XMPPErrorException
191     * @throws NotConnectedException
192     * @throws InterruptedException
193     * @throws NotLoggedInException
194     */
195    public MamQueryResult queryArchiveWithStartDate(Date start) throws NoResponseException, XMPPErrorException,
196            NotConnectedException, InterruptedException, NotLoggedInException {
197        return queryArchive(null, null, start, null, null, null);
198    }
199
200    /**
201     * Query archive filtering by end date. The value of 'start' will be equal
202     * to the date/time of the earliest message stored in the archive.
203     * 
204     * @param end
205     * @return the MAM query result
206     * @throws NoResponseException
207     * @throws XMPPErrorException
208     * @throws NotConnectedException
209     * @throws InterruptedException
210     * @throws NotLoggedInException
211     */
212    public MamQueryResult queryArchiveWithEndDate(Date end) throws NoResponseException, XMPPErrorException,
213            NotConnectedException, InterruptedException, NotLoggedInException {
214        return queryArchive(null, null, null, end, null, null);
215    }
216
217
218    /**
219     * Query archive applying filters: max count, start date, end date, from/to
220     * JID and with additional fields.
221     * 
222     * @param max
223     * @param start
224     * @param end
225     * @param withJid
226     * @param additionalFields
227     * @return the MAM query result
228     * @throws NoResponseException
229     * @throws XMPPErrorException
230     * @throws NotConnectedException
231     * @throws InterruptedException
232     * @throws NotLoggedInException
233     */
234    public MamQueryResult queryArchive(Integer max, Date start, Date end, Jid withJid, List<FormField> additionalFields)
235            throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException,
236            NotLoggedInException {
237      return queryArchive(null, max, start, end, withJid, additionalFields);
238    }
239
240
241    /**
242     * Query an message archive like a MUC archive or a PubSub node archive, addressed by an archiveAddress, applying
243     * filters: max count, start date, end date, from/to JID and with additional fields. When archiveAddress is null the
244     * default, the server will be requested.
245     * 
246     * @param node The PubSub node name, can be null
247     * @param max
248     * @param start
249     * @param end
250     * @param withJid
251     * @param additionalFields
252     * @return the MAM query result
253     * @throws NoResponseException
254     * @throws XMPPErrorException
255     * @throws NotConnectedException
256     * @throws InterruptedException
257     * @throws NotLoggedInException
258     */
259    public MamQueryResult queryArchive(String node, Integer max, Date start, Date end, Jid withJid,
260                    List<FormField> additionalFields)
261            throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException,
262            NotLoggedInException {
263        DataForm dataForm = null;
264        String queryId = UUID.randomUUID().toString();
265
266        if (start != null || end != null || withJid != null || additionalFields != null) {
267            dataForm = getNewMamForm();
268            addStart(start, dataForm);
269            addEnd(end, dataForm);
270            addWithJid(withJid, dataForm);
271            addAdditionalFields(additionalFields, dataForm);
272        }
273
274        MamQueryIQ mamQueryIQ = new MamQueryIQ(queryId, node, dataForm);
275        mamQueryIQ.setType(IQ.Type.set);
276        mamQueryIQ.setTo(archiveAddress);
277
278        addResultsLimit(max, mamQueryIQ);
279        return queryArchive(mamQueryIQ);
280    }
281
282    private static void addAdditionalFields(List<FormField> additionalFields, DataForm dataForm) {
283        if (additionalFields == null) {
284            return;
285        }
286        for (FormField formField : additionalFields) {
287            dataForm.addField(formField);
288        }
289    }
290
291    private static void addResultsLimit(Integer max, MamQueryIQ mamQueryIQ) {
292        if (max == null) {
293            return;
294        }
295        RSMSet rsmSet = new RSMSet(max);
296        mamQueryIQ.addExtension(rsmSet);
297
298    }
299
300    private static void addWithJid(Jid withJid, DataForm dataForm) {
301        if (withJid == null) {
302            return;
303        }
304        FormField formField = new FormField("with");
305        formField.addValue(withJid.toString());
306        dataForm.addField(formField);
307    }
308
309    private static void addEnd(Date end, DataForm dataForm) {
310        if (end == null) {
311            return;
312        }
313        FormField formField = new FormField("end");
314        formField.addValue(XmppDateTime.formatXEP0082Date(end));
315        dataForm.addField(formField);
316    }
317
318    private static void addStart(Date start, DataForm dataForm) {
319        if (start == null) {
320            return;
321        }
322        FormField formField = new FormField("start");
323        formField.addValue(XmppDateTime.formatXEP0082Date(start));
324        dataForm.addField(formField);
325    }
326
327    /**
328     * Returns a page of the archive.
329     * 
330     * @param dataForm
331     * @param rsmSet
332     * @return the MAM query result
333     * @throws NoResponseException
334     * @throws XMPPErrorException
335     * @throws NotConnectedException
336     * @throws InterruptedException
337     * @throws NotLoggedInException
338     */
339    public MamQueryResult page(DataForm dataForm, RSMSet rsmSet) throws NoResponseException, XMPPErrorException,
340                    NotConnectedException, InterruptedException, NotLoggedInException {
341
342        return page(null, dataForm, rsmSet);
343
344    }
345
346    /**
347     * Returns a page of the archive.
348     * 
349     * @param node The PubSub node name, can be null
350     * @param dataForm
351     * @param rsmSet
352     * @return the MAM query result
353     * @throws NoResponseException
354     * @throws XMPPErrorException
355     * @throws NotConnectedException
356     * @throws InterruptedException
357     * @throws NotLoggedInException
358     */
359    public MamQueryResult page(String node, DataForm dataForm, RSMSet rsmSet)
360                    throws NoResponseException, XMPPErrorException,
361            NotConnectedException, InterruptedException, NotLoggedInException {
362        MamQueryIQ mamQueryIQ = new MamQueryIQ(UUID.randomUUID().toString(), node, dataForm);
363        mamQueryIQ.setType(IQ.Type.set);
364        mamQueryIQ.setTo(archiveAddress);
365        mamQueryIQ.addExtension(rsmSet);
366        return queryArchive(mamQueryIQ);
367    }
368
369    /**
370     * Returns the next page of the archive.
371     * 
372     * @param mamQueryResult
373     *            is the previous query result
374     * @param count
375     *            is the amount of messages that a page contains
376     * @return the MAM query result
377     * @throws NoResponseException
378     * @throws XMPPErrorException
379     * @throws NotConnectedException
380     * @throws InterruptedException
381     * @throws NotLoggedInException
382     */
383    public MamQueryResult pageNext(MamQueryResult mamQueryResult, int count) throws NoResponseException,
384            XMPPErrorException, NotConnectedException, InterruptedException, NotLoggedInException {
385        RSMSet previousResultRsmSet = mamQueryResult.mamFin.getRSMSet();
386        RSMSet requestRsmSet = new RSMSet(count, previousResultRsmSet.getLast(), RSMSet.PageDirection.after);
387        return page(mamQueryResult, requestRsmSet);
388    }
389
390    /**
391     * Returns the previous page of the archive.
392     * 
393     * @param mamQueryResult
394     *            is the previous query result
395     * @param count
396     *            is the amount of messages that a page contains
397     * @return the MAM query result
398     * @throws NoResponseException
399     * @throws XMPPErrorException
400     * @throws NotConnectedException
401     * @throws InterruptedException
402     * @throws NotLoggedInException
403     */
404    public MamQueryResult pagePrevious(MamQueryResult mamQueryResult, int count) throws NoResponseException,
405            XMPPErrorException, NotConnectedException, InterruptedException, NotLoggedInException {
406        RSMSet previousResultRsmSet = mamQueryResult.mamFin.getRSMSet();
407        RSMSet requestRsmSet = new RSMSet(count, previousResultRsmSet.getFirst(), RSMSet.PageDirection.before);
408        return page(mamQueryResult, requestRsmSet);
409    }
410
411    private MamQueryResult page(MamQueryResult mamQueryResult, RSMSet requestRsmSet) throws NoResponseException,
412                    XMPPErrorException, NotConnectedException, NotLoggedInException, InterruptedException {
413        ensureMamQueryResultMatchesThisManager(mamQueryResult);
414
415        return page(mamQueryResult.node, mamQueryResult.form, requestRsmSet);
416    }
417
418    /**
419     * Obtain page before the first message saved (specific chat).
420     * <p>
421     * Note that the messageUid is the XEP-0313 UID and <b>not</b> the stanza ID of the message.
422     * </p>
423     *
424     * @param chatJid
425     * @param messageUid the UID of the message of which messages before should be received.
426     * @param max
427     * @return the MAM query result
428     * @throws XMPPErrorException
429     * @throws NotLoggedInException
430     * @throws NotConnectedException
431     * @throws InterruptedException
432     * @throws NoResponseException
433     */
434    public MamQueryResult pageBefore(Jid chatJid, String messageUid, int max) throws XMPPErrorException,
435            NotLoggedInException, NotConnectedException, InterruptedException, NoResponseException {
436        RSMSet rsmSet = new RSMSet(null, messageUid, -1, -1, null, max, null, -1);
437        DataForm dataForm = getNewMamForm();
438        addWithJid(chatJid, dataForm);
439        return page(null, dataForm, rsmSet);
440    }
441
442    /**
443     * Obtain page after the last message saved (specific chat).
444     * <p>
445     * Note that the messageUid is the XEP-0313 UID and <b>not</b> the stanza ID of the message.
446     * </p>
447     *
448     * @param chatJid
449     * @param messageUid the UID of the message of which messages after should be received.
450     * @param max
451     * @return the MAM query result
452     * @throws XMPPErrorException
453     * @throws NotLoggedInException
454     * @throws NotConnectedException
455     * @throws InterruptedException
456     * @throws NoResponseException
457     */
458    public MamQueryResult pageAfter(Jid chatJid, String messageUid, int max) throws XMPPErrorException,
459            NotLoggedInException, NotConnectedException, InterruptedException, NoResponseException {
460        RSMSet rsmSet = new RSMSet(messageUid, null, -1, -1, null, max, null, -1);
461        DataForm dataForm = getNewMamForm();
462        addWithJid(chatJid, dataForm);
463        return page(null, dataForm, rsmSet);
464    }
465
466    /**
467     * Obtain the most recent page of a chat.
468     *
469     * @param chatJid
470     * @param max
471     * @return the MAM query result
472     * @throws XMPPErrorException
473     * @throws NotLoggedInException
474     * @throws NotConnectedException
475     * @throws InterruptedException
476     * @throws NoResponseException
477     */
478    public MamQueryResult mostRecentPage(Jid chatJid, int max) throws XMPPErrorException, NotLoggedInException,
479            NotConnectedException, InterruptedException, NoResponseException {
480        return pageBefore(chatJid, "", max);
481    }
482
483    /**
484     * Get the form fields supported by the server.
485     * 
486     * @return the list of form fields.
487     * @throws NoResponseException
488     * @throws XMPPErrorException
489     * @throws NotConnectedException
490     * @throws InterruptedException
491     * @throws NotLoggedInException
492     */
493    public List<FormField> retrieveFormFields() throws NoResponseException, XMPPErrorException, NotConnectedException,
494                    InterruptedException, NotLoggedInException {
495        return retrieveFormFields(null);
496    }
497
498    /**
499     * Get the form fields supported by the server.
500     * 
501     * @param node The PubSub node name, can be null
502     * @return the list of form fields.
503     * @throws NoResponseException
504     * @throws XMPPErrorException
505     * @throws NotConnectedException
506     * @throws InterruptedException
507     * @throws NotLoggedInException
508     */
509    public List<FormField> retrieveFormFields(String node)
510                    throws NoResponseException, XMPPErrorException, NotConnectedException,
511            InterruptedException, NotLoggedInException {
512        String queryId = UUID.randomUUID().toString();
513        MamQueryIQ mamQueryIq = new MamQueryIQ(queryId, node, null);
514        mamQueryIq.setTo(archiveAddress);
515
516        MamQueryIQ mamResponseQueryIq = connection().createStanzaCollectorAndSend(mamQueryIq).nextResultOrThrow();
517
518        return mamResponseQueryIq.getDataForm().getFields();
519    }
520
521    private MamQueryResult queryArchive(MamQueryIQ mamQueryIq) throws NoResponseException, XMPPErrorException,
522            NotConnectedException, InterruptedException, NotLoggedInException {
523        final XMPPConnection connection = getAuthenticatedConnectionOrThrow();
524        MamFinIQ mamFinIQ;
525
526        StanzaCollector mamFinIQCollector = connection.createStanzaCollector(new IQReplyFilter(mamQueryIq, connection));
527
528        StanzaCollector.Configuration resultCollectorConfiguration = StanzaCollector.newConfiguration()
529                .setStanzaFilter(new MamResultFilter(mamQueryIq)).setCollectorToReset(mamFinIQCollector);
530        StanzaCollector resultCollector = connection.createStanzaCollector(resultCollectorConfiguration);
531
532        try {
533            connection.sendStanza(mamQueryIq);
534            mamFinIQ = mamFinIQCollector.nextResultOrThrow();
535        } finally {
536            mamFinIQCollector.cancel();
537            resultCollector.cancel();
538        }
539
540        List<Forwarded> forwardedMessages = new ArrayList<>(resultCollector.getCollectedCount());
541
542        for (Message resultMessage = resultCollector
543                .pollResult(); resultMessage != null; resultMessage = resultCollector.pollResult()) {
544            MamElements.MamResultExtension mamResultExtension = MamElements.MamResultExtension.from(resultMessage);
545            forwardedMessages.add(mamResultExtension.getForwarded());
546        }
547
548        return new MamQueryResult(forwardedMessages, mamFinIQ, mamQueryIq.getNode(), DataForm.from(mamQueryIq));
549    }
550
551    /**
552     * MAM query result class.
553     *
554     */
555    public final static class MamQueryResult {
556        public final List<Forwarded> forwardedMessages;
557        public final MamFinIQ mamFin;
558        private final String node;
559        private final DataForm form;
560
561        private MamQueryResult(List<Forwarded> forwardedMessages, MamFinIQ mamFin, String node, DataForm form) {
562            this.forwardedMessages = forwardedMessages;
563            this.mamFin = mamFin;
564            this.node = node;
565            this.form = form;
566        }
567    }
568
569    private void ensureMamQueryResultMatchesThisManager(MamQueryResult mamQueryResult) {
570        EntityFullJid localAddress = connection().getUser();
571        EntityBareJid localBareAddress = null;
572        if (localAddress != null) {
573            localBareAddress = localAddress.asEntityBareJid();
574        }
575        boolean isLocalUserArchive = archiveAddress == null || archiveAddress.equals(localBareAddress);
576
577        Jid finIqFrom = mamQueryResult.mamFin.getFrom();
578
579        if (finIqFrom != null) {
580            if (finIqFrom.equals(archiveAddress) || (isLocalUserArchive && finIqFrom.equals(localBareAddress))) {
581                return;
582            }
583            throw new IllegalArgumentException("The given MamQueryResult is from the MAM archive '" + finIqFrom
584                            + "' whereas this MamManager is responsible for '" + archiveAddress + '\'');
585        }
586        else if (!isLocalUserArchive) {
587            throw new IllegalArgumentException(
588                            "The given MamQueryResult is from the local entity (user) MAM archive, whereas this MamManager is responsible for '"
589                                            + archiveAddress + '\'');
590        }
591    }
592
593    /**
594     * Check if MAM is supported for the XMPP connection managed by this MamManager.
595     *
596     * @return true if MAM is supported for the XMPP connection, <code>false</code>otherwhise.
597     *
598     * @throws NoResponseException
599     * @throws XMPPErrorException
600     * @throws NotConnectedException
601     * @throws InterruptedException
602     * @since 4.2.1
603     * @see <a href="https://xmpp.org/extensions/xep-0313.html#support">XEP-0313 § 7. Determining support</a>
604     */
605    public boolean isSupported() throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
606        BareJid myBareJid = connection().getUser().asBareJid();
607        return serviceDiscoveryManager.supportsFeature(myBareJid, MamElements.NAMESPACE);
608    }
609
610    /**
611     * Returns true if Message Archive Management is supported by the server.
612     * 
613     * @return true if Message Archive Management is supported by the server.
614     * @throws NotConnectedException
615     * @throws XMPPErrorException
616     * @throws NoResponseException
617     * @throws InterruptedException
618     * @deprecated use {@link #isSupported()} instead.
619     */
620    @Deprecated
621    // TODO Remove in Smack 4.3
622    public boolean isSupportedByServer()
623            throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
624        return ServiceDiscoveryManager.getInstanceFor(connection()).serverSupportsFeature(MamElements.NAMESPACE);
625    }
626
627    private static DataForm getNewMamForm() {
628        FormField field = new FormField(FormField.FORM_TYPE);
629        field.setType(FormField.Type.hidden);
630        field.addValue(MamElements.NAMESPACE);
631        DataForm form = new DataForm(DataForm.Type.submit);
632        form.addField(field);
633        return form;
634    }
635
636    /**
637     * Get the preferences stored in the server.
638     * 
639     * @return the MAM preferences result
640     * @throws NoResponseException
641     * @throws XMPPErrorException
642     * @throws NotConnectedException
643     * @throws InterruptedException
644     * @throws NotLoggedInException
645     */
646    public MamPrefsResult retrieveArchivingPreferences() throws NoResponseException, XMPPErrorException,
647            NotConnectedException, InterruptedException, NotLoggedInException {
648        MamPrefsIQ mamPrefIQ = new MamPrefsIQ();
649        return queryMamPrefs(mamPrefIQ);
650    }
651
652    /**
653     * Update the preferences in the server.
654     * 
655     * @param alwaysJids
656     *            is the list of JIDs that should always have messages to/from
657     *            archived in the user's store
658     * @param neverJids
659     *            is the list of JIDs that should never have messages to/from
660     *            archived in the user's store
661     * @param defaultBehavior
662     *            can be "roster", "always", "never" (see XEP-0313)
663     * @return the MAM preferences result
664     * @throws NoResponseException
665     * @throws XMPPErrorException
666     * @throws NotConnectedException
667     * @throws InterruptedException
668     * @throws NotLoggedInException
669     */
670    public MamPrefsResult updateArchivingPreferences(List<Jid> alwaysJids, List<Jid> neverJids, DefaultBehavior defaultBehavior)
671            throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException,
672            NotLoggedInException {
673        Objects.requireNonNull(defaultBehavior, "Default behavior must be set");
674        MamPrefsIQ mamPrefIQ = new MamPrefsIQ(alwaysJids, neverJids, defaultBehavior);
675        return queryMamPrefs(mamPrefIQ);
676    }
677
678    /**
679     * MAM preferences result class.
680     *
681     */
682    public final static class MamPrefsResult {
683        public final MamPrefsIQ mamPrefs;
684        public final DataForm form;
685
686        private MamPrefsResult(MamPrefsIQ mamPrefs, DataForm form) {
687            this.mamPrefs = mamPrefs;
688            this.form = form;
689        }
690    }
691
692    private MamPrefsResult queryMamPrefs(MamPrefsIQ mamPrefsIQ) throws NoResponseException, XMPPErrorException,
693            NotConnectedException, InterruptedException, NotLoggedInException {
694        final XMPPConnection connection = getAuthenticatedConnectionOrThrow();
695
696        MamPrefsIQ mamPrefsResultIQ = connection.createStanzaCollectorAndSend(mamPrefsIQ).nextResultOrThrow();
697
698        return new MamPrefsResult(mamPrefsResultIQ, DataForm.from(mamPrefsIQ));
699    }
700
701}