diff --git a/documentation/extensions/mam.md b/documentation/extensions/mam.md index 868163b16..012132c58 100644 --- a/documentation/extensions/mam.md +++ b/documentation/extensions/mam.md @@ -3,192 +3,4 @@ Message Archive Management [Back](index.md) -Query and control an archive of messages stored on a server. - - * Check MAM support - * Query archive - * Paging - * Get form fields - * Get preferences - * Update preferences - - -**XEP related:** [XEP-0313](http://xmpp.org/extensions/xep-0313.html) - - -Get an instance of Message Archive Management Manager ------------------------------------------------------ - -``` -MamManager mamManager = MamManager.getInstanceFor(connection); -``` - - -Check MAM support ------------------ - -``` -boolean isSupported = mamManager.isSupported(); -``` - - -Query archive -------------- - -``` -MamQueryResult mamQueryResult = mamManager.queryArchive(max); -``` -*max* is an `Integer` - -or - -``` -MamQueryResult mamQueryResult = mamManager.queryArchive(withJid); -``` -*withJid* is a `Jid` - -or - -``` -MamQueryResult mamQueryResult = mamManager.queryArchive(start, end); -``` -*start* is a `Date` - -*end* is a `Date` - -or - -``` -MamQueryResult mamQueryResult = mamManager.queryArchive(additionalFields); -``` -*additionalFields* is a `List` - -or - -``` -MamQueryResult mamQueryResult = mamManager.queryArchiveWithStartDate(start); -``` -*start* is a `Date` - -or - -``` -MamQueryResult mamQueryResult = mamManager.queryArchiveWithEndDate(end); -``` -*end* is a `Date` - -or - -``` -MamQueryResult mamQueryResult = mamManager.queryArchive(max, start, end, withJid, additionalFields); -``` -*max* is an `Integer` - -*start* is a `Date` - -*end* is a `Date` - -*withJid* is a `Jid` - -*additionalFields* is a `List` - - -**Get data from mamQueryResult object** - -``` -// Get forwarded messages -List forwardedMessages = mamQueryResult.forwardedMessages; - -// Get fin IQ -MamFinIQ mamFinIQ = mamQueryResult.mamFin; -``` - - -Paging ------- - -**Get a page** - -``` -MamQueryResult mamQueryResult = mamManager.page(dataForm, rsmSet); -``` -*dataForm* is a `DataForm` - -*rsmSet* is a `RSMSet` - - -**Get the next page** - -``` -MamQueryResult mamQueryResult = mamManager.pageNext(previousMamQueryResult, count); -``` -*previousMamQueryResult* is a `MamQueryResult` - -*count* is an `int` - - -**Get page before the first message saved (specific chat)** - -``` -MamQueryResult mamQueryResult = mamManager.pageBefore(chatJid, firstMessageId, max); -``` -*chatJid* is a `Jid` - -*firstMessageId* is a `String` - -*max* is an `int` - - -**Get page after the last message saved (specific chat)** - -``` -MamQueryResult mamQueryResult = mamManager.pageAfter(chatJid, lastMessageId, max); -``` -*chatJid* is a `Jid` - -*lastMessageId* is a `String` - -*max* is an `int` - - -Get form fields ---------------- - -``` -List formFields = mamManager.retrieveFormFields(); -``` - - -Get preferences ---------------- - -``` -MamPrefsResult mamPrefsResult = mamManager.retrieveArchivingPreferences(); - -// Get preferences IQ -MamPrefsIQ mamPrefs = mamPrefsResult.mamPrefs; - -// Obtain always and never list -List alwaysJids = mamPrefs.getAlwaysJids(); -List neverJids = mamPrefs.getNeverJids(); - -// Obtain default behaviour (can be 'always', 'never' or 'roster') -DefaultBehavior defaultBehavior = mamPrefs.getDefault(); - -// Get the data form -DataForm dataForm = mamPrefsResult.form; -``` - - -Update preferences ------------------- - -``` -MamPrefsResult mamPrefsResult = mamManager.updateArchivingPreferences(alwaysJids, neverJids, defaultBehavior); -``` -*alwaysJids* is a `List` - -*neverJids* is a `List` - -*defaultBehavior* is a `DefaultBehavior` - +See the javadoc of `MamManager` for details. diff --git a/smack-experimental/src/main/java/org/jivesoftware/smackx/mam/MamManager.java b/smack-experimental/src/main/java/org/jivesoftware/smackx/mam/MamManager.java index 762bdaa9e..45496060d 100644 --- a/smack-experimental/src/main/java/org/jivesoftware/smackx/mam/MamManager.java +++ b/smack-experimental/src/main/java/org/jivesoftware/smackx/mam/MamManager.java @@ -16,7 +16,9 @@ */ package org.jivesoftware.smackx.mam; +import java.text.ParseException; import java.util.ArrayList; +import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.List; @@ -36,11 +38,14 @@ import org.jivesoftware.smack.XMPPException.XMPPErrorException; import org.jivesoftware.smack.filter.IQReplyFilter; import org.jivesoftware.smack.packet.IQ; import org.jivesoftware.smack.packet.Message; +import org.jivesoftware.smack.packet.Stanza; import org.jivesoftware.smack.util.Objects; +import org.jivesoftware.smack.util.StringUtils; import org.jivesoftware.smackx.disco.ServiceDiscoveryManager; import org.jivesoftware.smackx.forward.packet.Forwarded; import org.jivesoftware.smackx.mam.element.MamElements; +import org.jivesoftware.smackx.mam.element.MamElements.MamResultExtension; import org.jivesoftware.smackx.mam.element.MamFinIQ; import org.jivesoftware.smackx.mam.element.MamPrefsIQ; import org.jivesoftware.smackx.mam.element.MamPrefsIQ.DefaultBehavior; @@ -54,10 +59,95 @@ import org.jivesoftware.smackx.xdata.packet.DataForm; import org.jxmpp.jid.EntityBareJid; import org.jxmpp.jid.EntityFullJid; import org.jxmpp.jid.Jid; -import org.jxmpp.util.XmppDateTime; /** - * A Manager for Message Archive Management (XEP-0313). + * A Manager for Message Archive Management (MAM, XEP-0313). + * + *

Get an instance of a manager for a message archive

+ * + * In order to work with {@link MamManager} you need to obtain an instance for a particular archive. + * To get the instance for the default archive on the user's server, use the {@link #getInstanceFor(XMPPConnection)} method. + * + *
+ * {@code
+ * XMPPConnection connection = ...
+ * MamManager mamManager = MamManager.getInstanceFor(connection);
+ * }
+ * 
+ * + * If you want to retrieve a manager for a different archive use {@link #getInstanceFor(XMPPConnection, Jid)}, which takes the archive's XMPP address as second argument. + * + *

Check if MAM is supported

+ * + * After you got your manager instance, you probably first want to check if MAM is supported. + * Simply use {@link #isSupported()} to check if there is a MAM archive available. + * + *
+ * {@code
+ * boolean isSupported = mamManager.isSupported();
+ * }
+ * 
+ * + *

Message Archive Preferences

+ * + * After you have verified that the MAM is supported, you probably want to configure the archive first before using it. + * One of the most important preference is to enable MAM for your account. + * Some servers set up new accounts with MAM disabled by default. + * You can do so by calling {@link #enableMamForAllMessages()}. + * + *

Retrieve current preferences

+ * + * The archive's preferences can be retrieved using {@link #retrieveArchivingPreferences()}. + * + *

Update preferences

+ * + * Use {@link MamPrefsResult#asMamPrefs()} to get a modifiable {@link MamPrefs} instance. + * After performing the desired changes, use {@link #updateArchivingPreferences(MamPrefs)} to update the preferences. + * + *

Query the message archive

+ * + * Querying a message archive involves a two step process. First you need specify the query's arguments, for example a date range. + * The query arguments of a particular query are represented by a {@link MamQueryArgs} instance, which can be build using {@link MamQueryArgs.Builder}. + * + * After you have build such an instance, use {@link #queryArchive(MamQueryArgs)} to issue the query. + * + *
+ * {@code
+ * MamQueryArgs mamQueryArgs = MamQueryArgs.builder()
+ *                                 .withJid(jid)
+ *                                 .setResultPageSize(10)
+ *                                 .queryRecentPage()
+ *                                 .build();
+ * MamQuery mamQuery = mamManager.queryArchive(mamQueryArgs);
+ * }
+ * 
+ * + * On success {@link #queryArchive(MamQueryArgs)} returns a {@link MamQuery} instance. + * The instance will hold one page of the queries result set. + * Use {@link MamQuery#getMessages()} to retrieve the messages of the archive belonging to the page. + * + * You can get the whole page including all metadata using {@link MamQuery#getPage()}. + * + *

Paging through the results

+ * + * Because the matching result set could be potentially very big, a MAM service will probably not return all matching messages. + * Instead the results are possibly send in multiple pages. + * To check if the result was complete or if there are further pages, use {@link MamQuery#isComplete()}. + * If this method returns {@code false}, then you may want to page through the archive. + * + * {@link MamQuery} provides convince methods to do so: {@link MamQuery#pageNext(int)} and {@link MamQuery#pagePrevious(int)}. + * + *
+ * {@code
+ * MamQuery nextPageMamQuery = mamQuery.pageNext(10);
+ * }
+ * 
+ * + *

Get the supported form fields

+ * + * You can use {@link #retrieveFormFields()} to retrieve a list of the supported additional form fields by this archive. + * Those fields can be used for further restrict a query. + * * * @see XEP-0313: Message * Archive Management @@ -76,6 +166,10 @@ public final class MamManager extends Manager { }); } + private static final String FORM_FIELD_WITH = "with"; + private static final String FORM_FIELD_START = "start"; + private static final String FORM_FIELD_END = "end"; + private static final Map> INSTANCES = new WeakHashMap<>(); /** @@ -144,6 +238,213 @@ public final class MamManager extends Manager { return archiveAddress; } + public static final class MamQueryArgs { + private final String node; + + private final Map formFields; + + private final Integer maxResults; + + private final String afterUid; + + private final String beforeUid; + + private MamQueryArgs(Builder builder) { + node = builder.node; + formFields = builder.formFields; + if (builder.maxResults > 0) { + maxResults = builder.maxResults; + } else { + maxResults = null; + } + afterUid = builder.afterUid; + beforeUid = builder.beforeUid; + } + + private DataForm dataForm; + + DataForm getDataForm() { + if (dataForm != null) { + return dataForm; + } + dataForm = getNewMamForm(); + dataForm.addFields(formFields.values()); + return dataForm; + } + + void maybeAddRsmSet(MamQueryIQ mamQueryIQ) { + if (maxResults == null && afterUid == null && beforeUid == null) { + return; + } + + int max; + if (maxResults != null) { + max = maxResults; + } else { + max = -1; + } + + RSMSet rsmSet = new RSMSet(afterUid, beforeUid, -1, -1, null, max, null, -1); + mamQueryIQ.addExtension(rsmSet); + } + + public static Builder builder() { + return new Builder(); + } + + public static final class Builder { + private String node; + + private final Map formFields = new HashMap<>(8); + + private int maxResults = -1; + + private String afterUid; + + private String beforeUid; + + public Builder queryNode(String node) { + if (node == null) { + return this; + } + + this.node = node; + + return this; + } + + public Builder limitResultsToJid(Jid withJid) { + if (withJid == null) { + return this; + } + + FormField formField = getWithFormField(withJid); + formFields.put(formField.getVariable(), formField); + + return this; + } + + public Builder limitResultsSince(Date start) { + if (start == null) { + return this; + } + + FormField formField = new FormField(FORM_FIELD_START); + formField.addValue(start); + formFields.put(formField.getVariable(), formField); + + FormField endFormField = formFields.get(FORM_FIELD_END); + if (endFormField != null) { + Date end; + try { + end = endFormField.getFirstValueAsDate(); + } + catch (ParseException e) { + throw new IllegalStateException(e); + } + if (end.getTime() <= start.getTime()) { + throw new IllegalArgumentException("Given start date (" + start + + ") is after the existing end date (" + end + ')'); + } + } + + return this; + } + + public Builder limitResultsBefore(Date end) { + if (end == null) { + return this; + } + + FormField formField = new FormField(FORM_FIELD_END); + formField.addValue(end); + formFields.put(formField.getVariable(), formField); + + FormField startFormField = formFields.get(FORM_FIELD_START); + if (startFormField != null) { + Date start; + try { + start = startFormField.getFirstValueAsDate(); + } catch (ParseException e) { + throw new IllegalStateException(e); + } + if (end.getTime() <= start.getTime()) { + throw new IllegalArgumentException("Given end date (" + end + + ") is before the existing start date (" + start + ')'); + } + } + + return this; + } + + public Builder setResultPageSize(Integer max) { + if (max == null) { + maxResults = -1; + return this; + } + return setResultPageSizeTo(max.intValue()); + } + + public Builder setResultPageSizeTo(int max) { + if (max < 0) { + throw new IllegalArgumentException(); + } + this.maxResults = max; + return this; + } + + /** + * Only return the count of messages the query yields, not the actual messages. Note that not all services + * return a correct count, some return an approximate count. + * + * @return an reference to this builder. + * @see XEP-0059 § 2.7 + */ + public Builder onlyReturnMessageCount() { + return setResultPageSizeTo(0); + } + + public Builder withAdditionalFormField(FormField formField) { + formFields.put(formField.getVariable(), formField); + return this; + } + + public Builder withAdditionalFormFields(List additionalFields) { + for (FormField formField : additionalFields) { + withAdditionalFormField(formField); + } + return this; + } + + public Builder afterUid(String afterUid) { + this.afterUid = StringUtils.requireNullOrNotEmpty(afterUid, "afterUid must not be empty"); + return this; + } + + /** + * Specifies a message UID as 'before' anchor for the query. Note that unlike {@link #afterUid(String)} this + * method also accepts the empty String to query the last page of an archive (c.f. XEP-0059 § 2.5). + * + * @param beforeUid a message UID acting as 'before' query anchor. + * @return an instance to this builder. + */ + public Builder beforeUid(String beforeUid) { + // We don't perform any argument validation, since every possible argument (null, empty string, + // non-empty string) is valid. + this.beforeUid = beforeUid; + return this; + } + + public Builder queryLastPage() { + return beforeUid(""); + } + + public MamQueryArgs build() { + return new MamQueryArgs(this); + } + } + } + /** * Query archive with a maximum amount of results. * @@ -154,7 +455,10 @@ public final class MamManager extends Manager { * @throws NotConnectedException * @throws InterruptedException * @throws NotLoggedInException + * @deprecated use {@link #queryArchive(MamQueryArgs)} instead. */ + @Deprecated + // TODO Remove in Smack 4.4 public MamQueryResult queryArchive(Integer max) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException, NotLoggedInException { return queryArchive(null, max, null, null, null, null); @@ -170,7 +474,10 @@ public final class MamManager extends Manager { * @throws NotConnectedException * @throws InterruptedException * @throws NotLoggedInException + * @deprecated use {@link #queryArchive(MamQueryArgs)} instead. */ + @Deprecated + // TODO Remove in Smack 4.4 public MamQueryResult queryArchive(Jid withJid) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException, NotLoggedInException { return queryArchive(null, null, null, null, withJid, null); @@ -190,7 +497,10 @@ public final class MamManager extends Manager { * @throws NotConnectedException * @throws InterruptedException * @throws NotLoggedInException + * @deprecated use {@link #queryArchive(MamQueryArgs)} instead. */ + @Deprecated + // TODO Remove in Smack 4.4 public MamQueryResult queryArchive(Date start, Date end) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException, NotLoggedInException { return queryArchive(null, null, start, end, null, null); @@ -206,7 +516,10 @@ public final class MamManager extends Manager { * @throws NotConnectedException * @throws InterruptedException * @throws NotLoggedInException + * @deprecated use {@link #queryArchive(MamQueryArgs)} instead. */ + @Deprecated + // TODO Remove in Smack 4.4 public MamQueryResult queryArchive(List additionalFields) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException, NotLoggedInException { return queryArchive(null, null, null, null, null, additionalFields); @@ -223,7 +536,10 @@ public final class MamManager extends Manager { * @throws NotConnectedException * @throws InterruptedException * @throws NotLoggedInException + * @deprecated use {@link #queryArchive(MamQueryArgs)} instead. */ + @Deprecated + // TODO Remove in Smack 4.4 public MamQueryResult queryArchiveWithStartDate(Date start) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException, NotLoggedInException { return queryArchive(null, null, start, null, null, null); @@ -240,7 +556,10 @@ public final class MamManager extends Manager { * @throws NotConnectedException * @throws InterruptedException * @throws NotLoggedInException + * @deprecated use {@link #queryArchive(MamQueryArgs)} instead. */ + @Deprecated + // TODO Remove in Smack 4.4 public MamQueryResult queryArchiveWithEndDate(Date end) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException, NotLoggedInException { return queryArchive(null, null, null, end, null, null); @@ -262,7 +581,10 @@ public final class MamManager extends Manager { * @throws NotConnectedException * @throws InterruptedException * @throws NotLoggedInException + * @deprecated use {@link #queryArchive(MamQueryArgs)} instead. */ + @Deprecated + // TODO Remove in Smack 4.4 public MamQueryResult queryArchive(Integer max, Date start, Date end, Jid withJid, List additionalFields) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException, NotLoggedInException { @@ -287,72 +609,53 @@ public final class MamManager extends Manager { * @throws NotConnectedException * @throws InterruptedException * @throws NotLoggedInException + * @deprecated use {@link #queryArchive(MamQueryArgs)} instead. */ + @Deprecated + // TODO Remove in Smack 4.4 public MamQueryResult queryArchive(String node, Integer max, Date start, Date end, Jid withJid, List additionalFields) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException, NotLoggedInException { - DataForm dataForm = null; - String queryId = UUID.randomUUID().toString(); + MamQueryArgs mamQueryArgs = MamQueryArgs.builder() + .queryNode(node) + .setResultPageSize(max) + .limitResultsSince(start) + .limitResultsBefore(end) + .limitResultsToJid(withJid) + .withAdditionalFormFields(additionalFields) + .build(); - if (start != null || end != null || withJid != null || additionalFields != null) { - dataForm = getNewMamForm(); - addStart(start, dataForm); - addEnd(end, dataForm); - addWithJid(withJid, dataForm); - addAdditionalFields(additionalFields, dataForm); - } + MamQuery mamQuery = queryArchive(mamQueryArgs); + return new MamQueryResult(mamQuery); + } + + public MamQuery queryArchive(MamQueryArgs mamQueryArgs) throws NoResponseException, XMPPErrorException, + NotConnectedException, NotLoggedInException, InterruptedException { + String queryId = UUID.randomUUID().toString(); + String node = mamQueryArgs.node; + DataForm dataForm = mamQueryArgs.getDataForm(); MamQueryIQ mamQueryIQ = new MamQueryIQ(queryId, node, dataForm); mamQueryIQ.setType(IQ.Type.set); mamQueryIQ.setTo(archiveAddress); - addResultsLimit(max, mamQueryIQ); + mamQueryArgs.maybeAddRsmSet(mamQueryIQ); + return queryArchive(mamQueryIQ); } - private static void addAdditionalFields(List additionalFields, DataForm dataForm) { - if (additionalFields == null) { - return; - } - for (FormField formField : additionalFields) { - dataForm.addField(formField); - } - } - - private static void addResultsLimit(Integer max, MamQueryIQ mamQueryIQ) { - if (max == null) { - return; - } - RSMSet rsmSet = new RSMSet(max); - mamQueryIQ.addExtension(rsmSet); - + private static FormField getWithFormField(Jid withJid) { + FormField formField = new FormField(FORM_FIELD_WITH); + formField.addValue(withJid.toString()); + return formField; } private static void addWithJid(Jid withJid, DataForm dataForm) { if (withJid == null) { return; } - FormField formField = new FormField("with"); - formField.addValue(withJid.toString()); - dataForm.addField(formField); - } - - private static void addEnd(Date end, DataForm dataForm) { - if (end == null) { - return; - } - FormField formField = new FormField("end"); - formField.addValue(XmppDateTime.formatXEP0082Date(end)); - dataForm.addField(formField); - } - - private static void addStart(Date start, DataForm dataForm) { - if (start == null) { - return; - } - FormField formField = new FormField("start"); - formField.addValue(XmppDateTime.formatXEP0082Date(start)); + FormField formField = getWithFormField(withJid); dataForm.addField(formField); } @@ -367,16 +670,18 @@ public final class MamManager extends Manager { * @throws NotConnectedException * @throws InterruptedException * @throws NotLoggedInException + * @deprecated use {@link #queryArchive(MamQueryArgs)} instead. */ + @Deprecated + // TODO Remove in Smack 4.4 public MamQueryResult page(DataForm dataForm, RSMSet rsmSet) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException, NotLoggedInException { - return page(null, dataForm, rsmSet); - } /** - * Returns a page of the archive. + * Returns a page of the archive. This is a low-level method, you possibly do not want to use it directly unless you + * know what you are doing. * * @param node The PubSub node name, can be null * @param dataForm @@ -387,7 +692,10 @@ public final class MamManager extends Manager { * @throws NotConnectedException * @throws InterruptedException * @throws NotLoggedInException + * @deprecated use {@link #queryArchive(MamQueryArgs)} instead. */ + @Deprecated + // TODO Remove in Smack 4.4 public MamQueryResult page(String node, DataForm dataForm, RSMSet rsmSet) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException, NotLoggedInException { @@ -395,7 +703,8 @@ public final class MamManager extends Manager { mamQueryIQ.setType(IQ.Type.set); mamQueryIQ.setTo(archiveAddress); mamQueryIQ.addExtension(rsmSet); - return queryArchive(mamQueryIQ); + MamQuery mamQuery = queryArchive(mamQueryIQ); + return new MamQueryResult(mamQuery); } /** @@ -411,7 +720,10 @@ public final class MamManager extends Manager { * @throws NotConnectedException * @throws InterruptedException * @throws NotLoggedInException + * @deprecated use {@link MamQuery#pageNext(int)} instead. */ + @Deprecated + // TODO Remove in Smack 4.4 public MamQueryResult pageNext(MamQueryResult mamQueryResult, int count) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException, NotLoggedInException { RSMSet previousResultRsmSet = mamQueryResult.mamFin.getRSMSet(); @@ -432,7 +744,10 @@ public final class MamManager extends Manager { * @throws NotConnectedException * @throws InterruptedException * @throws NotLoggedInException + * @deprecated use {@link MamQuery#pagePrevious(int)} instead. */ + @Deprecated + // TODO Remove in Smack 4.4 public MamQueryResult pagePrevious(MamQueryResult mamQueryResult, int count) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException, NotLoggedInException { RSMSet previousResultRsmSet = mamQueryResult.mamFin.getRSMSet(); @@ -462,7 +777,10 @@ public final class MamManager extends Manager { * @throws NotConnectedException * @throws InterruptedException * @throws NoResponseException + * @deprecated use {@link #queryArchive(MamQueryArgs)} instead. */ + @Deprecated + // TODO Remove in Smack 4.4 public MamQueryResult pageBefore(Jid chatJid, String messageUid, int max) throws XMPPErrorException, NotLoggedInException, NotConnectedException, InterruptedException, NoResponseException { RSMSet rsmSet = new RSMSet(null, messageUid, -1, -1, null, max, null, -1); @@ -486,7 +804,10 @@ public final class MamManager extends Manager { * @throws NotConnectedException * @throws InterruptedException * @throws NoResponseException + * @deprecated use {@link #queryArchive(MamQueryArgs)} instead. */ + @Deprecated + // TODO Remove in Smack 4.4 public MamQueryResult pageAfter(Jid chatJid, String messageUid, int max) throws XMPPErrorException, NotLoggedInException, NotConnectedException, InterruptedException, NoResponseException { RSMSet rsmSet = new RSMSet(messageUid, null, -1, -1, null, max, null, -1); @@ -506,12 +827,26 @@ public final class MamManager extends Manager { * @throws NotConnectedException * @throws InterruptedException * @throws NoResponseException + * @deprecated use {@link #queryMostRecentPage(Jid, int)} instead. */ + @Deprecated + // TODO Remove in Smack 4.4 public MamQueryResult mostRecentPage(Jid chatJid, int max) throws XMPPErrorException, NotLoggedInException, NotConnectedException, InterruptedException, NoResponseException { return pageBefore(chatJid, "", max); } + public MamQuery queryMostRecentPage(Jid jid, int max) throws NoResponseException, XMPPErrorException, + NotConnectedException, NotLoggedInException, InterruptedException { + MamQueryArgs mamQueryArgs = MamQueryArgs.builder() + // Produces an empty element for XEP-0059 § 2.5 + .queryLastPage() + .limitResultsToJid(jid) + .setResultPageSize(max) + .build(); + return queryArchive(mamQueryArgs); + } + /** * Get the form fields supported by the server. * @@ -550,8 +885,14 @@ public final class MamManager extends Manager { return mamResponseQueryIq.getDataForm().getFields(); } - private MamQueryResult queryArchive(MamQueryIQ mamQueryIq) throws NoResponseException, XMPPErrorException, - NotConnectedException, InterruptedException, NotLoggedInException { + private MamQuery queryArchive(MamQueryIQ mamQueryIq) throws NoResponseException, XMPPErrorException, + NotConnectedException, InterruptedException, NotLoggedInException { + MamQueryPage mamQueryPage = queryArchivePage(mamQueryIq); + return new MamQuery(mamQueryPage, mamQueryIq.getNode(), DataForm.from(mamQueryIq)); + } + + private MamQueryPage queryArchivePage(MamQueryIQ mamQueryIq) throws NoResponseException, XMPPErrorException, + NotConnectedException, InterruptedException, NotLoggedInException { final XMPPConnection connection = getAuthenticatedConnectionOrThrow(); MamFinIQ mamFinIQ; @@ -569,27 +910,24 @@ public final class MamManager extends Manager { resultCollector.cancel(); } - List forwardedMessages = new ArrayList<>(resultCollector.getCollectedCount()); - - for (Message resultMessage = resultCollector - .pollResult(); resultMessage != null; resultMessage = resultCollector.pollResult()) { - MamElements.MamResultExtension mamResultExtension = MamElements.MamResultExtension.from(resultMessage); - forwardedMessages.add(mamResultExtension.getForwarded()); - } - - return new MamQueryResult(forwardedMessages, mamFinIQ, mamQueryIq.getNode(), DataForm.from(mamQueryIq)); + return new MamQueryPage(resultCollector, mamFinIQ); } /** * MAM query result class. * */ + @Deprecated public static final class MamQueryResult { public final List forwardedMessages; public final MamFinIQ mamFin; private final String node; private final DataForm form; + private MamQueryResult(MamQuery mamQuery) { + this(mamQuery.mamQueryPage.forwardedMessages, mamQuery.mamQueryPage.mamFin, mamQuery.node, mamQuery.form); + } + private MamQueryResult(List forwardedMessages, MamFinIQ mamFin, String node, DataForm form) { this.forwardedMessages = forwardedMessages; this.mamFin = mamFin; @@ -598,6 +936,124 @@ public final class MamManager extends Manager { } } + public final class MamQuery { + private final String node; + private final DataForm form; + + private MamQueryPage mamQueryPage; + + private MamQuery(MamQueryPage mamQueryPage, String node, DataForm form) { + this.node = node; + this.form = form; + + this.mamQueryPage = mamQueryPage; + } + + public boolean isComplete() { + return mamQueryPage.getMamFinIq().isComplete(); + } + + public List getMessages() { + return mamQueryPage.messages; + } + + public List getMamResultExtensions() { + return mamQueryPage.mamResultExtensions; + } + + private List page(RSMSet requestRsmSet) throws NoResponseException, XMPPErrorException, + NotConnectedException, NotLoggedInException, InterruptedException { + MamQueryIQ mamQueryIQ = new MamQueryIQ(UUID.randomUUID().toString(), node, form); + mamQueryIQ.setType(IQ.Type.set); + mamQueryIQ.setTo(archiveAddress); + mamQueryIQ.addExtension(requestRsmSet); + + mamQueryPage = queryArchivePage(mamQueryIQ); + + return mamQueryPage.messages; + } + + private RSMSet getPreviousRsmSet() { + return mamQueryPage.getMamFinIq().getRSMSet(); + } + + public List pageNext(int count) throws NoResponseException, XMPPErrorException, NotConnectedException, + NotLoggedInException, InterruptedException { + RSMSet previousResultRsmSet = getPreviousRsmSet(); + RSMSet requestRsmSet = new RSMSet(count, previousResultRsmSet.getLast(), RSMSet.PageDirection.after); + return page(requestRsmSet); + } + + public List pagePrevious(int count) throws NoResponseException, XMPPErrorException, + NotConnectedException, NotLoggedInException, InterruptedException { + RSMSet previousResultRsmSet = getPreviousRsmSet(); + RSMSet requestRsmSet = new RSMSet(count, previousResultRsmSet.getLast(), RSMSet.PageDirection.before); + return page(requestRsmSet); + } + + public int getMessageCount() { + return getMessages().size(); + } + + public MamQueryPage getPage() { + return mamQueryPage; + } + } + + public static final class MamQueryPage { + private final MamFinIQ mamFin; + private final List mamResultCarrierMessages; + private final List mamResultExtensions; + private final List forwardedMessages; + private final List messages; + + private MamQueryPage(StanzaCollector stanzaCollector, MamFinIQ mamFin) { + this.mamFin = mamFin; + + List mamResultCarrierStanzas = stanzaCollector.getCollectedStanzasAfterCancelled(); + + List mamResultCarrierMessages = new ArrayList<>(mamResultCarrierStanzas.size()); + List mamResultExtensions = new ArrayList<>(mamResultCarrierStanzas.size()); + List forwardedMessages = new ArrayList<>(mamResultCarrierStanzas.size()); + + for (Stanza mamResultStanza : mamResultCarrierStanzas) { + Message resultMessage = (Message) mamResultStanza; + + mamResultCarrierMessages.add(resultMessage); + + MamElements.MamResultExtension mamResultExtension = MamElements.MamResultExtension.from(resultMessage); + mamResultExtensions.add(mamResultExtension); + + forwardedMessages.add(mamResultExtension.getForwarded()); + } + + this.mamResultCarrierMessages = Collections.unmodifiableList(mamResultCarrierMessages); + this.mamResultExtensions = Collections.unmodifiableList(mamResultExtensions); + this.forwardedMessages = Collections.unmodifiableList(forwardedMessages); + this.messages = Collections.unmodifiableList(Forwarded.extractMessagesFrom(forwardedMessages)); + } + + public List getMessages() { + return messages; + } + + public List getForwarded() { + return forwardedMessages; + } + + public List getMamResultExtensions() { + return mamResultExtensions; + } + + public List getMamResultCarrierMessages() { + return mamResultCarrierMessages; + } + + public MamFinIQ getMamFinIq() { + return mamFin; + } + } + private void ensureMamQueryResultMatchesThisManager(MamQueryResult mamQueryResult) { EntityFullJid localAddress = connection().getUser(); EntityBareJid localBareAddress = null; @@ -649,6 +1105,32 @@ public final class MamManager extends Manager { return form; } + /** + * Lookup the archive's message ID of the latest message in the archive. Returns {@code null} if the archive is + * empty. + * + * @return the ID of the lastest message or {@code null}. + * @throws NoResponseException + * @throws XMPPErrorException + * @throws NotConnectedException + * @throws NotLoggedInException + * @throws InterruptedException + * @since 4.3.0 + */ + public String getMessageUidOfLatestMessage() throws NoResponseException, XMPPErrorException, NotConnectedException, NotLoggedInException, InterruptedException { + MamQueryArgs mamQueryArgs = MamQueryArgs.builder() + .setResultPageSize(1) + .queryLastPage() + .build(); + + MamQuery mamQuery = queryArchive(mamQueryArgs); + if (mamQuery.getMessages().isEmpty()) { + return null; + } + + return mamQuery.getMamResultExtensions().get(0).getId(); + } + /** * Get the preferences stored in the server. * @@ -682,7 +1164,9 @@ public final class MamManager extends Manager { * @throws NotConnectedException * @throws InterruptedException * @throws NotLoggedInException + * @deprecated use {@link #updateArchivingPreferences(MamPrefs)} instead. */ + @Deprecated public MamPrefsResult updateArchivingPreferences(List alwaysJids, List neverJids, DefaultBehavior defaultBehavior) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException, NotLoggedInException { @@ -691,6 +1175,46 @@ public final class MamManager extends Manager { return queryMamPrefs(mamPrefIQ); } + /** + * Update the preferences in the server. + * + * @param mamPrefs + * @return the currently active preferences after the operation. + * @throws NoResponseException + * @throws XMPPErrorException + * @throws NotConnectedException + * @throws InterruptedException + * @throws NotLoggedInException + * @since 4.3.0 + */ + public MamPrefsResult updateArchivingPreferences(MamPrefs mamPrefs) throws NoResponseException, XMPPErrorException, + NotConnectedException, InterruptedException, NotLoggedInException { + MamPrefsIQ mamPrefIQ = mamPrefs.constructMamPrefsIq(); + return queryMamPrefs(mamPrefIQ); + } + + public MamPrefsResult enableMamForAllMessages() throws NoResponseException, XMPPErrorException, + NotConnectedException, NotLoggedInException, InterruptedException { + return setDefaultBehavior(DefaultBehavior.always); + } + + public MamPrefsResult enableMamForRosterMessages() throws NoResponseException, XMPPErrorException, + NotConnectedException, NotLoggedInException, InterruptedException { + return setDefaultBehavior(DefaultBehavior.roster); + } + + public MamPrefsResult setDefaultBehavior(DefaultBehavior desiredDefaultBehavior) throws NoResponseException, + XMPPErrorException, NotConnectedException, NotLoggedInException, InterruptedException { + MamPrefsResult mamPrefsResult = retrieveArchivingPreferences(); + if (mamPrefsResult.mamPrefs.getDefault() == desiredDefaultBehavior) { + return mamPrefsResult; + } + + MamPrefs mamPrefs = mamPrefsResult.asMamPrefs(); + mamPrefs.setDefaultBehavior(desiredDefaultBehavior); + return updateArchivingPreferences(mamPrefs); + } + /** * MAM preferences result class. * @@ -703,6 +1227,43 @@ public final class MamManager extends Manager { this.mamPrefs = mamPrefs; this.form = form; } + + public MamPrefs asMamPrefs() { + return new MamPrefs(this); + } + } + + public static final class MamPrefs { + private final List alwaysJids; + private final List neverJids; + private DefaultBehavior defaultBehavior; + + private MamPrefs(MamPrefsResult mamPrefsResult) { + MamPrefsIQ mamPrefsIq = mamPrefsResult.mamPrefs; + this.alwaysJids = new ArrayList<>(mamPrefsIq.getAlwaysJids()); + this.neverJids = new ArrayList<>(mamPrefsIq.getNeverJids()); + this.defaultBehavior = mamPrefsIq.getDefault(); + } + + public void setDefaultBehavior(DefaultBehavior defaultBehavior) { + this.defaultBehavior = Objects.requireNonNull(defaultBehavior, "defaultBehavior must not be null"); + } + + public DefaultBehavior getDefaultBehavior() { + return defaultBehavior; + } + + public List getAlwaysJids() { + return alwaysJids; + } + + public List getNeverJids() { + return neverJids; + } + + private MamPrefsIQ constructMamPrefsIq() { + return new MamPrefsIQ(alwaysJids, neverJids, defaultBehavior); + } } private MamPrefsResult queryMamPrefs(MamPrefsIQ mamPrefsIQ) throws NoResponseException, XMPPErrorException, diff --git a/smack-experimental/src/test/java/org/jivesoftware/smackx/mam/FiltersTest.java b/smack-experimental/src/test/java/org/jivesoftware/smackx/mam/FiltersTest.java index c25790544..d6655ae4f 100644 --- a/smack-experimental/src/test/java/org/jivesoftware/smackx/mam/FiltersTest.java +++ b/smack-experimental/src/test/java/org/jivesoftware/smackx/mam/FiltersTest.java @@ -1,6 +1,6 @@ /** * - * Copyright 2016 Fernando Ramirez + * Copyright 2016 Fernando Ramirez, 2018 Florian Schmaus * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,23 +16,24 @@ */ package org.jivesoftware.smackx.mam; -import java.lang.reflect.Method; +import static org.junit.Assert.assertEquals; + import java.util.ArrayList; import java.util.Date; import java.util.List; +import org.jivesoftware.smackx.mam.MamManager.MamQueryArgs; import org.jivesoftware.smackx.mam.element.MamElements; import org.jivesoftware.smackx.xdata.packet.DataForm; -import org.junit.Assert; import org.junit.Test; import org.jxmpp.jid.Jid; -import org.jxmpp.jid.impl.JidCreate; +import org.jxmpp.jid.JidTestUtil; import org.jxmpp.util.XmppDateTime; public class FiltersTest extends MamTest { - private static String getMamXMemberWith(List fieldsNames, List fieldsValues) { + private static String getMamXMemberWith(List fieldsNames, List fieldsValues) { String xml = "" + "" + "" + MamElements.NAMESPACE + "" + ""; @@ -47,115 +48,47 @@ public class FiltersTest extends MamTest { @Test public void checkStartDateFilter() throws Exception { - Method methodAddStartDateFilter = MamManager.class.getDeclaredMethod("addStart", Date.class, DataForm.class); - methodAddStartDateFilter.setAccessible(true); - Date date = new Date(); - DataForm dataForm = getNewMamForm(); - methodAddStartDateFilter.invoke(mamManager, date, dataForm); + + MamQueryArgs mamQueryArgs = MamQueryArgs.builder().limitResultsSince(date).build(); + DataForm dataForm = mamQueryArgs.getDataForm(); List fields = new ArrayList<>(); fields.add("start"); List values = new ArrayList<>(); values.add(XmppDateTime.formatXEP0082Date(date)); - Assert.assertEquals(dataForm.toXML(null).toString(), getMamXMemberWith(fields, values)); + assertEquals(getMamXMemberWith(fields, values), dataForm.toXML(null).toString()); } @Test public void checkEndDateFilter() throws Exception { - Method methodAddEndDateFilter = MamManager.class.getDeclaredMethod("addEnd", Date.class, DataForm.class); - methodAddEndDateFilter.setAccessible(true); - Date date = new Date(); - DataForm dataForm = getNewMamForm(); - methodAddEndDateFilter.invoke(mamManager, date, dataForm); + + MamQueryArgs mamQueryArgs = MamQueryArgs.builder().limitResultsBefore(date).build(); + DataForm dataForm = mamQueryArgs.getDataForm(); List fields = new ArrayList<>(); fields.add("end"); List values = new ArrayList<>(); values.add(XmppDateTime.formatXEP0082Date(date)); - Assert.assertEquals(dataForm.toXML(null).toString(), getMamXMemberWith(fields, values)); + assertEquals(getMamXMemberWith(fields, values), dataForm.toXML(null).toString()); } @Test public void checkWithJidFilter() throws Exception { - Method methodAddJidFilter = MamManager.class.getDeclaredMethod("addWithJid", Jid.class, DataForm.class); - methodAddJidFilter.setAccessible(true); + Jid jid = JidTestUtil.BARE_JID_1; - String jid = "test@jid.com"; - DataForm dataForm = getNewMamForm(); - methodAddJidFilter.invoke(mamManager, JidCreate.from(jid), dataForm); + MamQueryArgs mamQueryArgs = MamQueryArgs.builder().limitResultsToJid(jid).build(); + DataForm dataForm = mamQueryArgs.getDataForm(); List fields = new ArrayList<>(); fields.add("with"); - List values = new ArrayList<>(); + List values = new ArrayList<>(); values.add(jid); - Assert.assertEquals(dataForm.toXML(null).toString(), getMamXMemberWith(fields, values)); - } - - @Test - public void checkMultipleFilters() throws Exception { - Method methodAddStartDateFilter = MamManager.class.getDeclaredMethod("addStart", Date.class, DataForm.class); - methodAddStartDateFilter.setAccessible(true); - Method methodAddEndDateFilter = MamManager.class.getDeclaredMethod("addEnd", Date.class, DataForm.class); - methodAddEndDateFilter.setAccessible(true); - Method methodAddJidFilter = MamManager.class.getDeclaredMethod("addWithJid", Jid.class, DataForm.class); - methodAddJidFilter.setAccessible(true); - - DataForm dataForm = getNewMamForm(); - Date date = new Date(); - String dateString = XmppDateTime.formatXEP0082Date(date); - String jid = "test@jid.com"; - - methodAddStartDateFilter.invoke(mamManager, date, dataForm); - methodAddEndDateFilter.invoke(mamManager, date, dataForm); - methodAddJidFilter.invoke(mamManager, JidCreate.from(jid), dataForm); - String dataFormResult = dataForm.toXML(null).toString(); - - List fields = new ArrayList<>(); - List values = new ArrayList<>(); - - fields.add("start"); - values.add(dateString); - Assert.assertNotEquals(dataFormResult, getMamXMemberWith(fields, values)); - - fields.add("end"); - values.add(dateString); - Assert.assertNotEquals(dataFormResult, getMamXMemberWith(fields, values)); - - fields.clear(); - values.clear(); - - fields.add("start"); - values.add(dateString); - fields.add("with"); - values.add(jid); - Assert.assertNotEquals(dataFormResult, getMamXMemberWith(fields, values)); - - fields.clear(); - values.clear(); - - fields.add("end"); - values.add(dateString); - fields.add("with"); - values.add(jid); - fields.add("start"); - values.add(dateString); - Assert.assertNotEquals(dataFormResult, getMamXMemberWith(fields, values)); - - fields.clear(); - values.clear(); - - fields.add("start"); - values.add(dateString); - fields.add("end"); - values.add(dateString); - fields.add("with"); - values.add(jid); - Assert.assertEquals(dataFormResult, getMamXMemberWith(fields, values)); + assertEquals(getMamXMemberWith(fields, values), dataForm.toXML(null).toString()); } } diff --git a/smack-experimental/src/test/java/org/jivesoftware/smackx/mam/ResultsLimitTest.java b/smack-experimental/src/test/java/org/jivesoftware/smackx/mam/ResultsLimitTest.java index 48b014201..162d06564 100644 --- a/smack-experimental/src/test/java/org/jivesoftware/smackx/mam/ResultsLimitTest.java +++ b/smack-experimental/src/test/java/org/jivesoftware/smackx/mam/ResultsLimitTest.java @@ -1,6 +1,6 @@ /** * - * Copyright 2016 Fernando Ramirez + * Copyright 2016 Fernando Ramirez, 2018 Florian Schmaus * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,11 +16,10 @@ */ package org.jivesoftware.smackx.mam; -import java.lang.reflect.Method; - import org.jivesoftware.smack.packet.IQ; import org.jivesoftware.smack.packet.StreamOpen; +import org.jivesoftware.smackx.mam.MamManager.MamQueryArgs; import org.jivesoftware.smackx.mam.element.MamElements; import org.jivesoftware.smackx.mam.element.MamQueryIQ; import org.jivesoftware.smackx.xdata.packet.DataForm; @@ -37,16 +36,14 @@ public class ResultsLimitTest extends MamTest { @Test public void checkResultsLimit() throws Exception { - Method methodAddResultsLimit = MamManager.class.getDeclaredMethod("addResultsLimit", Integer.class, - MamQueryIQ.class); - methodAddResultsLimit.setAccessible(true); DataForm dataForm = getNewMamForm(); MamQueryIQ mamQueryIQ = new MamQueryIQ(queryId, dataForm); mamQueryIQ.setType(IQ.Type.set); mamQueryIQ.setStanzaId("sarasa"); - methodAddResultsLimit.invoke(mamManager, 10, mamQueryIQ); + MamQueryArgs mamQueryArgs = MamQueryArgs.builder().setResultPageSize(10).build(); + mamQueryArgs.maybeAddRsmSet(mamQueryIQ); Assert.assertEquals(mamQueryIQ.toXML(StreamOpen.CLIENT_NAMESPACE).toString(), resultsLimitStanza); } diff --git a/smack-experimental/src/test/java/org/jivesoftware/smackx/mam/RetrieveFormFieldsTest.java b/smack-experimental/src/test/java/org/jivesoftware/smackx/mam/RetrieveFormFieldsTest.java index 86ecb8ead..8e0557b85 100644 --- a/smack-experimental/src/test/java/org/jivesoftware/smackx/mam/RetrieveFormFieldsTest.java +++ b/smack-experimental/src/test/java/org/jivesoftware/smackx/mam/RetrieveFormFieldsTest.java @@ -1,6 +1,6 @@ /** * - * Copyright 2016 Fernando Ramirez + * Copyright 2016 Fernando Ramirez, 2018 Florian Schmaus * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,18 +16,17 @@ */ package org.jivesoftware.smackx.mam; -import java.lang.reflect.Method; -import java.util.ArrayList; -import java.util.List; +import static org.jivesoftware.smack.test.util.XmlUnitUtils.assertXmlSimilar; +import static org.junit.Assert.assertEquals; import org.jivesoftware.smack.packet.StreamOpen; +import org.jivesoftware.smackx.mam.MamManager.MamQueryArgs; import org.jivesoftware.smackx.mam.element.MamElements; import org.jivesoftware.smackx.mam.element.MamQueryIQ; import org.jivesoftware.smackx.xdata.FormField; import org.jivesoftware.smackx.xdata.packet.DataForm; -import org.junit.Assert; import org.junit.Test; public class RetrieveFormFieldsTest extends MamTest { @@ -46,19 +45,11 @@ public class RetrieveFormFieldsTest extends MamTest { MamQueryIQ mamQueryIQ = new MamQueryIQ(queryId); mamQueryIQ.setStanzaId("sarasa"); - Assert.assertEquals(mamQueryIQ.toXML(StreamOpen.CLIENT_NAMESPACE).toString(), retrieveFormFieldStanza); + assertEquals(retrieveFormFieldStanza, mamQueryIQ.toXML(StreamOpen.CLIENT_NAMESPACE).toString()); } @Test public void checkAddAdditionalFieldsStanza() throws Exception { - Method methodAddAdditionalFields = MamManager.class.getDeclaredMethod("addAdditionalFields", List.class, - DataForm.class); - methodAddAdditionalFields.setAccessible(true); - - DataForm dataForm = getNewMamForm(); - - List additionalFields = new ArrayList<>(); - FormField field1 = new FormField("urn:example:xmpp:free-text-search"); field1.setType(FormField.Type.text_single); field1.addValue("Hi"); @@ -67,14 +58,15 @@ public class RetrieveFormFieldsTest extends MamTest { field2.setType(FormField.Type.jid_single); field2.addValue("Hi2"); - additionalFields.add(field1); - additionalFields.add(field2); - - methodAddAdditionalFields.invoke(mamManager, additionalFields, dataForm); + MamQueryArgs mamQueryArgs = MamQueryArgs.builder() + .withAdditionalFormField(field1) + .withAdditionalFormField(field2) + .build(); + DataForm dataForm = mamQueryArgs.getDataForm(); String dataFormResult = dataForm.toXML(null).toString(); - Assert.assertEquals(dataFormResult, additionalFieldsStanza); + assertXmlSimilar(additionalFieldsStanza, dataFormResult); } } diff --git a/smack-integration-test/src/main/java/org/jivesoftware/smackx/mam/MamIntegrationTest.java b/smack-integration-test/src/main/java/org/jivesoftware/smackx/mam/MamIntegrationTest.java index 940381398..99168f78a 100644 --- a/smack-integration-test/src/main/java/org/jivesoftware/smackx/mam/MamIntegrationTest.java +++ b/smack-integration-test/src/main/java/org/jivesoftware/smackx/mam/MamIntegrationTest.java @@ -1,6 +1,6 @@ /** * - * Copyright 2016 Fernando Ramirez + * Copyright 2016 Fernando Ramirez, 2018 Florian Schmaus * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,23 +17,30 @@ package org.jivesoftware.smackx.mam; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import java.util.ArrayList; import java.util.List; +import java.util.concurrent.TimeoutException; import org.jivesoftware.smack.SmackException.NoResponseException; import org.jivesoftware.smack.SmackException.NotConnectedException; import org.jivesoftware.smack.SmackException.NotLoggedInException; +import org.jivesoftware.smack.StanzaListener; import org.jivesoftware.smack.XMPPException.XMPPErrorException; +import org.jivesoftware.smack.filter.MessageWithBodiesFilter; import org.jivesoftware.smack.packet.Message; +import org.jivesoftware.smack.packet.Stanza; -import org.jivesoftware.smackx.forward.packet.Forwarded; -import org.jivesoftware.smackx.mam.MamManager.MamQueryResult; -import org.jivesoftware.smackx.mam.element.MamPrefsIQ; +import org.jivesoftware.smackx.mam.MamManager.MamQuery; +import org.jivesoftware.smackx.mam.MamManager.MamQueryArgs; import org.igniterealtime.smack.inttest.AbstractSmackIntegrationTest; import org.igniterealtime.smack.inttest.SmackIntegrationTest; import org.igniterealtime.smack.inttest.SmackIntegrationTestEnvironment; import org.igniterealtime.smack.inttest.TestNotPossibleException; +import org.igniterealtime.smack.inttest.util.SimpleResultSyncPoint; import org.jxmpp.jid.EntityBareJid; public class MamIntegrationTest extends AbstractSmackIntegrationTest { @@ -41,7 +48,7 @@ public class MamIntegrationTest extends AbstractSmackIntegrationTest { private final MamManager mamManagerConTwo; public MamIntegrationTest(SmackIntegrationTestEnvironment environment) throws NoResponseException, - XMPPErrorException, NotConnectedException, InterruptedException, TestNotPossibleException { + XMPPErrorException, NotConnectedException, InterruptedException, TestNotPossibleException, NotLoggedInException { super(environment); mamManagerConTwo = MamManager.getInstanceFor(conTwo); @@ -50,33 +57,51 @@ public class MamIntegrationTest extends AbstractSmackIntegrationTest { throw new TestNotPossibleException("Message Archive Management (XEP-0313) is not supported by the server."); } + // Make sure MAM is archiving messages. + mamManagerConTwo.enableMamForAllMessages(); } @SmackIntegrationTest - public void mamTest() throws NotConnectedException, InterruptedException, NoResponseException, XMPPErrorException, - NotLoggedInException { + public void mamTest() throws TimeoutException, Exception { EntityBareJid userOne = conOne.getUser().asEntityBareJid(); EntityBareJid userTwo = conTwo.getUser().asEntityBareJid(); - // Make sure MAM is archiving messages. - mamManagerConTwo.updateArchivingPreferences(null, null, MamPrefsIQ.DefaultBehavior.always); - Message message = new Message(userTwo); String messageId = message.setStanzaId(); - String messageBody = "test message"; + final String messageBody = "Test MAM message (" + testRunId + ')'; message.setBody(messageBody); - conOne.sendStanza(message); + final SimpleResultSyncPoint messageReceived = new SimpleResultSyncPoint(); - int pageSize = 20; - MamQueryResult mamQueryResult = mamManagerConTwo.queryArchive(pageSize, null, null, userOne, null); + final StanzaListener stanzaListener = new StanzaListener() { + @Override + public void processStanza(Stanza stanza) { + Message message = (Message) stanza; + if (message.getBody().equals(messageBody)) { + messageReceived.signal(); + } + } + }; + conTwo.addAsyncStanzaListener(stanzaListener, MessageWithBodiesFilter.INSTANCE); - while (!mamQueryResult.mamFin.isComplete()) { - mamQueryResult = mamManagerConTwo.pageNext(mamQueryResult, pageSize); + try { + conOne.sendStanza(message); + + messageReceived.waitForResult(timeout); + } finally { + conTwo.removeAsyncStanzaListener(stanzaListener); } - List forwardedMessages = mamQueryResult.forwardedMessages; - Message mamMessage = (Message) forwardedMessages.get(forwardedMessages.size() - 1).getForwardedStanza(); + MamQueryArgs mamQueryArgs = MamQueryArgs.builder() + .setResultPageSizeTo(1) + .limitResultsToJid(userOne) + .queryLastPage() + .build(); + MamQuery mamQuery = mamManagerConTwo.queryArchive(mamQueryArgs); + + assertEquals(1, mamQuery.getMessages().size()); + + Message mamMessage = mamQuery.getMessages().get(0); assertEquals(messageId, mamMessage.getStanzaId()); assertEquals(messageBody, mamMessage.getBody()); @@ -84,4 +109,87 @@ public class MamIntegrationTest extends AbstractSmackIntegrationTest { assertEquals(userTwo, mamMessage.getTo()); } + @SmackIntegrationTest + public void mamPageTest() throws TimeoutException, Exception { + final int messagesPerPage = 10; + final int numPages = 3; + final int totalMessages = messagesPerPage * numPages; + final List outgoingMessages = new ArrayList<>(totalMessages); + final EntityBareJid userOne = conOne.getUser().asEntityBareJid(); + final EntityBareJid userTwo = conTwo.getUser().asEntityBareJid(); + final SimpleResultSyncPoint allMessagesReceived = new SimpleResultSyncPoint(); + final String lastMessageArchiveUid = mamManagerConTwo.getMessageUidOfLatestMessage(); + + for (int i = 0; i < totalMessages; i++) { + String messageBody = "MAM Page Test " + testRunId + ' ' + (i + 1); + Message message = new Message(userTwo, messageBody); + outgoingMessages.add(message); + } + + final String lastBody = outgoingMessages.get(outgoingMessages.size() - 1).getBody(); + + final StanzaListener stanzaListener = new StanzaListener() { + @Override + public void processStanza(Stanza stanza) { + Message message = (Message) stanza; + if (message.getBody().equals(lastBody)) { + allMessagesReceived.signal(); + } + } + }; + conTwo.addAsyncStanzaListener(stanzaListener, MessageWithBodiesFilter.INSTANCE); + + try { + for (Message message : outgoingMessages) { + conOne.sendStanza(message); + } + + allMessagesReceived.waitForResult(timeout); + } finally { + conTwo.removeAsyncStanzaListener(stanzaListener); + } + + MamQueryArgs mamQueryArgs = MamQueryArgs.builder() + .setResultPageSize(messagesPerPage) + .limitResultsToJid(userOne) + .afterUid(lastMessageArchiveUid) + .build(); + + MamQuery mamQuery = mamManagerConTwo.queryArchive(mamQueryArgs); + + assertFalse(mamQuery.isComplete()); + assertEquals(messagesPerPage, mamQuery.getMessageCount()); + + List> pages = new ArrayList<>(numPages); + pages.add(mamQuery.getMessages()); + + for (int additionalPageRequestNum = 0; additionalPageRequestNum < numPages - 1; additionalPageRequestNum++) { + List page = mamQuery.pageNext(messagesPerPage); + + boolean isLastQuery = additionalPageRequestNum == numPages - 2; + if (isLastQuery) { + assertTrue(mamQuery.isComplete()); + } else { + assertFalse(mamQuery.isComplete()); + } + + assertEquals(messagesPerPage, page.size()); + + pages.add(page); + } + + List queriedMessages = new ArrayList<>(totalMessages); + for (List messages : pages) { + queriedMessages.addAll(messages); + } + + assertEquals(outgoingMessages.size(), queriedMessages.size()); + + for (int i = 0; i < outgoingMessages.size(); i++) { + Message outgoingMessage = outgoingMessages.get(i); + Message queriedMessage = queriedMessages.get(i); + + assertEquals(outgoingMessage.getBody(), queriedMessage.getBody()); + } + } } diff --git a/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/OmemoManager.java b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/OmemoManager.java index 174063acd..d598136a9 100644 --- a/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/OmemoManager.java +++ b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/OmemoManager.java @@ -325,16 +325,16 @@ public final class OmemoManager extends Manager { * Return a list of all OMEMO messages that were found in the MAM query result, that could be successfully decrypted. * Normal cleartext messages are also added to this list. * - * @param mamQueryResult mamQueryResult + * @param mamQuery The MAM query * @return list of decrypted OmemoMessages * @throws InterruptedException Exception * @throws XMPPException.XMPPErrorException Exception * @throws SmackException.NotConnectedException Exception * @throws SmackException.NoResponseException Exception */ - public List decryptMamQueryResult(MamManager.MamQueryResult mamQueryResult) throws InterruptedException, XMPPException.XMPPErrorException, SmackException.NotConnectedException, SmackException.NoResponseException { + public List decryptMamQueryResult(MamManager.MamQuery mamQuery) throws InterruptedException, XMPPException.XMPPErrorException, SmackException.NotConnectedException, SmackException.NoResponseException { List l = new ArrayList<>(); - l.addAll(getOmemoService().decryptMamQueryResult(this, mamQueryResult)); + l.addAll(getOmemoService().decryptMamQueryResult(this, mamQuery)); return l; } diff --git a/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/OmemoService.java b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/OmemoService.java index 1ad4ae3ea..d6781fff2 100644 --- a/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/OmemoService.java +++ b/smack-omemo/src/main/java/org/jivesoftware/smackx/omemo/OmemoService.java @@ -54,7 +54,6 @@ import org.jivesoftware.smack.util.Async; import org.jivesoftware.smackx.carbons.CarbonCopyReceivedListener; import org.jivesoftware.smackx.carbons.CarbonManager; import org.jivesoftware.smackx.carbons.packet.CarbonExtension; -import org.jivesoftware.smackx.forward.packet.Forwarded; import org.jivesoftware.smackx.mam.MamManager; import org.jivesoftware.smackx.muc.MultiUserChat; import org.jivesoftware.smackx.muc.MultiUserChatManager; @@ -1127,21 +1126,21 @@ public abstract class OmemoService decryptMamQueryResult(OmemoManager omemoManager, MamManager.MamQueryResult mamQueryResult) + List decryptMamQueryResult(OmemoManager omemoManager, MamManager.MamQuery mamQuery) throws InterruptedException, XMPPException.XMPPErrorException, SmackException.NotConnectedException, SmackException.NoResponseException { List result = new ArrayList<>(); - for (Forwarded f : mamQueryResult.forwardedMessages) { - if (OmemoManager.stanzaContainsOmemoElement(f.getForwardedStanza())) { + for (Message message : mamQuery.getMessages()) { + if (OmemoManager.stanzaContainsOmemoElement(message)) { // Decrypt OMEMO messages try { - result.add(processLocalMessage(omemoManager, f.getForwardedStanza().getFrom().asBareJid(), (Message) f.getForwardedStanza())); + result.add(processLocalMessage(omemoManager, message.getFrom().asBareJid(), message)); } catch (NoRawSessionException | CorruptedOmemoKeyException | CryptoFailedException e) { LOGGER.log(Level.WARNING, "decryptMamQueryResult failed to decrypt message from " - + f.getForwardedStanza().getFrom() + " due to corrupted session/key: " + e.getMessage()); + + message.getFrom() + " due to corrupted session/key: " + e.getMessage()); } } else { // Wrap cleartext messages - Message m = (Message) f.getForwardedStanza(); + Message m = message; result.add(new ClearTextMessage(m.getBody(), m, new OmemoMessageInformation(null, null, OmemoMessageInformation.CARBON.NONE, false))); }