diff --git a/common/archive-impl/impl2/src/webapp/WEB-INF/components.xml b/common/archive-impl/impl2/src/webapp/WEB-INF/components.xml
index 6e61ec57d3d8..a3c895456d6f 100644
--- a/common/archive-impl/impl2/src/webapp/WEB-INF/components.xml
+++ b/common/archive-impl/impl2/src/webapp/WEB-INF/components.xml
@@ -23,6 +23,7 @@
AssignmentService
AssessmentEntityProducer
ContentHostingService
+ conversations
CalendarService
ChatEntityProducer
DiscussionService
diff --git a/conversations/api/src/main/java/org/sakaiproject/conversations/api/ConversationsService.java b/conversations/api/src/main/java/org/sakaiproject/conversations/api/ConversationsService.java
index 5cd548e6db4a..1194a04dd2a7 100644
--- a/conversations/api/src/main/java/org/sakaiproject/conversations/api/ConversationsService.java
+++ b/conversations/api/src/main/java/org/sakaiproject/conversations/api/ConversationsService.java
@@ -34,8 +34,9 @@
import org.sakaiproject.conversations.api.model.Tag;
import org.sakaiproject.conversations.api.model.ConversationsTopic;
import org.sakaiproject.entity.api.Entity;
+import org.sakaiproject.entity.api.EntityProducer;
-public interface ConversationsService {
+public interface ConversationsService extends EntityProducer {
public static final String TOOL_ID = "sakai.conversations";
public static final String REFERENCE_ROOT = Entity.SEPARATOR + "conversations";
diff --git a/conversations/impl/pom.xml b/conversations/impl/pom.xml
index ab16cb493880..d0b6af59f8f1 100644
--- a/conversations/impl/pom.xml
+++ b/conversations/impl/pom.xml
@@ -129,6 +129,11 @@
org.apache.commons
commons-lang3
+
+ org.sakaiproject.common
+ archive-api
+ test
+
org.opensearch
opensearch
diff --git a/conversations/impl/src/main/java/org/sakaiproject/conversations/impl/ConversationsServiceImpl.java b/conversations/impl/src/main/java/org/sakaiproject/conversations/impl/ConversationsServiceImpl.java
index a62191fc6c03..df8d141949b6 100644
--- a/conversations/impl/src/main/java/org/sakaiproject/conversations/impl/ConversationsServiceImpl.java
+++ b/conversations/impl/src/main/java/org/sakaiproject/conversations/impl/ConversationsServiceImpl.java
@@ -32,8 +32,13 @@
import java.util.Observer;
import java.util.Optional;
import java.util.Set;
+import java.util.Stack;
import java.util.stream.Collectors;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.NodeList;
+
import org.sakaiproject.api.app.scheduler.ScheduledInvocationManager;
import org.sakaiproject.authz.api.AuthzGroup;
import org.sakaiproject.authz.api.AuthzGroupService;
@@ -85,7 +90,6 @@
import org.sakaiproject.conversations.api.repository.TopicStatusRepository;
import org.sakaiproject.entity.api.Entity;
import org.sakaiproject.entity.api.EntityManager;
-import org.sakaiproject.entity.api.EntityProducer;
import org.sakaiproject.entity.api.Reference;
import org.sakaiproject.event.api.Event;
import org.sakaiproject.event.api.EventTrackingService;
@@ -135,7 +139,7 @@
@Slf4j
@Setter
@Transactional
-public class ConversationsServiceImpl implements ConversationsService, EntityProducer, EntityTransferrer, Observer {
+public class ConversationsServiceImpl implements ConversationsService, EntityTransferrer, Observer {
private AuthzGroupService authzGroupService;
@@ -423,7 +427,7 @@ public Optional getCommentPortalUrl(String commentId) {
@Transactional
public TopicTransferBean saveTopic(final TopicTransferBean topicBean, boolean sendMessage) throws ConversationsPermissionsException {
- String currentUserId = getCheckedCurrentUserId();
+ String currentUserId = StringUtils.isNotBlank(topicBean.creator) ? topicBean.creator : getCheckedCurrentUserId();
String siteRef = siteService.siteReference(topicBean.siteId);
@@ -2540,6 +2544,7 @@ public Map transferCopyEntities(String fromContext, String toCon
return traversalMap;
}
+ @Override
public Map transferCopyEntities(String fromContext, String toContext, List ids, List transferOptions, boolean cleanup) {
if (cleanup) {
@@ -2556,6 +2561,108 @@ public Map transferCopyEntities(String fromContext, String toCon
return transferCopyEntities(fromContext, toContext, ids, transferOptions);
}
+ @Override
+ public boolean willArchiveMerge() {
+ return true;
+ }
+
+ @Override
+ public String getLabel() {
+ return "conversations";
+ }
+
+ @Override
+ public String archive(String siteId, Document doc, Stack stack, String archivePath, List attachments) {
+
+ StringBuilder results = new StringBuilder();
+ results.append("begin archiving ").append(getLabel()).append(" for site ").append(siteId).append(System.lineSeparator());
+
+ Element element = doc.createElement(getLabel());
+ stack.peek().appendChild(element);
+ stack.push(element);
+
+ Element topicsEl = doc.createElement("topics");
+ element.appendChild(topicsEl);
+
+ topicRepository.findBySiteId(siteId).stream().sorted((t1, t2) -> t1.getTitle().compareTo(t2.getTitle())).forEach(topic -> {
+
+ Element topicEl = doc.createElement("topic");
+ topicsEl.appendChild(topicEl);
+ topicEl.setAttribute("title", topic.getTitle());
+ topicEl.setAttribute("type", topic.getType().name());
+ topicEl.setAttribute("post-before-viewing", Boolean.toString(topic.getMustPostBeforeViewing()));
+ topicEl.setAttribute("allow-anonymous-posts", Boolean.toString(topic.getAllowAnonymousPosts()));
+ topicEl.setAttribute("pinned", Boolean.toString(topic.getPinned()));
+ topicEl.setAttribute("draft", Boolean.toString(topic.getDraft()));
+ topicEl.setAttribute("visibility", topic.getVisibility().name());
+ topicEl.setAttribute("creator", topic.getMetadata().getCreator());
+ topicEl.setAttribute("created", Long.toString(topic.getMetadata().getCreated().getEpochSecond()));
+
+ Element messageEl = doc.createElement("message");
+ messageEl.appendChild(doc.createCDATASection(topic.getMessage()));
+ topicEl.appendChild(messageEl);
+ });
+
+ results.append("completed archiving ").append(getLabel()).append(" for site ").append(siteId).append(System.lineSeparator());
+ return results.toString();
+ }
+
+ @Override
+ public String merge(String toSiteId, Element root, String archivePath, String fromSiteId, Map attachmentNames, Map userIdTrans, Set userListAllowImport) {
+
+ StringBuilder results = new StringBuilder();
+ results.append("begin merging ").append(getLabel()).append(" for site ").append(toSiteId).append(System.lineSeparator());
+
+ if (!root.getTagName().equals(getLabel())) {
+ log.warn("Tried to merge a non <{}> xml document", getLabel());
+ return "Invalid xml document";
+ }
+
+ Set currentTitles = topicRepository.findBySiteId(toSiteId)
+ .stream().map(ConversationsTopic::getTitle).collect(Collectors.toSet());
+
+ NodeList topicNodes = root.getElementsByTagName("topic");
+
+ Instant now = Instant.now();
+
+ for (int i = 0; i < topicNodes.getLength(); i++) {
+
+ Element topicEl = (Element) topicNodes.item(i);
+ String title = topicEl.getAttribute("title");
+
+ if (currentTitles.contains(title)) {
+ log.debug("Topic \"{}\" already exists in site {}. Skipping merge ...", title, toSiteId);
+ continue;
+ }
+
+ TopicTransferBean topicBean = new TopicTransferBean();
+ topicBean.siteId = toSiteId;
+ topicBean.title = title;
+ topicBean.type = topicEl.getAttribute("type");
+ topicBean.created = now;
+ topicBean.mustPostBeforeViewing = Boolean.parseBoolean(topicEl.getAttribute("post-before-viewing"));
+ topicBean.anonymous = Boolean.parseBoolean(topicEl.getAttribute("anonymous"));
+ topicBean.allowAnonymousPosts = Boolean.parseBoolean(topicEl.getAttribute("allow-anonymous-posts"));
+ topicBean.draft = Boolean.parseBoolean(topicEl.getAttribute("draft"));
+ topicBean.pinned = Boolean.parseBoolean(topicEl.getAttribute("pinned"));
+ topicBean.visibility = topicEl.getAttribute("visibility");
+
+ NodeList messageNodes = topicEl.getElementsByTagName("message");
+ if (messageNodes.getLength() == 1) {
+ topicBean.message = ((Element) messageNodes.item(0)).getFirstChild().getNodeValue();
+ }
+
+ try {
+ saveTopic(topicBean, false);
+ } catch (Exception e) {
+ log.warn("Failed to merge topic \"{}\": {}", topicBean.title, e.toString());
+ }
+ }
+
+ return "";
+ }
+
+ @Override
public boolean parseEntityReference(String referenceString, Reference ref) {
if (referenceString.startsWith(REFERENCE_ROOT)) {
diff --git a/conversations/impl/src/test/org/sakaiproject/conversations/impl/ConversationsServiceTests.java b/conversations/impl/src/test/org/sakaiproject/conversations/impl/ConversationsServiceTests.java
index be3be75adef7..71f4d61e93f9 100644
--- a/conversations/impl/src/test/org/sakaiproject/conversations/impl/ConversationsServiceTests.java
+++ b/conversations/impl/src/test/org/sakaiproject/conversations/impl/ConversationsServiceTests.java
@@ -15,7 +15,7 @@
*/
package org.sakaiproject.conversations.impl;
-import org.junit.Assume;
+import org.sakaiproject.archive.api.ArchiveService;
import org.sakaiproject.authz.api.AuthzGroup;
import org.sakaiproject.authz.api.AuthzGroupService;
import org.sakaiproject.authz.api.SecurityService;
@@ -56,6 +56,7 @@
import org.sakaiproject.user.api.UserDirectoryService;
import org.sakaiproject.user.api.UserNotDefinedException;
import org.sakaiproject.util.ResourceLoader;
+import org.sakaiproject.util.Xml;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
@@ -76,16 +77,20 @@
import java.util.Map;
import java.util.Optional;
import java.util.Set;
+import java.util.Stack;
import java.util.stream.Collectors;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
-import static org.mockito.Mockito.*;
-
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.NodeList;
import lombok.extern.slf4j.Slf4j;
+import static org.mockito.Mockito.*;
import static org.junit.Assert.*;
+import org.junit.Assume;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -1968,6 +1973,163 @@ public void grading() {
assertNull(savedBean.gradingItemId);
}
+ @Test
+ public void archive() {
+
+ switchToInstructor(null);
+
+ String title1 = "Topic 1";
+ TopicTransferBean topic1 = new TopicTransferBean();
+ topic1.aboutReference = site1Ref;
+ topic1.title = title1;
+ topic1.message = "Something about topic1";
+ topic1.siteId = site1Id;
+ topic1 = saveTopic(topic1);
+
+ String title2 = "Topic 2";
+ TopicTransferBean topic2 = new TopicTransferBean();
+ topic2.aboutReference = site1Ref;
+ topic2.title = title2;
+ topic2.siteId = site1Id;
+ topic2 = saveTopic(topic2);
+
+ String title3 = "Topic 3";
+ TopicTransferBean topic3 = new TopicTransferBean();
+ topic3.aboutReference = site1Ref;
+ topic3.title = title3;
+ topic3.siteId = site1Id;
+ topic3 = saveTopic(topic3);
+
+ String title4 = "Topic 4";
+ TopicTransferBean topic4 = new TopicTransferBean();
+ topic4.aboutReference = site1Ref;
+ topic4.title = title4;
+ topic4.siteId = site1Id;
+ topic4 = saveTopic(topic4);
+
+ TopicTransferBean[] topicBeans = new TopicTransferBean[] { topic1, topic2, topic3, topic4 };
+
+ Document doc = Xml.createDocument();
+ Stack stack = new Stack<>();
+
+ Element root = doc.createElement("archive");
+ doc.appendChild(root);
+ root.setAttribute("source", site1Id);
+ root.setAttribute("xmlns:sakai", ArchiveService.SAKAI_ARCHIVE_NS);
+ root.setAttribute("xmlns:CHEF", ArchiveService.SAKAI_ARCHIVE_NS.concat("CHEF"));
+ root.setAttribute("xmlns:DAV", ArchiveService.SAKAI_ARCHIVE_NS.concat("DAV"));
+ stack.push(root);
+
+ assertEquals(1, stack.size());
+
+ String results = conversationsService.archive(site1Id, doc, stack, "", null);
+
+ assertEquals(2, stack.size());
+
+ NodeList conversationsNode = root.getElementsByTagName(conversationsService.getLabel());
+ assertEquals(1, conversationsNode.getLength());
+
+ NodeList topicsNode = ((Element) conversationsNode.item(0)).getElementsByTagName("topics");
+ assertEquals(1, topicsNode.getLength());
+
+ NodeList topicNodes = ((Element) topicsNode.item(0)).getElementsByTagName("topic");
+ assertEquals(topicBeans.length, topicNodes.getLength());
+
+ for (int i = 0; i < topicNodes.getLength(); i++) {
+ Element topicEl = (Element) topicNodes.item(i);
+ assertEquals(topicBeans[i].title, topicEl.getAttribute("title"));
+ assertEquals(topicBeans[i].type, topicEl.getAttribute("type"));
+ assertEquals(topicBeans[i].anonymous, Boolean.parseBoolean(topicEl.getAttribute("anonymous")));
+ assertEquals(topicBeans[i].allowAnonymousPosts, Boolean.parseBoolean(topicEl.getAttribute("allow-anonymous-posts")));
+ assertEquals(topicBeans[i].pinned, Boolean.parseBoolean(topicEl.getAttribute("pinned")));
+ assertEquals(topicBeans[i].draft, Boolean.parseBoolean(topicEl.getAttribute("draft")));
+ assertEquals(topicBeans[i].visibility, topicEl.getAttribute("visibility"));
+ assertEquals(topicBeans[i].creator, topicEl.getAttribute("creator"));
+ assertEquals(topicBeans[i].created.getEpochSecond(), Long.parseLong(topicEl.getAttribute("created")));
+
+ NodeList messageNodes = topicEl.getElementsByTagName("message");
+ assertEquals(1, messageNodes.getLength());
+
+ assertEquals(topicBeans[i].message, ((Element) messageNodes.item(0)).getFirstChild().getNodeValue());
+ }
+ }
+
+ @Test
+ public void merge() {
+
+ Document doc = Xml.readDocumentFromStream(this.getClass().getResourceAsStream("/archive/conversations.xml"));
+
+ Element root = doc.getDocumentElement();
+
+ String fromSite = root.getAttribute("source");
+ String toSite = "my-new-site";
+
+ String toSiteRef = "/site/" + toSite;
+ switchToInstructor(toSiteRef);
+
+ when(siteService.siteReference(toSite)).thenReturn(toSiteRef);
+
+ Element conversationsElement = doc.createElement("not-conversations");
+
+ conversationsService.merge(toSite, conversationsElement, "", fromSite, null, null, null);
+
+ assertEquals("Invalid xml document", conversationsService.merge(toSite, conversationsElement, "", fromSite, null, null, null));
+
+ conversationsElement = (Element) root.getElementsByTagName(conversationsService.getLabel()).item(0);
+
+ conversationsService.merge(toSite, conversationsElement, "", fromSite, null, null, null);
+
+ NodeList topicNodes = ((Element) conversationsElement.getElementsByTagName("topics").item(0)).getElementsByTagName("topic");
+
+ List topics = topicRepository.findBySiteId(toSite);
+
+ assertEquals(topics.size(), topicNodes.getLength());
+
+ for (int i = 0; i < topicNodes.getLength(); i++) {
+
+ Element topicEl = (Element) topicNodes.item(i);
+
+ String title = topicEl.getAttribute("title");
+ Optional optTopic = topics.stream().filter(t -> t.getTitle().equals(title)).findAny();
+ assertTrue(optTopic.isPresent());
+
+ ConversationsTopic topic = optTopic.get();
+
+ assertEquals(topic.getType().name(), topicEl.getAttribute("type"));
+ assertEquals(topic.getPinned(), Boolean.parseBoolean(topicEl.getAttribute("pinned")));
+ assertEquals(topic.getAnonymous(), Boolean.parseBoolean(topicEl.getAttribute("anonymous")));
+ assertEquals(topic.getDraft(), Boolean.parseBoolean(topicEl.getAttribute("draft")));
+ assertEquals(topic.getMustPostBeforeViewing(), Boolean.parseBoolean(topicEl.getAttribute("post-before-viewing")));
+
+ NodeList messageNodes = topicEl.getElementsByTagName("message");
+ assertEquals(1, messageNodes.getLength());
+
+ assertEquals(topic.getMessage(), messageNodes.item(0).getFirstChild().getNodeValue());
+ }
+
+ Set oldTitles = topics.stream().map(ConversationsTopic::getTitle).collect(Collectors.toSet());
+
+ // Now let's try and merge this set of rubrics. It has one with a different title, but the
+ // rest the same, so we should end up with only one rubric being added.
+ Document doc2 = Xml.readDocumentFromStream(this.getClass().getResourceAsStream("/archive/conversations2.xml"));
+
+ Element root2 = doc2.getDocumentElement();
+
+ conversationsElement = (Element) root2.getElementsByTagName(conversationsService.getLabel()).item(0);
+
+ conversationsService.merge(toSite, conversationsElement, "", fromSite, null, null, null);
+
+ String extraTitle = "Smurfs";
+
+ assertEquals(topics.size() + 1, topicRepository.findBySiteId(toSite).size());
+
+ Set newTitles = topicRepository.findBySiteId(toSite)
+ .stream().map(ConversationsTopic::getTitle).collect(Collectors.toSet());
+
+ assertFalse(oldTitles.contains(extraTitle));
+ assertTrue(newTitles.contains(extraTitle));
+ }
+
private TopicTransferBean saveTopic(TopicTransferBean topicBean) {
try {
diff --git a/conversations/impl/src/test/resources/archive/conversations.xml b/conversations/impl/src/test/resources/archive/conversations.xml
new file mode 100644
index 000000000000..88ef43f84aac
--- /dev/null
+++ b/conversations/impl/src/test/resources/archive/conversations.xml
@@ -0,0 +1,5 @@
+Let's discuss aliens, right here.
+]]>It's philosophy, innit?
+]]>Does anybody know where the toilets actually are?
+]]>sporting stuff
+]]>
\ No newline at end of file
diff --git a/conversations/impl/src/test/resources/archive/conversations2.xml b/conversations/impl/src/test/resources/archive/conversations2.xml
new file mode 100644
index 000000000000..548cf7137aca
--- /dev/null
+++ b/conversations/impl/src/test/resources/archive/conversations2.xml
@@ -0,0 +1,5 @@
+
+Let's discuss aliens, right here.
+]]>It's philosophy, innit?
+]]>Does anybody know where the toilets actually are?
+]]>