/** * * Copyright © 2017-2018 Florian Schmaus, 2016-2017 Fernando Ramirez * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ 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; import java.util.Map; import java.util.UUID; import java.util.WeakHashMap; import org.jivesoftware.smack.ConnectionCreationListener; import org.jivesoftware.smack.Manager; import org.jivesoftware.smack.SmackException.NoResponseException; import org.jivesoftware.smack.SmackException.NotConnectedException; import org.jivesoftware.smack.SmackException.NotLoggedInException; import org.jivesoftware.smack.StanzaCollector; import org.jivesoftware.smack.XMPPConnection; import org.jivesoftware.smack.XMPPConnectionRegistry; 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; import org.jivesoftware.smackx.mam.element.MamQueryIQ; import org.jivesoftware.smackx.mam.filter.MamResultFilter; import org.jivesoftware.smackx.muc.MultiUserChat; import org.jivesoftware.smackx.rsm.packet.RSMSet; import org.jivesoftware.smackx.xdata.FormField; import org.jivesoftware.smackx.xdata.packet.DataForm; import org.jxmpp.jid.EntityBareJid; import org.jxmpp.jid.EntityFullJid; import org.jxmpp.jid.Jid; /** * 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)
 *                                 .queryLastPage()
 *                                 .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 * @author Florian Schmaus * @author Fernando Ramirez * */ public final class MamManager extends Manager { static { XMPPConnectionRegistry.addConnectionCreationListener(new ConnectionCreationListener() { @Override public void connectionCreated(XMPPConnection connection) { getInstanceFor(connection); } }); } 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<>(); /** * Get a MamManager for the MAM archive of the local entity (the "user") of the given connection. * * @param connection the XMPP connection to get the archive for. * @return the instance of MamManager. */ public static MamManager getInstanceFor(XMPPConnection connection) { return getInstanceFor(connection, (Jid) null); } /** * Get a MamManager for the MAM archive of the given {@code MultiUserChat}. Note that not all MUCs support MAM, * hence it is recommended to use {@link #isSupported()} to check if MAM is supported by the MUC. * * @param multiUserChat the MultiUserChat to retrieve the MamManager for. * @return the MamManager for the given MultiUserChat. * @since 4.3.0 */ public static MamManager getInstanceFor(MultiUserChat multiUserChat) { XMPPConnection connection = multiUserChat.getXmppConnection(); Jid archiveAddress = multiUserChat.getRoom(); return getInstanceFor(connection, archiveAddress); } public static synchronized MamManager getInstanceFor(XMPPConnection connection, Jid archiveAddress) { Map managers = INSTANCES.get(connection); if (managers == null) { managers = new HashMap<>(); INSTANCES.put(connection, managers); } MamManager mamManager = managers.get(archiveAddress); if (mamManager == null) { mamManager = new MamManager(connection, archiveAddress); managers.put(archiveAddress, mamManager); } return mamManager; } private final Jid archiveAddress; private final ServiceDiscoveryManager serviceDiscoveryManager; private MamManager(XMPPConnection connection, Jid archiveAddress) { super(connection); this.archiveAddress = archiveAddress; serviceDiscoveryManager = ServiceDiscoveryManager.getInstanceFor(connection); } /** * The the XMPP address of this MAM archive. Note that this method may return {@code null} if this MamManager * handles the local entity's archive and if the connection has never been authenticated at least once. * * @return the XMPP address of this MAM archive or {@code null}. * @since 4.3.0 */ public Jid getArchiveAddress() { if (archiveAddress == null) { EntityFullJid localJid = connection().getUser(); if (localJid == null) { return null; } return localJid.asBareJid(); } 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; } /** * Query from the last, i.e. most recent, page of the archive. This will return the very last page of the * archive holding the most recent matching messages. You usually would page backwards from there on. * * @return a reference to this builder. * @see XEP-0059 § 2.5. Requesting the Last Page in * a Result Set */ public Builder queryLastPage() { return beforeUid(""); } public MamQueryArgs build() { return new MamQueryArgs(this); } } } /** * Query archive with a maximum amount of results. * * @param max * @return the MAM query result * @throws NoResponseException * @throws XMPPErrorException * @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); } /** * Query archive with a JID (only messages from/to the JID). * * @param withJid * @return the MAM query result * @throws NoResponseException * @throws XMPPErrorException * @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); } /** * Query archive filtering by start and/or end date. If start == null, the * value of 'start' will be equal to the date/time of the earliest message * stored in the archive. If end == null, the value of 'end' will be equal * to the date/time of the most recent message stored in the archive. * * @param start * @param end * @return the MAM query result * @throws NoResponseException * @throws XMPPErrorException * @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); } /** * Query Archive adding filters with additional fields. * * @param additionalFields * @return the MAM query result * @throws NoResponseException * @throws XMPPErrorException * @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); } /** * Query archive filtering by start date. The value of 'end' will be equal * to the date/time of the most recent message stored in the archive. * * @param start * @return the MAM query result * @throws NoResponseException * @throws XMPPErrorException * @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); } /** * Query archive filtering by end date. The value of 'start' will be equal * to the date/time of the earliest message stored in the archive. * * @param end * @return the MAM query result * @throws NoResponseException * @throws XMPPErrorException * @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); } /** * Query archive applying filters: max count, start date, end date, from/to * JID and with additional fields. * * @param max * @param start * @param end * @param withJid * @param additionalFields * @return the MAM query result * @throws NoResponseException * @throws XMPPErrorException * @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 { return queryArchive(null, max, start, end, withJid, additionalFields); } /** * Query an message archive like a MUC archive or a PubSub node archive, addressed by an archiveAddress, applying * filters: max count, start date, end date, from/to JID and with additional fields. When archiveAddress is null the * default, the server will be requested. * * @param node The PubSub node name, can be null * @param max * @param start * @param end * @param withJid * @param additionalFields * @return the MAM query result * @throws NoResponseException * @throws XMPPErrorException * @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 { MamQueryArgs mamQueryArgs = MamQueryArgs.builder() .queryNode(node) .setResultPageSize(max) .limitResultsSince(start) .limitResultsBefore(end) .limitResultsToJid(withJid) .withAdditionalFormFields(additionalFields) .build(); 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); mamQueryArgs.maybeAddRsmSet(mamQueryIQ); return queryArchive(mamQueryIQ); } 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 = getWithFormField(withJid); dataForm.addField(formField); } /** * Returns a page of the archive. * * @param dataForm * @param rsmSet * @return the MAM query result * @throws NoResponseException * @throws XMPPErrorException * @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. 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 * @param rsmSet * @return the MAM query result * @throws NoResponseException * @throws XMPPErrorException * @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 { MamQueryIQ mamQueryIQ = new MamQueryIQ(UUID.randomUUID().toString(), node, dataForm); mamQueryIQ.setType(IQ.Type.set); mamQueryIQ.setTo(archiveAddress); mamQueryIQ.addExtension(rsmSet); MamQuery mamQuery = queryArchive(mamQueryIQ); return new MamQueryResult(mamQuery); } /** * Returns the next page of the archive. * * @param mamQueryResult * is the previous query result * @param count * is the amount of messages that a page contains * @return the MAM query result * @throws NoResponseException * @throws XMPPErrorException * @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(); RSMSet requestRsmSet = new RSMSet(count, previousResultRsmSet.getLast(), RSMSet.PageDirection.after); return page(mamQueryResult, requestRsmSet); } /** * Returns the previous page of the archive. * * @param mamQueryResult * is the previous query result * @param count * is the amount of messages that a page contains * @return the MAM query result * @throws NoResponseException * @throws XMPPErrorException * @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(); RSMSet requestRsmSet = new RSMSet(count, previousResultRsmSet.getFirst(), RSMSet.PageDirection.before); return page(mamQueryResult, requestRsmSet); } private MamQueryResult page(MamQueryResult mamQueryResult, RSMSet requestRsmSet) throws NoResponseException, XMPPErrorException, NotConnectedException, NotLoggedInException, InterruptedException { ensureMamQueryResultMatchesThisManager(mamQueryResult); return page(mamQueryResult.node, mamQueryResult.form, requestRsmSet); } /** * Obtain page before the first message saved (specific chat). *

* Note that the messageUid is the XEP-0313 UID and not the stanza ID of the message. *

* * @param chatJid * @param messageUid the UID of the message of which messages before should be received. * @param max * @return the MAM query result * @throws XMPPErrorException * @throws NotLoggedInException * @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); DataForm dataForm = getNewMamForm(); addWithJid(chatJid, dataForm); return page(null, dataForm, rsmSet); } /** * Obtain page after the last message saved (specific chat). *

* Note that the messageUid is the XEP-0313 UID and not the stanza ID of the message. *

* * @param chatJid * @param messageUid the UID of the message of which messages after should be received. * @param max * @return the MAM query result * @throws XMPPErrorException * @throws NotLoggedInException * @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); DataForm dataForm = getNewMamForm(); addWithJid(chatJid, dataForm); return page(null, dataForm, rsmSet); } /** * Obtain the most recent page of a chat. * * @param chatJid * @param max * @return the MAM query result * @throws XMPPErrorException * @throws NotLoggedInException * @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. * * @return the list of form fields. * @throws NoResponseException * @throws XMPPErrorException * @throws NotConnectedException * @throws InterruptedException * @throws NotLoggedInException */ public List retrieveFormFields() throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException, NotLoggedInException { return retrieveFormFields(null); } /** * Get the form fields supported by the server. * * @param node The PubSub node name, can be null * @return the list of form fields. * @throws NoResponseException * @throws XMPPErrorException * @throws NotConnectedException * @throws InterruptedException * @throws NotLoggedInException */ public List retrieveFormFields(String node) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException, NotLoggedInException { String queryId = UUID.randomUUID().toString(); MamQueryIQ mamQueryIq = new MamQueryIQ(queryId, node, null); mamQueryIq.setTo(archiveAddress); MamQueryIQ mamResponseQueryIq = connection().createStanzaCollectorAndSend(mamQueryIq).nextResultOrThrow(); return mamResponseQueryIq.getDataForm().getFields(); } 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; StanzaCollector mamFinIQCollector = connection.createStanzaCollector(new IQReplyFilter(mamQueryIq, connection)); StanzaCollector.Configuration resultCollectorConfiguration = StanzaCollector.newConfiguration() .setStanzaFilter(new MamResultFilter(mamQueryIq)).setCollectorToReset(mamFinIQCollector); StanzaCollector resultCollector = connection.createStanzaCollector(resultCollectorConfiguration); try { connection.sendStanza(mamQueryIq); mamFinIQ = mamFinIQCollector.nextResultOrThrow(); } finally { mamFinIQCollector.cancel(); resultCollector.cancel(); } 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; this.node = node; this.form = form; } } 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; if (localAddress != null) { localBareAddress = localAddress.asEntityBareJid(); } boolean isLocalUserArchive = archiveAddress == null || archiveAddress.equals(localBareAddress); Jid finIqFrom = mamQueryResult.mamFin.getFrom(); if (finIqFrom != null) { if (finIqFrom.equals(archiveAddress) || (isLocalUserArchive && finIqFrom.equals(localBareAddress))) { return; } throw new IllegalArgumentException("The given MamQueryResult is from the MAM archive '" + finIqFrom + "' whereas this MamManager is responsible for '" + archiveAddress + '\''); } else if (!isLocalUserArchive) { throw new IllegalArgumentException( "The given MamQueryResult is from the local entity (user) MAM archive, whereas this MamManager is responsible for '" + archiveAddress + '\''); } } /** * Check if this MamManager's archive address supports MAM. * * @return true if MAM is supported, falseotherwise. * * @throws NoResponseException * @throws XMPPErrorException * @throws NotConnectedException * @throws InterruptedException * @since 4.2.1 * @see XEP-0313 § 7. Determining support */ public boolean isSupported() throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { // Note that this may return 'null' but SDM's supportsFeature() does the right thing™ then. Jid archiveAddress = getArchiveAddress(); return serviceDiscoveryManager.supportsFeature(archiveAddress, MamElements.NAMESPACE); } private static DataForm getNewMamForm() { FormField field = new FormField(FormField.FORM_TYPE); field.setType(FormField.Type.hidden); field.addValue(MamElements.NAMESPACE); DataForm form = new DataForm(DataForm.Type.submit); form.addField(field); 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. * * @return the MAM preferences result * @throws NoResponseException * @throws XMPPErrorException * @throws NotConnectedException * @throws InterruptedException * @throws NotLoggedInException */ public MamPrefsResult retrieveArchivingPreferences() throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException, NotLoggedInException { MamPrefsIQ mamPrefIQ = new MamPrefsIQ(); return queryMamPrefs(mamPrefIQ); } /** * Update the preferences in the server. * * @param alwaysJids * is the list of JIDs that should always have messages to/from * archived in the user's store * @param neverJids * is the list of JIDs that should never have messages to/from * archived in the user's store * @param defaultBehavior * can be "roster", "always", "never" (see XEP-0313) * @return the MAM preferences result * @throws NoResponseException * @throws XMPPErrorException * @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 { Objects.requireNonNull(defaultBehavior, "Default behavior must be set"); MamPrefsIQ mamPrefIQ = new MamPrefsIQ(alwaysJids, neverJids, defaultBehavior); 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. * */ public static final class MamPrefsResult { public final MamPrefsIQ mamPrefs; public final DataForm form; private MamPrefsResult(MamPrefsIQ mamPrefs, DataForm form) { 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, NotConnectedException, InterruptedException, NotLoggedInException { final XMPPConnection connection = getAuthenticatedConnectionOrThrow(); MamPrefsIQ mamPrefsResultIQ = connection.createStanzaCollectorAndSend(mamPrefsIQ).nextResultOrThrow(); return new MamPrefsResult(mamPrefsResultIQ, DataForm.from(mamPrefsIQ)); } }