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