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?

+]]>