XEP-0059 Result Set Management (#11)

diff --git a/server/core/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0059_result_set_management/ResultSetManagementModule.java b/server/core/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0059_result_set_management/ResultSetManagementModule.java
new file mode 100644
index 0000000..eba188e
--- /dev/null
+++ b/server/core/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0059_result_set_management/ResultSetManagementModule.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.xep0059_result_set_management;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.apache.commons.lang.StringUtils;
+import org.apache.vysper.xmpp.modules.DefaultDiscoAwareModule;
+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.NamespaceURIs;
+
+/**
+ * @author Réda Housni Alaoui
+ */
+public class ResultSetManagementModule extends DefaultDiscoAwareModule implements ServerInfoRequestListener {
+    @Override
+    public String getName() {
+        return "XEP-0059 Result Set Management";
+    }
+
+    @Override
+    public String getVersion() {
+        return "1.0";
+    }
+
+    @Override
+    public List<InfoElement> getServerInfosFor(InfoRequest request) {
+        if (StringUtils.isNotEmpty(request.getNode())) {
+            return null;
+        }
+
+        List<InfoElement> infoElements = new ArrayList<>();
+        infoElements.add(new Feature(NamespaceURIs.XEP0059_RESULT_SET_MANAGEMENT));
+        return infoElements;
+    }
+}
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
new file mode 100644
index 0000000..680ce68
--- /dev/null
+++ b/server/core/src/main/java/org/apache/vysper/xmpp/modules/extension/xep0059_result_set_management/Set.java
@@ -0,0 +1,240 @@
+/*
+ *  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.xep0059_result_set_management;
+
+import static java.util.Objects.requireNonNull;
+import static java.util.Optional.ofNullable;
+
+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;
+import org.apache.vysper.xml.fragment.XMLFragment;
+import org.apache.vysper.xml.fragment.XMLSemanticError;
+import org.apache.vysper.xml.fragment.XMLText;
+import org.apache.vysper.xmpp.protocol.NamespaceURIs;
+
+/**
+ * The Set element as defined by
+ * <a href="https://xmpp.org/extensions/xep-0059.html#schema">XEP-0059 Result
+ * Set Management</a>
+ * 
+ * @author Réda Housni Alaoui
+ */
+public class Set {
+
+    private static final String ELEMENT_NAME = "set";
+
+    private static final String FIRST = "first";
+
+    private static final String INDEX = "index";
+
+    private final XMLElement element;
+
+    public Set(XMLElement element) {
+        if (!ELEMENT_NAME.equals(element.getName())) {
+            throw new IllegalArgumentException("ResultSetManagement element must be named '" + ELEMENT_NAME
+                    + "' instead of '" + element.getName() + "'");
+        }
+        if (!NamespaceURIs.XEP0059_RESULT_SET_MANAGEMENT.equals(element.getNamespaceURI())) {
+            throw new IllegalArgumentException("ResultSetManagement element must be bound to namespace uri '"
+                    + NamespaceURIs.XEP0059_RESULT_SET_MANAGEMENT + "' instead of '" + element.getNamespaceURI() + "'");
+        }
+        this.element = element;
+    }
+
+    public static Builder builder() {
+        return new Builder();
+    }
+
+    public Optional<String> getAfter() throws XMLSemanticError {
+        return getElementText("after");
+    }
+
+    public Optional<String> getBefore() throws XMLSemanticError {
+        return getElementText("before");
+    }
+
+    public OptionalInt getCount() throws XMLSemanticError {
+        return getElementInt("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<String> getLast() throws XMLSemanticError {
+        return getElementText("last");
+    }
+
+    public OptionalInt getMax() throws XMLSemanticError {
+        return getElementInt("max");
+    }
+
+    private Optional<String> getElementText(String elementName) throws XMLSemanticError {
+        return ofNullable(element.getSingleInnerElementsNamed(elementName)).map(XMLElement::getInnerText)
+                .map(XMLText::getText);
+    }
+
+    private OptionalInt getElementInt(String elementName) throws XMLSemanticError {
+        return getElementText(elementName).map(Integer::parseInt).map(OptionalInt::of).orElse(OptionalInt.empty());
+    }
+
+    public static class First {
+
+        private final XMLElement element;
+
+        private First(XMLElement element) {
+            this.element = element;
+        }
+
+        private XMLElement element() {
+            return element;
+        }
+
+        public String getValue() {
+            return element.getInnerText().getText();
+        }
+
+        public OptionalInt getIndex() {
+            return ofNullable(element.getAttributeValue(INDEX)).map(Integer::parseInt).map(OptionalInt::of)
+                    .orElse(OptionalInt.empty());
+        }
+
+    }
+
+    public static class Builder {
+
+        private String after;
+
+        private String before;
+
+        private Integer count;
+
+        private First first;
+
+        private Integer index;
+
+        private String last;
+
+        private Integer max;
+
+        private Builder() {
+
+        }
+
+        public Builder after(String after) {
+            this.after = after;
+            return this;
+        }
+
+        public Builder before(String before) {
+            this.before = before;
+            return this;
+        }
+
+        public Builder count(Integer count) {
+            this.count = count;
+            return this;
+        }
+
+        public FirstBuilder startFirst() {
+            return new FirstBuilder(this);
+        }
+
+        public Builder index(Integer index) {
+            this.index = index;
+            return this;
+        }
+
+        public Builder last(String last) {
+            this.last = last;
+            return this;
+        }
+
+        public Builder max(Integer max) {
+            this.max = max;
+            return this;
+        }
+
+        private XMLFragment createElement(String name, Object innerValue) {
+            return new XMLElement(null, name, null, Collections.emptyList(),
+                    Collections.singletonList(new XMLText(String.valueOf(innerValue))));
+        }
+
+        public Set build() {
+            List<XMLFragment> innerFragments = new ArrayList<>();
+            ofNullable(after).map(innerValue -> createElement("after", innerValue)).ifPresent(innerFragments::add);
+            ofNullable(before).map(innerValue -> createElement("before", innerValue)).ifPresent(innerFragments::add);
+            ofNullable(count).map(innerValue -> createElement("count", innerValue)).ifPresent(innerFragments::add);
+            ofNullable(first).map(First::element).ifPresent(innerFragments::add);
+            ofNullable(index).map(innerValue -> createElement("index", innerValue)).ifPresent(innerFragments::add);
+            ofNullable(last).map(innerValue -> createElement("last", innerValue)).ifPresent(innerFragments::add);
+            ofNullable(max).map(innerValue -> createElement("max", innerValue)).ifPresent(innerFragments::add);
+
+            return new Set(new XMLElement(NamespaceURIs.XEP0059_RESULT_SET_MANAGEMENT, ELEMENT_NAME, null,
+                    Collections.emptyList(), innerFragments));
+        }
+
+    }
+
+    public static class FirstBuilder {
+
+        private final Builder builder;
+
+        private String value;
+
+        private Integer index;
+
+        private FirstBuilder(Builder builder) {
+            this.builder = requireNonNull(builder);
+        }
+
+        public FirstBuilder value(String value) {
+            this.value = value;
+            return this;
+        }
+
+        public FirstBuilder index(Integer index) {
+            this.index = index;
+            return this;
+        }
+
+        public Builder endFirst() {
+            List<Attribute> attributes = new ArrayList<>();
+            ofNullable(index).map(value -> new Attribute(INDEX, String.valueOf(index))).ifPresent(attributes::add);
+            List<XMLFragment> innerFragments = new ArrayList<>();
+            ofNullable(value).map(XMLText::new).ifPresent(innerFragments::add);
+
+            builder.first = new First(new XMLElement(null, FIRST, null, attributes, innerFragments));
+            return builder;
+        }
+
+    }
+
+}
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 551a99e..56bc78c 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
@@ -109,4 +109,6 @@
     public static final String XEP0124_BOSH = "http://jabber.org/protocol/httpbind";
     
     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";
 }
diff --git a/server/core/src/main/java/org/apache/vysper/xmpp/server/XMPPServer.java b/server/core/src/main/java/org/apache/vysper/xmpp/server/XMPPServer.java
index f22f66b..8f52c4e 100644
--- a/server/core/src/main/java/org/apache/vysper/xmpp/server/XMPPServer.java
+++ b/server/core/src/main/java/org/apache/vysper/xmpp/server/XMPPServer.java
@@ -42,6 +42,7 @@
 import org.apache.vysper.xmpp.delivery.inbound.DeliveringExternalInboundStanzaRelay;
 import org.apache.vysper.xmpp.delivery.inbound.DeliveringInternalInboundStanzaRelay;
 import org.apache.vysper.xmpp.modules.Module;
+import org.apache.vysper.xmpp.modules.extension.xep0059_result_set_management.ResultSetManagementModule;
 import org.apache.vysper.xmpp.modules.extension.xep0160_offline_storage.OfflineStorageProvider;
 import org.apache.vysper.xmpp.modules.roster.RosterModule;
 import org.apache.vysper.xmpp.modules.servicediscovery.ServiceDiscoveryModule;
@@ -108,6 +109,7 @@
         // add default modules
         initialModules.add(new ServiceDiscoveryModule());
         initialModules.add(new RosterModule());
+        initialModules.add(new ResultSetManagementModule());
     }
 
     public void setSASLMechanisms(List<SASLMechanism> validMechanisms) {
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
new file mode 100644
index 0000000..c52dfd6
--- /dev/null
+++ b/server/core/src/test/java/org/apache/vysper/xmpp/modules/extension/xep0059_result_set_management/ResultSetManagementTest.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.xep0059_result_set_management;
+
+import static org.junit.Assert.assertEquals;
+
+import org.apache.vysper.xml.fragment.XMLSemanticError;
+import org.junit.Test;
+
+/**
+ * @author Réda Housni Alaoui
+ */
+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();
+
+        assertEquals("after-value", tested.getAfter().get());
+        assertEquals("before-value", tested.getBefore().get());
+        assertEquals(5, tested.getCount().getAsInt());
+        assertEquals(10, tested.getIndex().getAsInt());
+        assertEquals("last-value", tested.getLast().get());
+        assertEquals(15, tested.getMax().getAsInt());
+
+        Set.First first = tested.getFirst().get();
+        assertEquals("first-value", first.getValue());
+        assertEquals(20, first.getIndex().getAsInt());
+    }
+
+}
\ No newline at end of file