Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

SAK-50748 conversations Implement archive/merge #13104

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this not "ConversationsServiceImpl" - is there more than one way to add a service to the list (i.e. by service name)?

Copy link
Contributor Author

@adrianfish adrianfish Dec 11, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's what EntityProducer.getLabel returns. TBH, I don't know where those class names come from. I just got "rubrics" and "conversations" for the ones I did. Maybe I'm doing something wrong.

<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
Loading