Skip to content

Commit

Permalink
SAK-50748 conversations Implement archive/merge
Browse files Browse the repository at this point in the history
  • Loading branch information
adrianfish committed Dec 11, 2024
1 parent 02b2465 commit fb26109
Show file tree
Hide file tree
Showing 7 changed files with 293 additions and 7 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
<value>AssignmentService</value>
<value>AssessmentEntityProducer</value>
<value>ContentHostingService</value>
<value>conversations</value>
<value>CalendarService</value>
<value>ChatEntityProducer</value>
<value>DiscussionService</value>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
5 changes: 5 additions & 0 deletions conversations/impl/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,11 @@
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<dependency>
<groupId>org.sakaiproject.common</groupId>
<artifactId>archive-api</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.opensearch</groupId>
<artifactId>opensearch</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -423,7 +427,7 @@ public Optional<String> 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);

Expand Down Expand Up @@ -2540,6 +2544,7 @@ public Map<String, String> transferCopyEntities(String fromContext, String toCon
return traversalMap;
}

@Override
public Map<String, String> transferCopyEntities(String fromContext, String toContext, List<String> ids, List<String> transferOptions, boolean cleanup) {

if (cleanup) {
Expand All @@ -2556,6 +2561,108 @@ public Map<String, String> 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<Element> stack, String archivePath, List<Reference> 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<String, String> attachmentNames, Map<String, String> userIdTrans, Set<String> 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<String> 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)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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 = "<strong>Something about topic1</strong>";
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<Element> 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<ConversationsTopic> 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<ConversationsTopic> 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<String> 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<String> 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 {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?><archive date="20241211103321736" server="sakai1" source="1c51551f-947d-438c-bcb6-e5598dd84585" system="Sakai 2.8" xmlns:CHEF="https://www.sakailms.org/xmlns/archive/CHEF" xmlns:DAV="https://www.sakailms.org/xmlns/archive/DAV" xmlns:sakai="https://www.sakailms.org/xmlns/archive/"><conversations><topics><topic allow-anonymous-posts="false" created="1733859568" creator="admin" draft="false" pinned="false" post-before-viewing="false" title="Are aliens real?" type="DISCUSSION" visibility="INSTRUCTORS"><message><![CDATA[<p>Let&#39;s discuss <strong>aliens</strong>, right here.</p>
]]></message></topic><topic allow-anonymous-posts="true" created="1733859508" creator="admin" draft="false" pinned="true" post-before-viewing="false" title="How many angels can dance on the end of a pin?" type="QUESTION" visibility="SITE"><message><![CDATA[<p>It&#39;s philosophy, innit?</p>
]]></message></topic><topic allow-anonymous-posts="false" created="1733859443" creator="admin" draft="false" pinned="false" post-before-viewing="true" title="Where are the toilets?" type="QUESTION" visibility="SITE"><message><![CDATA[<p>Does anybody know <strong>where&nbsp;</strong>the toilets actually are?</p>
]]></message></topic><topic allow-anonymous-posts="false" created="1733913178" creator="5d525dc9-5eb8-4afc-9294-061e7fbec373" draft="false" pinned="true" post-before-viewing="true" title="let's talk sports" type="DISCUSSION" visibility="SITE"><message><![CDATA[<p>sporting <strong>stuff</strong></p>
]]></message></topic></topics></conversations></archive>
Loading

0 comments on commit fb26109

Please sign in to comment.