From 8a5e56518c192b8257e0e5c9f5957bc5aaa982c7 Mon Sep 17 00:00:00 2001 From: Nick Burch Date: Fri, 12 Aug 2011 17:35:19 +0000 Subject: [PATCH] ALF-9153 Start on the lucene-free Discussions Webscripts git-svn-id: https://svn.alfresco.com/repos/alfresco-enterprise/alfresco/HEAD/root@29729 c4b6b30b-aa2e-2d43-bbcb-ca4b014f7261 --- .../discussions/posts/forum-post.delete.js | 89 ----- .../web-scripts-application-context.xml | 38 ++ .../AbstractDiscussionWebScript.java | 353 ++++++++++++++++++ .../scripts/discussion/ForumPostDelete.java | 117 ++++++ 4 files changed, 508 insertions(+), 89 deletions(-) delete mode 100644 config/alfresco/templates/webscripts/org/alfresco/repository/discussions/posts/forum-post.delete.js create mode 100644 source/java/org/alfresco/repo/web/scripts/discussion/AbstractDiscussionWebScript.java create mode 100644 source/java/org/alfresco/repo/web/scripts/discussion/ForumPostDelete.java diff --git a/config/alfresco/templates/webscripts/org/alfresco/repository/discussions/posts/forum-post.delete.js b/config/alfresco/templates/webscripts/org/alfresco/repository/discussions/posts/forum-post.delete.js deleted file mode 100644 index 95bf8c7bb6..0000000000 --- a/config/alfresco/templates/webscripts/org/alfresco/repository/discussions/posts/forum-post.delete.js +++ /dev/null @@ -1,89 +0,0 @@ - - - -const DELETED_REPLY_POST_MARKER = "[[deleted]]"; - -/** - * Deletes a topic node. - */ -function deleteTopicPost(topicNode) -{ - // fetch the topic info as we need to get the title for the post for the activity - var data = getTopicPostData(topicNode); - var topicTitle = data.post.properties.title; - - // we simply delete the complete topic - var nodeRef = topicNode.nodeRef; - var isDeleted = topicNode.remove(); - if (! isDeleted) - { - status.setCode(status.STATUS_INTERNAL_SERVER_ERROR, "Unable to delete node: " + nodeRef); - return; - } - model.message = "Node " + nodeRef + " deleted"; - - // post an activitiy item, but only if we got a site - if (url.templateArgs["site"] != undefined && args["page"] != undefined) - { - var data = - { - title: topicTitle, - page: decodeURIComponent(args["page"]) - } - activities.postActivity("org.alfresco.discussions.post-deleted", url.templateArgs["site"], "discussions", jsonUtils.toJSONString(data)); - } -} - -/** - * Delete a reply post. - * Note: Because posts are recursive, we can't simply delete the node. - * For now we set a marker text [[delete]] as title and content. - */ -function deleteReplyPost(node) -{ - var title = DELETED_REPLY_POST_MARKER; - var content = DELETED_REPLY_POST_MARKER; - - // update the topic title - node.properties.title = title; - node.mimetype = "text/html"; - node.content = content; - node.save(); - - model.message = "Node " + node.nodeRef + " marked as removed"; -} - -/** - * Deletes a post. - * Note: this function also deletes all replies of the post - */ -function deletePost(node) -{ - // simple case: topic post - if (node.type == "{http://www.alfresco.org/model/forum/1.0}topic") - { - deleteTopicPost(node); - } - else if (node.type == "{http://www.alfresco.org/model/forum/1.0}post") - { - deleteReplyPost(node); - } - else - { - status.setCode(status.STATUS_BAD_REQUEST, "Node is not of type fm:topic or fm:post"); - } -} - -function main() -{ - // get requested node - var node = getRequestNode(); - if (status.getCode() != status.STATUS_OK) - { - return; - } - - deletePost(node); -} - -main(); diff --git a/config/alfresco/web-scripts-application-context.xml b/config/alfresco/web-scripts-application-context.xml index d4414c8ae7..24dbd6f6ca 100644 --- a/config/alfresco/web-scripts-application-context.xml +++ b/config/alfresco/web-scripts-application-context.xml @@ -1555,6 +1555,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/source/java/org/alfresco/repo/web/scripts/discussion/AbstractDiscussionWebScript.java b/source/java/org/alfresco/repo/web/scripts/discussion/AbstractDiscussionWebScript.java new file mode 100644 index 0000000000..109890df2f --- /dev/null +++ b/source/java/org/alfresco/repo/web/scripts/discussion/AbstractDiscussionWebScript.java @@ -0,0 +1,353 @@ +/* + * Copyright (C) 2005-2011 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.repo.web.scripts.discussion; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +import org.alfresco.query.PagingRequest; +import org.alfresco.repo.content.MimetypeMap; +import org.alfresco.service.cmr.activities.ActivityService; +import org.alfresco.service.cmr.discussion.DiscussionService; +import org.alfresco.service.cmr.discussion.PostInfo; +import org.alfresco.service.cmr.discussion.TopicInfo; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.repository.StoreRef; +import org.alfresco.service.cmr.security.PersonService; +import org.alfresco.service.cmr.site.SiteInfo; +import org.alfresco.service.cmr.site.SiteService; +import org.alfresco.util.Pair; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.json.JSONException; +import org.json.JSONObject; +import org.json.JSONTokener; +import org.springframework.extensions.webscripts.Cache; +import org.springframework.extensions.webscripts.DeclarativeWebScript; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptException; +import org.springframework.extensions.webscripts.WebScriptRequest; + +/** + * @author Nick Burch + * @since 4.0 + */ +public abstract class AbstractDiscussionWebScript extends DeclarativeWebScript +{ + public static final String DISCUSSIONS_SERVICE_ACTIVITY_APP_NAME = "discussions"; + + /** + * When no maximum or paging info is given, what should we use? + */ + protected static final int MAX_QUERY_ENTRY_COUNT = 1000; + + private static Log logger = LogFactory.getLog(AbstractDiscussionWebScript.class); + + // Injected services + protected NodeService nodeService; + protected SiteService siteService; + protected PersonService personService; + protected ActivityService activityService; + protected DiscussionService discussionService; + + public void setNodeService(NodeService nodeService) + { + this.nodeService = nodeService; + } + + public void setSiteService(SiteService siteService) + { + this.siteService = siteService; + } + + public void setDiscussionService(DiscussionService discussionService) + { + this.discussionService = discussionService; + } + + public void setPersonService(PersonService personService) + { + this.personService = personService; + } + + public void setActivityService(ActivityService activityService) + { + this.activityService = activityService; + } + + + protected String getOrNull(JSONObject json, String key) throws JSONException + { + if(json.has(key)) + { + return json.getString(key); + } + return null; + } + + /** + * Builds up a listing Paging request, based on the arguments + * specified in the URL + */ + protected PagingRequest buildPagingRequest(WebScriptRequest req) + { + int pageSize = MAX_QUERY_ENTRY_COUNT; + int startIndex = 0; + + String pageSizeS = req.getParameter("pageSize"); + if(pageSizeS != null) + { + try + { + pageSize = Integer.parseInt(pageSizeS); + } + catch(NumberFormatException e) + { + throw new WebScriptException(Status.STATUS_BAD_REQUEST, "Paging size parameters invalid"); + } + } + + String startIndexS = req.getParameter("startIndex"); + if(startIndexS != null) + { + try + { + startIndex = Integer.parseInt(startIndexS); + } + catch(NumberFormatException e) + { + throw new WebScriptException(Status.STATUS_BAD_REQUEST, "Paging size parameters invalid"); + } + } + + PagingRequest paging = new PagingRequest( startIndex, pageSize ); + paging.setRequestTotalCountMax( Math.max(10,startIndex+1) * pageSize ); + return paging; + } + + /** + * Generates an activity entry for the discussion item + * + * @param thing Either post or reply + * @param event One of created, updated, deleted + */ + protected void addActivityEntry(String thing, String event, TopicInfo topic, + PostInfo post, SiteInfo site, WebScriptRequest req, JSONObject json) + { + // What page is this for? + String page = req.getParameter("page"); + if(page == null && json != null) + { + if(json.has("page")) + { + try + { + page = json.getString("page"); + } + catch(JSONException e) {} + } + } + if(page == null) + { + // Default + page = "discussions-topicview"; + } + + // Get the title + String title = topic.getTitle(); + if(post != null) + { + String postTitle = post.getTitle(); + if(postTitle != null && postTitle.length() > 0) + { + title = postTitle; + } + } + + try + { + JSONObject params = new JSONObject(); + params.put("topicId", topic.getSystemName()); + + JSONObject activity = new JSONObject(); + activity.put("title", title); + activity.put("page", page + "?topicId=" + topic.getSystemName()); + activity.put("params", params); + + activityService.postActivity( + "org.alfresco.discussions." + thing + "-" + event, + site.getShortName(), + DISCUSSIONS_SERVICE_ACTIVITY_APP_NAME, + activity.toString() + ); + } + catch(Exception e) + { + // Warn, but carry on + logger.warn("Error adding discussions " + thing + " " + event + " to activities feed", e); + } + } + + // TODO Is this needed? + protected Object buildPerson(String username) + { + if(username == null || username.length() == 0) + { + // Empty string needed + return ""; + } + + // Will turn into a Script Node needed of the person + NodeRef person = personService.getPerson(username); + return person; + } + + // TODO Match JS + protected Map renderTopic(TopicInfo topic) + { + Map res = new HashMap(); + res.put("topic", topic); + res.put("node", topic.getNodeRef()); + res.put("name", topic.getSystemName()); + res.put("title", topic.getTitle()); + res.put("tags", topic.getTags()); + + // Both forms used for dates + res.put("createdOn", topic.getCreatedAt()); + res.put("modifiedOn", topic.getModifiedAt()); + res.put("created", topic.getCreatedAt()); + res.put("modified", topic.getModifiedAt()); + + // FTL needs a script node of the people + res.put("createdBy", buildPerson(topic.getCreator())); + res.put("modifiedBY", buildPerson(topic.getModifier())); + + // We want blank instead of null + for(String key : res.keySet()) + { + if(res.get(key) == null) + { + res.put(key, ""); + } + } + + return res; + } + + @Override + protected Map executeImpl(WebScriptRequest req, + Status status, Cache cache) + { + Map templateVars = req.getServiceMatch().getTemplateVars(); + if(templateVars == null) + { + String error = "No parameters supplied"; + throw new WebScriptException(Status.STATUS_BAD_REQUEST, error); + } + + + // Parse the JSON, if supplied + JSONObject json = null; + String contentType = req.getContentType(); + if(contentType != null && contentType.indexOf(';') != -1) + { + contentType = contentType.substring(0, contentType.indexOf(';')); + } + if(MimetypeMap.MIMETYPE_JSON.equals(contentType)) + { + try + { + json = new JSONObject(new JSONTokener(req.getContent().getContent())); + } + catch(IOException io) + { + throw new WebScriptException(Status.STATUS_BAD_REQUEST, "Invalid JSON: " + io.getMessage()); + } + catch(JSONException je) + { + throw new WebScriptException(Status.STATUS_BAD_REQUEST, "Invalid JSON: " + je.getMessage()); + } + } + + + // Did they request it by node reference or site? + NodeRef nodeRef = null; + SiteInfo site = null; + TopicInfo topic = null; + PostInfo post = null; + + if(templateVars.containsKey("site")) + { + // Site, and optionally topic + String siteName = templateVars.get("site"); + site = siteService.getSite(siteName); + if(site == null) + { + String error = "Could not find site: " + siteName; + throw new WebScriptException(Status.STATUS_NOT_FOUND, error); + } + + // Did they give a topic name too? + if(templateVars.containsKey("path")) + { + String name = templateVars.get("path"); + topic = discussionService.getTopic(site.getShortName(), name); + } + } + else if(templateVars.containsKey("store_type") && + templateVars.containsKey("store_id") && + templateVars.containsKey("id")) + { + // NodeRef, normally Topic or Discussion + StoreRef store = new StoreRef( + templateVars.get("store_type"), + templateVars.get("store_id") + ); + nodeRef = new NodeRef(store, templateVars.get("id")); + if(! nodeService.exists(nodeRef)) + { + String error = "Could not find node: " + nodeRef; + throw new WebScriptException(Status.STATUS_NOT_FOUND, error); + } + + // Try to build the appropriate object for it + Pair objects = discussionService.getForNodeRef(nodeRef); + if(objects != null) + { + topic = objects.getFirst(); + post = objects.getSecond(); + } + } + else + { + String error = "Unsupported template parameters found"; + throw new WebScriptException(Status.STATUS_BAD_REQUEST, error); + } + + // Have the real work done + return executeImpl(site, nodeRef, topic, post, req, json, status, cache); + } + + protected abstract Map executeImpl(SiteInfo site, + NodeRef nodeRef, TopicInfo topic, PostInfo post, + WebScriptRequest req, JSONObject json, Status status, Cache cache); + +} diff --git a/source/java/org/alfresco/repo/web/scripts/discussion/ForumPostDelete.java b/source/java/org/alfresco/repo/web/scripts/discussion/ForumPostDelete.java new file mode 100644 index 0000000000..d8783f419a --- /dev/null +++ b/source/java/org/alfresco/repo/web/scripts/discussion/ForumPostDelete.java @@ -0,0 +1,117 @@ +/* + * Copyright (C) 2005-2011 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.repo.web.scripts.discussion; + +import java.util.HashMap; +import java.util.Map; + +import org.alfresco.service.cmr.discussion.PostInfo; +import org.alfresco.service.cmr.discussion.TopicInfo; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.site.SiteInfo; +import org.json.JSONObject; +import org.springframework.extensions.webscripts.Cache; +import org.springframework.extensions.webscripts.Status; +import org.springframework.extensions.webscripts.WebScriptException; +import org.springframework.extensions.webscripts.WebScriptRequest; + +/** + * This class is the controller for the forum post deleting forum-post.delete webscript. + * + * @author Nick Burch + * @since 4.0 + */ +public class ForumPostDelete extends AbstractDiscussionWebScript +{ + private static final String DELETED_POST_TEXT = "[[deleted]]"; + + @Override + protected Map executeImpl(SiteInfo site, NodeRef nodeRef, + TopicInfo topic, PostInfo post, WebScriptRequest req, JSONObject json, + Status status, Cache cache) + { + // Build the common model parts + Map model = new HashMap(); + model.put("topic", topic); + model.put("post", post); + if(site != null) + { + model.put("siteId", site.getShortName()); + model.put("site", site); + } + + + // Are we deleting a topic, or a post in it? + String message = null; + if(post != null) + { + message = doDeletePost(topic, post); + } + else if(topic != null) + { + message = doDeleteTopic(topic, site, req, json); + } + else + { + String error = "Node was of the wrong type, only Topic and Post are supported"; + throw new WebScriptException(Status.STATUS_BAD_REQUEST, error); + } + + + // Finish the model and return + model.put("message", message); + return model; + } + + private String doDeleteTopic(TopicInfo topic, SiteInfo site, + WebScriptRequest req, JSONObject json) + { + // Delete the topic, which removes all its posts too + discussionService.deleteTopic(topic); + + // Add an activity entry for this if it's site based + if(site != null) + { + addActivityEntry("post", "deleted", topic, null, site, req, json); + } + + // All done + return "Node " + topic.getNodeRef() + " deleted"; + } + + /** + * We can't just delete posts with replies attached to them, + * as that breaks the reply threading. + * For that reason, we mark deleted posts with a special + * text contents. + * TODO If a post has no replies, then delete it fully + */ + private String doDeletePost(TopicInfo topic, PostInfo post) + { + // Set the marker text and save + post.setTitle(DELETED_POST_TEXT); + post.setContents(DELETED_POST_TEXT); + discussionService.updatePost(post); + + // Note - we don't add activity feed entries for deleted posts + // Only deleted whole topic qualify for that at the moment + + return "Node " + post.getNodeRef() + " marked as removed"; + } +}