Skip to content

Commit fb26109

Browse files
committed
SAK-50748 conversations Implement archive/merge
https://sakaiproject.atlassian.net/browse/SAK-50748
1 parent 02b2465 commit fb26109

File tree

7 files changed

+293
-7
lines changed

7 files changed

+293
-7
lines changed

common/archive-impl/impl2/src/webapp/WEB-INF/components.xml

+1
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
<value>AssignmentService</value>
2424
<value>AssessmentEntityProducer</value>
2525
<value>ContentHostingService</value>
26+
<value>conversations</value>
2627
<value>CalendarService</value>
2728
<value>ChatEntityProducer</value>
2829
<value>DiscussionService</value>

conversations/api/src/main/java/org/sakaiproject/conversations/api/ConversationsService.java

+2-1
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,9 @@
3434
import org.sakaiproject.conversations.api.model.Tag;
3535
import org.sakaiproject.conversations.api.model.ConversationsTopic;
3636
import org.sakaiproject.entity.api.Entity;
37+
import org.sakaiproject.entity.api.EntityProducer;
3738

38-
public interface ConversationsService {
39+
public interface ConversationsService extends EntityProducer {
3940

4041
public static final String TOOL_ID = "sakai.conversations";
4142
public static final String REFERENCE_ROOT = Entity.SEPARATOR + "conversations";

conversations/impl/pom.xml

+5
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,11 @@
129129
<groupId>org.apache.commons</groupId>
130130
<artifactId>commons-lang3</artifactId>
131131
</dependency>
132+
<dependency>
133+
<groupId>org.sakaiproject.common</groupId>
134+
<artifactId>archive-api</artifactId>
135+
<scope>test</scope>
136+
</dependency>
132137
<dependency>
133138
<groupId>org.opensearch</groupId>
134139
<artifactId>opensearch</artifactId>

conversations/impl/src/main/java/org/sakaiproject/conversations/impl/ConversationsServiceImpl.java

+110-3
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,13 @@
3232
import java.util.Observer;
3333
import java.util.Optional;
3434
import java.util.Set;
35+
import java.util.Stack;
3536
import java.util.stream.Collectors;
3637

38+
import org.w3c.dom.Document;
39+
import org.w3c.dom.Element;
40+
import org.w3c.dom.NodeList;
41+
3742
import org.sakaiproject.api.app.scheduler.ScheduledInvocationManager;
3843
import org.sakaiproject.authz.api.AuthzGroup;
3944
import org.sakaiproject.authz.api.AuthzGroupService;
@@ -85,7 +90,6 @@
8590
import org.sakaiproject.conversations.api.repository.TopicStatusRepository;
8691
import org.sakaiproject.entity.api.Entity;
8792
import org.sakaiproject.entity.api.EntityManager;
88-
import org.sakaiproject.entity.api.EntityProducer;
8993
import org.sakaiproject.entity.api.Reference;
9094
import org.sakaiproject.event.api.Event;
9195
import org.sakaiproject.event.api.EventTrackingService;
@@ -135,7 +139,7 @@
135139
@Slf4j
136140
@Setter
137141
@Transactional
138-
public class ConversationsServiceImpl implements ConversationsService, EntityProducer, EntityTransferrer, Observer {
142+
public class ConversationsServiceImpl implements ConversationsService, EntityTransferrer, Observer {
139143

140144
private AuthzGroupService authzGroupService;
141145

@@ -423,7 +427,7 @@ public Optional<String> getCommentPortalUrl(String commentId) {
423427
@Transactional
424428
public TopicTransferBean saveTopic(final TopicTransferBean topicBean, boolean sendMessage) throws ConversationsPermissionsException {
425429

426-
String currentUserId = getCheckedCurrentUserId();
430+
String currentUserId = StringUtils.isNotBlank(topicBean.creator) ? topicBean.creator : getCheckedCurrentUserId();
427431

428432
String siteRef = siteService.siteReference(topicBean.siteId);
429433

@@ -2540,6 +2544,7 @@ public Map<String, String> transferCopyEntities(String fromContext, String toCon
25402544
return traversalMap;
25412545
}
25422546

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

25452550
if (cleanup) {
@@ -2556,6 +2561,108 @@ public Map<String, String> transferCopyEntities(String fromContext, String toCon
25562561
return transferCopyEntities(fromContext, toContext, ids, transferOptions);
25572562
}
25582563

2564+
@Override
2565+
public boolean willArchiveMerge() {
2566+
return true;
2567+
}
2568+
2569+
@Override
2570+
public String getLabel() {
2571+
return "conversations";
2572+
}
2573+
2574+
@Override
2575+
public String archive(String siteId, Document doc, Stack<Element> stack, String archivePath, List<Reference> attachments) {
2576+
2577+
StringBuilder results = new StringBuilder();
2578+
results.append("begin archiving ").append(getLabel()).append(" for site ").append(siteId).append(System.lineSeparator());
2579+
2580+
Element element = doc.createElement(getLabel());
2581+
stack.peek().appendChild(element);
2582+
stack.push(element);
2583+
2584+
Element topicsEl = doc.createElement("topics");
2585+
element.appendChild(topicsEl);
2586+
2587+
topicRepository.findBySiteId(siteId).stream().sorted((t1, t2) -> t1.getTitle().compareTo(t2.getTitle())).forEach(topic -> {
2588+
2589+
Element topicEl = doc.createElement("topic");
2590+
topicsEl.appendChild(topicEl);
2591+
topicEl.setAttribute("title", topic.getTitle());
2592+
topicEl.setAttribute("type", topic.getType().name());
2593+
topicEl.setAttribute("post-before-viewing", Boolean.toString(topic.getMustPostBeforeViewing()));
2594+
topicEl.setAttribute("allow-anonymous-posts", Boolean.toString(topic.getAllowAnonymousPosts()));
2595+
topicEl.setAttribute("pinned", Boolean.toString(topic.getPinned()));
2596+
topicEl.setAttribute("draft", Boolean.toString(topic.getDraft()));
2597+
topicEl.setAttribute("visibility", topic.getVisibility().name());
2598+
topicEl.setAttribute("creator", topic.getMetadata().getCreator());
2599+
topicEl.setAttribute("created", Long.toString(topic.getMetadata().getCreated().getEpochSecond()));
2600+
2601+
Element messageEl = doc.createElement("message");
2602+
messageEl.appendChild(doc.createCDATASection(topic.getMessage()));
2603+
topicEl.appendChild(messageEl);
2604+
});
2605+
2606+
results.append("completed archiving ").append(getLabel()).append(" for site ").append(siteId).append(System.lineSeparator());
2607+
return results.toString();
2608+
}
2609+
2610+
@Override
2611+
public String merge(String toSiteId, Element root, String archivePath, String fromSiteId, Map<String, String> attachmentNames, Map<String, String> userIdTrans, Set<String> userListAllowImport) {
2612+
2613+
StringBuilder results = new StringBuilder();
2614+
results.append("begin merging ").append(getLabel()).append(" for site ").append(toSiteId).append(System.lineSeparator());
2615+
2616+
if (!root.getTagName().equals(getLabel())) {
2617+
log.warn("Tried to merge a non <{}> xml document", getLabel());
2618+
return "Invalid xml document";
2619+
}
2620+
2621+
Set<String> currentTitles = topicRepository.findBySiteId(toSiteId)
2622+
.stream().map(ConversationsTopic::getTitle).collect(Collectors.toSet());
2623+
2624+
NodeList topicNodes = root.getElementsByTagName("topic");
2625+
2626+
Instant now = Instant.now();
2627+
2628+
for (int i = 0; i < topicNodes.getLength(); i++) {
2629+
2630+
Element topicEl = (Element) topicNodes.item(i);
2631+
String title = topicEl.getAttribute("title");
2632+
2633+
if (currentTitles.contains(title)) {
2634+
log.debug("Topic \"{}\" already exists in site {}. Skipping merge ...", title, toSiteId);
2635+
continue;
2636+
}
2637+
2638+
TopicTransferBean topicBean = new TopicTransferBean();
2639+
topicBean.siteId = toSiteId;
2640+
topicBean.title = title;
2641+
topicBean.type = topicEl.getAttribute("type");
2642+
topicBean.created = now;
2643+
topicBean.mustPostBeforeViewing = Boolean.parseBoolean(topicEl.getAttribute("post-before-viewing"));
2644+
topicBean.anonymous = Boolean.parseBoolean(topicEl.getAttribute("anonymous"));
2645+
topicBean.allowAnonymousPosts = Boolean.parseBoolean(topicEl.getAttribute("allow-anonymous-posts"));
2646+
topicBean.draft = Boolean.parseBoolean(topicEl.getAttribute("draft"));
2647+
topicBean.pinned = Boolean.parseBoolean(topicEl.getAttribute("pinned"));
2648+
topicBean.visibility = topicEl.getAttribute("visibility");
2649+
2650+
NodeList messageNodes = topicEl.getElementsByTagName("message");
2651+
if (messageNodes.getLength() == 1) {
2652+
topicBean.message = ((Element) messageNodes.item(0)).getFirstChild().getNodeValue();
2653+
}
2654+
2655+
try {
2656+
saveTopic(topicBean, false);
2657+
} catch (Exception e) {
2658+
log.warn("Failed to merge topic \"{}\": {}", topicBean.title, e.toString());
2659+
}
2660+
}
2661+
2662+
return "";
2663+
}
2664+
2665+
@Override
25592666
public boolean parseEntityReference(String referenceString, Reference ref) {
25602667

25612668
if (referenceString.startsWith(REFERENCE_ROOT)) {

conversations/impl/src/test/org/sakaiproject/conversations/impl/ConversationsServiceTests.java

+165-3
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
*/
1616
package org.sakaiproject.conversations.impl;
1717

18-
import org.junit.Assume;
18+
import org.sakaiproject.archive.api.ArchiveService;
1919
import org.sakaiproject.authz.api.AuthzGroup;
2020
import org.sakaiproject.authz.api.AuthzGroupService;
2121
import org.sakaiproject.authz.api.SecurityService;
@@ -56,6 +56,7 @@
5656
import org.sakaiproject.user.api.UserDirectoryService;
5757
import org.sakaiproject.user.api.UserNotDefinedException;
5858
import org.sakaiproject.util.ResourceLoader;
59+
import org.sakaiproject.util.Xml;
5960

6061
import org.springframework.beans.factory.annotation.Autowired;
6162
import org.springframework.test.context.ContextConfiguration;
@@ -76,16 +77,20 @@
7677
import java.util.Map;
7778
import java.util.Optional;
7879
import java.util.Set;
80+
import java.util.Stack;
7981
import java.util.stream.Collectors;
8082
import java.time.Instant;
8183
import java.time.temporal.ChronoUnit;
8284

83-
import static org.mockito.Mockito.*;
84-
85+
import org.w3c.dom.Document;
86+
import org.w3c.dom.Element;
87+
import org.w3c.dom.NodeList;
8588

8689
import lombok.extern.slf4j.Slf4j;
8790

91+
import static org.mockito.Mockito.*;
8892
import static org.junit.Assert.*;
93+
import org.junit.Assume;
8994
import org.junit.Before;
9095
import org.junit.Test;
9196
import org.junit.runner.RunWith;
@@ -1968,6 +1973,163 @@ public void grading() {
19681973
assertNull(savedBean.gradingItemId);
19691974
}
19701975

1976+
@Test
1977+
public void archive() {
1978+
1979+
switchToInstructor(null);
1980+
1981+
String title1 = "Topic 1";
1982+
TopicTransferBean topic1 = new TopicTransferBean();
1983+
topic1.aboutReference = site1Ref;
1984+
topic1.title = title1;
1985+
topic1.message = "<strong>Something about topic1</strong>";
1986+
topic1.siteId = site1Id;
1987+
topic1 = saveTopic(topic1);
1988+
1989+
String title2 = "Topic 2";
1990+
TopicTransferBean topic2 = new TopicTransferBean();
1991+
topic2.aboutReference = site1Ref;
1992+
topic2.title = title2;
1993+
topic2.siteId = site1Id;
1994+
topic2 = saveTopic(topic2);
1995+
1996+
String title3 = "Topic 3";
1997+
TopicTransferBean topic3 = new TopicTransferBean();
1998+
topic3.aboutReference = site1Ref;
1999+
topic3.title = title3;
2000+
topic3.siteId = site1Id;
2001+
topic3 = saveTopic(topic3);
2002+
2003+
String title4 = "Topic 4";
2004+
TopicTransferBean topic4 = new TopicTransferBean();
2005+
topic4.aboutReference = site1Ref;
2006+
topic4.title = title4;
2007+
topic4.siteId = site1Id;
2008+
topic4 = saveTopic(topic4);
2009+
2010+
TopicTransferBean[] topicBeans = new TopicTransferBean[] { topic1, topic2, topic3, topic4 };
2011+
2012+
Document doc = Xml.createDocument();
2013+
Stack<Element> stack = new Stack<>();
2014+
2015+
Element root = doc.createElement("archive");
2016+
doc.appendChild(root);
2017+
root.setAttribute("source", site1Id);
2018+
root.setAttribute("xmlns:sakai", ArchiveService.SAKAI_ARCHIVE_NS);
2019+
root.setAttribute("xmlns:CHEF", ArchiveService.SAKAI_ARCHIVE_NS.concat("CHEF"));
2020+
root.setAttribute("xmlns:DAV", ArchiveService.SAKAI_ARCHIVE_NS.concat("DAV"));
2021+
stack.push(root);
2022+
2023+
assertEquals(1, stack.size());
2024+
2025+
String results = conversationsService.archive(site1Id, doc, stack, "", null);
2026+
2027+
assertEquals(2, stack.size());
2028+
2029+
NodeList conversationsNode = root.getElementsByTagName(conversationsService.getLabel());
2030+
assertEquals(1, conversationsNode.getLength());
2031+
2032+
NodeList topicsNode = ((Element) conversationsNode.item(0)).getElementsByTagName("topics");
2033+
assertEquals(1, topicsNode.getLength());
2034+
2035+
NodeList topicNodes = ((Element) topicsNode.item(0)).getElementsByTagName("topic");
2036+
assertEquals(topicBeans.length, topicNodes.getLength());
2037+
2038+
for (int i = 0; i < topicNodes.getLength(); i++) {
2039+
Element topicEl = (Element) topicNodes.item(i);
2040+
assertEquals(topicBeans[i].title, topicEl.getAttribute("title"));
2041+
assertEquals(topicBeans[i].type, topicEl.getAttribute("type"));
2042+
assertEquals(topicBeans[i].anonymous, Boolean.parseBoolean(topicEl.getAttribute("anonymous")));
2043+
assertEquals(topicBeans[i].allowAnonymousPosts, Boolean.parseBoolean(topicEl.getAttribute("allow-anonymous-posts")));
2044+
assertEquals(topicBeans[i].pinned, Boolean.parseBoolean(topicEl.getAttribute("pinned")));
2045+
assertEquals(topicBeans[i].draft, Boolean.parseBoolean(topicEl.getAttribute("draft")));
2046+
assertEquals(topicBeans[i].visibility, topicEl.getAttribute("visibility"));
2047+
assertEquals(topicBeans[i].creator, topicEl.getAttribute("creator"));
2048+
assertEquals(topicBeans[i].created.getEpochSecond(), Long.parseLong(topicEl.getAttribute("created")));
2049+
2050+
NodeList messageNodes = topicEl.getElementsByTagName("message");
2051+
assertEquals(1, messageNodes.getLength());
2052+
2053+
assertEquals(topicBeans[i].message, ((Element) messageNodes.item(0)).getFirstChild().getNodeValue());
2054+
}
2055+
}
2056+
2057+
@Test
2058+
public void merge() {
2059+
2060+
Document doc = Xml.readDocumentFromStream(this.getClass().getResourceAsStream("/archive/conversations.xml"));
2061+
2062+
Element root = doc.getDocumentElement();
2063+
2064+
String fromSite = root.getAttribute("source");
2065+
String toSite = "my-new-site";
2066+
2067+
String toSiteRef = "/site/" + toSite;
2068+
switchToInstructor(toSiteRef);
2069+
2070+
when(siteService.siteReference(toSite)).thenReturn(toSiteRef);
2071+
2072+
Element conversationsElement = doc.createElement("not-conversations");
2073+
2074+
conversationsService.merge(toSite, conversationsElement, "", fromSite, null, null, null);
2075+
2076+
assertEquals("Invalid xml document", conversationsService.merge(toSite, conversationsElement, "", fromSite, null, null, null));
2077+
2078+
conversationsElement = (Element) root.getElementsByTagName(conversationsService.getLabel()).item(0);
2079+
2080+
conversationsService.merge(toSite, conversationsElement, "", fromSite, null, null, null);
2081+
2082+
NodeList topicNodes = ((Element) conversationsElement.getElementsByTagName("topics").item(0)).getElementsByTagName("topic");
2083+
2084+
List<ConversationsTopic> topics = topicRepository.findBySiteId(toSite);
2085+
2086+
assertEquals(topics.size(), topicNodes.getLength());
2087+
2088+
for (int i = 0; i < topicNodes.getLength(); i++) {
2089+
2090+
Element topicEl = (Element) topicNodes.item(i);
2091+
2092+
String title = topicEl.getAttribute("title");
2093+
Optional<ConversationsTopic> optTopic = topics.stream().filter(t -> t.getTitle().equals(title)).findAny();
2094+
assertTrue(optTopic.isPresent());
2095+
2096+
ConversationsTopic topic = optTopic.get();
2097+
2098+
assertEquals(topic.getType().name(), topicEl.getAttribute("type"));
2099+
assertEquals(topic.getPinned(), Boolean.parseBoolean(topicEl.getAttribute("pinned")));
2100+
assertEquals(topic.getAnonymous(), Boolean.parseBoolean(topicEl.getAttribute("anonymous")));
2101+
assertEquals(topic.getDraft(), Boolean.parseBoolean(topicEl.getAttribute("draft")));
2102+
assertEquals(topic.getMustPostBeforeViewing(), Boolean.parseBoolean(topicEl.getAttribute("post-before-viewing")));
2103+
2104+
NodeList messageNodes = topicEl.getElementsByTagName("message");
2105+
assertEquals(1, messageNodes.getLength());
2106+
2107+
assertEquals(topic.getMessage(), messageNodes.item(0).getFirstChild().getNodeValue());
2108+
}
2109+
2110+
Set<String> oldTitles = topics.stream().map(ConversationsTopic::getTitle).collect(Collectors.toSet());
2111+
2112+
// Now let's try and merge this set of rubrics. It has one with a different title, but the
2113+
// rest the same, so we should end up with only one rubric being added.
2114+
Document doc2 = Xml.readDocumentFromStream(this.getClass().getResourceAsStream("/archive/conversations2.xml"));
2115+
2116+
Element root2 = doc2.getDocumentElement();
2117+
2118+
conversationsElement = (Element) root2.getElementsByTagName(conversationsService.getLabel()).item(0);
2119+
2120+
conversationsService.merge(toSite, conversationsElement, "", fromSite, null, null, null);
2121+
2122+
String extraTitle = "Smurfs";
2123+
2124+
assertEquals(topics.size() + 1, topicRepository.findBySiteId(toSite).size());
2125+
2126+
Set<String> newTitles = topicRepository.findBySiteId(toSite)
2127+
.stream().map(ConversationsTopic::getTitle).collect(Collectors.toSet());
2128+
2129+
assertFalse(oldTitles.contains(extraTitle));
2130+
assertTrue(newTitles.contains(extraTitle));
2131+
}
2132+
19712133
private TopicTransferBean saveTopic(TopicTransferBean topicBean) {
19722134

19732135
try {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<?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>
2+
]]></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>
3+
]]></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>
4+
]]></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>
5+
]]></message></topic></topics></conversations></archive>

0 commit comments

Comments
 (0)