You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@mina.apache.org by ra...@apache.org on 2019/09/01 10:54:16 UTC
[mina-vysper] 01/01: XEP-0313 Message Archive Management
This is an automated email from the ASF dual-hosted git repository.
ralaoui pushed a commit to branch xep-0313
in repository https://gitbox.apache.org/repos/asf/mina-vysper.git
commit aff6f45a6f65d7eb4dec179c88b4540084929d80
Author: Réda Housni Alaoui <re...@gmail.com>
AuthorDate: Tue Aug 6 13:54:52 2019 +0200
XEP-0313 Message Archive Management
---
pom.xml | 6 +
.../vysper/xmpp/datetime/DateTimeProfile.java | 10 +
.../core/base/handler/AcceptedMessageEvent.java | 75 ++++++++
.../modules/core/base/handler/MessageHandler.java | 21 ++-
.../xep0059_result_set_management/Set.java | 67 ++++---
.../apache/vysper/xmpp/parser/XMLParserUtil.java | 5 +
.../apache/vysper/xmpp/protocol/NamespaceURIs.java | 4 +
.../apache/vysper/xmpp/stanza/StanzaBuilder.java | 4 +-
.../xmpp/stanza/dataforms/DataFormParser.java | 4 +-
.../ResultSetManagementTest.java | 12 +-
.../stanza/dataforms/DataFormParserTestCase.java | 4 +-
server/extensions/pom.xml | 1 +
server/extensions/xep0313-mam/pom.xml | 106 +++++++++++
.../modules/extension/xep0313_mam/MAMModule.java | 108 +++++++++++
.../extension/xep0313_mam/SimpleMessage.java | 57 ++++++
.../InMemoryArchivedMessagesLastPage.java | 70 +++++++
.../in_memory/InMemoryArchivedMessagesPage.java | 90 +++++++++
.../in_memory/InMemoryDateTimeFilter.java | 48 +++++
.../in_memory/InMemoryEntityFilter.java | 71 +++++++
.../in_memory/InMemoryMessageArchive.java | 64 +++++++
.../in_memory/InMemoryMessageArchives.java | 43 +++++
.../in_memory/InMemoryMessageFilter.java | 49 +++++
.../xep0313_mam/muc/MUCArchiveQueryHandler.java | 57 ++++++
.../pubsub/PubsubNodeArchiveQueryHandler.java | 53 ++++++
.../xep0313_mam/query/ArchiveEntityFilter.java | 79 ++++++++
.../extension/xep0313_mam/query/ArchiveFilter.java | 52 ++++++
.../extension/xep0313_mam/query/ArchiveQuery.java | 59 ++++++
.../extension/xep0313_mam/query/MAMIQHandler.java | 88 +++++++++
.../query/MatchingArchivedMessageResult.java | 108 +++++++++++
.../query/MatchingArchivedMessageResults.java | 93 ++++++++++
.../modules/extension/xep0313_mam/query/Query.java | 88 +++++++++
.../extension/xep0313_mam/query/QueryHandler.java | 37 ++++
.../extension/xep0313_mam/query/QuerySet.java | 78 ++++++++
.../modules/extension/xep0313_mam/query/X.java | 83 +++++++++
.../extension/xep0313_mam/spi/ArchivedMessage.java | 34 ++++
.../xep0313_mam/spi/ArchivedMessages.java | 60 ++++++
.../extension/xep0313_mam/spi/DateTimeFilter.java | 48 +++++
.../extension/xep0313_mam/spi/EntityFilter.java | 56 ++++++
.../modules/extension/xep0313_mam/spi/Message.java | 35 ++++
.../extension/xep0313_mam/spi/MessageArchive.java | 43 +++++
.../extension/xep0313_mam/spi/MessageArchives.java | 34 ++++
.../extension/xep0313_mam/spi/MessageFilter.java | 33 ++++
.../xep0313_mam/spi/MessagePageRequest.java | 35 ++++
.../xep0313_mam/spi/SimpleArchivedMessage.java | 82 +++++++++
.../xep0313_mam/spi/SimpleArchivedMessages.java | 82 +++++++++
.../xep0313_mam/user/UserArchiveQueryHandler.java | 93 ++++++++++
.../xep0313_mam/user/UserMessageListener.java | 93 ++++++++++
.../extension/xep0313_mam/IntegrationTest.java | 203 +++++++++++++++++++++
.../extension/xep0313_mam/MAMModuleTest.java | 84 +++++++++
.../xep0313_mam/ServerRuntimeContextMock.java | 158 ++++++++++++++++
.../extension/xep0313_mam/SessionContextMock.java | 145 +++++++++++++++
.../extension/xep0313_mam/StanzaAssert.java | 37 ++++
.../extension/xep0313_mam/UserArchiveTest.java | 149 +++++++++++++++
.../in_memory/InMemoryEntityFilterTest.java | 99 ++++++++++
.../InMemoryPageLimitedArchivedMessagesTest.java | 160 ++++++++++++++++
.../xep0313_mam/query/ArchiveEntityFilterTest.java | 68 +++++++
.../xep0313_mam/query/ArchiveQueryTest.java | 85 +++++++++
.../xep0313_mam/query/MAMIQHandlerTest.java | 140 ++++++++++++++
.../query/MatchingArchivedMessageResultsTest.java | 181 ++++++++++++++++++
.../extension/xep0313_mam/query/QuerySetTest.java | 58 ++++++
.../xep0313_mam/spi/MessageArchiveMock.java | 65 +++++++
.../xep0313_mam/spi/MessageArchivesMock.java | 45 +++++
.../xep0313_mam/spi/SimpleEntityFilter.java | 57 ++++++
.../xep0313_mam/spi/SimpleMessagePageRequest.java | 58 ++++++
.../user/UserArchiveQueryHandlerTest.java | 124 +++++++++++++
.../xep0313_mam/user/UserMessageListenerTest.java | 155 ++++++++++++++++
.../src/test/resources/bogus_mina_tls.cert | Bin 0 -> 2623 bytes
67 files changed, 4553 insertions(+), 40 deletions(-)
diff --git a/pom.xml b/pom.xml
index b3470ee..5281b32 100644
--- a/pom.xml
+++ b/pom.xml
@@ -291,6 +291,12 @@
</dependency>
<dependency>
+ <groupId>org.igniterealtime.smack</groupId>
+ <artifactId>smack-experimental</artifactId>
+ <version>4.3.4</version>
+ </dependency>
+
+ <dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.6.1</version>
diff --git a/server/core/src/main/java/org/apache/vysper/xmpp/datetime/DateTimeProfile.java b/server/core/src/main/java/org/apache/vysper/xmpp/datetime/DateTimeProfile.java
index 21377b4..aefe644 100644
--- a/server/core/src/main/java/org/apache/vysper/xmpp/datetime/DateTimeProfile.java
+++ b/server/core/src/main/java/org/apache/vysper/xmpp/datetime/DateTimeProfile.java
@@ -22,6 +22,7 @@ package org.apache.vysper.xmpp.datetime;
import static org.apache.vysper.compliance.SpecCompliant.ComplianceCoverage.COMPLETE;
import static org.apache.vysper.compliance.SpecCompliant.ComplianceStatus.IN_PROGRESS;
+import java.time.ZonedDateTime;
import java.util.Calendar;
import java.util.Date;
import java.util.TimeZone;
@@ -80,6 +81,10 @@ public class DateTimeProfile {
public String getDateTimeInUTC(Date time) {
return utcDateTimeFormatter.format(time);
}
+
+ public String getDateTimeInUTC(ZonedDateTime dateTime){
+ return getDateTimeInUTC(Date.from(dateTime.toInstant()));
+ }
public String getDateInUTC(Date time) {
return utcDateFormatter.format(time);
@@ -122,6 +127,11 @@ public class DateTimeProfile {
throw new IllegalArgumentException("Invalid date time: " + time);
}
}
+
+ public ZonedDateTime fromZonedDateTime(String time){
+ Calendar calendar = fromDateTime(time);
+ return ZonedDateTime.ofInstant(calendar.toInstant(), calendar.getTimeZone().toZoneId());
+ }
/**
* Parses a time, compliant with ISO-8601 and XEP-0082.
diff --git a/server/core/src/main/java/org/apache/vysper/xmpp/modules/core/base/handler/AcceptedMessageEvent.java b/server/core/src/main/java/org/apache/vysper/xmpp/modules/core/base/handler/AcceptedMessageEvent.java
new file mode 100644
index 0000000..a81426c
--- /dev/null
+++ b/server/core/src/main/java/org/apache/vysper/xmpp/modules/core/base/handler/AcceptedMessageEvent.java
@@ -0,0 +1,75 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.apache.vysper.xmpp.modules.core.base.handler;
+
+import static java.util.Objects.requireNonNull;
+import static java.util.Optional.ofNullable;
+
+import java.util.Optional;
+
+import org.apache.vysper.xmpp.server.ServerRuntimeContext;
+import org.apache.vysper.xmpp.server.SessionContext;
+import org.apache.vysper.xmpp.stanza.MessageStanza;
+
+/**
+ * @author Réda Housni Alaoui
+ */
+public class AcceptedMessageEvent {
+
+ private final ServerRuntimeContext serverRuntimeContext;
+
+ private final SessionContext sessionContext;
+
+ private final boolean outbound;
+
+ private final MessageStanza messageStanza;
+
+ public AcceptedMessageEvent(ServerRuntimeContext serverRuntimeContext, SessionContext sessionContext,
+ boolean outbound, MessageStanza messageStanza) {
+ this.serverRuntimeContext = requireNonNull(serverRuntimeContext);
+ this.sessionContext = requireNonNull(sessionContext);
+ this.outbound = outbound;
+ this.messageStanza = requireNonNull(messageStanza);
+ }
+
+ public ServerRuntimeContext serverRuntimeContext() {
+ return serverRuntimeContext;
+ }
+
+ public SessionContext sessionContext() {
+ return sessionContext;
+ }
+
+ public boolean isOutbound() {
+ return outbound;
+ }
+
+ public MessageStanza messageStanza() {
+ return messageStanza;
+ }
+
+ @Override
+ public String toString() {
+ return "AcceptedMessageEvent{" + "serverRuntimeContext=" + serverRuntimeContext + ", sessionContext="
+ + sessionContext + ", outbound=" + outbound + ", messageStanza=" + messageStanza + '}';
+ }
+
+
+}
diff --git a/server/core/src/main/java/org/apache/vysper/xmpp/modules/core/base/handler/MessageHandler.java b/server/core/src/main/java/org/apache/vysper/xmpp/modules/core/base/handler/MessageHandler.java
index 4958685..639a230 100644
--- a/server/core/src/main/java/org/apache/vysper/xmpp/modules/core/base/handler/MessageHandler.java
+++ b/server/core/src/main/java/org/apache/vysper/xmpp/modules/core/base/handler/MessageHandler.java
@@ -22,6 +22,7 @@ package org.apache.vysper.xmpp.modules.core.base.handler;
import java.util.List;
+import org.apache.vysper.event.EventBus;
import org.apache.vysper.xml.fragment.Attribute;
import org.apache.vysper.xml.fragment.XMLElement;
import org.apache.vysper.xml.fragment.XMLSemanticError;
@@ -42,6 +43,7 @@ import org.apache.vysper.xmpp.stanza.XMPPCoreStanza;
* @author The Apache MINA Project (dev@mina.apache.org)
*/
public class MessageHandler extends XMPPCoreStanzaHandler {
+
public String getName() {
return "message";
}
@@ -89,13 +91,8 @@ public class MessageHandler extends XMPPCoreStanzaHandler {
// TODO inspect all BODY elements and make sure they conform to the spec
+ EventBus eventBus = serverRuntimeContext.getEventBus();
if (isOutboundStanza) {
- // check if message reception is turned of either globally or locally
- if (!serverRuntimeContext.getServerFeatures().isRelayingMessages()
- || (sessionContext != null && sessionContext
- .getAttribute(SessionContext.SESSION_ATTRIBUTE_MESSAGE_STANZA_NO_RECEIVE) != null)) {
- return null;
- }
Entity from = stanza.getFrom();
if (from == null || !from.isResourceSet()) {
@@ -118,6 +115,16 @@ public class MessageHandler extends XMPPCoreStanzaHandler {
stanza = XMPPCoreStanza.getWrapper(stanzaBuilder.build());
}
+ eventBus.publish(AcceptedMessageEvent.class,
+ new AcceptedMessageEvent(serverRuntimeContext, sessionContext, true, new MessageStanza(stanza)));
+
+ // check if message reception is turned of either globally or locally
+ if (!serverRuntimeContext.getServerFeatures().isRelayingMessages()
+ || (sessionContext != null && sessionContext
+ .getAttribute(SessionContext.SESSION_ATTRIBUTE_MESSAGE_STANZA_NO_RECEIVE) != null)) {
+ return null;
+ }
+
try {
stanzaBroker.write(stanza.getTo(), stanza, new ReturnErrorToSenderFailureStrategy(stanzaBroker));
} catch (Exception e) {
@@ -125,6 +132,8 @@ public class MessageHandler extends XMPPCoreStanzaHandler {
e.printStackTrace(); // To change body of catch statement use File | Settings | File Templates.
}
} else {
+ eventBus.publish(AcceptedMessageEvent.class,
+ new AcceptedMessageEvent(serverRuntimeContext, sessionContext, false, new MessageStanza(stanza)));
stanzaBroker.writeToSession(stanza);
}
return null;
diff --git a/server/core/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0059_result_set_management/Set.java b/server/core/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0059_result_set_management/Set.java
index 680ce68..c6da782 100644
--- a/server/core/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0059_result_set_management/Set.java
+++ b/server/core/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0059_result_set_management/Set.java
@@ -26,7 +26,6 @@ import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
-import java.util.OptionalInt;
import org.apache.vysper.xml.fragment.Attribute;
import org.apache.vysper.xml.fragment.XMLElement;
@@ -44,7 +43,7 @@ import org.apache.vysper.xmpp.protocol.NamespaceURIs;
*/
public class Set {
- private static final String ELEMENT_NAME = "set";
+ public static final String ELEMENT_NAME = "set";
private static final String FIRST = "first";
@@ -64,6 +63,15 @@ public class Set {
this.element = element;
}
+ public static Set empty() {
+ return new Set(new XMLElement(NamespaceURIs.XEP0059_RESULT_SET_MANAGEMENT, ELEMENT_NAME, null,
+ Collections.emptyList(), Collections.emptyList()));
+ }
+
+ public XMLElement element() {
+ return element;
+ }
+
public static Builder builder() {
return new Builder();
}
@@ -76,33 +84,47 @@ public class Set {
return getElementText("before");
}
- public OptionalInt getCount() throws XMLSemanticError {
- return getElementInt("count");
+ public Optional<Long> getCount() throws XMLSemanticError {
+ return getElementLong("count");
}
public Optional<First> getFirst() throws XMLSemanticError {
return ofNullable(element.getSingleInnerElementsNamed(FIRST)).map(First::new);
}
- public OptionalInt getIndex() throws XMLSemanticError {
- return getElementInt(INDEX);
+ public Optional<Long> getIndex() throws XMLSemanticError {
+ return getElementLong(INDEX);
}
public Optional<String> getLast() throws XMLSemanticError {
return getElementText("last");
}
- public OptionalInt getMax() throws XMLSemanticError {
- return getElementInt("max");
+ public Optional<Long> getMax() throws XMLSemanticError {
+ return getElementLong("max");
}
private Optional<String> getElementText(String elementName) throws XMLSemanticError {
- return ofNullable(element.getSingleInnerElementsNamed(elementName)).map(XMLElement::getInnerText)
- .map(XMLText::getText);
+ XMLElement xmlElement = element.getSingleInnerElementsNamed(elementName);
+ if (xmlElement == null) {
+ return Optional.empty();
+ }
+
+ XMLText xmlText = xmlElement.getInnerText();
+ if (xmlText == null) {
+ return Optional.of("");
+ }
+
+ String text = xmlText.getText();
+ if (text == null) {
+ return Optional.of("");
+ }
+
+ return Optional.of(text);
}
- private OptionalInt getElementInt(String elementName) throws XMLSemanticError {
- return getElementText(elementName).map(Integer::parseInt).map(OptionalInt::of).orElse(OptionalInt.empty());
+ private Optional<Long> getElementLong(String elementName) throws XMLSemanticError {
+ return getElementText(elementName).map(Long::parseLong);
}
public static class First {
@@ -121,9 +143,8 @@ public class Set {
return element.getInnerText().getText();
}
- public OptionalInt getIndex() {
- return ofNullable(element.getAttributeValue(INDEX)).map(Integer::parseInt).map(OptionalInt::of)
- .orElse(OptionalInt.empty());
+ public Optional<Long> getIndex() {
+ return ofNullable(element.getAttributeValue(INDEX)).map(Long::parseLong);
}
}
@@ -134,15 +155,15 @@ public class Set {
private String before;
- private Integer count;
+ private Long count;
private First first;
- private Integer index;
+ private Long index;
private String last;
- private Integer max;
+ private Long max;
private Builder() {
@@ -158,7 +179,7 @@ public class Set {
return this;
}
- public Builder count(Integer count) {
+ public Builder count(Long count) {
this.count = count;
return this;
}
@@ -167,7 +188,7 @@ public class Set {
return new FirstBuilder(this);
}
- public Builder index(Integer index) {
+ public Builder index(Long index) {
this.index = index;
return this;
}
@@ -177,7 +198,7 @@ public class Set {
return this;
}
- public Builder max(Integer max) {
+ public Builder max(Long max) {
this.max = max;
return this;
}
@@ -209,7 +230,7 @@ public class Set {
private String value;
- private Integer index;
+ private Long index;
private FirstBuilder(Builder builder) {
this.builder = requireNonNull(builder);
@@ -220,7 +241,7 @@ public class Set {
return this;
}
- public FirstBuilder index(Integer index) {
+ public FirstBuilder index(Long index) {
this.index = index;
return this;
}
diff --git a/server/core/src/main/java/org/apache/vysper/xmpp/parser/XMLParserUtil.java b/server/core/src/main/java/org/apache/vysper/xmpp/parser/XMLParserUtil.java
index 1487ca0..cd59af2 100644
--- a/server/core/src/main/java/org/apache/vysper/xmpp/parser/XMLParserUtil.java
+++ b/server/core/src/main/java/org/apache/vysper/xmpp/parser/XMLParserUtil.java
@@ -33,6 +33,8 @@ import org.apache.vysper.xml.sax.NonBlockingXMLReader;
import org.apache.vysper.xml.sax.impl.DefaultNonBlockingXMLReader;
import org.xml.sax.SAXException;
+import static java.util.Optional.ofNullable;
+
/**
*
* @author The Apache MINA Project (dev@mina.apache.org)
@@ -73,5 +75,8 @@ public class XMLParserUtil {
}
}
+ public static XMLElement parseRequiredDocument(String xml) throws IOException, SAXException {
+ return ofNullable(parseDocument(xml)).orElseThrow(() -> new IllegalStateException("Parsed element should not be null"));
+ }
}
diff --git a/server/core/src/main/java/org/apache/vysper/xmpp/protocol/NamespaceURIs.java b/server/core/src/main/java/org/apache/vysper/xmpp/protocol/NamespaceURIs.java
index 56bc78c..d86a161 100644
--- a/server/core/src/main/java/org/apache/vysper/xmpp/protocol/NamespaceURIs.java
+++ b/server/core/src/main/java/org/apache/vysper/xmpp/protocol/NamespaceURIs.java
@@ -111,4 +111,8 @@ public class NamespaceURIs {
public static final String XEP0133_SERVICE_ADMIN = "http://jabber.org/protocol/admin";
public static final String XEP0059_RESULT_SET_MANAGEMENT = "http://jabber.org/protocol/rsm";
+
+ public static final String XEP0297_STANZA_FORWARDING = "urn:xmpp:forward:0";
+
+ public static final String XEP0359_STANZA_IDS = "urn:xmpp:sid:0";
}
diff --git a/server/core/src/main/java/org/apache/vysper/xmpp/stanza/StanzaBuilder.java b/server/core/src/main/java/org/apache/vysper/xmpp/stanza/StanzaBuilder.java
index da697a1..fb28a97 100644
--- a/server/core/src/main/java/org/apache/vysper/xmpp/stanza/StanzaBuilder.java
+++ b/server/core/src/main/java/org/apache/vysper/xmpp/stanza/StanzaBuilder.java
@@ -55,7 +55,9 @@ public class StanzaBuilder extends AbstractXMLElementBuilder<StanzaBuilder, Stan
public static StanzaBuilder createMessageStanza(Entity from, Entity to, String lang, String body) {
StanzaBuilder stanzaBuilder = new StanzaBuilder("message", NamespaceURIs.JABBER_CLIENT);
- stanzaBuilder.addAttribute("from", from.getFullQualifiedName());
+ if (from != null) {
+ stanzaBuilder.addAttribute("from", from.getFullQualifiedName());
+ }
stanzaBuilder.addAttribute("to", to.getFullQualifiedName());
if (lang != null)
stanzaBuilder.addAttribute(NamespaceURIs.XML, "lang", lang);
diff --git a/server/core/src/main/java/org/apache/vysper/xmpp/stanza/dataforms/DataFormParser.java b/server/core/src/main/java/org/apache/vysper/xmpp/stanza/dataforms/DataFormParser.java
index 0682bca..6efd971 100644
--- a/server/core/src/main/java/org/apache/vysper/xmpp/stanza/dataforms/DataFormParser.java
+++ b/server/core/src/main/java/org/apache/vysper/xmpp/stanza/dataforms/DataFormParser.java
@@ -76,7 +76,7 @@ public class DataFormParser {
public Map<String, Object> extractFieldValues() throws IllegalArgumentException {
Map<String,Object> map = new LinkedHashMap<String, Object>();
- for (XMLElement fields : form.getInnerElementsNamed("field", NamespaceURIs.JABBER_X_DATA)) {
+ for (XMLElement fields : form.getInnerElementsNamed("field")) {
final String varName = fields.getAttributeValue("var");
final String typeName = fields.getAttributeValue("type");
String valueAsString = null;
@@ -93,7 +93,7 @@ public class DataFormParser {
boolean isMulti = Field.Type.isMulti(fieldType);
List<Object> values = isMulti ? new ArrayList<Object>() : null;
- for (XMLElement valueCandidates : fields.getInnerElementsNamed("value", NamespaceURIs.JABBER_X_DATA)) {
+ for (XMLElement valueCandidates : fields.getInnerElementsNamed("value")) {
final XMLText firstInnerText = valueCandidates.getFirstInnerText();
if (firstInnerText != null) valueAsString = firstInnerText.getText();
Object value;
diff --git a/server/core/src/test/java/org/apache/vysper/xmpp/modules/extension/xep0059_result_set_management/ResultSetManagementTest.java b/server/core/src/test/java/org/apache/vysper/xmpp/modules/extension/xep0059_result_set_management/ResultSetManagementTest.java
index c52dfd6..5049e14 100644
--- a/server/core/src/test/java/org/apache/vysper/xmpp/modules/extension/xep0059_result_set_management/ResultSetManagementTest.java
+++ b/server/core/src/test/java/org/apache/vysper/xmpp/modules/extension/xep0059_result_set_management/ResultSetManagementTest.java
@@ -31,19 +31,19 @@ public class ResultSetManagementTest {
@Test
public void parse() throws XMLSemanticError {
- Set tested = Set.builder().after("after-value").before("before-value").count(5)
- .index(10).last("last-value").max(15).startFirst().index(20).value("first-value").endFirst().build();
+ Set tested = Set.builder().after("after-value").before("before-value").count(5L).index(10L).last("last-value")
+ .max(15L).startFirst().index(20L).value("first-value").endFirst().build();
assertEquals("after-value", tested.getAfter().get());
assertEquals("before-value", tested.getBefore().get());
- assertEquals(5, tested.getCount().getAsInt());
- assertEquals(10, tested.getIndex().getAsInt());
+ assertEquals(5L, (long) tested.getCount().orElse(0L));
+ assertEquals(10L, (long) tested.getIndex().orElse(0L));
assertEquals("last-value", tested.getLast().get());
- assertEquals(15, tested.getMax().getAsInt());
+ assertEquals(15L, (long) tested.getMax().orElse(0L));
Set.First first = tested.getFirst().get();
assertEquals("first-value", first.getValue());
- assertEquals(20, first.getIndex().getAsInt());
+ assertEquals(20L, (long) first.getIndex().orElse(0L));
}
}
\ No newline at end of file
diff --git a/server/core/src/test/java/org/apache/vysper/xmpp/stanza/dataforms/DataFormParserTestCase.java b/server/core/src/test/java/org/apache/vysper/xmpp/stanza/dataforms/DataFormParserTestCase.java
index 650ce83..a3c92a0 100644
--- a/server/core/src/test/java/org/apache/vysper/xmpp/stanza/dataforms/DataFormParserTestCase.java
+++ b/server/core/src/test/java/org/apache/vysper/xmpp/stanza/dataforms/DataFormParserTestCase.java
@@ -134,14 +134,14 @@ public class DataFormParserTestCase {
private void assertMultiValue(String type, List<String> values, List<Object> expectedValues) {
XMLElementBuilder builder = new XMLElementBuilder("x", NamespaceURIs.JABBER_X_DATA)
- .startInnerElement("field", NamespaceURIs.JABBER_X_DATA)
+ .startInnerElement("field")
.addAttribute("var", "fie1");
if(type != null) builder.addAttribute("type", type);
for(String value : values) {
- builder.startInnerElement("value", NamespaceURIs.JABBER_X_DATA);
+ builder.startInnerElement("value");
builder.addText(value);
builder.endInnerElement();
}
diff --git a/server/extensions/pom.xml b/server/extensions/pom.xml
index f785d50..e11e83b 100644
--- a/server/extensions/pom.xml
+++ b/server/extensions/pom.xml
@@ -37,6 +37,7 @@
<module>xep0065-socks</module>
<module>xep0124-xep0206-bosh</module>
<module>websockets</module>
+ <module>xep0313-mam</module>
</modules>
</project>
diff --git a/server/extensions/xep0313-mam/pom.xml b/server/extensions/xep0313-mam/pom.xml
new file mode 100644
index 0000000..12314c4
--- /dev/null
+++ b/server/extensions/xep0313-mam/pom.xml
@@ -0,0 +1,106 @@
+<?xml version="1.0" encoding="UTF-8"?>
+ <!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to you 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.
+ -->
+
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+ <parent>
+ <artifactId>vysper-extensions</artifactId>
+ <groupId>org.apache.vysper</groupId>
+ <version>0.8-SNAPSHOT</version>
+ </parent>
+ <modelVersion>4.0.0</modelVersion>
+ <groupId>org.apache.vysper.extensions</groupId>
+ <artifactId>xep0313-mam</artifactId>
+ <name>Apache Vysper XEP-0313 Message Archive Management</name>
+ <version>0.8-SNAPSHOT</version>
+
+
+ <dependencies>
+ <dependency>
+ <groupId>org.apache.vysper</groupId>
+ <artifactId>spec-compliance</artifactId>
+ <optional>true</optional>
+ </dependency>
+
+ <dependency>
+ <groupId>org.apache.vysper</groupId>
+ <artifactId>vysper-core</artifactId>
+ </dependency>
+
+ <!-- Runtime dependencies -->
+ <dependency>
+ <groupId>org.slf4j</groupId>
+ <artifactId>slf4j-log4j12</artifactId>
+ <scope>runtime</scope>
+ </dependency>
+
+ <dependency>
+ <groupId>log4j</groupId>
+ <artifactId>log4j</artifactId>
+ <scope>runtime</scope>
+ </dependency>
+
+ <!-- Test dependencies -->
+ <dependency>
+ <groupId>junit</groupId>
+ <artifactId>junit</artifactId>
+ <scope>test</scope>
+ </dependency>
+
+ <dependency>
+ <groupId>org.mockito</groupId>
+ <artifactId>mockito-all</artifactId>
+ <scope>test</scope>
+ </dependency>
+
+ <dependency>
+ <groupId>org.igniterealtime.smack</groupId>
+ <artifactId>smack-tcp</artifactId>
+ <scope>test</scope>
+ </dependency>
+
+ <dependency>
+ <groupId>org.igniterealtime.smack</groupId>
+ <artifactId>smack-resolver-javax</artifactId>
+ <scope>test</scope>
+ </dependency>
+
+ <dependency>
+ <groupId>org.igniterealtime.smack</groupId>
+ <artifactId>smack-sasl-javax</artifactId>
+ <scope>test</scope>
+ </dependency>
+
+ <dependency>
+ <groupId>org.igniterealtime.smack</groupId>
+ <artifactId>smack-java7</artifactId>
+ <scope>test</scope>
+ </dependency>
+
+ <dependency>
+ <groupId>org.igniterealtime.smack</groupId>
+ <artifactId>smack-experimental</artifactId>
+ <scope>test</scope>
+ </dependency>
+
+ <dependency>
+ <groupId>org.igniterealtime.smack</groupId>
+ <artifactId>smack-extensions</artifactId>
+ <scope>test</scope>
+ </dependency>
+ </dependencies>
+</project>
diff --git a/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/MAMModule.java b/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/MAMModule.java
new file mode 100644
index 0000000..b3c3ece
--- /dev/null
+++ b/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/MAMModule.java
@@ -0,0 +1,108 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.apache.vysper.xmpp.modules.extension.xep0313_mam;
+
+import static java.util.Objects.requireNonNull;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+
+import org.apache.commons.lang.StringUtils;
+import org.apache.vysper.event.EventListenerDictionary;
+import org.apache.vysper.event.SimpleEventListenerDictionary;
+import org.apache.vysper.xmpp.modules.DefaultDiscoAwareModule;
+import org.apache.vysper.xmpp.modules.core.base.handler.AcceptedMessageEvent;
+import org.apache.vysper.xmpp.modules.extension.xep0313_mam.query.MAMIQHandler;
+import org.apache.vysper.xmpp.modules.extension.xep0313_mam.spi.MessageArchives;
+import org.apache.vysper.xmpp.modules.extension.xep0313_mam.user.UserMessageListener;
+import org.apache.vysper.xmpp.modules.servicediscovery.management.Feature;
+import org.apache.vysper.xmpp.modules.servicediscovery.management.InfoElement;
+import org.apache.vysper.xmpp.modules.servicediscovery.management.InfoRequest;
+import org.apache.vysper.xmpp.modules.servicediscovery.management.ServerInfoRequestListener;
+import org.apache.vysper.xmpp.protocol.HandlerDictionary;
+import org.apache.vysper.xmpp.protocol.NamespaceHandlerDictionary;
+import org.apache.vysper.xmpp.protocol.NamespaceURIs;
+import org.apache.vysper.xmpp.server.ServerRuntimeContext;
+
+/**
+ * A module for <a href="https://xmpp.org/extensions/xep-0313.html">XEP-0313
+ * Message Archive Management</a>
+ *
+ * @author Réda Housni Alaoui
+ */
+public class MAMModule extends DefaultDiscoAwareModule implements ServerInfoRequestListener {
+
+ private static final String NAMESPACE_V1 = "urn:xmpp:mam:1";
+
+ private static final String NAMESPACE_V2 = "urn:xmpp:mam:2";
+
+ private final UserMessageListener userMessageListener = new UserMessageListener();
+
+ @Override
+ public void initialize(ServerRuntimeContext serverRuntimeContext) {
+ super.initialize(serverRuntimeContext);
+
+ requireNonNull((MessageArchives) serverRuntimeContext.getStorageProvider(MessageArchives.class),
+ "Could not find an instance of " + MessageArchives.class);
+ }
+
+ @Override
+ public String getName() {
+ return "XEP-0313 Message Archive Management";
+ }
+
+ @Override
+ public String getVersion() {
+ return "0.6.3";
+ }
+
+ @Override
+ protected void addServerInfoRequestListeners(List<ServerInfoRequestListener> serverInfoRequestListeners) {
+ serverInfoRequestListeners.add(this);
+ }
+
+ @Override
+ public List<InfoElement> getServerInfosFor(InfoRequest request) {
+ if (StringUtils.isNotEmpty(request.getNode())) {
+ return null;
+ }
+
+ List<InfoElement> infoElements = new ArrayList<>();
+ infoElements.add(new Feature(NAMESPACE_V1));
+ infoElements.add(new Feature(NAMESPACE_V2));
+ infoElements.add(new Feature(NamespaceURIs.XEP0359_STANZA_IDS));
+ infoElements.add(new Feature(NamespaceURIs.JABBER_X_DATA));
+ return infoElements;
+ }
+
+ @Override
+ protected void addHandlerDictionaries(List<HandlerDictionary> dictionary) {
+ dictionary.add(new NamespaceHandlerDictionary(NAMESPACE_V1, new MAMIQHandler(NAMESPACE_V1)));
+ dictionary.add(new NamespaceHandlerDictionary(NAMESPACE_V2, new MAMIQHandler(NAMESPACE_V2)));
+ }
+
+ @Override
+ public Optional<EventListenerDictionary> getEventListenerDictionary() {
+ EventListenerDictionary dictionary = SimpleEventListenerDictionary.builder()
+ .register(AcceptedMessageEvent.class, userMessageListener).build();
+ return Optional.of(dictionary);
+ }
+}
diff --git a/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/SimpleMessage.java b/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/SimpleMessage.java
new file mode 100644
index 0000000..9e3a42b
--- /dev/null
+++ b/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/SimpleMessage.java
@@ -0,0 +1,57 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.apache.vysper.xmpp.modules.extension.xep0313_mam;
+
+import static java.util.Objects.requireNonNull;
+
+import java.time.ZonedDateTime;
+
+import org.apache.vysper.xmpp.modules.extension.xep0313_mam.spi.Message;
+import org.apache.vysper.xmpp.stanza.MessageStanza;
+
+/**
+ * @author Réda Housni Alaoui
+ */
+public class SimpleMessage implements Message {
+
+ private final MessageStanza stanza;
+
+ private final ZonedDateTime dateTime;
+
+ public SimpleMessage(MessageStanza stanza) {
+ this.stanza = requireNonNull(stanza);
+ this.dateTime = ZonedDateTime.now();
+ }
+
+ @Override
+ public MessageStanza stanza() {
+ return stanza;
+ }
+
+ @Override
+ public ZonedDateTime dateTime() {
+ return dateTime;
+ }
+
+ @Override
+ public String toString() {
+ return "SimpleMessage{" + "stanza=" + stanza + ", dateTime=" + dateTime + '}';
+ }
+}
diff --git a/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/in_memory/InMemoryArchivedMessagesLastPage.java b/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/in_memory/InMemoryArchivedMessagesLastPage.java
new file mode 100644
index 0000000..e7e49df
--- /dev/null
+++ b/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/in_memory/InMemoryArchivedMessagesLastPage.java
@@ -0,0 +1,70 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.apache.vysper.xmpp.modules.extension.xep0313_mam.in_memory;
+
+import java.util.List;
+import java.util.Optional;
+import java.util.stream.Collectors;
+
+import org.apache.vysper.xmpp.modules.extension.xep0313_mam.spi.ArchivedMessage;
+import org.apache.vysper.xmpp.modules.extension.xep0313_mam.spi.ArchivedMessages;
+
+/**
+ * @author Réda Housni Alaoui
+ */
+public class InMemoryArchivedMessagesLastPage implements ArchivedMessages {
+
+ private final List<ArchivedMessage> list;
+
+ private final Long firstMessageIndex;
+
+ private final long totalNumberOfMessages;
+
+ public InMemoryArchivedMessagesLastPage(long pageSize, List<ArchivedMessage> unlimitedMessages) {
+ totalNumberOfMessages = unlimitedMessages.size();
+ this.firstMessageIndex = totalNumberOfMessages - Math.min(totalNumberOfMessages, pageSize);
+ this.list = unlimitedMessages.stream().skip(firstMessageIndex).collect(Collectors.toList());
+ }
+
+ @Override
+ public List<ArchivedMessage> list() {
+ return list;
+ }
+
+ @Override
+ public boolean isEmpty() {
+ return list.isEmpty();
+ }
+
+ @Override
+ public boolean isComplete() {
+ return list.size() == totalNumberOfMessages;
+ }
+
+ @Override
+ public Optional<Long> firstMessageIndex() {
+ return Optional.ofNullable(firstMessageIndex);
+ }
+
+ @Override
+ public Optional<Long> totalNumberOfMessages() {
+ return Optional.of(totalNumberOfMessages);
+ }
+}
diff --git a/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/in_memory/InMemoryArchivedMessagesPage.java b/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/in_memory/InMemoryArchivedMessagesPage.java
new file mode 100644
index 0000000..3f5ed19
--- /dev/null
+++ b/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/in_memory/InMemoryArchivedMessagesPage.java
@@ -0,0 +1,90 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.apache.vysper.xmpp.modules.extension.xep0313_mam.in_memory;
+
+import java.util.List;
+import java.util.Optional;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.stream.Collectors;
+
+import org.apache.vysper.xmpp.modules.extension.xep0313_mam.spi.ArchivedMessage;
+import org.apache.vysper.xmpp.modules.extension.xep0313_mam.spi.ArchivedMessages;
+import org.apache.vysper.xmpp.modules.extension.xep0313_mam.spi.MessagePageRequest;
+
+/**
+ * @author Réda Housni Alaoui
+ */
+public class InMemoryArchivedMessagesPage implements ArchivedMessages {
+
+ private final List<ArchivedMessage> list;
+
+ private final Long firstMessageIndex;
+
+ private final long totalNumberOfMessages;
+
+ public InMemoryArchivedMessagesPage(MessagePageRequest request, List<ArchivedMessage> unlimitedMessages) {
+ totalNumberOfMessages = unlimitedMessages.size();
+
+ String firstMessageId = request.firstMessageId().orElse(null);
+ String lastMessageId = request.lastMessageId().orElse(null);
+ Long pageSize = request.pageSize().orElse(null);
+
+ AtomicBoolean firstMessageFound = new AtomicBoolean(firstMessageId == null);
+ AtomicBoolean lastMessageFound = new AtomicBoolean();
+ AtomicLong numberOfMessageKept = new AtomicLong();
+
+ list = unlimitedMessages.stream().filter(message -> pageSize == null || numberOfMessageKept.get() < pageSize)
+ .peek(message -> lastMessageFound.compareAndSet(false,
+ lastMessageId != null && lastMessageId.equals(message.id())))
+ .filter(message -> !lastMessageFound.get())
+ .peek(message -> firstMessageFound.compareAndSet(false, message.id().equals(firstMessageId)))
+ .filter(message -> firstMessageFound.get() && !message.id().equals(firstMessageId))
+ .peek(message -> numberOfMessageKept.incrementAndGet()).collect(Collectors.toList());
+
+ firstMessageIndex = list.stream().findFirst().map(unlimitedMessages::indexOf).map(index -> (long) index)
+ .orElse(null);
+ }
+
+ @Override
+ public List<ArchivedMessage> list() {
+ return list;
+ }
+
+ @Override
+ public boolean isEmpty() {
+ return list.isEmpty();
+ }
+
+ @Override
+ public boolean isComplete() {
+ return list.size() == totalNumberOfMessages;
+ }
+
+ @Override
+ public Optional<Long> firstMessageIndex() {
+ return Optional.ofNullable(firstMessageIndex);
+ }
+
+ @Override
+ public Optional<Long> totalNumberOfMessages() {
+ return Optional.of(totalNumberOfMessages);
+ }
+}
diff --git a/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/in_memory/InMemoryDateTimeFilter.java b/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/in_memory/InMemoryDateTimeFilter.java
new file mode 100644
index 0000000..2b55f5c
--- /dev/null
+++ b/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/in_memory/InMemoryDateTimeFilter.java
@@ -0,0 +1,48 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.apache.vysper.xmpp.modules.extension.xep0313_mam.in_memory;
+
+import static java.util.Objects.requireNonNull;
+
+import java.util.Optional;
+import java.util.function.Predicate;
+
+import org.apache.vysper.xmpp.modules.extension.xep0313_mam.spi.ArchivedMessage;
+import org.apache.vysper.xmpp.modules.extension.xep0313_mam.spi.DateTimeFilter;
+
+/**
+ * @author Réda Housni Alaoui
+ */
+public class InMemoryDateTimeFilter implements Predicate<ArchivedMessage> {
+
+ private final DateTimeFilter dateTimeFilter;
+
+ public InMemoryDateTimeFilter(DateTimeFilter dateTimeFilter) {
+ this.dateTimeFilter = requireNonNull(dateTimeFilter);
+ }
+
+ @Override
+ public boolean test(ArchivedMessage message) {
+ return Optional.of(message)
+ .filter(msg -> dateTimeFilter.start().map(start -> msg.dateTime().isAfter(start)).orElse(true))
+ .filter(msg -> dateTimeFilter.end().map(end -> msg.dateTime().isBefore(end)).orElse(true)).isPresent();
+ }
+
+}
diff --git a/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/in_memory/InMemoryEntityFilter.java b/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/in_memory/InMemoryEntityFilter.java
new file mode 100644
index 0000000..ef0b137
--- /dev/null
+++ b/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/in_memory/InMemoryEntityFilter.java
@@ -0,0 +1,71 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.apache.vysper.xmpp.modules.extension.xep0313_mam.in_memory;
+
+import static java.util.Objects.requireNonNull;
+
+import java.util.function.Predicate;
+
+import org.apache.vysper.xmpp.addressing.Entity;
+import org.apache.vysper.xmpp.modules.extension.xep0313_mam.spi.ArchivedMessage;
+import org.apache.vysper.xmpp.modules.extension.xep0313_mam.spi.EntityFilter;
+import org.apache.vysper.xmpp.stanza.MessageStanza;
+
+/**
+ * @author Réda Housni Alaoui
+ */
+public class InMemoryEntityFilter implements Predicate<ArchivedMessage> {
+
+ private final EntityFilter filter;
+
+ public InMemoryEntityFilter(EntityFilter filter) {
+ this.filter = requireNonNull(filter);
+ }
+
+ @Override
+ public boolean test(ArchivedMessage message) {
+ EntityFilter.Type type = filter.type();
+ if (type == EntityFilter.Type.TO_AND_FROM) {
+ return matchToAndFrom(message);
+ } else if (type == EntityFilter.Type.TO_OR_FROM) {
+ return matchToOrFrom(message);
+ } else {
+ throw new IllegalArgumentException("Unexpected entity filter type '" + type + "'");
+ }
+ }
+
+ private boolean matchToAndFrom(ArchivedMessage message) {
+ MessageStanza stanza = message.stanza();
+ return entitiesEquals(filter.entity(), stanza.getTo()) && entitiesEquals(filter.entity(), stanza.getFrom());
+ }
+
+ private boolean matchToOrFrom(ArchivedMessage message) {
+ MessageStanza stanza = message.stanza();
+ return entitiesEquals(filter.entity(), stanza.getTo()) || entitiesEquals(filter.entity(), stanza.getFrom());
+ }
+
+ private boolean entitiesEquals(Entity entity1, Entity entity2) {
+ if (filter.ignoreResource()) {
+ return entity1.getBareJID().equals(entity2.getBareJID());
+ } else {
+ return entity1.equals(entity2);
+ }
+ }
+}
diff --git a/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/in_memory/InMemoryMessageArchive.java b/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/in_memory/InMemoryMessageArchive.java
new file mode 100644
index 0000000..c0bcc91
--- /dev/null
+++ b/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/in_memory/InMemoryMessageArchive.java
@@ -0,0 +1,64 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.apache.vysper.xmpp.modules.extension.xep0313_mam.in_memory;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.UUID;
+import java.util.function.Predicate;
+import java.util.stream.Collectors;
+
+import org.apache.vysper.xmpp.modules.extension.xep0313_mam.spi.ArchivedMessage;
+import org.apache.vysper.xmpp.modules.extension.xep0313_mam.spi.ArchivedMessages;
+import org.apache.vysper.xmpp.modules.extension.xep0313_mam.spi.Message;
+import org.apache.vysper.xmpp.modules.extension.xep0313_mam.spi.MessageArchive;
+import org.apache.vysper.xmpp.modules.extension.xep0313_mam.spi.MessageFilter;
+import org.apache.vysper.xmpp.modules.extension.xep0313_mam.spi.MessagePageRequest;
+import org.apache.vysper.xmpp.modules.extension.xep0313_mam.spi.SimpleArchivedMessage;
+
+/**
+ * @author Réda Housni Alaoui
+ */
+public class InMemoryMessageArchive implements MessageArchive {
+
+ private final List<SimpleArchivedMessage> messages = new ArrayList<>();
+
+ @Override
+ public void archive(Message message) {
+ messages.add(new SimpleArchivedMessage(UUID.randomUUID().toString(), message));
+ }
+
+ @Override
+ public ArchivedMessages fetchSortedByOldestFirst(MessageFilter messageFilter, MessagePageRequest pageRequest) {
+ List<ArchivedMessage> filteredMessages = filterMessages(new InMemoryMessageFilter(messageFilter));
+ return new InMemoryArchivedMessagesPage(pageRequest, filteredMessages);
+ }
+
+ @Override
+ public ArchivedMessages fetchLastPageSortedByOldestFirst(MessageFilter messageFilter, long pageSize) {
+ List<ArchivedMessage> filteredMessages = filterMessages(new InMemoryMessageFilter(messageFilter));
+ return new InMemoryArchivedMessagesLastPage(pageSize, filteredMessages);
+ }
+
+ private List<ArchivedMessage> filterMessages(Predicate<ArchivedMessage> predicate) {
+ return messages.stream().filter(predicate).collect(Collectors.toList());
+ }
+
+}
diff --git a/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/in_memory/InMemoryMessageArchives.java b/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/in_memory/InMemoryMessageArchives.java
new file mode 100644
index 0000000..52b35da
--- /dev/null
+++ b/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/in_memory/InMemoryMessageArchives.java
@@ -0,0 +1,43 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.apache.vysper.xmpp.modules.extension.xep0313_mam.in_memory;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Optional;
+
+import org.apache.vysper.xmpp.addressing.Entity;
+import org.apache.vysper.xmpp.modules.extension.xep0313_mam.spi.MessageArchive;
+import org.apache.vysper.xmpp.modules.extension.xep0313_mam.spi.MessageArchives;
+
+/**
+ * @author Réda Housni Alaoui
+ */
+public class InMemoryMessageArchives implements MessageArchives {
+
+ private final Map<Entity, MessageArchive> userMessageArchiveById = new HashMap<>();
+
+ @Override
+ public Optional<MessageArchive> retrieveUserMessageArchive(Entity userBareJid) {
+ MessageArchive messageArchive = userMessageArchiveById.computeIfAbsent(userBareJid,
+ id -> new InMemoryMessageArchive());
+ return Optional.of(messageArchive);
+ }
+}
diff --git a/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/in_memory/InMemoryMessageFilter.java b/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/in_memory/InMemoryMessageFilter.java
new file mode 100644
index 0000000..5f8f1cf
--- /dev/null
+++ b/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/in_memory/InMemoryMessageFilter.java
@@ -0,0 +1,49 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.apache.vysper.xmpp.modules.extension.xep0313_mam.in_memory;
+
+import java.util.Optional;
+import java.util.function.Predicate;
+import java.util.stream.Stream;
+
+import org.apache.vysper.xmpp.modules.extension.xep0313_mam.spi.ArchivedMessage;
+import org.apache.vysper.xmpp.modules.extension.xep0313_mam.spi.MessageFilter;
+
+/**
+ * @author Réda Housni Alaoui
+ */
+public class InMemoryMessageFilter implements Predicate<ArchivedMessage> {
+
+ private final Predicate<ArchivedMessage> predicate;
+
+ public InMemoryMessageFilter(MessageFilter messageFilter) {
+ Optional<Predicate<ArchivedMessage>> dateTimeFilter = messageFilter.dateTimeFilter()
+ .map(InMemoryDateTimeFilter::new);
+ Optional<Predicate<ArchivedMessage>> entityFilter = messageFilter.entityFilter().map(InMemoryEntityFilter::new);
+
+ predicate = Stream.of(dateTimeFilter, entityFilter).filter(Optional::isPresent).map(Optional::get)
+ .reduce(Predicate::and).orElse(message -> true);
+ }
+
+ @Override
+ public boolean test(ArchivedMessage message) {
+ return predicate.test(message);
+ }
+}
diff --git a/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/muc/MUCArchiveQueryHandler.java b/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/muc/MUCArchiveQueryHandler.java
new file mode 100644
index 0000000..7ecd3ff
--- /dev/null
+++ b/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/muc/MUCArchiveQueryHandler.java
@@ -0,0 +1,57 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.apache.vysper.xmpp.modules.extension.xep0313_mam.muc;
+
+import static java.util.Optional.ofNullable;
+
+import java.util.Collections;
+import java.util.List;
+
+import org.apache.vysper.xmpp.addressing.Entity;
+import org.apache.vysper.xmpp.modules.extension.xep0313_mam.query.QueryHandler;
+import org.apache.vysper.xmpp.modules.extension.xep0313_mam.query.Query;
+import org.apache.vysper.xmpp.server.ServerRuntimeContext;
+import org.apache.vysper.xmpp.server.SessionContext;
+import org.apache.vysper.xmpp.server.response.ServerErrorResponses;
+import org.apache.vysper.xmpp.stanza.Stanza;
+import org.apache.vysper.xmpp.stanza.StanzaErrorCondition;
+import org.apache.vysper.xmpp.stanza.StanzaErrorType;
+
+/**
+ * @author Réda Housni Alaoui
+ */
+public class MUCArchiveQueryHandler implements QueryHandler {
+
+ @Override
+ public boolean supports(Query query, ServerRuntimeContext serverRuntimeContext, SessionContext sessionContext) {
+ Entity queriedEntity = ofNullable(query.iqStanza().getTo()).orElse(sessionContext.getInitiatingEntity());
+ return !serverRuntimeContext.getServerEntity().getDomain().equals(queriedEntity.getDomain());
+ }
+
+ @Override
+ public List<Stanza> handle(Query query, ServerRuntimeContext serverRuntimeContext,
+ SessionContext sessionContext) {
+ // MUC archives is not yet implemented
+ Stanza notImplemented = ServerErrorResponses.getStanzaError(StanzaErrorCondition.FEATURE_NOT_IMPLEMENTED,
+ query.iqStanza(), StanzaErrorType.MODIFY,
+ "Multi-User Chat message archive feature is not yet implemented", null, null);
+ return Collections.singletonList(notImplemented);
+ }
+}
diff --git a/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/pubsub/PubsubNodeArchiveQueryHandler.java b/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/pubsub/PubsubNodeArchiveQueryHandler.java
new file mode 100644
index 0000000..be88bbb
--- /dev/null
+++ b/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/pubsub/PubsubNodeArchiveQueryHandler.java
@@ -0,0 +1,53 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.apache.vysper.xmpp.modules.extension.xep0313_mam.pubsub;
+
+import java.util.Collections;
+import java.util.List;
+
+import org.apache.vysper.xmpp.modules.extension.xep0313_mam.query.QueryHandler;
+import org.apache.vysper.xmpp.modules.extension.xep0313_mam.query.Query;
+import org.apache.vysper.xmpp.server.ServerRuntimeContext;
+import org.apache.vysper.xmpp.server.SessionContext;
+import org.apache.vysper.xmpp.server.response.ServerErrorResponses;
+import org.apache.vysper.xmpp.stanza.Stanza;
+import org.apache.vysper.xmpp.stanza.StanzaErrorCondition;
+import org.apache.vysper.xmpp.stanza.StanzaErrorType;
+
+/**
+ * @author Réda Housni Alaoui
+ */
+public class PubsubNodeArchiveQueryHandler implements QueryHandler {
+
+ @Override
+ public boolean supports(Query query, ServerRuntimeContext serverRuntimeContext, SessionContext sessionContext) {
+ return query.getNode().isPresent();
+ }
+
+ @Override
+ public List<Stanza> handle(Query query, ServerRuntimeContext serverRuntimeContext,
+ SessionContext sessionContext) {
+ // PubSub node archives is not yet implemented
+ Stanza notImplemented = ServerErrorResponses.getStanzaError(StanzaErrorCondition.FEATURE_NOT_IMPLEMENTED,
+ query.iqStanza(), StanzaErrorType.CANCEL, "Pubsub node message archive feature is not yet implemented",
+ null, null);
+ return Collections.singletonList(notImplemented);
+ }
+}
diff --git a/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/query/ArchiveEntityFilter.java b/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/query/ArchiveEntityFilter.java
new file mode 100644
index 0000000..c17603e
--- /dev/null
+++ b/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/query/ArchiveEntityFilter.java
@@ -0,0 +1,79 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.apache.vysper.xmpp.modules.extension.xep0313_mam.query;
+
+import static java.util.Objects.requireNonNull;
+
+import org.apache.vysper.xmpp.addressing.Entity;
+import org.apache.vysper.xmpp.modules.extension.xep0313_mam.spi.EntityFilter;
+
+/**
+ * @author Réda Housni Alaoui
+ */
+public class ArchiveEntityFilter implements EntityFilter {
+
+ private final Entity with;
+
+ private final Type type;
+
+ private final boolean ignoreResource;
+
+ public ArchiveEntityFilter(Entity archiveId, Entity with) {
+ this.with = requireNonNull(with);
+ if (archiveId.equals(with)) {
+ // If the 'with' field's value is the bare JID of the archive, the server must
+ // only return results where both
+ // the 'to' and 'from' match the bare JID (either as bare or by ignoring the
+ // resource), as otherwise every
+ // message in the archive would match
+ this.type = Type.TO_AND_FROM;
+ this.ignoreResource = true;
+ } else if (!with.isResourceSet()) {
+ // If (and only if) the supplied JID is a bare JID (i.e. no resource is
+ // present),
+ // then the server SHOULD return messages if their bare to/from address for a
+ // user archive would match it.
+ this.type = Type.TO_OR_FROM;
+ this.ignoreResource = true;
+ } else {
+ // A message in a user's archive matches if the JID matches either the to or
+ // from of the message.
+ this.type = Type.TO_OR_FROM;
+ this.ignoreResource = false;
+ }
+ }
+
+ @Override
+ public Entity entity() {
+ return with;
+ }
+
+ @Override
+ public Type type() {
+ return type;
+ }
+
+ @Override
+ public boolean ignoreResource() {
+ return ignoreResource;
+ }
+
+
+}
diff --git a/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/query/ArchiveFilter.java b/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/query/ArchiveFilter.java
new file mode 100644
index 0000000..db8fed8
--- /dev/null
+++ b/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/query/ArchiveFilter.java
@@ -0,0 +1,52 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.apache.vysper.xmpp.modules.extension.xep0313_mam.query;
+
+import java.util.Optional;
+
+import org.apache.vysper.xmpp.addressing.Entity;
+import org.apache.vysper.xmpp.modules.extension.xep0313_mam.spi.DateTimeFilter;
+import org.apache.vysper.xmpp.modules.extension.xep0313_mam.spi.EntityFilter;
+import org.apache.vysper.xmpp.modules.extension.xep0313_mam.spi.MessageFilter;
+
+/**
+ * @author Réda Housni Alaoui
+ */
+public class ArchiveFilter implements MessageFilter {
+
+ private final EntityFilter entityFilter;
+
+ private final DateTimeFilter dateTimeFilter;
+
+ public ArchiveFilter(Entity archiveId, X x) {
+ entityFilter = x.getWith().map(entity -> new ArchiveEntityFilter(archiveId, entity)).orElse(null);
+ dateTimeFilter = x;
+ }
+
+ @Override
+ public Optional<EntityFilter> entityFilter() {
+ return Optional.ofNullable(entityFilter);
+ }
+
+ @Override
+ public Optional<DateTimeFilter> dateTimeFilter() {
+ return Optional.of(dateTimeFilter);
+ }
+}
diff --git a/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/query/ArchiveQuery.java b/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/query/ArchiveQuery.java
new file mode 100644
index 0000000..7b4fde6
--- /dev/null
+++ b/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/query/ArchiveQuery.java
@@ -0,0 +1,59 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.apache.vysper.xmpp.modules.extension.xep0313_mam.query;
+
+import static java.util.Objects.requireNonNull;
+
+import org.apache.vysper.xml.fragment.XMLSemanticError;
+import org.apache.vysper.xmpp.addressing.Entity;
+import org.apache.vysper.xmpp.modules.extension.xep0313_mam.spi.ArchivedMessages;
+import org.apache.vysper.xmpp.modules.extension.xep0313_mam.spi.MessageArchive;
+
+/**
+ * @author Réda Housni Alaoui
+ */
+public class ArchiveQuery {
+
+ private final MessageArchive archive;
+
+ private final Entity archiveId;
+
+ private final Query query;
+
+ public ArchiveQuery(MessageArchive archive, Entity archiveId, Query query) {
+ this.archive = requireNonNull(archive);
+ this.archiveId = requireNonNull(archiveId);
+ this.query = requireNonNull(query);
+ }
+
+ public ArchivedMessages execute() throws XMLSemanticError {
+ ArchiveFilter archiveFilter = new ArchiveFilter(archiveId, query.getX());
+ QuerySet querySet = query.getSet();
+
+ if (querySet.lastPage()) {
+ long pageSize = querySet.pageSize().orElseThrow(
+ () -> new IllegalArgumentException("Page size must be defined when requesting last page"));
+ return archive.fetchLastPageSortedByOldestFirst(archiveFilter, pageSize);
+ } else {
+ return archive.fetchSortedByOldestFirst(archiveFilter, querySet);
+ }
+ }
+
+}
diff --git a/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/query/MAMIQHandler.java b/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/query/MAMIQHandler.java
new file mode 100644
index 0000000..53ec4ca
--- /dev/null
+++ b/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/query/MAMIQHandler.java
@@ -0,0 +1,88 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.apache.vysper.xmpp.modules.extension.xep0313_mam.query;
+
+import static java.util.Objects.requireNonNull;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import org.apache.vysper.xml.fragment.XMLSemanticError;
+import org.apache.vysper.xmpp.modules.core.base.handler.DefaultIQHandler;
+import org.apache.vysper.xmpp.modules.extension.xep0313_mam.muc.MUCArchiveQueryHandler;
+import org.apache.vysper.xmpp.modules.extension.xep0313_mam.pubsub.PubsubNodeArchiveQueryHandler;
+import org.apache.vysper.xmpp.modules.extension.xep0313_mam.user.UserArchiveQueryHandler;
+import org.apache.vysper.xmpp.protocol.StanzaBroker;
+import org.apache.vysper.xmpp.server.ServerRuntimeContext;
+import org.apache.vysper.xmpp.server.SessionContext;
+import org.apache.vysper.xmpp.server.response.ServerErrorResponses;
+import org.apache.vysper.xmpp.stanza.IQStanza;
+import org.apache.vysper.xmpp.stanza.Stanza;
+import org.apache.vysper.xmpp.stanza.StanzaErrorCondition;
+import org.apache.vysper.xmpp.stanza.StanzaErrorType;
+
+/**
+ * @author Réda Housni Alaoui
+ */
+public class MAMIQHandler extends DefaultIQHandler {
+
+ private final String namespace;
+
+ private final List<QueryHandler> queryHandlers;
+
+ public MAMIQHandler(String namespace) {
+ this(namespace, new PubsubNodeArchiveQueryHandler(), new MUCArchiveQueryHandler(),
+ new UserArchiveQueryHandler());
+ }
+
+ public MAMIQHandler(String namespace, QueryHandler pubsubNodeArchiveQueryHandler,
+ QueryHandler mucArchiveQueryHandler, QueryHandler userArchiveQueryHandler) {
+ this.namespace = requireNonNull(namespace);
+ List<QueryHandler> queryHandlers = new ArrayList<>();
+ queryHandlers.add(pubsubNodeArchiveQueryHandler);
+ queryHandlers.add(mucArchiveQueryHandler);
+ queryHandlers.add(userArchiveQueryHandler);
+ this.queryHandlers = Collections.unmodifiableList(queryHandlers);
+ }
+
+ @Override
+ protected boolean verifyInnerElement(Stanza stanza) {
+ return verifyInnerElementWorker(stanza, Query.ELEMENT_NAME) && verifyInnerNamespace(stanza, namespace);
+ }
+
+ @Override
+ protected List<Stanza> handleSet(IQStanza stanza, ServerRuntimeContext serverRuntimeContext,
+ SessionContext sessionContext, StanzaBroker broker) {
+ Query query;
+ try {
+ query = new Query(namespace, stanza);
+ } catch (XMLSemanticError xmlSemanticError) {
+ return Collections.singletonList(ServerErrorResponses.getStanzaError(StanzaErrorCondition.NOT_ACCEPTABLE,
+ stanza, StanzaErrorType.CANCEL, null, null, null));
+ }
+
+ return queryHandlers.stream().filter(handler -> handler.supports(query, serverRuntimeContext, sessionContext))
+ .map(handler -> handler.handle(query, serverRuntimeContext, sessionContext)).findFirst()
+ .orElseGet(() -> Collections.singletonList(ServerErrorResponses.getStanzaError(
+ StanzaErrorCondition.NOT_ACCEPTABLE, stanza, StanzaErrorType.CANCEL, null, null, null)));
+ }
+
+}
diff --git a/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/query/MatchingArchivedMessageResult.java b/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/query/MatchingArchivedMessageResult.java
new file mode 100644
index 0000000..3f9c638
--- /dev/null
+++ b/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/query/MatchingArchivedMessageResult.java
@@ -0,0 +1,108 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.apache.vysper.xmpp.modules.extension.xep0313_mam.query;
+
+import static java.util.Objects.requireNonNull;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import org.apache.vysper.xml.fragment.Attribute;
+import org.apache.vysper.xml.fragment.XMLElement;
+import org.apache.vysper.xml.fragment.XMLFragment;
+import org.apache.vysper.xmpp.addressing.Entity;
+import org.apache.vysper.xmpp.datetime.DateTimeProfile;
+import org.apache.vysper.xmpp.modules.extension.xep0313_mam.spi.ArchivedMessage;
+import org.apache.vysper.xmpp.protocol.NamespaceURIs;
+import org.apache.vysper.xmpp.stanza.MessageStanza;
+import org.apache.vysper.xmpp.stanza.Stanza;
+import org.apache.vysper.xmpp.stanza.StanzaBuilder;
+
+/**
+ * @author Réda Housni Alaoui
+ */
+class MatchingArchivedMessageResult {
+
+ private static final String STANZA_ID = "stanza-id";
+
+ private final Entity initiatingEntity;
+
+ private final Entity archiveId;
+
+ private final Query query;
+
+ private final ArchivedMessage archivedMessage;
+
+ MatchingArchivedMessageResult(Entity initiatingEntity, Entity archiveId, Query query,
+ ArchivedMessage archivedMessage) {
+ this.initiatingEntity = requireNonNull(initiatingEntity);
+ this.archiveId = requireNonNull(archiveId);
+ this.query = requireNonNull(query);
+ this.archivedMessage = requireNonNull(archivedMessage);
+ }
+
+ Stanza toStanza() {
+ XMLElement result = createResult();
+ return new StanzaBuilder("message").addAttribute("to", initiatingEntity.getFullQualifiedName())
+ .addPreparedElement(result).build();
+ }
+
+ private XMLElement createResult() {
+ XMLElement forwarded = createForwarded();
+ List<Attribute> attributes = new ArrayList<>();
+ attributes.add(new Attribute("id", archivedMessage.id()));
+ query.getQueryId().map(queryId -> new Attribute("queryid", queryId)).ifPresent(attributes::add);
+ return new XMLElement(query.getNamespace(), "result", null, attributes, Collections.singletonList(forwarded));
+ }
+
+ private XMLElement createForwarded() {
+ Stanza archivedStanzaWithId = completeMessageStanzaWithId();
+
+ String stamp = DateTimeProfile.getInstance().getDateTimeInUTC(archivedMessage.dateTime());
+
+ List<XMLFragment> innerElements = new ArrayList<>();
+ innerElements.add(new XMLElement(NamespaceURIs.URN_XMPP_DELAY, "delay", null,
+ Collections.singletonList(new Attribute("stamp", stamp)), Collections.emptyList()));
+ innerElements.add(archivedStanzaWithId);
+ return new XMLElement(NamespaceURIs.XEP0297_STANZA_FORWARDING, "forwarded", null, Collections.emptyList(),
+ innerElements);
+ }
+
+ private Stanza completeMessageStanzaWithId() {
+ MessageStanza archivedMessageStanza = archivedMessage.stanza();
+
+ List<XMLElement> innerElements = new ArrayList<>();
+ archivedMessageStanza.getInnerElements().stream().filter(xmlElement -> !STANZA_ID.equals(xmlElement.getName()))
+ .filter(xmlElement -> !NamespaceURIs.XEP0359_STANZA_IDS.equals(xmlElement.getNamespaceURI()))
+ .forEach(innerElements::add);
+ List<Attribute> stanzaIdAttributes = new ArrayList<>();
+ stanzaIdAttributes.add(new Attribute("by", archiveId.getFullQualifiedName()));
+ stanzaIdAttributes.add(new Attribute("id", archivedMessage.id()));
+ innerElements.add(new XMLElement(NamespaceURIs.XEP0359_STANZA_IDS, STANZA_ID, null, stanzaIdAttributes,
+ Collections.emptyList()));
+
+ StanzaBuilder archivedMessageStanzaWithIdBuilder = StanzaBuilder.createClone(archivedMessageStanza, false,
+ Collections.emptyList());
+ innerElements.forEach(archivedMessageStanzaWithIdBuilder::addPreparedElement);
+ return archivedMessageStanzaWithIdBuilder.build();
+ }
+
+}
diff --git a/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/query/MatchingArchivedMessageResults.java b/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/query/MatchingArchivedMessageResults.java
new file mode 100644
index 0000000..d02931a
--- /dev/null
+++ b/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/query/MatchingArchivedMessageResults.java
@@ -0,0 +1,93 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.apache.vysper.xmpp.modules.extension.xep0313_mam.query;
+
+import static java.util.Objects.requireNonNull;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import org.apache.vysper.xml.fragment.Attribute;
+import org.apache.vysper.xml.fragment.XMLElement;
+import org.apache.vysper.xmpp.addressing.Entity;
+import org.apache.vysper.xmpp.modules.extension.xep0059_result_set_management.Set;
+import org.apache.vysper.xmpp.modules.extension.xep0313_mam.spi.ArchivedMessage;
+import org.apache.vysper.xmpp.modules.extension.xep0313_mam.spi.ArchivedMessages;
+import org.apache.vysper.xmpp.stanza.IQStanzaType;
+import org.apache.vysper.xmpp.stanza.Stanza;
+import org.apache.vysper.xmpp.stanza.StanzaBuilder;
+
+/**
+ * @author Réda Housni Alaoui
+ */
+public class MatchingArchivedMessageResults {
+
+ private final Entity initiatingEntity;
+
+ private final Entity archiveId;
+
+ private final Query query;
+
+ private final ArchivedMessages archivedMessages;
+
+ public MatchingArchivedMessageResults(Entity initiatingEntity, Entity archiveId, Query query,
+ ArchivedMessages archivedMessages) {
+ this.initiatingEntity = requireNonNull(initiatingEntity);
+ this.archiveId = requireNonNull(archiveId);
+ this.query = requireNonNull(query);
+ this.archivedMessages = requireNonNull(archivedMessages);
+ }
+
+ public List<Stanza> toStanzas() {
+ List<Stanza> stanzas = new ArrayList<>();
+ archivedMessages.list().stream().map(archivedMessage -> new MatchingArchivedMessageResult(initiatingEntity,
+ archiveId, query, archivedMessage)).map(MatchingArchivedMessageResult::toStanza).forEach(stanzas::add);
+ stanzas.add(buildResultIq());
+ return stanzas;
+ }
+
+ private Stanza buildResultIq() {
+ Set set = buildSet();
+ List<Attribute> finAttributes = new ArrayList<>();
+ if (archivedMessages.isComplete()) {
+ finAttributes.add(new Attribute("complete", "true"));
+ }
+ XMLElement fin = new XMLElement(query.getNamespace(), "fin", null, finAttributes,
+ Collections.singletonList(set.element()));
+ return StanzaBuilder.createDirectReply(query.iqStanza(), false, IQStanzaType.RESULT).addPreparedElement(fin)
+ .build();
+ }
+
+ private Set buildSet() {
+ if (archivedMessages.isEmpty()) {
+ return Set.builder().count(archivedMessages.totalNumberOfMessages().orElse(null)).build();
+ }
+
+ List<ArchivedMessage> messagesList = archivedMessages.list();
+
+ ArchivedMessage firstMessage = messagesList.get(0);
+ ArchivedMessage lastMessage = messagesList.get(messagesList.size() - 1);
+
+ return Set.builder().startFirst().index(archivedMessages.firstMessageIndex().orElse(null))
+ .value(firstMessage.id()).endFirst().last(lastMessage.id())
+ .count(archivedMessages.totalNumberOfMessages().orElse(null)).build();
+ }
+}
diff --git a/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/query/Query.java b/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/query/Query.java
new file mode 100644
index 0000000..a17ef65
--- /dev/null
+++ b/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/query/Query.java
@@ -0,0 +1,88 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.apache.vysper.xmpp.modules.extension.xep0313_mam.query;
+
+import static java.util.Objects.requireNonNull;
+import static java.util.Optional.ofNullable;
+
+import java.util.Optional;
+
+import org.apache.vysper.xml.fragment.XMLElement;
+import org.apache.vysper.xml.fragment.XMLSemanticError;
+import org.apache.vysper.xmpp.modules.extension.xep0059_result_set_management.Set;
+import org.apache.vysper.xmpp.stanza.IQStanza;
+
+/**
+ * @author Réda Housni Alaoui
+ */
+public class Query {
+
+ public static final String ELEMENT_NAME = "query";
+
+ private final String namespace;
+
+ private final IQStanza iqStanza;
+
+ private final XMLElement element;
+
+ public Query(String namespace, IQStanza iqStanza) throws XMLSemanticError {
+ this.namespace = requireNonNull(namespace);
+ this.iqStanza = iqStanza;
+ this.element = iqStanza.getSingleInnerElementsNamed(ELEMENT_NAME);
+ if (!ELEMENT_NAME.equals(element.getName())) {
+ throw new IllegalArgumentException(
+ "Query element must be named '" + ELEMENT_NAME + "' instead of '" + element.getName() + "'");
+ }
+ if (!namespace.equals(element.getNamespaceURI())) {
+ throw new IllegalArgumentException("Query element must be bound to namespace uri '" + namespace
+ + "' instead of '" + element.getNamespaceURI() + "'");
+ }
+ }
+
+ public String getNamespace() {
+ return namespace;
+ }
+
+ public Optional<String> getQueryId() {
+ return ofNullable(element.getAttributeValue("queryid"));
+ }
+
+ public Optional<String> getNode() {
+ return ofNullable(element.getAttributeValue("node"));
+ }
+
+ public X getX() throws XMLSemanticError {
+ return ofNullable(element.getSingleInnerElementsNamed(X.ELEMENT_NAME))
+ .map(element1 -> new X(namespace, element1)).orElseGet(() -> X.empty(namespace));
+ }
+
+ public QuerySet getSet() throws XMLSemanticError {
+ XMLElement setElement = element.getSingleInnerElementsNamed(Set.ELEMENT_NAME);
+ if (setElement == null) {
+ return QuerySet.empty();
+ }
+ return new QuerySet(new Set(setElement));
+ }
+
+ public IQStanza iqStanza() {
+ return iqStanza;
+ }
+
+}
diff --git a/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/query/QueryHandler.java b/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/query/QueryHandler.java
new file mode 100644
index 0000000..0ce937c
--- /dev/null
+++ b/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/query/QueryHandler.java
@@ -0,0 +1,37 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.apache.vysper.xmpp.modules.extension.xep0313_mam.query;
+
+import java.util.List;
+
+import org.apache.vysper.xmpp.server.ServerRuntimeContext;
+import org.apache.vysper.xmpp.server.SessionContext;
+import org.apache.vysper.xmpp.stanza.Stanza;
+
+/**
+ * @author Réda Housni Alaoui
+ */
+public interface QueryHandler {
+
+ boolean supports(Query query, ServerRuntimeContext serverRuntimeContext, SessionContext sessionContext);
+
+ List<Stanza> handle(Query query, ServerRuntimeContext serverRuntimeContext, SessionContext sessionContext);
+
+}
diff --git a/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/query/QuerySet.java b/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/query/QuerySet.java
new file mode 100644
index 0000000..968f3be
--- /dev/null
+++ b/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/query/QuerySet.java
@@ -0,0 +1,78 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.apache.vysper.xmpp.modules.extension.xep0313_mam.query;
+
+import static java.util.Optional.ofNullable;
+
+import java.util.Optional;
+
+import org.apache.commons.lang.StringUtils;
+import org.apache.vysper.xml.fragment.XMLSemanticError;
+import org.apache.vysper.xmpp.modules.extension.xep0059_result_set_management.Set;
+import org.apache.vysper.xmpp.modules.extension.xep0313_mam.spi.MessagePageRequest;
+
+/**
+ * @author Réda Housni Alaoui
+ */
+public class QuerySet implements MessagePageRequest {
+
+ private final Long max;
+
+ private final String after;
+
+ private final String before;
+
+ public QuerySet(Set set) throws XMLSemanticError {
+ this.max = set.getMax().orElse(null);
+ this.after = set.getAfter().orElse(null);
+ this.before = set.getBefore().orElse(null);
+ }
+
+ public static QuerySet empty() throws XMLSemanticError {
+ return new QuerySet(Set.empty());
+ }
+
+ @Override
+ public Optional<Long> pageSize() {
+ return ofNullable(max);
+ }
+
+ @Override
+ public Optional<String> firstMessageId() {
+ return ofNullable(after);
+ }
+
+ @Override
+ public Optional<String> lastMessageId() {
+ return ofNullable(before);
+ }
+
+ /**
+ * <a href="https://xmpp.org/extensions/xep-0059.html#last">Requesting the Last
+ * Page in a Result Set</a>
+ *
+ * The requesting entity MAY ask for the last page in a result set by including
+ * in its request an empty <before/> element, and the maximum number of items to
+ * return.
+ */
+ public boolean lastPage() {
+ return max != null && after == null && ofNullable(before).filter(StringUtils::isBlank).isPresent();
+ }
+}
diff --git a/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/query/X.java b/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/query/X.java
new file mode 100644
index 0000000..86faa91
--- /dev/null
+++ b/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/query/X.java
@@ -0,0 +1,83 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.apache.vysper.xmpp.modules.extension.xep0313_mam.query;
+
+import static java.util.Optional.ofNullable;
+
+import java.time.ZonedDateTime;
+import java.util.Collections;
+import java.util.Map;
+import java.util.Optional;
+
+import org.apache.vysper.xml.fragment.XMLElement;
+import org.apache.vysper.xmpp.addressing.Entity;
+import org.apache.vysper.xmpp.addressing.EntityImpl;
+import org.apache.vysper.xmpp.datetime.DateTimeProfile;
+import org.apache.vysper.xmpp.modules.extension.xep0313_mam.spi.DateTimeFilter;
+import org.apache.vysper.xmpp.protocol.NamespaceURIs;
+import org.apache.vysper.xmpp.stanza.dataforms.DataFormParser;
+
+/**
+ * @author Réda Housni Alaoui
+ */
+public class X implements DateTimeFilter {
+
+ public static final String ELEMENT_NAME = "x";
+
+ private final Map<String, Object> fields;
+
+ public X(String namespace, XMLElement element) {
+ if (!ELEMENT_NAME.equals(element.getName())) {
+ throw new IllegalArgumentException(
+ "Query element must be named '" + ELEMENT_NAME + "' instead of '" + element.getName() + "'");
+ }
+ if (!NamespaceURIs.JABBER_X_DATA.equals(element.getNamespaceURI())) {
+ throw new IllegalArgumentException("Query element must be bound to namespace uri '" + namespace
+ + "' instead of '" + element.getNamespaceURI() + "'");
+ }
+ fields = new DataFormParser(element).extractFieldValues();
+ }
+
+ public static X empty(String namespace) {
+ return new X(namespace, new XMLElement(NamespaceURIs.JABBER_X_DATA, ELEMENT_NAME, null, Collections.emptyList(),
+ Collections.emptyList()));
+ }
+
+ private Optional<String> getStringValue(String fieldName) {
+ return ofNullable(fields.get(fieldName)).map(String.class::cast);
+ }
+
+ private Optional<ZonedDateTime> getZonedDateTime(String fieldName) {
+ return getStringValue(fieldName).map(stringDate -> DateTimeProfile.getInstance().fromZonedDateTime(stringDate));
+ }
+
+ public Optional<Entity> getWith() {
+ return getStringValue("with").map(EntityImpl::parseUnchecked);
+ }
+
+ public Optional<ZonedDateTime> start() {
+ return getZonedDateTime("start");
+ }
+
+ public Optional<ZonedDateTime> end() {
+ return getZonedDateTime("end");
+ }
+
+}
diff --git a/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/spi/ArchivedMessage.java b/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/spi/ArchivedMessage.java
new file mode 100644
index 0000000..3a20f30
--- /dev/null
+++ b/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/spi/ArchivedMessage.java
@@ -0,0 +1,34 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.apache.vysper.xmpp.modules.extension.xep0313_mam.spi;
+
+/**
+ * @author Réda Housni Alaoui
+ */
+public interface ArchivedMessage extends Message {
+
+ /**
+ * @return The id of the message in its archive as defined in <a
+ * href="https://xmpp.org/extensions/xep-0313.html#archives_id">Communicating
+ * the archive ID</a>
+ */
+ String id();
+
+}
diff --git a/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/spi/ArchivedMessages.java b/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/spi/ArchivedMessages.java
new file mode 100644
index 0000000..a8e3cb1
--- /dev/null
+++ b/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/spi/ArchivedMessages.java
@@ -0,0 +1,60 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.apache.vysper.xmpp.modules.extension.xep0313_mam.spi;
+
+import java.util.List;
+import java.util.Optional;
+
+/**
+ * @author Réda Housni Alaoui
+ */
+public interface ArchivedMessages {
+
+ List<ArchivedMessage> list();
+
+ boolean isEmpty();
+
+ /**
+ * When the results returned by the server are complete (that is: when they have
+ * not been limited by the maximum size of the result page (either as specified
+ * or enforced by the server)), the server MUST include a 'complete' attribute
+ * on the <fin> element, with a value of 'true'; this informs the client that it
+ * doesn't need to perform further paging to retreive the requested data. If it
+ * is not the last page of the result set, the server MUST either omit the
+ * 'complete' attribute, or give it a value of 'false'.
+ */
+ boolean isComplete();
+
+ /**
+ * This integer specifies the position within the full set (which MAY be
+ * approximate) of the first message in the page. If that message is the first
+ * in the full set, then the index SHOULD be '0'. If the last message in the
+ * page is the last message in the full set, then the value SHOULD be the
+ * specified count minus the number of messages in the last page.
+ */
+ Optional<Long> firstMessageIndex();
+
+ /**
+ * @return The total number of messages that could be retrieved by paging
+ * through the pages.
+ */
+ Optional<Long> totalNumberOfMessages();
+
+}
diff --git a/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/spi/DateTimeFilter.java b/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/spi/DateTimeFilter.java
new file mode 100644
index 0000000..0a033d8
--- /dev/null
+++ b/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/spi/DateTimeFilter.java
@@ -0,0 +1,48 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.apache.vysper.xmpp.modules.extension.xep0313_mam.spi;
+
+import java.time.ZonedDateTime;
+import java.util.Optional;
+
+/**
+ * <a href="https://xmpp.org/extensions/xep-0313.html#filter-time">Filtering by
+ * time received</a>
+ *
+ * @author Réda Housni Alaoui
+ */
+public interface DateTimeFilter {
+
+ /**
+ * The 'start' field is used to filter out messages before a certain date/time.
+ * If specified, a server MUST only return messages whose timestamp is equal to
+ * or later than the given timestamp.
+ */
+ Optional<ZonedDateTime> start();
+
+ /**
+ * The 'end' field is used to exclude from the results messages
+ * after a certain point in time. If specified, a server MUST only return
+ * messages whose timestamp is equal to or earlier than the timestamp given in
+ * the 'end' field.
+ */
+ Optional<ZonedDateTime> end();
+
+}
diff --git a/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/spi/EntityFilter.java b/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/spi/EntityFilter.java
new file mode 100644
index 0000000..f274c11
--- /dev/null
+++ b/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/spi/EntityFilter.java
@@ -0,0 +1,56 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.apache.vysper.xmpp.modules.extension.xep0313_mam.spi;
+
+import org.apache.vysper.xmpp.addressing.Entity;
+
+/**
+ * @author Réda Housni Alaoui
+ */
+public interface EntityFilter {
+
+ enum Type {
+ /**
+ * The message 'to' or 'from' must match the provided entity
+ */
+ TO_OR_FROM,
+ /**
+ * The message 'to' and 'from' must match the provided entity
+ */
+ TO_AND_FROM;
+ }
+
+ /**
+ * @return The entity to filter by
+ */
+ Entity entity();
+
+ /**
+ * @return The type of filtering
+ */
+ Type type();
+
+ /**
+ * @return True to ignore the filtering entity, the message 'to' and the message
+ * 'from' <b>resource part</b>.
+ */
+ boolean ignoreResource();
+
+}
diff --git a/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/spi/Message.java b/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/spi/Message.java
new file mode 100644
index 0000000..0ef98e5
--- /dev/null
+++ b/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/spi/Message.java
@@ -0,0 +1,35 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.apache.vysper.xmpp.modules.extension.xep0313_mam.spi;
+
+import java.time.ZonedDateTime;
+
+import org.apache.vysper.xmpp.stanza.MessageStanza;
+
+/**
+ * @author Réda Housni Alaoui
+ */
+public interface Message {
+
+ MessageStanza stanza();
+
+ ZonedDateTime dateTime();
+
+}
diff --git a/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/spi/MessageArchive.java b/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/spi/MessageArchive.java
new file mode 100644
index 0000000..99d2d06
--- /dev/null
+++ b/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/spi/MessageArchive.java
@@ -0,0 +1,43 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.apache.vysper.xmpp.modules.extension.xep0313_mam.spi;
+
+/**
+ * <a href=
+ * "https://xmpp.org/extensions/xep-0313.html#business-storeret">Storage and
+ * Retrieval Rules</a>
+ *
+ * @author Réda Housni Alaoui
+ */
+public interface MessageArchive {
+
+ /**
+ * At a minimum, the server MUST store the <body> elements of a stanza. It is
+ * suggested that other elements that are used in a given deployment to
+ * supplement conversations (e.g. XHTML-IM payloads) are also stored. Other
+ * elements MAY be stored.
+ */
+ void archive(Message message);
+
+ ArchivedMessages fetchSortedByOldestFirst(MessageFilter messageFilter, MessagePageRequest pageRequest);
+
+ ArchivedMessages fetchLastPageSortedByOldestFirst(MessageFilter messageFilter, long pageSize);
+
+}
diff --git a/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/spi/MessageArchives.java b/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/spi/MessageArchives.java
new file mode 100644
index 0000000..e4df43b
--- /dev/null
+++ b/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/spi/MessageArchives.java
@@ -0,0 +1,34 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.apache.vysper.xmpp.modules.extension.xep0313_mam.spi;
+
+import java.util.Optional;
+
+import org.apache.vysper.storage.StorageProvider;
+import org.apache.vysper.xmpp.addressing.Entity;
+
+/**
+ * @author Réda Housni Alaoui
+ */
+public interface MessageArchives extends StorageProvider {
+
+ Optional<MessageArchive> retrieveUserMessageArchive(Entity userBareJid);
+
+}
diff --git a/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/spi/MessageFilter.java b/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/spi/MessageFilter.java
new file mode 100644
index 0000000..6110f2b
--- /dev/null
+++ b/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/spi/MessageFilter.java
@@ -0,0 +1,33 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.apache.vysper.xmpp.modules.extension.xep0313_mam.spi;
+
+import java.util.Optional;
+
+/**
+ * @author Réda Housni Alaoui
+ */
+public interface MessageFilter {
+
+ Optional<EntityFilter> entityFilter();
+
+ Optional<DateTimeFilter> dateTimeFilter();
+
+}
diff --git a/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/spi/MessagePageRequest.java b/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/spi/MessagePageRequest.java
new file mode 100644
index 0000000..6f04adf
--- /dev/null
+++ b/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/spi/MessagePageRequest.java
@@ -0,0 +1,35 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.apache.vysper.xmpp.modules.extension.xep0313_mam.spi;
+
+import java.util.Optional;
+
+/**
+ * @author Réda Housni Alaoui
+ */
+public interface MessagePageRequest {
+
+ Optional<Long> pageSize();
+
+ Optional<String> firstMessageId();
+
+ Optional<String> lastMessageId();
+
+}
diff --git a/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/spi/SimpleArchivedMessage.java b/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/spi/SimpleArchivedMessage.java
new file mode 100644
index 0000000..80e271a
--- /dev/null
+++ b/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/spi/SimpleArchivedMessage.java
@@ -0,0 +1,82 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.apache.vysper.xmpp.modules.extension.xep0313_mam.spi;
+
+import static java.util.Objects.requireNonNull;
+
+import java.time.ZonedDateTime;
+
+import org.apache.vysper.xmpp.stanza.MessageStanza;
+
+/**
+ * @author Réda Housni Alaoui
+ */
+public class SimpleArchivedMessage implements ArchivedMessage {
+
+ private final String id;
+
+ private final ZonedDateTime dateTime;
+
+ private final MessageStanza stanza;
+
+ public SimpleArchivedMessage(String id, Message message) {
+ this(id, message.dateTime(), message.stanza());
+ }
+
+ public SimpleArchivedMessage(String id, ZonedDateTime dateTime, MessageStanza stanza) {
+ this.id = requireNonNull(id);
+ this.dateTime = requireNonNull(dateTime);
+ this.stanza = requireNonNull(stanza);
+ }
+
+ @Override
+ public MessageStanza stanza() {
+ return stanza;
+ }
+
+ @Override
+ public ZonedDateTime dateTime() {
+ return dateTime;
+ }
+
+ @Override
+ public String id() {
+ return id;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ SimpleArchivedMessage that = (SimpleArchivedMessage) o;
+
+ return id.equals(that.id);
+ }
+
+ @Override
+ public int hashCode() {
+ return id.hashCode();
+ }
+}
diff --git a/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/spi/SimpleArchivedMessages.java b/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/spi/SimpleArchivedMessages.java
new file mode 100644
index 0000000..342c346
--- /dev/null
+++ b/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/spi/SimpleArchivedMessages.java
@@ -0,0 +1,82 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.apache.vysper.xmpp.modules.extension.xep0313_mam.spi;
+
+import static java.util.Optional.ofNullable;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Optional;
+
+/**
+ * @author Réda Housni Alaoui
+ */
+public class SimpleArchivedMessages implements ArchivedMessages {
+
+ private final List<ArchivedMessage> list;
+
+ private final Long firstMessageIndex;
+
+ private final Long totalNumberOfMessages;
+
+ public SimpleArchivedMessages(List<ArchivedMessage> list) {
+ this(list, null, null);
+ }
+
+ public SimpleArchivedMessages(List<ArchivedMessage> list, Long firstMessageIndex) {
+ this(list, firstMessageIndex, null);
+ }
+
+ public SimpleArchivedMessages(List<ArchivedMessage> list, Long firstMessageIndex, Long totalNumberOfMessages) {
+ this.list = new ArrayList<>(list);
+ this.firstMessageIndex = firstMessageIndex;
+ this.totalNumberOfMessages = totalNumberOfMessages;
+ }
+
+ public static SimpleArchivedMessages empty() {
+ return new SimpleArchivedMessages(Collections.emptyList());
+ }
+
+ @Override
+ public List<ArchivedMessage> list() {
+ return new ArrayList<>(list);
+ }
+
+ @Override
+ public boolean isEmpty() {
+ return list.isEmpty();
+ }
+
+ @Override
+ public boolean isComplete() {
+ return list.size() == totalNumberOfMessages;
+ }
+
+ @Override
+ public Optional<Long> firstMessageIndex() {
+ return ofNullable(firstMessageIndex);
+ }
+
+ @Override
+ public Optional<Long> totalNumberOfMessages() {
+ return ofNullable(totalNumberOfMessages);
+ }
+}
diff --git a/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/user/UserArchiveQueryHandler.java b/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/user/UserArchiveQueryHandler.java
new file mode 100644
index 0000000..1d28b37
--- /dev/null
+++ b/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/user/UserArchiveQueryHandler.java
@@ -0,0 +1,93 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.apache.vysper.xmpp.modules.extension.xep0313_mam.user;
+
+import static java.util.Objects.requireNonNull;
+import static java.util.Optional.ofNullable;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Optional;
+
+import org.apache.vysper.xml.fragment.XMLSemanticError;
+import org.apache.vysper.xmpp.addressing.Entity;
+import org.apache.vysper.xmpp.modules.extension.xep0313_mam.query.ArchiveQuery;
+import org.apache.vysper.xmpp.modules.extension.xep0313_mam.query.QueryHandler;
+import org.apache.vysper.xmpp.modules.extension.xep0313_mam.query.MatchingArchivedMessageResults;
+import org.apache.vysper.xmpp.modules.extension.xep0313_mam.query.Query;
+import org.apache.vysper.xmpp.modules.extension.xep0313_mam.spi.ArchivedMessages;
+import org.apache.vysper.xmpp.modules.extension.xep0313_mam.spi.MessageArchive;
+import org.apache.vysper.xmpp.modules.extension.xep0313_mam.spi.MessageArchives;
+import org.apache.vysper.xmpp.server.ServerRuntimeContext;
+import org.apache.vysper.xmpp.server.SessionContext;
+import org.apache.vysper.xmpp.server.response.ServerErrorResponses;
+import org.apache.vysper.xmpp.stanza.Stanza;
+import org.apache.vysper.xmpp.stanza.StanzaErrorCondition;
+import org.apache.vysper.xmpp.stanza.StanzaErrorType;
+
+/**
+ * @author Réda Housni Alaoui
+ */
+public class UserArchiveQueryHandler implements QueryHandler {
+
+ @Override
+ public boolean supports(Query query, ServerRuntimeContext serverRuntimeContext, SessionContext sessionContext) {
+ return true;
+ }
+
+ @Override
+ public List<Stanza> handle(Query query, ServerRuntimeContext serverRuntimeContext, SessionContext sessionContext) {
+ try {
+ return doHandle(query, serverRuntimeContext, sessionContext);
+ } catch (XMLSemanticError xmlSemanticError) {
+ Stanza internalServerError = ServerErrorResponses.getStanzaError(StanzaErrorCondition.INTERNAL_SERVER_ERROR,
+ query.iqStanza(), StanzaErrorType.CANCEL, null, null, null);
+ return Collections.singletonList(internalServerError);
+ }
+ }
+
+ private List<Stanza> doHandle(Query query, ServerRuntimeContext serverRuntimeContext, SessionContext sessionContext)
+ throws XMLSemanticError {
+ Entity initiatingEntity = sessionContext.getInitiatingEntity();
+ Entity archiveId = ofNullable(query.iqStanza().getTo()).orElse(initiatingEntity).getBareJID();
+
+ if (!sessionContext.getInitiatingEntity().getBareJID().equals(archiveId)) {
+ // The initiating user is trying to read the archive of another user
+ return Collections.singletonList(ServerErrorResponses.getStanzaError(StanzaErrorCondition.FORBIDDEN,
+ query.iqStanza(), StanzaErrorType.CANCEL,
+ "Entity " + initiatingEntity + " is not allowed to query " + archiveId + " message archives.", null,
+ null));
+ }
+
+ MessageArchives archives = requireNonNull(
+ (MessageArchives) serverRuntimeContext.getStorageProvider(MessageArchives.class),
+ "Could not find an instance of " + MessageArchives.class);
+
+ Optional<MessageArchive> archive = archives.retrieveUserMessageArchive(archiveId);
+ if (!archive.isPresent()) {
+ return Collections.singletonList(ServerErrorResponses.getStanzaError(StanzaErrorCondition.ITEM_NOT_FOUND,
+ query.iqStanza(), StanzaErrorType.CANCEL, "No user message archive found for entity " + archiveId,
+ null, null));
+ }
+
+ ArchivedMessages archivedMessages = new ArchiveQuery(archive.get(), archiveId, query).execute();
+ return new MatchingArchivedMessageResults(initiatingEntity, archiveId, query, archivedMessages).toStanzas();
+ }
+}
diff --git a/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/user/UserMessageListener.java b/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/user/UserMessageListener.java
new file mode 100644
index 0000000..97998ea
--- /dev/null
+++ b/server/extensions/xep0313-mam/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/user/UserMessageListener.java
@@ -0,0 +1,93 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.apache.vysper.xmpp.modules.extension.xep0313_mam.user;
+
+import static java.util.Objects.requireNonNull;
+
+import java.util.Optional;
+
+import org.apache.vysper.event.EventListener;
+import org.apache.vysper.xmpp.addressing.Entity;
+import org.apache.vysper.xmpp.modules.core.base.handler.AcceptedMessageEvent;
+import org.apache.vysper.xmpp.modules.core.base.handler.XMPPCoreStanzaHandler;
+import org.apache.vysper.xmpp.modules.extension.xep0313_mam.SimpleMessage;
+import org.apache.vysper.xmpp.modules.extension.xep0313_mam.spi.MessageArchive;
+import org.apache.vysper.xmpp.modules.extension.xep0313_mam.spi.MessageArchives;
+import org.apache.vysper.xmpp.server.ServerRuntimeContext;
+import org.apache.vysper.xmpp.server.SessionContext;
+import org.apache.vysper.xmpp.stanza.MessageStanza;
+import org.apache.vysper.xmpp.stanza.MessageStanzaType;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * <a href=
+ * "https://xmpp.org/extensions/xep-0313.html#business-storeret-user-archives">User
+ * Archives</a>
+ *
+ * @author Réda Housni Alaoui
+ */
+public class UserMessageListener implements EventListener<AcceptedMessageEvent> {
+
+ private static final Logger LOG = LoggerFactory.getLogger(UserMessageListener.class);
+
+ @Override
+ public void onEvent(AcceptedMessageEvent acceptedMessage) {
+ archive(acceptedMessage);
+ }
+
+ private void archive(AcceptedMessageEvent acceptedMessage) {
+ MessageStanza messageStanza = acceptedMessage.messageStanza();
+ MessageStanzaType messageStanzaType = messageStanza.getMessageType();
+ if (messageStanzaType != MessageStanzaType.NORMAL && messageStanzaType != MessageStanzaType.CHAT) {
+ // A server SHOULD include in a user archive all of the messages a user sends
+ // or receives of type 'normal' or 'chat' that contain a <body> element.
+ LOG.debug("Message {} is neither of type 'normal' or 'chat'. It will not be archived.", messageStanza);
+ return;
+ }
+
+ Entity archiveJID;
+ if (acceptedMessage.isOutbound()) {
+ // We will store the message in the sender archive
+ SessionContext sessionContext = acceptedMessage.sessionContext();
+ archiveJID = XMPPCoreStanzaHandler.extractSenderJID(messageStanza, sessionContext);
+ } else {
+ // We will store the message in the receiver archive
+ archiveJID = requireNonNull(messageStanza.getTo(), "No 'to' found in " + messageStanza);
+ }
+
+ // Servers that expose archive messages of sent/received messages on behalf of
+ // local users MUST expose these archives to the user on the user's bare JID.
+ archiveJID = archiveJID.getBareJID();
+
+ ServerRuntimeContext serverRuntimeContext = acceptedMessage.serverRuntimeContext();
+ MessageArchives archives = requireNonNull(
+ (MessageArchives) serverRuntimeContext.getStorageProvider(MessageArchives.class),
+ "Could not find an instance of " + MessageArchives.class);
+
+ Optional<MessageArchive> userArchive = archives.retrieveUserMessageArchive(archiveJID);
+ if (!userArchive.isPresent()) {
+ LOG.debug("No archive returned for user with bare JID '{}'", archiveJID);
+ return;
+ }
+
+ userArchive.get().archive(new SimpleMessage(messageStanza));
+ }
+}
diff --git a/server/extensions/xep0313-mam/src/test/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/IntegrationTest.java b/server/extensions/xep0313-mam/src/test/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/IntegrationTest.java
new file mode 100644
index 0000000..035ba88
--- /dev/null
+++ b/server/extensions/xep0313-mam/src/test/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/IntegrationTest.java
@@ -0,0 +1,203 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.apache.vysper.xmpp.modules.extension.xep0313_mam;
+
+import java.io.File;
+import java.io.IOException;
+import java.net.ServerSocket;
+
+import org.apache.vysper.mina.C2SEndpoint;
+import org.apache.vysper.mina.TCPEndpoint;
+import org.apache.vysper.storage.StorageProviderRegistry;
+import org.apache.vysper.storage.inmemory.MemoryStorageProviderRegistry;
+import org.apache.vysper.xmpp.addressing.EntityImpl;
+import org.apache.vysper.xmpp.authentication.AccountManagement;
+import org.apache.vysper.xmpp.cryptography.NonCheckingX509TrustManagerFactory;
+import org.apache.vysper.xmpp.modules.extension.xep0313_mam.in_memory.InMemoryMessageArchives;
+import org.apache.vysper.xmpp.server.XMPPServer;
+import org.jivesoftware.smack.ConnectionConfiguration;
+import org.jivesoftware.smack.SmackConfiguration;
+import org.jivesoftware.smack.SmackException;
+import org.jivesoftware.smack.StanzaCollector;
+import org.jivesoftware.smack.XMPPConnection;
+import org.jivesoftware.smack.debugger.ConsoleDebugger;
+import org.jivesoftware.smack.filter.StanzaIdFilter;
+import org.jivesoftware.smack.packet.Stanza;
+import org.jivesoftware.smack.sasl.SASLMechanism;
+import org.jivesoftware.smack.tcp.XMPPTCPConnection;
+import org.jivesoftware.smack.tcp.XMPPTCPConnectionConfiguration;
+import org.junit.After;
+import org.junit.Before;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * @author Réda Housni Alaoui
+ */
+public abstract class IntegrationTest {
+
+ private final Logger logger = LoggerFactory.getLogger(IntegrationTest.class);
+
+ private static final String TLS_CERTIFICATE_PATH = "src/test/resources/bogus_mina_tls.cert";
+
+ private static final String TLS_CERTIFICATE_PASSWORD = "boguspw";
+
+ private static final String SERVER_DOMAIN = "vysper.org";
+
+ private static final String PASSWORD = "password";
+
+ private static final int DEFAULT_SERVER_PORT = 25222;
+
+ private static final String ALICE_USERNAME = "test1@" + SERVER_DOMAIN;
+
+ private static final String CAROL_USERNAME = "test2@" + SERVER_DOMAIN;
+
+ private XMPPTCPConnection aliceClient;
+
+ private XMPPTCPConnection carolClient;
+
+ private XMPPServer server;
+
+ @Before
+ public void setUp() throws Exception {
+ SmackConfiguration.setDefaultReplyTimeout(5000);
+
+ int port = findFreePort();
+
+ startServer(port);
+
+ aliceClient = connectClient(port, ALICE_USERNAME);
+ carolClient = connectClient(port, CAROL_USERNAME);
+ }
+
+ protected XMPPConnection alice() {
+ return aliceClient;
+ }
+
+ protected XMPPConnection carol() {
+ return carolClient;
+ }
+
+ protected Stanza sendSync(XMPPConnection client, Stanza request)
+ throws SmackException.NotConnectedException, InterruptedException {
+ StanzaCollector collector = client.createStanzaCollector(new StanzaIdFilter(request.getStanzaId()));
+
+ client.sendStanza(request);
+
+ return collector.nextResult(5000);
+ }
+
+ private void startServer(int port) throws Exception {
+ StorageProviderRegistry providerRegistry = new MemoryStorageProviderRegistry();
+
+ AccountManagement accountManagement = (AccountManagement) providerRegistry.retrieve(AccountManagement.class);
+ accountManagement.addUser(EntityImpl.parseUnchecked(ALICE_USERNAME), PASSWORD);
+ accountManagement.addUser(EntityImpl.parseUnchecked(CAROL_USERNAME), PASSWORD);
+
+ server = new XMPPServer(SERVER_DOMAIN);
+
+ TCPEndpoint endpoint = new C2SEndpoint();
+ endpoint.setPort(port);
+ server.addEndpoint(endpoint);
+ server.setStorageProviderRegistry(providerRegistry);
+
+ server.setTLSCertificateInfo(new File(TLS_CERTIFICATE_PATH), TLS_CERTIFICATE_PASSWORD);
+
+ server.start();
+
+ providerRegistry.add(new InMemoryMessageArchives());
+ server.addModule(new MAMModule());
+
+ Thread.sleep(200);
+ }
+
+ private XMPPTCPConnection connectClient(int port, String username) throws Exception {
+ XMPPTCPConnectionConfiguration connectionConfiguration = XMPPTCPConnectionConfiguration.builder()
+ .setHost("localhost").setPort(port).setXmppDomain(SERVER_DOMAIN)
+ .setHostnameVerifier((hostname, session) -> true).setCompressionEnabled(false)
+ .setSecurityMode(ConnectionConfiguration.SecurityMode.required)
+ .addEnabledSaslMechanism(SASLMechanism.PLAIN).setDebuggerFactory(ConsoleDebugger.Factory.INSTANCE)
+ .setKeystorePath(TLS_CERTIFICATE_PATH)
+ .setCustomX509TrustManager(NonCheckingX509TrustManagerFactory.X509).build();
+
+ XMPPTCPConnection client = new XMPPTCPConnection(connectionConfiguration);
+
+ client.connect();
+
+ client.login(username, PASSWORD);
+ return client;
+ }
+
+ private int findFreePort() {
+ ServerSocket ss = null;
+
+ // try using a predefined default port
+ // makes netstat -a debugging easier
+ try {
+ ss = new ServerSocket(DEFAULT_SERVER_PORT);
+ ss.setReuseAddress(true);
+
+ // succeeded, return the default port
+ logger.info("Test is using the default test port {}", DEFAULT_SERVER_PORT);
+ return DEFAULT_SERVER_PORT;
+ } catch (IOException e) {
+ try {
+ ss = new ServerSocket(0);
+ ss.setReuseAddress(true);
+ int port = ss.getLocalPort();
+ logger.info("Failed to use default test port ({}), using {} instead", DEFAULT_SERVER_PORT, port);
+ return port;
+ } catch (IOException ee) {
+ // we could not even open a random port so
+ // the test will probably fail, anyways
+ // return the default port
+ return DEFAULT_SERVER_PORT;
+ }
+ } finally {
+ if (ss != null) {
+ try {
+ ss.close();
+ } catch (IOException ignored) {
+ }
+ }
+ }
+ }
+
+ @After
+ public void tearDown() {
+ try {
+ aliceClient.disconnect();
+ } catch (Exception ignored) {
+
+ }
+
+ try {
+ carolClient.disconnect();
+ } catch (Exception ignored) {
+
+ }
+
+ try {
+ server.stop();
+ } catch (Exception ignored) {
+ }
+ }
+
+}
diff --git a/server/extensions/xep0313-mam/src/test/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/MAMModuleTest.java b/server/extensions/xep0313-mam/src/test/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/MAMModuleTest.java
new file mode 100644
index 0000000..115e23a
--- /dev/null
+++ b/server/extensions/xep0313-mam/src/test/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/MAMModuleTest.java
@@ -0,0 +1,84 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.apache.vysper.xmpp.modules.extension.xep0313_mam;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.apache.vysper.xmpp.modules.servicediscovery.management.Feature;
+import org.apache.vysper.xmpp.modules.servicediscovery.management.InfoElement;
+import org.apache.vysper.xmpp.modules.servicediscovery.management.InfoRequest;
+import org.apache.vysper.xmpp.modules.servicediscovery.management.ServerInfoRequestListener;
+import org.apache.vysper.xmpp.modules.servicediscovery.management.ServiceDiscoveryRequestException;
+import org.apache.vysper.xmpp.protocol.NamespaceURIs;
+import org.junit.Test;
+
+import junit.framework.Assert;
+
+/**
+ * @author The Apache MINA Project (dev@mina.apache.org)
+ */
+public class MAMModuleTest {
+
+ private MAMModule tested = new MAMModule();
+
+ @Test
+ public void nameMustBeProvided() {
+ Assert.assertNotNull(tested.getName());
+ }
+
+ @Test
+ public void versionMustBeProvided() {
+ Assert.assertNotNull(tested.getVersion());
+ }
+
+ @Test
+ public void getServerInfosFor() throws ServiceDiscoveryRequestException {
+ List<ServerInfoRequestListener> serverInfoRequestListeners = new ArrayList<>();
+
+ tested.addServerInfoRequestListeners(serverInfoRequestListeners);
+
+ Assert.assertEquals(1, serverInfoRequestListeners.size());
+
+ List<InfoElement> infoElements = serverInfoRequestListeners.get(0)
+ .getServerInfosFor(new InfoRequest(null, null, null, null));
+
+ Assert.assertEquals(4, infoElements.size());
+ Assert.assertTrue(infoElements.get(0) instanceof Feature);
+ Assert.assertTrue(infoElements.get(1) instanceof Feature);
+ Assert.assertTrue(infoElements.get(2) instanceof Feature);
+ Assert.assertTrue(infoElements.get(3) instanceof Feature);
+ Assert.assertEquals("urn:xmpp:mam:1", ((Feature) infoElements.get(0)).getVar());
+ Assert.assertEquals("urn:xmpp:mam:2", ((Feature) infoElements.get(1)).getVar());
+ Assert.assertEquals(NamespaceURIs.XEP0359_STANZA_IDS, ((Feature) infoElements.get(2)).getVar());
+ Assert.assertEquals(NamespaceURIs.JABBER_X_DATA, ((Feature) infoElements.get(3)).getVar());
+ }
+
+ @Test
+ public void getServerInfosForWithNode() throws ServiceDiscoveryRequestException {
+ List<ServerInfoRequestListener> serverInfoRequestListeners = new ArrayList<>();
+ tested.addServerInfoRequestListeners(serverInfoRequestListeners);
+ Assert.assertEquals(1, serverInfoRequestListeners.size());
+
+ Assert.assertNull(
+ serverInfoRequestListeners.get(0).getServerInfosFor(new InfoRequest(null, null, "node", null)));
+ }
+
+}
\ No newline at end of file
diff --git a/server/extensions/xep0313-mam/src/test/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/ServerRuntimeContextMock.java b/server/extensions/xep0313-mam/src/test/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/ServerRuntimeContextMock.java
new file mode 100644
index 0000000..a78326a
--- /dev/null
+++ b/server/extensions/xep0313-mam/src/test/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/ServerRuntimeContextMock.java
@@ -0,0 +1,158 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.apache.vysper.xmpp.modules.extension.xep0313_mam;
+
+import static org.mockito.Mockito.mock;
+
+import java.util.List;
+
+import javax.net.ssl.SSLContext;
+
+import org.apache.vysper.event.EventBus;
+import org.apache.vysper.storage.StorageProvider;
+import org.apache.vysper.xmpp.addressing.Entity;
+import org.apache.vysper.xmpp.authentication.UserAuthentication;
+import org.apache.vysper.xmpp.delivery.StanzaRelay;
+import org.apache.vysper.xmpp.modules.Module;
+import org.apache.vysper.xmpp.modules.ServerRuntimeContextService;
+import org.apache.vysper.xmpp.modules.extension.xep0313_mam.spi.MessageArchives;
+import org.apache.vysper.xmpp.modules.extension.xep0313_mam.spi.MessageArchivesMock;
+import org.apache.vysper.xmpp.protocol.StanzaHandler;
+import org.apache.vysper.xmpp.protocol.StanzaProcessor;
+import org.apache.vysper.xmpp.server.ServerFeatures;
+import org.apache.vysper.xmpp.server.ServerRuntimeContext;
+import org.apache.vysper.xmpp.server.components.Component;
+import org.apache.vysper.xmpp.server.components.ComponentStanzaProcessor;
+import org.apache.vysper.xmpp.server.s2s.XMPPServerConnectorRegistry;
+import org.apache.vysper.xmpp.stanza.Stanza;
+import org.apache.vysper.xmpp.state.presence.LatestPresenceCache;
+import org.apache.vysper.xmpp.state.resourcebinding.ResourceRegistry;
+
+/**
+ * @author Réda Housni Alaoui
+ */
+public class ServerRuntimeContextMock implements ServerRuntimeContext {
+
+ private MessageArchivesMock userMessageArchives;
+
+ public MessageArchivesMock givenUserMessageArchives() {
+ userMessageArchives = new MessageArchivesMock();
+ return userMessageArchives;
+ }
+
+ @Override
+ public StanzaHandler getHandler(Stanza stanza) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public String getNextSessionId() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public Entity getServerEntity() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public String getDefaultXMLLang() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public ServerFeatures getServerFeatures() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public SSLContext getSslContext() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public UserAuthentication getUserAuthentication() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public ResourceRegistry getResourceRegistry() {
+ return mock(ResourceRegistry.class);
+ }
+
+ @Override
+ public LatestPresenceCache getPresenceCache() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void registerServerRuntimeContextService(ServerRuntimeContextService service) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public ServerRuntimeContextService getServerRuntimeContextService(String name) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ @SuppressWarnings("unchecked")
+ public <T extends StorageProvider> T getStorageProvider(Class<T> clazz) {
+ if (MessageArchives.class.equals(clazz)) {
+ return (T) userMessageArchives;
+ }
+ return null;
+ }
+
+ @Override
+ public void registerComponent(Component component) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public boolean hasComponentStanzaProcessor(Entity entity) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public XMPPServerConnectorRegistry getServerConnectorRegistry() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public List<Module> getModules() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public <T> T getModule(Class<T> clazz) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void addModule(Module module) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public EventBus getEventBus() {
+ throw new UnsupportedOperationException();
+ }
+}
diff --git a/server/extensions/xep0313-mam/src/test/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/SessionContextMock.java b/server/extensions/xep0313-mam/src/test/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/SessionContextMock.java
new file mode 100644
index 0000000..5b69042
--- /dev/null
+++ b/server/extensions/xep0313-mam/src/test/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/SessionContextMock.java
@@ -0,0 +1,145 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.apache.vysper.xmpp.modules.extension.xep0313_mam;
+
+import org.apache.vysper.xmpp.addressing.Entity;
+import org.apache.vysper.xmpp.server.ServerRuntimeContext;
+import org.apache.vysper.xmpp.server.SessionContext;
+import org.apache.vysper.xmpp.server.SessionState;
+import org.apache.vysper.xmpp.state.resourcebinding.BindException;
+import org.apache.vysper.xmpp.writer.StanzaWriter;
+
+/**
+ * @author Réda Housni Alaoui
+ */
+public class SessionContextMock implements SessionContext {
+
+ private ServerRuntimeContext serverRuntimeContext;
+
+ private Entity initiatingEntity;
+
+ public void givenServerRuntimeContext(ServerRuntimeContext serverRuntimeContext) {
+ this.serverRuntimeContext = serverRuntimeContext;
+ }
+
+ public void givenInitiatingEntity(Entity initiatingEntity) {
+ this.initiatingEntity = initiatingEntity;
+ }
+
+ @Override
+ public ServerRuntimeContext getServerRuntimeContext() {
+ return serverRuntimeContext;
+ }
+
+ @Override
+ public boolean isRemotelyInitiatedSession() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public Entity getInitiatingEntity() {
+ return initiatingEntity;
+ }
+
+ @Override
+ public void setInitiatingEntity(Entity entity) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public boolean isServerToServer() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void setServerToServer() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void setClientToServer() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public SessionState getState() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public String getSessionId() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public String getXMLLang() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void setXMLLang(String languageCode) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public StanzaWriter getResponseWriter() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void endSession(SessionTerminationCause terminationCause) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public Entity getServerJID() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void switchToTLS(boolean delayed, boolean clientTls) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void setIsReopeningXMLStream() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public String bindResource() throws BindException {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public String nextSequenceValue() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public Object putAttribute(String key, Object value) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public Object getAttribute(String key) {
+ throw new UnsupportedOperationException();
+ }
+}
diff --git a/server/extensions/xep0313-mam/src/test/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/StanzaAssert.java b/server/extensions/xep0313-mam/src/test/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/StanzaAssert.java
new file mode 100644
index 0000000..bbf9369
--- /dev/null
+++ b/server/extensions/xep0313-mam/src/test/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/StanzaAssert.java
@@ -0,0 +1,37 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.apache.vysper.xmpp.modules.extension.xep0313_mam;
+
+import org.apache.vysper.xml.fragment.Renderer;
+import org.apache.vysper.xmpp.stanza.Stanza;
+import org.junit.Assert;
+
+public class StanzaAssert {
+
+ public static void assertEquals(Stanza expected, Stanza actual) {
+ try {
+ Assert.assertEquals(expected, actual);
+ } catch(Throwable e) {
+ // print something useful
+ Assert.assertEquals(new Renderer(expected).getComplete(), new Renderer(actual).getComplete());
+ }
+ }
+}
diff --git a/server/extensions/xep0313-mam/src/test/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/UserArchiveTest.java b/server/extensions/xep0313-mam/src/test/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/UserArchiveTest.java
new file mode 100644
index 0000000..bd0eed0
--- /dev/null
+++ b/server/extensions/xep0313-mam/src/test/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/UserArchiveTest.java
@@ -0,0 +1,149 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.apache.vysper.xmpp.modules.extension.xep0313_mam;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+import org.apache.vysper.xmpp.protocol.NamespaceURIs;
+import org.jivesoftware.smack.SmackException;
+import org.jivesoftware.smack.XMPPException;
+import org.jivesoftware.smack.chat2.Chat;
+import org.jivesoftware.smack.chat2.ChatManager;
+import org.jivesoftware.smack.packet.Message;
+import org.jivesoftware.smackx.forward.packet.Forwarded;
+import org.jivesoftware.smackx.mam.MamManager;
+import org.jivesoftware.smackx.sid.element.StableAndUniqueIdElement;
+import org.jivesoftware.smackx.sid.element.StanzaIdElement;
+import org.junit.Test;
+
+/**
+ * @author Réda Housni Alaoui
+ */
+public class UserArchiveTest extends IntegrationTest {
+
+ @Test
+ public void queryEmptyArchive()
+ throws XMPPException.XMPPErrorException, InterruptedException, SmackException.NotConnectedException,
+ SmackException.NotLoggedInException, SmackException.NoResponseException {
+ MamManager.MamQueryArgs mamQueryArgs = MamManager.MamQueryArgs.builder().build();
+ MamManager.MamQuery mamQuery = MamManager.getInstanceFor(alice()).queryArchive(mamQueryArgs);
+
+ assertEquals(0, mamQuery.getMessageCount());
+ }
+
+ @Test
+ public void sendMessageAndQuerySenderAndReceiverArchive()
+ throws SmackException.NotConnectedException, InterruptedException, SmackException.NotLoggedInException,
+ XMPPException.XMPPErrorException, SmackException.NoResponseException {
+ Chat chat = ChatManager.getInstanceFor(alice()).chatWith(carol().getUser().asEntityBareJid());
+ chat.send("Hello carol");
+
+ MamManager.MamQueryArgs fullQuery = MamManager.MamQueryArgs.builder().build();
+
+ MamManager.MamQuery aliceArchive = MamManager.getInstanceFor(alice()).queryArchive(fullQuery);
+
+ assertEquals(1, aliceArchive.getMessageCount());
+ Message toCarolMessage = aliceArchive.getMessages().get(0);
+ assertEquals("Hello carol", toCarolMessage.getBody());
+ assertEquals(alice().getUser(), toCarolMessage.getFrom());
+ assertEquals(carol().getUser().asEntityBareJidOrThrow(), toCarolMessage.getTo().asEntityBareJidOrThrow());
+
+ MamManager.MamQuery carolArchive = MamManager.getInstanceFor(carol()).queryArchive(fullQuery);
+
+ assertEquals(1, carolArchive.getMessageCount());
+
+ Message fromAliceMessage = carolArchive.getMessages().get(0);
+ assertEquals("Hello carol", fromAliceMessage.getBody());
+ assertEquals(alice().getUser(), fromAliceMessage.getFrom());
+ assertEquals(carol().getUser().asEntityBareJidOrThrow(), fromAliceMessage.getTo().asEntityBareJidOrThrow());
+ }
+
+ @Test
+ public void checkSorting() throws SmackException.NotConnectedException, InterruptedException,
+ SmackException.NotLoggedInException, XMPPException.XMPPErrorException, SmackException.NoResponseException {
+ Chat chat = ChatManager.getInstanceFor(alice()).chatWith(carol().getUser().asEntityBareJid());
+ for (int index = 1; index <= 10; index++) {
+ chat.send("Hello " + index);
+ }
+
+ MamManager.MamQueryArgs fullQuery = MamManager.MamQueryArgs.builder().build();
+ MamManager.MamQuery aliceArchive = MamManager.getInstanceFor(alice()).queryArchive(fullQuery);
+
+ assertEquals(10, aliceArchive.getMessageCount());
+ assertTrue(aliceArchive.isComplete());
+
+ for (int index = 1; index <= 10; index++) {
+ assertEquals("Hello " + index, aliceArchive.getMessages().get(index - 1).getBody());
+ }
+ }
+
+ @Test
+ public void paginate() throws SmackException.NotConnectedException, InterruptedException,
+ SmackException.NotLoggedInException, XMPPException.XMPPErrorException, SmackException.NoResponseException {
+ Chat chat = ChatManager.getInstanceFor(alice()).chatWith(carol().getUser().asEntityBareJid());
+ for (int index = 1; index <= 10; index++) {
+ chat.send("Hello " + index);
+ }
+
+ MamManager mamManager = MamManager.getInstanceFor(alice());
+
+ MamManager.MamQueryArgs firstHalfPageable = MamManager.MamQueryArgs.builder().setResultPageSizeTo(5).build();
+
+ MamManager.MamQuery page = mamManager.queryArchive(firstHalfPageable);
+ assertFalse(page.isComplete());
+ assertEquals(5, page.getMessageCount());
+ for (int index = 1; index <= 5; index++) {
+ assertEquals("Hello " + index, page.getMessages().get(index - 1).getBody());
+ }
+
+ page.pageNext(5);
+ assertFalse(page.isComplete());
+ assertEquals(5, page.getMessageCount());
+ for (int index = 6; index <= 10; index++) {
+ assertEquals("Hello " + index, page.getMessages().get(index - 6).getBody());
+ }
+
+ }
+
+ @Test
+ public void lastPage() throws SmackException.NotConnectedException, InterruptedException,
+ SmackException.NotLoggedInException, XMPPException.XMPPErrorException, SmackException.NoResponseException {
+ Chat chat = ChatManager.getInstanceFor(alice()).chatWith(carol().getUser().asEntityBareJid());
+ for (int index = 1; index <= 10; index++) {
+ chat.send("Hello " + index);
+ }
+
+ MamManager mamManager = MamManager.getInstanceFor(alice());
+ MamManager.MamQuery fullQuery = mamManager.queryArchive(MamManager.MamQueryArgs.builder().build());
+
+ List<String> allMessageIds = fullQuery.getPage().getForwarded().stream().map(Forwarded::getForwardedStanza)
+ .map(stanza -> stanza.getExtension("stanza-id", NamespaceURIs.XEP0359_STANZA_IDS))
+ .map(StanzaIdElement.class::cast).map(StableAndUniqueIdElement::getId).collect(Collectors.toList());
+
+ String lastMessageUid = mamManager.getMessageUidOfLatestMessage();
+ assertEquals(allMessageIds.get(allMessageIds.size() - 1), lastMessageUid);
+ }
+
+}
diff --git a/server/extensions/xep0313-mam/src/test/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/in_memory/InMemoryEntityFilterTest.java b/server/extensions/xep0313-mam/src/test/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/in_memory/InMemoryEntityFilterTest.java
new file mode 100644
index 0000000..9f80418
--- /dev/null
+++ b/server/extensions/xep0313-mam/src/test/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/in_memory/InMemoryEntityFilterTest.java
@@ -0,0 +1,99 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.apache.vysper.xmpp.modules.extension.xep0313_mam.in_memory;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import org.apache.vysper.xmpp.addressing.Entity;
+import org.apache.vysper.xmpp.addressing.EntityImpl;
+import org.apache.vysper.xmpp.modules.extension.xep0313_mam.spi.ArchivedMessage;
+import org.apache.vysper.xmpp.modules.extension.xep0313_mam.spi.EntityFilter;
+import org.apache.vysper.xmpp.modules.extension.xep0313_mam.spi.SimpleEntityFilter;
+import org.apache.vysper.xmpp.stanza.MessageStanza;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * @author Réda Housni Alaoui
+ */
+public class InMemoryEntityFilterTest {
+
+ private static final Entity ROMEO_SIDEWALK = EntityImpl.parseUnchecked("romeo@foo.com/sidewalk");
+
+ private static final Entity ROMEO_CAR = EntityImpl.parseUnchecked("romeo@foo.com/car");
+
+ private static final Entity JULIET_BALCONY = EntityImpl.parseUnchecked("juliet@foo.com/balcony");
+
+ private static final Entity JULIET_TRAIN = EntityImpl.parseUnchecked("juliet@foo.com/train");
+
+ private ArchivedMessage romeoSidewalkToJulietBalcony;
+
+ private ArchivedMessage romeoSidewalkToRomeoCar;
+
+ @Before
+ public void before() {
+ romeoSidewalkToJulietBalcony = mock(ArchivedMessage.class);
+ MessageStanza romeoSidewalkToJulietBalconyStanza = mock(MessageStanza.class);
+ when(romeoSidewalkToJulietBalconyStanza.getFrom()).thenReturn(ROMEO_SIDEWALK);
+ when(romeoSidewalkToJulietBalconyStanza.getTo()).thenReturn(JULIET_BALCONY);
+ when(romeoSidewalkToJulietBalcony.stanza()).thenReturn(romeoSidewalkToJulietBalconyStanza);
+
+ romeoSidewalkToRomeoCar = mock(ArchivedMessage.class);
+ MessageStanza romeoSidewalkToRomeoCarStanza = mock(MessageStanza.class);
+ when(romeoSidewalkToRomeoCarStanza.getFrom()).thenReturn(ROMEO_SIDEWALK);
+ when(romeoSidewalkToRomeoCarStanza.getTo()).thenReturn(ROMEO_CAR);
+ when(romeoSidewalkToRomeoCar.stanza()).thenReturn(romeoSidewalkToRomeoCarStanza);
+ }
+
+ @Test
+ public void toAndFromIgnoringResource() {
+ EntityFilter entityFilter = new SimpleEntityFilter(ROMEO_CAR, EntityFilter.Type.TO_AND_FROM, true);
+ InMemoryEntityFilter tested = new InMemoryEntityFilter(entityFilter);
+ assertTrue(tested.test(romeoSidewalkToRomeoCar));
+ assertFalse(tested.test(romeoSidewalkToJulietBalcony));
+ }
+
+ @Test
+ public void toAndFromNotIgnoringResource() {
+ EntityFilter entityFilter = new SimpleEntityFilter(ROMEO_CAR, EntityFilter.Type.TO_AND_FROM, false);
+ InMemoryEntityFilter tested = new InMemoryEntityFilter(entityFilter);
+ assertFalse(tested.test(romeoSidewalkToRomeoCar));
+ assertFalse(tested.test(romeoSidewalkToJulietBalcony));
+ }
+
+ @Test
+ public void toOrFromIgnoringResource() {
+ EntityFilter entityFilter = new SimpleEntityFilter(ROMEO_CAR, EntityFilter.Type.TO_OR_FROM, true);
+ InMemoryEntityFilter tested = new InMemoryEntityFilter(entityFilter);
+ assertTrue(tested.test(romeoSidewalkToRomeoCar));
+ assertTrue(tested.test(romeoSidewalkToJulietBalcony));
+ }
+
+ @Test
+ public void toOrFromNotIgnoringResource() {
+ EntityFilter entityFilter = new SimpleEntityFilter(ROMEO_CAR, EntityFilter.Type.TO_OR_FROM, false);
+ InMemoryEntityFilter tested = new InMemoryEntityFilter(entityFilter);
+ assertTrue(tested.test(romeoSidewalkToRomeoCar));
+ assertFalse(tested.test(romeoSidewalkToJulietBalcony));
+ }
+}
\ No newline at end of file
diff --git a/server/extensions/xep0313-mam/src/test/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/in_memory/InMemoryPageLimitedArchivedMessagesTest.java b/server/extensions/xep0313-mam/src/test/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/in_memory/InMemoryPageLimitedArchivedMessagesTest.java
new file mode 100644
index 0000000..cb3ec9e
--- /dev/null
+++ b/server/extensions/xep0313-mam/src/test/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/in_memory/InMemoryPageLimitedArchivedMessagesTest.java
@@ -0,0 +1,160 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.apache.vysper.xmpp.modules.extension.xep0313_mam.in_memory;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import org.apache.vysper.xmpp.modules.extension.xep0313_mam.spi.ArchivedMessage;
+import org.apache.vysper.xmpp.modules.extension.xep0313_mam.spi.MessagePageRequest;
+import org.apache.vysper.xmpp.modules.extension.xep0313_mam.spi.SimpleMessagePageRequest;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * @author Réda Housni Alaoui
+ */
+public class InMemoryPageLimitedArchivedMessagesTest {
+
+ private static final String MESSAGE_1_ID = "message-1";
+
+ private static final String MESSAGE_2_ID = "message-2";
+
+ private static final String MESSAGE_3_ID = "message-3";
+
+ private ArchivedMessage message1;
+
+ private ArchivedMessage message2;
+
+ private ArchivedMessage message3;
+
+ private List<ArchivedMessage> messages;
+
+ @Before
+ public void before() {
+ message1 = mock(ArchivedMessage.class);
+ when(message1.id()).thenReturn(MESSAGE_1_ID);
+ message2 = mock(ArchivedMessage.class);
+ when(message2.id()).thenReturn(MESSAGE_2_ID);
+ message3 = mock(ArchivedMessage.class);
+ when(message3.id()).thenReturn(MESSAGE_3_ID);
+
+ messages = new ArrayList<>();
+ messages.add(message1);
+ messages.add(message2);
+ messages.add(message3);
+ }
+
+ @Test
+ public void withoutLimit() {
+ MessagePageRequest pageLimit = new SimpleMessagePageRequest(null, null, null);
+ InMemoryArchivedMessagesPage tested = new InMemoryArchivedMessagesPage(pageLimit, messages);
+
+ assertFalse(tested.isEmpty());
+ assertTrue(tested.isComplete());
+ assertEquals(3, (long) tested.totalNumberOfMessages().orElse(0L));
+ assertEquals(0, (long) tested.firstMessageIndex().orElse(-1L));
+ List<ArchivedMessage> list = tested.list();
+ assertEquals(3, list.size());
+ assertEquals(message1, list.get(0));
+ assertEquals(message2, list.get(1));
+ assertEquals(message3, list.get(2));
+ }
+
+ @Test
+ public void withoutFirstOrLastMessageId() {
+ MessagePageRequest pageLimit = new SimpleMessagePageRequest(2L, null, null);
+ InMemoryArchivedMessagesPage tested = new InMemoryArchivedMessagesPage(pageLimit, messages);
+
+ assertFalse(tested.isEmpty());
+ assertFalse(tested.isComplete());
+ assertEquals(3, (long) tested.totalNumberOfMessages().orElse(0L));
+ assertEquals(0, (long) tested.firstMessageIndex().orElse(-1L));
+ List<ArchivedMessage> list = tested.list();
+ assertEquals(2, list.size());
+ assertEquals(message1, list.get(0));
+ assertEquals(message2, list.get(1));
+ }
+
+ @Test
+ public void withFirstMessageId() {
+ MessagePageRequest pageLimit = new SimpleMessagePageRequest(2L, MESSAGE_2_ID, null);
+ InMemoryArchivedMessagesPage tested = new InMemoryArchivedMessagesPage(pageLimit, messages);
+
+ assertFalse(tested.isEmpty());
+ assertFalse(tested.isComplete());
+ assertEquals(3, (long) tested.totalNumberOfMessages().orElse(0L));
+ assertEquals(2, (long) tested.firstMessageIndex().orElse(-1L));
+ List<ArchivedMessage> list = tested.list();
+ assertEquals(1, list.size());
+ assertEquals(message3, list.get(0));
+ }
+
+ @Test
+ public void withLastMessageId() {
+ MessagePageRequest pageLimit = new SimpleMessagePageRequest(2L, null, MESSAGE_2_ID);
+ InMemoryArchivedMessagesPage tested = new InMemoryArchivedMessagesPage(pageLimit, messages);
+
+ assertFalse(tested.isEmpty());
+ assertFalse(tested.isComplete());
+ assertEquals(3, (long) tested.totalNumberOfMessages().orElse(0L));
+ assertEquals(0, (long) tested.firstMessageIndex().orElse(-1L));
+ List<ArchivedMessage> list = tested.list();
+ assertEquals(1, list.size());
+ assertEquals(message1, list.get(0));
+ }
+
+ @Test
+ public void withFirstAndLastMessageId() {
+ MessagePageRequest pageLimit = new SimpleMessagePageRequest(2L, MESSAGE_1_ID, MESSAGE_3_ID);
+ InMemoryArchivedMessagesPage tested = new InMemoryArchivedMessagesPage(pageLimit, messages);
+
+ assertFalse(tested.isEmpty());
+ assertFalse(tested.isComplete());
+ assertEquals(3, (long) tested.totalNumberOfMessages().orElse(0L));
+ assertEquals(1, (long) tested.firstMessageIndex().orElse(-1L));
+ List<ArchivedMessage> list = tested.list();
+ assertEquals(1, list.size());
+ assertEquals(message2, list.get(0));
+ }
+
+ @Test
+ public void empty() {
+ MessagePageRequest pageLimit = new SimpleMessagePageRequest(2L, MESSAGE_1_ID, MESSAGE_3_ID);
+ InMemoryArchivedMessagesPage tested = new InMemoryArchivedMessagesPage(pageLimit,
+ Collections.emptyList());
+
+ assertTrue(tested.isEmpty());
+ assertTrue(tested.isComplete());
+ assertEquals(0, (long) tested.totalNumberOfMessages().orElse(0L));
+ assertNull(tested.firstMessageIndex().orElse(null));
+ List<ArchivedMessage> list = tested.list();
+ assertTrue(list.isEmpty());
+ }
+
+}
\ No newline at end of file
diff --git a/server/extensions/xep0313-mam/src/test/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/query/ArchiveEntityFilterTest.java b/server/extensions/xep0313-mam/src/test/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/query/ArchiveEntityFilterTest.java
new file mode 100644
index 0000000..913b23c
--- /dev/null
+++ b/server/extensions/xep0313-mam/src/test/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/query/ArchiveEntityFilterTest.java
@@ -0,0 +1,68 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.apache.vysper.xmpp.modules.extension.xep0313_mam.query;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import org.apache.vysper.xmpp.addressing.Entity;
+import org.apache.vysper.xmpp.addressing.EntityImpl;
+import org.apache.vysper.xmpp.modules.extension.xep0313_mam.spi.EntityFilter;
+import org.junit.Test;
+
+/**
+ * @author Réda Housni Alaoui
+ */
+public class ArchiveEntityFilterTest {
+
+ @Test
+ public void withMatchingArchiveId() {
+ Entity with = EntityImpl.parseUnchecked("juliet@foo.com");
+ ArchiveEntityFilter tested = new ArchiveEntityFilter(with, with);
+
+ assertEquals(with, tested.entity());
+ assertEquals(EntityFilter.Type.TO_AND_FROM, tested.type());
+ assertTrue(tested.ignoreResource());
+ }
+
+ @Test
+ public void withBeingBareJid() {
+ Entity archiveId = EntityImpl.parseUnchecked("romeo@foo.com");
+ Entity with = EntityImpl.parseUnchecked("juliet@foo.com");
+ ArchiveEntityFilter tested = new ArchiveEntityFilter(archiveId, with);
+
+ assertEquals(with, tested.entity());
+ assertEquals(EntityFilter.Type.TO_OR_FROM, tested.type());
+ assertTrue(tested.ignoreResource());
+ }
+
+ @Test
+ public void withNotMatchingArchiveIdAndNotBeingBareJid() {
+ Entity archiveId = EntityImpl.parseUnchecked("romeo@foo.com");
+ Entity with = EntityImpl.parseUnchecked("juliet@foo.com/balcony");
+ ArchiveEntityFilter tested = new ArchiveEntityFilter(archiveId, with);
+
+ assertEquals(with, tested.entity());
+ assertEquals(EntityFilter.Type.TO_OR_FROM, tested.type());
+ assertFalse(tested.ignoreResource());
+ }
+
+}
\ No newline at end of file
diff --git a/server/extensions/xep0313-mam/src/test/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/query/ArchiveQueryTest.java b/server/extensions/xep0313-mam/src/test/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/query/ArchiveQueryTest.java
new file mode 100644
index 0000000..fd03ddc
--- /dev/null
+++ b/server/extensions/xep0313-mam/src/test/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/query/ArchiveQueryTest.java
@@ -0,0 +1,85 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.apache.vysper.xmpp.modules.extension.xep0313_mam.query;
+
+import static org.junit.Assert.assertEquals;
+import static org.mockito.Matchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.util.Optional;
+
+import org.apache.vysper.xml.fragment.XMLSemanticError;
+import org.apache.vysper.xmpp.addressing.Entity;
+import org.apache.vysper.xmpp.addressing.EntityImpl;
+import org.apache.vysper.xmpp.modules.extension.xep0313_mam.spi.ArchivedMessages;
+import org.apache.vysper.xmpp.modules.extension.xep0313_mam.spi.MessageArchive;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * @author Réda Housni Alaoui
+ */
+public class ArchiveQueryTest {
+
+ private static final Entity JULIET = EntityImpl.parseUnchecked("juliet@foo.com");
+
+ private static final Entity ARCHIVE_ID = JULIET;
+
+ private MessageArchive archive;
+
+ private X x;
+
+ private QuerySet querySet;
+
+ private ArchiveQuery tested;
+
+ @Before
+ public void before() throws XMLSemanticError {
+ archive = mock(MessageArchive.class);
+ Query query = mock(Query.class);
+ x = mock(X.class);
+ when(query.getX()).thenReturn(x);
+ querySet = mock(QuerySet.class);
+ when(query.getSet()).thenReturn(querySet);
+ tested = new ArchiveQuery(archive, ARCHIVE_ID, query);
+ }
+
+ @Test
+ public void executeWithoutWith() throws XMLSemanticError {
+ when(x.getWith()).thenReturn(Optional.empty());
+
+ ArchivedMessages archivedMessages = mock(ArchivedMessages.class);
+ when(archive.fetchSortedByOldestFirst(any(), any())).thenReturn(archivedMessages);
+
+ assertEquals(archivedMessages, tested.execute());
+ }
+
+ @Test
+ public void executeWithWith() throws XMLSemanticError {
+ when(x.getWith()).thenReturn(Optional.of(JULIET));
+
+ ArchivedMessages archivedMessages = mock(ArchivedMessages.class);
+ when(archive.fetchSortedByOldestFirst(any(), any())).thenReturn(archivedMessages);
+
+ assertEquals(archivedMessages, tested.execute());
+ }
+
+}
\ No newline at end of file
diff --git a/server/extensions/xep0313-mam/src/test/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/query/MAMIQHandlerTest.java b/server/extensions/xep0313-mam/src/test/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/query/MAMIQHandlerTest.java
new file mode 100644
index 0000000..d0f5592
--- /dev/null
+++ b/server/extensions/xep0313-mam/src/test/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/query/MAMIQHandlerTest.java
@@ -0,0 +1,140 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.apache.vysper.xmpp.modules.extension.xep0313_mam.query;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Matchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import java.io.IOException;
+import java.util.Collections;
+
+import org.apache.vysper.xml.fragment.XMLElement;
+import org.apache.vysper.xmpp.modules.extension.xep0313_mam.muc.MUCArchiveQueryHandler;
+import org.apache.vysper.xmpp.modules.extension.xep0313_mam.user.UserArchiveQueryHandler;
+import org.apache.vysper.xmpp.parser.XMLParserUtil;
+import org.apache.vysper.xmpp.server.ServerRuntimeContext;
+import org.apache.vysper.xmpp.server.SessionContext;
+import org.apache.vysper.xmpp.stanza.IQStanza;
+import org.apache.vysper.xmpp.stanza.Stanza;
+import org.apache.vysper.xmpp.stanza.StanzaBuilder;
+import org.junit.Before;
+import org.junit.Test;
+import org.xml.sax.SAXException;
+
+/**
+ * @author Réda Housni Alaoui
+ */
+public class MAMIQHandlerTest {
+
+ private QueryHandler pubsubNodeArchiveQueryHandler;
+
+ private QueryHandler mucArchiveQueryHandler;
+
+ private QueryHandler userArchiveQueryHandler;
+
+ private IQStanza stanza;
+
+ private ServerRuntimeContext serverRuntimeContext;
+
+ private SessionContext sessionContext;
+
+ private MAMIQHandler tested;
+
+ @Before
+ public void before() throws IOException, SAXException {
+ pubsubNodeArchiveQueryHandler = mock(QueryHandler.class);
+ mucArchiveQueryHandler = mock(MUCArchiveQueryHandler.class);
+ userArchiveQueryHandler = mock(UserArchiveQueryHandler.class);
+
+ tested = new MAMIQHandler("urn:xmpp:mam:2", pubsubNodeArchiveQueryHandler, mucArchiveQueryHandler,
+ userArchiveQueryHandler);
+
+ XMLElement queryIqElement = XMLParserUtil.parseRequiredDocument(
+ "<iq type='set' id='juliet1'><query xmlns='urn:xmpp:mam:2' queryid='f27'/></iq>");
+ stanza = new IQStanza(StanzaBuilder.createClone(queryIqElement, true, Collections.emptyList()).build());
+ serverRuntimeContext = mock(ServerRuntimeContext.class);
+ sessionContext = mock(SessionContext.class);
+ }
+
+ @Test
+ public void verifySupportStanza() {
+ assertTrue(tested.verifyInnerElement(stanza));
+ }
+
+ @Test
+ public void verifyUnsupportStanzaNamespace() throws IOException, SAXException {
+ XMLElement queryIqElement = XMLParserUtil
+ .parseRequiredDocument("<iq type='set' id='juliet1'><query queryid='f27'/></iq>");
+ Stanza stanza = StanzaBuilder.createClone(queryIqElement, true, Collections.emptyList()).build();
+
+ assertFalse(tested.verifyInnerElement(stanza));
+ }
+
+ @Test
+ public void verifyUnsupportStanzaName() throws IOException, SAXException {
+ XMLElement queryIqElement = XMLParserUtil
+ .parseRequiredDocument("<iq type='set' id='juliet1'><foo xmlns='urn:xmpp:mam:2' queryid='f27'/></iq>");
+ Stanza stanza = StanzaBuilder.createClone(queryIqElement, true, Collections.emptyList()).build();
+
+ assertFalse(tested.verifyInnerElement(stanza));
+ }
+
+ @Test
+ public void pubsubHasHighestPriority() {
+ when(pubsubNodeArchiveQueryHandler.supports(any(), any(), any())).thenReturn(true);
+ when(mucArchiveQueryHandler.supports(any(), any(), any())).thenReturn(true);
+ when(userArchiveQueryHandler.supports(any(), any(), any())).thenReturn(true);
+
+ tested.handleSet(stanza, serverRuntimeContext, sessionContext, null);
+ verify(pubsubNodeArchiveQueryHandler).handle(any(), any(), any());
+ verify(mucArchiveQueryHandler, never()).handle(any(), any(), any());
+ verify(userArchiveQueryHandler, never()).handle(any(), any(), any());
+ }
+
+ @Test
+ public void mucHasLessPriorityThanPubsub() {
+ when(pubsubNodeArchiveQueryHandler.supports(any(), any(), any())).thenReturn(false);
+ when(mucArchiveQueryHandler.supports(any(), any(), any())).thenReturn(true);
+ when(userArchiveQueryHandler.supports(any(), any(), any())).thenReturn(true);
+
+ tested.handleSet(stanza, serverRuntimeContext, sessionContext, null);
+ verify(pubsubNodeArchiveQueryHandler, never()).handle(any(), any(), any());
+ verify(mucArchiveQueryHandler).handle(any(), any(), any());
+ verify(userArchiveQueryHandler, never()).handle(any(), any(), any());
+ }
+
+ @Test
+ public void userHasLowestPriority() {
+ when(pubsubNodeArchiveQueryHandler.supports(any(), any(), any())).thenReturn(false);
+ when(mucArchiveQueryHandler.supports(any(), any(), any())).thenReturn(false);
+ when(userArchiveQueryHandler.supports(any(), any(), any())).thenReturn(true);
+
+ tested.handleSet(stanza, serverRuntimeContext, sessionContext, null);
+ verify(pubsubNodeArchiveQueryHandler, never()).handle(any(), any(), any());
+ verify(mucArchiveQueryHandler, never()).handle(any(), any(), any());
+ verify(userArchiveQueryHandler).handle(any(), any(), any());
+ }
+
+}
\ No newline at end of file
diff --git a/server/extensions/xep0313-mam/src/test/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/query/MatchingArchivedMessageResultsTest.java b/server/extensions/xep0313-mam/src/test/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/query/MatchingArchivedMessageResultsTest.java
new file mode 100644
index 0000000..4533f57
--- /dev/null
+++ b/server/extensions/xep0313-mam/src/test/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/query/MatchingArchivedMessageResultsTest.java
@@ -0,0 +1,181 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.apache.vysper.xmpp.modules.extension.xep0313_mam.query;
+
+import static org.junit.Assert.assertEquals;
+
+import java.io.IOException;
+import java.time.LocalDateTime;
+import java.time.ZoneId;
+import java.time.ZonedDateTime;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import org.apache.vysper.xml.fragment.XMLElement;
+import org.apache.vysper.xml.fragment.XMLSemanticError;
+import org.apache.vysper.xmpp.addressing.Entity;
+import org.apache.vysper.xmpp.addressing.EntityImpl;
+import org.apache.vysper.xmpp.modules.extension.xep0313_mam.StanzaAssert;
+import org.apache.vysper.xmpp.modules.extension.xep0313_mam.spi.ArchivedMessage;
+import org.apache.vysper.xmpp.modules.extension.xep0313_mam.spi.ArchivedMessages;
+import org.apache.vysper.xmpp.modules.extension.xep0313_mam.spi.SimpleArchivedMessage;
+import org.apache.vysper.xmpp.modules.extension.xep0313_mam.spi.SimpleArchivedMessages;
+import org.apache.vysper.xmpp.parser.XMLParserUtil;
+import org.apache.vysper.xmpp.stanza.IQStanza;
+import org.apache.vysper.xmpp.stanza.MessageStanza;
+import org.apache.vysper.xmpp.stanza.Stanza;
+import org.apache.vysper.xmpp.stanza.StanzaBuilder;
+import org.junit.Before;
+import org.junit.Test;
+import org.xml.sax.SAXException;
+
+/**
+ * @author Réda Housni Alaoui
+ */
+public class MatchingArchivedMessageResultsTest {
+
+ private static final Entity INITIATING_ENTITY = EntityImpl.parseUnchecked("juliet@capulet.lit/chamber");
+
+ private static final Entity ARCHIVE_ID = EntityImpl.parseUnchecked("juliet@capulet.lit");
+
+ private Query query;
+
+ private MessageStanza messageStanza;
+
+ @Before
+ public void before() throws XMLSemanticError, IOException, SAXException {
+ XMLElement queryIqElement = XMLParserUtil.parseRequiredDocument(
+ "<iq type='set' id='juliet1'><query xmlns='urn:xmpp:mam:2' queryid='f27'/></iq>");
+ query = new Query("urn:xmpp:mam:2",
+ new IQStanza(StanzaBuilder.createClone(queryIqElement, true, Collections.emptyList()).build()));
+
+ XMLElement messageElement = XMLParserUtil.parseRequiredDocument(
+ "<message xmlns='jabber:client' from=\"witch@shakespeare.lit\" to=\"macbeth@shakespeare.lit\">"
+ + "<body>Hail to thee</body></message>");
+ messageStanza = new MessageStanza(
+ StanzaBuilder.createClone(messageElement, true, Collections.emptyList()).build());
+ }
+
+ @Test
+ public void testUncomplete() throws IOException, SAXException {
+ SimpleArchivedMessage archivedMessage1 = new SimpleArchivedMessage("28482-98726-73623",
+ ZonedDateTime.of(LocalDateTime.of(2010, 7, 10, 23, 8, 25), ZoneId.of("Z")), messageStanza);
+ SimpleArchivedMessage archivedMessage2 = new SimpleArchivedMessage("09af3-cc343-b409f",
+ ZonedDateTime.of(LocalDateTime.of(2010, 7, 10, 23, 8, 25), ZoneId.of("Z")), messageStanza);
+
+ List<ArchivedMessage> archivedMessagesList = new ArrayList<>();
+ archivedMessagesList.add(archivedMessage1);
+ archivedMessagesList.add(archivedMessage2);
+
+ SimpleArchivedMessages archivedMessages = new SimpleArchivedMessages(archivedMessagesList, 0L, 3L);
+
+ MatchingArchivedMessageResults tested = new MatchingArchivedMessageResults(INITIATING_ENTITY, ARCHIVE_ID, query,
+ archivedMessages);
+
+ List<Stanza> responseStanzas = tested.toStanzas();
+ assertEquals(3, responseStanzas.size());
+
+ StanzaAssert.assertEquals(StanzaBuilder
+ .createClone(XMLParserUtil.parseRequiredDocument("<message to='juliet@capulet.lit/chamber'>"
+ + " <result xmlns='urn:xmpp:mam:2' queryid='f27' id='28482-98726-73623'>"
+ + " <forwarded xmlns='urn:xmpp:forward:0'>"
+ + " <delay xmlns='urn:xmpp:delay' stamp='2010-07-10T23:08:25Z'/>"
+ + " <message xmlns='jabber:client' from='witch@shakespeare.lit' to='macbeth@shakespeare.lit'>"
+ + " <body>Hail to thee</body>"
+ + " <stanza-id xmlns='urn:xmpp:sid:0' by='juliet@capulet.lit' id='28482-98726-73623'/>"
+ + "</message></forwarded></result></message>"), true, null)
+ .build(), responseStanzas.get(0));
+
+ StanzaAssert.assertEquals(StanzaBuilder
+ .createClone(XMLParserUtil.parseRequiredDocument("<message to='juliet@capulet.lit/chamber'>"
+ + " <result xmlns='urn:xmpp:mam:2' queryid='f27' id='09af3-cc343-b409f'>"
+ + " <forwarded xmlns='urn:xmpp:forward:0'>"
+ + " <delay xmlns='urn:xmpp:delay' stamp='2010-07-10T23:08:25Z'/>"
+ + " <message xmlns='jabber:client' from='witch@shakespeare.lit' to='macbeth@shakespeare.lit'>"
+ + " <body>Hail to thee</body>"
+ + " <stanza-id xmlns='urn:xmpp:sid:0' by='juliet@capulet.lit' id='09af3-cc343-b409f'/>"
+ + "</message></forwarded></result></message>"), true, null)
+ .build(), responseStanzas.get(1));
+
+ StanzaAssert.assertEquals(
+ StanzaBuilder.createClone(XMLParserUtil
+ .parseRequiredDocument("<iq type='result' id='juliet1'><fin xmlns='urn:xmpp:mam:2'>"
+ + " <set xmlns='http://jabber.org/protocol/rsm'>"
+ + " <count>3</count><first index='0'>28482-98726-73623</first>"
+ + " <last>09af3-cc343-b409f</last></set></fin></iq>"),
+ true, null).build(),
+ responseStanzas.get(2));
+ }
+
+ @Test
+ public void testComplete() throws IOException, SAXException {
+ ArchivedMessage message = new SimpleArchivedMessage("28482-98726-73623",
+ ZonedDateTime.of(LocalDateTime.of(2010, 7, 10, 23, 8, 25), ZoneId.of("Z")), messageStanza);
+
+ ArchivedMessages archivedMessages = new SimpleArchivedMessages(Collections.singletonList(message), 0L, 1L);
+
+ MatchingArchivedMessageResults tested = new MatchingArchivedMessageResults(INITIATING_ENTITY, ARCHIVE_ID, query,
+ archivedMessages);
+
+ List<Stanza> responseStanzas = tested.toStanzas();
+ assertEquals(2, responseStanzas.size());
+
+ StanzaAssert.assertEquals(StanzaBuilder
+ .createClone(XMLParserUtil.parseRequiredDocument("<message to='juliet@capulet.lit/chamber'>"
+ + " <result xmlns='urn:xmpp:mam:2' queryid='f27' id='28482-98726-73623'>"
+ + " <forwarded xmlns='urn:xmpp:forward:0'>"
+ + " <delay xmlns='urn:xmpp:delay' stamp='2010-07-10T23:08:25Z'/>"
+ + " <message xmlns='jabber:client' from='witch@shakespeare.lit' to='macbeth@shakespeare.lit'>"
+ + " <body>Hail to thee</body>"
+ + " <stanza-id xmlns='urn:xmpp:sid:0' by='juliet@capulet.lit' id='28482-98726-73623'/>"
+ + "</message></forwarded></result></message>"), true, null)
+ .build(), responseStanzas.get(0));
+
+ StanzaAssert.assertEquals(
+ StanzaBuilder.createClone(XMLParserUtil.parseRequiredDocument(
+ "<iq type='result' id='juliet1'><fin xmlns='urn:xmpp:mam:2' complete='true'>"
+ + " <set xmlns='http://jabber.org/protocol/rsm'>"
+ + " <count>1</count><first index='0'>28482-98726-73623</first>"
+ + " <last>28482-98726-73623</last></set></fin></iq>"),
+ true, null).build(),
+ responseStanzas.get(1));
+ }
+
+ @Test
+ public void testEmptyPage() throws IOException, SAXException {
+ SimpleArchivedMessages archivedMessages = new SimpleArchivedMessages(Collections.emptyList(), 0L, 50L);
+
+ MatchingArchivedMessageResults tested = new MatchingArchivedMessageResults(INITIATING_ENTITY, ARCHIVE_ID, query,
+ archivedMessages);
+
+ List<Stanza> responseStanzas = tested.toStanzas();
+ assertEquals(1, responseStanzas.size());
+
+ StanzaAssert.assertEquals(
+ StanzaBuilder.createClone(XMLParserUtil
+ .parseRequiredDocument("<iq type='result' id='juliet1'><fin xmlns='urn:xmpp:mam:2'>"
+ + " <set xmlns='http://jabber.org/protocol/rsm'>"
+ + " <count>50</count></set></fin></iq>"),
+ true, null).build(),
+ responseStanzas.get(0));
+ }
+
+}
\ No newline at end of file
diff --git a/server/extensions/xep0313-mam/src/test/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/query/QuerySetTest.java b/server/extensions/xep0313-mam/src/test/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/query/QuerySetTest.java
new file mode 100644
index 0000000..7258208
--- /dev/null
+++ b/server/extensions/xep0313-mam/src/test/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/query/QuerySetTest.java
@@ -0,0 +1,58 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.apache.vysper.xmpp.modules.extension.xep0313_mam.query;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import org.apache.vysper.xml.fragment.XMLSemanticError;
+import org.apache.vysper.xmpp.modules.extension.xep0059_result_set_management.Set;
+import org.junit.Test;
+
+/**
+ * @author Réda Housni Alaoui
+ */
+public class QuerySetTest {
+
+ @Test
+ public void mainCase() throws XMLSemanticError {
+ Set set = Set.builder().max(10L).after("first").before("last").build();
+ QuerySet tested = new QuerySet(set);
+ assertEquals(10L, (long) tested.pageSize().orElse(0L));
+ assertEquals("first", tested.firstMessageId().orElse(null));
+ assertEquals("last", tested.lastMessageId().orElse(null));
+ }
+
+ @Test
+ public void maxAndFirstMessageIdBlankIsALastPageQuery() throws XMLSemanticError {
+ Set set = Set.builder().max(10L).before("").build();
+ QuerySet tested = new QuerySet(set);
+ assertTrue(tested.lastPage());
+ }
+
+ @Test
+ public void maxAndFirstMessageIdBlankAndLastMessageIdNotBlankIsNotALastPageQuery() throws XMLSemanticError {
+ Set set = Set.builder().max(10L).after("first").before("").build();
+ QuerySet tested = new QuerySet(set);
+ assertFalse(tested.lastPage());
+ }
+
+}
\ No newline at end of file
diff --git a/server/extensions/xep0313-mam/src/test/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/spi/MessageArchiveMock.java b/server/extensions/xep0313-mam/src/test/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/spi/MessageArchiveMock.java
new file mode 100644
index 0000000..48b20dd
--- /dev/null
+++ b/server/extensions/xep0313-mam/src/test/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/spi/MessageArchiveMock.java
@@ -0,0 +1,65 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.apache.vysper.xmpp.modules.extension.xep0313_mam.spi;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import java.util.Collections;
+import java.util.LinkedList;
+import java.util.Queue;
+
+import org.apache.vysper.xmpp.stanza.MessageStanza;
+
+/**
+ * @author Réda Housni Alaoui
+ */
+public class MessageArchiveMock implements MessageArchive {
+
+ private final Queue<Message> messages = new LinkedList<>();
+
+ @Override
+ public void archive(Message message) {
+ messages.add(message);
+ }
+
+ @Override
+ public ArchivedMessages fetchSortedByOldestFirst(MessageFilter messageFilter, MessagePageRequest pageRequest) {
+ return new SimpleArchivedMessages(Collections.emptyList(), 0L, 0L);
+ }
+
+ @Override
+ public ArchivedMessages fetchLastPageSortedByOldestFirst(MessageFilter messageFilter, long pageSize) {
+ return new SimpleArchivedMessages(Collections.emptyList(), 0L, 0L);
+ }
+
+ public void clear() {
+ messages.clear();
+ }
+
+ public void assertUniqueArchivedMessageStanza(MessageStanza messageStanza) {
+ assertEquals(1, messages.size());
+ assertEquals(messageStanza, messages.peek().stanza());
+ }
+
+ public void assertEmpty() {
+ assertTrue(messages.isEmpty());
+ }
+}
diff --git a/server/extensions/xep0313-mam/src/test/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/spi/MessageArchivesMock.java b/server/extensions/xep0313-mam/src/test/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/spi/MessageArchivesMock.java
new file mode 100644
index 0000000..b484fe9
--- /dev/null
+++ b/server/extensions/xep0313-mam/src/test/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/spi/MessageArchivesMock.java
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.apache.vysper.xmpp.modules.extension.xep0313_mam.spi;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Optional;
+
+import org.apache.vysper.xmpp.addressing.Entity;
+
+/**
+ * @author Réda Housni Alaoui
+ */
+public class MessageArchivesMock implements MessageArchives {
+
+ private final Map<Entity, MessageArchive> archiveById = new HashMap<>();
+
+ public MessageArchiveMock givenArchive(Entity archiveId) {
+ MessageArchiveMock userMessageArchiveMock = new MessageArchiveMock();
+ archiveById.put(archiveId, userMessageArchiveMock);
+ return userMessageArchiveMock;
+ }
+
+ @Override
+ public Optional<MessageArchive> retrieveUserMessageArchive(Entity userBareJid) {
+ return Optional.ofNullable(archiveById.get(userBareJid));
+ }
+}
diff --git a/server/extensions/xep0313-mam/src/test/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/spi/SimpleEntityFilter.java b/server/extensions/xep0313-mam/src/test/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/spi/SimpleEntityFilter.java
new file mode 100644
index 0000000..78305ce
--- /dev/null
+++ b/server/extensions/xep0313-mam/src/test/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/spi/SimpleEntityFilter.java
@@ -0,0 +1,57 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.apache.vysper.xmpp.modules.extension.xep0313_mam.spi;
+
+import static java.util.Objects.requireNonNull;
+
+import org.apache.vysper.xmpp.addressing.Entity;
+
+/**
+ * @author Réda Housni Alaoui
+ */
+public class SimpleEntityFilter implements EntityFilter {
+
+ private final Entity entity;
+
+ private final Type type;
+
+ private final boolean ignoreResource;
+
+ public SimpleEntityFilter(Entity entity, Type type, boolean ignoreResource) {
+ this.entity = requireNonNull(entity);
+ this.type = requireNonNull(type);
+ this.ignoreResource = ignoreResource;
+ }
+
+ @Override
+ public Entity entity() {
+ return entity;
+ }
+
+ @Override
+ public Type type() {
+ return type;
+ }
+
+ @Override
+ public boolean ignoreResource() {
+ return ignoreResource;
+ }
+}
diff --git a/server/extensions/xep0313-mam/src/test/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/spi/SimpleMessagePageRequest.java b/server/extensions/xep0313-mam/src/test/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/spi/SimpleMessagePageRequest.java
new file mode 100644
index 0000000..585e7ce
--- /dev/null
+++ b/server/extensions/xep0313-mam/src/test/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/spi/SimpleMessagePageRequest.java
@@ -0,0 +1,58 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.apache.vysper.xmpp.modules.extension.xep0313_mam.spi;
+
+import java.util.Optional;
+
+import org.apache.commons.lang.StringUtils;
+
+/**
+ * @author Réda Housni Alaoui
+ */
+public class SimpleMessagePageRequest implements MessagePageRequest {
+
+ private final Long pageSize;
+
+ private final String firstMessageId;
+
+ private final String lastMessageId;
+
+ public SimpleMessagePageRequest(Long pageSize, String firstMessageId, String lastMessageId) {
+ this.pageSize = pageSize;
+ this.firstMessageId = firstMessageId;
+ this.lastMessageId = lastMessageId;
+ }
+
+ @Override
+ public Optional<Long> pageSize() {
+ return Optional.ofNullable(pageSize);
+ }
+
+ @Override
+ public Optional<String> firstMessageId() {
+ return Optional.ofNullable(firstMessageId).filter(StringUtils::isNotBlank);
+ }
+
+ @Override
+ public Optional<String> lastMessageId() {
+ return Optional.ofNullable(lastMessageId).filter(StringUtils::isNotBlank);
+ }
+
+}
diff --git a/server/extensions/xep0313-mam/src/test/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/user/UserArchiveQueryHandlerTest.java b/server/extensions/xep0313-mam/src/test/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/user/UserArchiveQueryHandlerTest.java
new file mode 100644
index 0000000..0150861
--- /dev/null
+++ b/server/extensions/xep0313-mam/src/test/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/user/UserArchiveQueryHandlerTest.java
@@ -0,0 +1,124 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.apache.vysper.xmpp.modules.extension.xep0313_mam.user;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import java.io.IOException;
+import java.util.Collections;
+import java.util.List;
+
+import org.apache.vysper.xml.fragment.XMLElement;
+import org.apache.vysper.xml.fragment.XMLSemanticError;
+import org.apache.vysper.xmpp.addressing.Entity;
+import org.apache.vysper.xmpp.addressing.EntityImpl;
+import org.apache.vysper.xmpp.modules.extension.xep0313_mam.ServerRuntimeContextMock;
+import org.apache.vysper.xmpp.modules.extension.xep0313_mam.SessionContextMock;
+import org.apache.vysper.xmpp.modules.extension.xep0313_mam.query.Query;
+import org.apache.vysper.xmpp.modules.extension.xep0313_mam.spi.MessageArchivesMock;
+import org.apache.vysper.xmpp.parser.XMLParserUtil;
+import org.apache.vysper.xmpp.stanza.IQStanza;
+import org.apache.vysper.xmpp.stanza.Stanza;
+import org.apache.vysper.xmpp.stanza.StanzaBuilder;
+import org.apache.vysper.xmpp.stanza.StanzaErrorCondition;
+import org.junit.Before;
+import org.junit.Test;
+import org.xml.sax.SAXException;
+
+/**
+ * @author Réda Housni Alaoui
+ */
+public class UserArchiveQueryHandlerTest {
+
+ private static final Entity JULIET = EntityImpl.parseUnchecked("juliet@foo.com/balcony");
+
+ private static final Entity ROMEO = EntityImpl.parseUnchecked("romeo@foo.com/floor");
+
+ private static final Entity INITIATING_ENTITY = JULIET;
+
+ private Query romeoTargetingQuery;
+
+ private Query untargetedQuery;
+
+ private MessageArchivesMock archives;
+
+ private ServerRuntimeContextMock serverRuntimeContext;
+
+ private SessionContextMock sessionContext;
+
+ private UserArchiveQueryHandler tested;
+
+ @Before
+ public void before() throws IOException, SAXException, XMLSemanticError {
+ romeoTargetingQuery = new Query("urn:xmpp:mam:2",
+ new IQStanza(StanzaBuilder.createClone(
+ XMLParserUtil.parseRequiredDocument(
+ "<iq type='set' to='romeo@foo.com'><query xmlns='urn:xmpp:mam:2'/></iq>"),
+ true, Collections.emptyList()).build()));
+
+ untargetedQuery = new Query("urn:xmpp:mam:2",
+ new IQStanza(StanzaBuilder.createClone(
+ XMLParserUtil.parseRequiredDocument("<iq type='set'><query xmlns='urn:xmpp:mam:2'/></iq>"),
+ true, Collections.emptyList()).build()));
+
+ serverRuntimeContext = new ServerRuntimeContextMock();
+ archives = serverRuntimeContext.givenUserMessageArchives();
+ sessionContext = new SessionContextMock();
+ sessionContext.givenInitiatingEntity(INITIATING_ENTITY);
+ tested = new UserArchiveQueryHandler();
+ }
+
+ @Test
+ public void supportsAllQueries() {
+ assertTrue(tested.supports(null, null, null));
+ }
+
+ @Test
+ public void untargetedQueryEndsUpInInitiatingEntityArchive() throws XMLSemanticError {
+ archives.givenArchive(INITIATING_ENTITY.getBareJID());
+ List<Stanza> stanzas = tested.handle(untargetedQuery, serverRuntimeContext, sessionContext);
+ assertEquals(1, stanzas.size());
+ Stanza stanza = stanzas.get(0);
+ assertEquals("iq", stanza.getName());
+ assertNotNull(stanza.getSingleInnerElementsNamed("fin"));
+ }
+
+ @Test
+ public void preventsUserFromQueryingOtherUserArchive() throws XMLSemanticError {
+ List<Stanza> stanzas = tested.handle(romeoTargetingQuery, serverRuntimeContext, sessionContext);
+ assertError(stanzas, StanzaErrorCondition.FORBIDDEN);
+ }
+
+ @Test
+ public void unexistingArchiveLeadsToItemNotFound() throws XMLSemanticError {
+ List<Stanza> stanzas = tested.handle(untargetedQuery, serverRuntimeContext, sessionContext);
+ assertError(stanzas, StanzaErrorCondition.ITEM_NOT_FOUND);
+ }
+
+ private void assertError(List<Stanza> stanzas, StanzaErrorCondition errorCondition) throws XMLSemanticError {
+ assertEquals(1, stanzas.size());
+ XMLElement error = stanzas.get(0).getSingleInnerElementsNamed("error");
+ assertNotNull(error);
+ assertNotNull(error.getSingleInnerElementsNamed(errorCondition.value()));
+ }
+
+}
\ No newline at end of file
diff --git a/server/extensions/xep0313-mam/src/test/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/user/UserMessageListenerTest.java b/server/extensions/xep0313-mam/src/test/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/user/UserMessageListenerTest.java
new file mode 100644
index 0000000..a70ddf7
--- /dev/null
+++ b/server/extensions/xep0313-mam/src/test/java/org/apache/vysper/xmpp/modules/extension/xep0313_mam/user/UserMessageListenerTest.java
@@ -0,0 +1,155 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.apache.vysper.xmpp.modules.extension.xep0313_mam.user;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.util.stream.Stream;
+
+import org.apache.vysper.xmpp.addressing.Entity;
+import org.apache.vysper.xmpp.addressing.EntityImpl;
+import org.apache.vysper.xmpp.modules.core.base.handler.AcceptedMessageEvent;
+import org.apache.vysper.xmpp.modules.extension.xep0313_mam.ServerRuntimeContextMock;
+import org.apache.vysper.xmpp.modules.extension.xep0313_mam.SessionContextMock;
+import org.apache.vysper.xmpp.modules.extension.xep0313_mam.spi.MessageArchiveMock;
+import org.apache.vysper.xmpp.modules.extension.xep0313_mam.spi.MessageArchivesMock;
+import org.apache.vysper.xmpp.stanza.MessageStanza;
+import org.apache.vysper.xmpp.stanza.MessageStanzaType;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * @author Réda Housni Alaoui
+ */
+public class UserMessageListenerTest {
+
+ private static final Entity JULIET_IN_CHAMBER = EntityImpl.parseUnchecked("juliet@capulet.lit/chamber");
+
+ private static final Entity ROMEO_IN_ORCHARD = EntityImpl.parseUnchecked("romeo@montague.lit/orchard");
+
+ private static final Entity MACBETH_IN_KITCHEN = EntityImpl.parseUnchecked("macbeth@shakespeare.lit/kitchen");
+
+ private static final Entity ALICE_IN_RABBIT_HOLE = EntityImpl.parseUnchecked("alice@carol.lit/rabbit-hole");
+
+ private static final Entity INITIATING_ENTITY = JULIET_IN_CHAMBER;
+
+ private MessageArchiveMock julietArchive;
+
+ private MessageArchiveMock romeoArchive;
+
+ private MessageArchiveMock macbethArchive;
+
+ private AcceptedMessageEvent acceptedMessage;
+
+ private MessageStanza messageStanza;
+
+ private UserMessageListener tested;
+
+ @Before
+ public void before() {
+ ServerRuntimeContextMock serverRuntimeContext = new ServerRuntimeContextMock();
+
+ MessageArchivesMock archives = serverRuntimeContext.givenUserMessageArchives();
+
+ julietArchive = archives.givenArchive(JULIET_IN_CHAMBER.getBareJID());
+ romeoArchive = archives.givenArchive(ROMEO_IN_ORCHARD.getBareJID());
+ macbethArchive = archives.givenArchive(MACBETH_IN_KITCHEN.getBareJID());
+
+ SessionContextMock sessionContext = new SessionContextMock();
+ sessionContext.givenInitiatingEntity(INITIATING_ENTITY);
+ sessionContext.givenServerRuntimeContext(serverRuntimeContext);
+
+ messageStanza = mock(MessageStanza.class);
+ when(messageStanza.getMessageType()).thenReturn(MessageStanzaType.NORMAL);
+ acceptedMessage = mock(AcceptedMessageEvent.class);
+ when(acceptedMessage.serverRuntimeContext()).thenReturn(serverRuntimeContext);
+ when(acceptedMessage.sessionContext()).thenReturn(sessionContext);
+ when(acceptedMessage.messageStanza()).thenReturn(messageStanza);
+
+ tested = new UserMessageListener();
+ }
+
+ @Test
+ public void onlyNormalAndChatMessageAreArchived() {
+ when(acceptedMessage.isOutbound()).thenReturn(true);
+
+ Stream.of(MessageStanzaType.values()).filter(messageStanzaType -> messageStanzaType != MessageStanzaType.NORMAL)
+ .filter(messageStanzaType -> messageStanzaType != MessageStanzaType.CHAT).forEach(messageStanzaType -> {
+ when(messageStanza.getMessageType()).thenReturn(messageStanzaType);
+ tested.onEvent(acceptedMessage);
+
+ julietArchive.assertEmpty();
+ romeoArchive.assertEmpty();
+ macbethArchive.assertEmpty();
+ });
+
+ Stream.of(MessageStanzaType.CHAT, MessageStanzaType.NORMAL).forEach(messageStanzaType -> {
+ julietArchive.clear();
+
+ when(messageStanza.getMessageType()).thenReturn(messageStanzaType);
+ tested.onEvent(acceptedMessage);
+
+ julietArchive.assertUniqueArchivedMessageStanza(messageStanza);
+ });
+ }
+
+ @Test
+ public void outboundMessageHavingFrom() {
+ when(acceptedMessage.isOutbound()).thenReturn(true);
+ when(messageStanza.getFrom()).thenReturn(ROMEO_IN_ORCHARD);
+
+ tested.onEvent(acceptedMessage);
+
+ romeoArchive.assertUniqueArchivedMessageStanza(messageStanza);
+ }
+
+ @Test
+ public void outboundMessageWithoutFrom() {
+ when(acceptedMessage.isOutbound()).thenReturn(true);
+
+ tested.onEvent(acceptedMessage);
+
+ julietArchive.assertUniqueArchivedMessageStanza(messageStanza);
+ }
+
+ @Test
+ public void inboundMessage() {
+ when(acceptedMessage.isOutbound()).thenReturn(false);
+ when(messageStanza.getTo()).thenReturn(MACBETH_IN_KITCHEN);
+
+ tested.onEvent(acceptedMessage);
+
+ macbethArchive.assertUniqueArchivedMessageStanza(messageStanza);
+ }
+
+ @Test
+ public void unexistingArchive() {
+ when(acceptedMessage.isOutbound()).thenReturn(true);
+ when(messageStanza.getFrom()).thenReturn(ALICE_IN_RABBIT_HOLE);
+
+ tested.onEvent(acceptedMessage);
+
+ julietArchive.assertEmpty();
+ romeoArchive.assertEmpty();
+ macbethArchive.assertEmpty();
+ }
+
+}
\ No newline at end of file
diff --git a/server/extensions/xep0313-mam/src/test/resources/bogus_mina_tls.cert b/server/extensions/xep0313-mam/src/test/resources/bogus_mina_tls.cert
new file mode 100644
index 0000000..5a8e025
Binary files /dev/null and b/server/extensions/xep0313-mam/src/test/resources/bogus_mina_tls.cert differ