diff --git a/l10n.properties b/l10n.properties
index 0cf8bc0491..5ee676302f 100644
--- a/l10n.properties
+++ b/l10n.properties
@@ -1,6 +1,8 @@
# Branch specific configuration file for localisation scripts
-MESSAGE_SEARCH_PATH="src/main/resources/alfresco/messages/admin-console*.properties src/main/resources/alfresco/messages/custommodel-restapi-messages*.properties src/main/resources/alfresco/messages/rest-framework-messages*.properties src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/admin/admin-communitysummary.get*.properties src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/admin/consoles/admin-repoconsole.get*.properties src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/admin/consoles/admin-tenantconsole.get*.properties src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/admin/consoles/admin-workflowconsole.get*.properties src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/admin/support-tools/admin-nodebrowser.get*.properties src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/audit/entry*.properties src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/links/links-delete.post*.properties src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/links/links.post*.properties src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/links/links.put*.properties src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/person/user-csv-upload.post*.properties src/main/resources/alfresco/templates/webscripts/org/alfresco/slingshot/calendar/event.get*.properties src/main/resources/alfresco/templates/webscripts/org/alfresco/slingshot/calendar/event.post*.properties src/main/resources/alfresco/templates/webscripts/org/alfresco/slingshot/calendar/event.put*.properties"
+
+MESSAGE_SEARCH_PATH="src/main/resources/alfresco/messages/admin-console*.properties src/main/resources/alfresco/messages/custommodel-restapi-messages*.properties src/main/resources/alfresco/messages/rest-framework-messages*.properties src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/admin/admin-communitysummary.get*.properties src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/admin/consoles/admin-repoconsole.get*.properties src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/admin/consoles/admin-tenantconsole.get*.properties src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/admin/consoles/admin-workflowconsole.get*.properties src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/admin/support-tools/admin-nodebrowser.get*.properties src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/audit/entry*.properties src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/blogs/post/blog-post.delete*.properties src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/discussions/posts/forum-post.delete*.properties src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/links/links-delete.post*.properties src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/links/links.post*.properties src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/links/links.put*.properties src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/person/user-csv-upload.post*.properties src/main/resources/alfresco/templates/webscripts/org/alfresco/slingshot/calendar/event.get*.properties src/main/resources/alfresco/templates/webscripts/org/alfresco/slingshot/calendar/event.post*.properties src/main/resources/alfresco/templates/webscripts/org/alfresco/slingshot/calendar/event.put*.properties"
+
EXCLUDED_FILES="src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/audit/control.properties"
diff --git a/pom.xml b/pom.xml
index 25c2c26fce..95d458a382 100644
--- a/pom.xml
+++ b/pom.xml
@@ -37,7 +37,7 @@
${project.build.directory}/alf_data
convert
- 7.125
+ 7.127
8.50.1
7.21
diff --git a/src/main/java/org/alfresco/repo/web/scripts/blogs/AbstractBlogWebScript.java b/src/main/java/org/alfresco/repo/web/scripts/blogs/AbstractBlogWebScript.java
new file mode 100644
index 0000000000..fa0e0d157a
--- /dev/null
+++ b/src/main/java/org/alfresco/repo/web/scripts/blogs/AbstractBlogWebScript.java
@@ -0,0 +1,311 @@
+/*
+ * #%L
+ * Alfresco Remote API
+ * %%
+ * Copyright (C) 2005 - 2016 Alfresco Software Limited
+ * %%
+ * This file is part of the Alfresco software.
+ * If the software was purchased under a paid Alfresco license, the terms of
+ * the paid license agreement will prevail. Otherwise, the software is
+ * provided under the following open source license terms:
+ *
+ * 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 .
+ * #L%
+ */
+package org.alfresco.repo.web.scripts.blogs;
+
+import java.io.IOException;
+import java.util.Map;
+
+import org.alfresco.repo.activities.post.lookup.PostLookup;
+import org.alfresco.repo.blog.BlogServiceImpl;
+import org.alfresco.repo.content.MimetypeMap;
+import org.alfresco.repo.model.Repository;
+import org.alfresco.service.ServiceRegistry;
+import org.alfresco.service.cmr.activities.ActivityService;
+import org.alfresco.service.cmr.blog.BlogPostInfo;
+import org.alfresco.service.cmr.blog.BlogService;
+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.site.SiteInfo;
+import org.alfresco.service.cmr.site.SiteService;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.json.JSONStringer;
+import org.json.JSONWriter;
+import org.json.simple.JSONObject;
+import org.json.simple.parser.JSONParser;
+import org.json.simple.parser.ParseException;
+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 Neil Mc Erlean
+ * @since 4.0
+ */
+public abstract class AbstractBlogWebScript extends DeclarativeWebScript
+{
+ // Various common parameter strings in the blog webscripts.
+ protected static final String CONTAINER = "container";
+ protected static final String CONTENT = "content";
+ protected static final String DATA = "data";
+ protected static final String DRAFT = "draft";
+ protected static final String EXTERNAL_BLOG_CONFIG = "externalBlogConfig";
+ protected static final String POST = "post";
+ protected static final String ITEM = "item";
+ protected static final String NODE = "node";
+ protected static final String PAGE = "page";
+ protected static final String SITE = "site";
+ protected static final String TAGS = "tags";
+ protected static final String TITLE = "title";
+
+ private static Log logger = LogFactory.getLog(AbstractBlogWebScript.class);
+
+ // Injected services
+ protected Repository repository;
+ protected BlogService blogService;
+ protected NodeService nodeService;
+ protected SiteService siteService;
+ protected ActivityService activityService;
+
+ //TODO Remove this after full refactor
+ protected ServiceRegistry services;
+
+ public void setServiceRegistry(ServiceRegistry services)
+ {
+ this.services = services;
+ }
+
+ public void setRepository(Repository repository)
+ {
+ this.repository = repository;
+ }
+
+ public void setBlogService(BlogService blogService)
+ {
+ this.blogService = blogService;
+ }
+
+ public void setNodeService(NodeService nodeService)
+ {
+ this.nodeService = nodeService;
+ }
+
+ public void setSiteService(SiteService siteService)
+ {
+ this.siteService = siteService;
+ }
+
+ public void setActivityService(ActivityService activityService)
+ {
+ this.activityService = activityService;
+ }
+
+ /**
+ * Generates an activity entry for the discussion item
+ *
+ * @param event One of created, updated, deleted
+ * @param blog Either post or reply
+ * @param site site
+ * @param req request
+ * @param json json
+ * @param nodeRef NodeRef
+ */
+ protected void addActivityEntry(String event, BlogPostInfo blog,
+ SiteInfo site, WebScriptRequest req, JSONObject json, NodeRef nodeRef)
+ {
+ // We can only add activities against a site
+ if (site == null)
+ {
+ logger.info("Unable to add activity entry for blog " + event + " as no site given");
+ return;
+ }
+
+ // What page is this for?
+ String page = req.getParameter("page");
+ if (page == null && json != null)
+ {
+ if (json.containsKey("page"))
+ {
+ page = (String)json.get("page");
+ }
+ }
+ if (page == null)
+ {
+ // Default
+ page = "blog-postview";
+ }
+ if (page.indexOf('?') == -1)
+ {
+ page += "?postId=" + blog.getSystemName();
+ }
+
+ // Get the title
+ String title = blog.getTitle();
+
+ try
+ {
+ JSONWriter jsonWriter = new JSONStringer()
+ .object()
+ .key(TITLE).value(title)
+ .key(PAGE).value(page);
+
+ if (nodeRef != null)
+ {
+ // ALF-10182: the nodeRef needs to be included in the activity
+ // post to ensure read permissions are respected.
+ jsonWriter.key(PostLookup.JSON_NODEREF).value(nodeRef.toString());
+ }
+
+ String data = jsonWriter.endObject().toString();
+
+ activityService.postActivity(
+ "org.alfresco.blog.post-" + event,
+ site.getShortName(),
+ "blog", data);
+ }
+ catch(Exception e)
+ {
+ // Warn, but carry on
+ logger.warn("Error adding blog post " + event + " to activities feed", e);
+ }
+ }
+
+ @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))
+ {
+ JSONParser parser = new JSONParser();
+ try
+ {
+ json = (JSONObject)parser.parse(req.getContent().getContent());
+ }
+ catch (IOException io)
+ {
+ throw new WebScriptException(Status.STATUS_BAD_REQUEST, "Invalid JSON: " + io.getMessage());
+ }
+ catch (ParseException pe)
+ {
+ throw new WebScriptException(Status.STATUS_BAD_REQUEST, "Invalid JSON: " + pe.getMessage());
+ }
+ }
+
+
+ // Did they request it by node reference or site?
+ NodeRef nodeRef = null;
+ SiteInfo site = null;
+ BlogPostInfo blog = null;
+
+ if (templateVars.containsKey("site"))
+ {
+ // Site, and Optionally Blog Post
+ 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 blog post name too?
+ if (templateVars.containsKey("path"))
+ {
+ String name = templateVars.get("path");
+ blog = blogService.getBlogPost(siteName, name);
+
+ if (blog == null)
+ {
+ String error = "Could not find blog '" + name + "' for site '" +
+ site.getShortName() + "'";
+ throw new WebScriptException(Status.STATUS_NOT_FOUND, error);
+ }
+ nodeRef = blog.getNodeRef();
+ }
+ else
+ {
+ // The NodeRef is the container (if it exists)
+ if (siteService.hasContainer(siteName, BlogServiceImpl.BLOG_COMPONENT))
+ {
+ nodeRef = siteService.getContainer(siteName, BlogServiceImpl.BLOG_COMPONENT);
+ }
+ }
+ }
+ else if (templateVars.containsKey("store_type") &&
+ templateVars.containsKey("store_id") &&
+ templateVars.containsKey("id"))
+ {
+ // NodeRef, should be a Blog Post
+ 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
+ blog = blogService.getForNodeRef(nodeRef);
+
+ // See if it's actually attached to a site
+ if (blog != null)
+ {
+ NodeRef container = blog.getContainerNodeRef();
+ if (container != null)
+ {
+ NodeRef maybeSite = nodeService.getPrimaryParent(container).getParentRef();
+ if (maybeSite != null)
+ {
+ // Try to make it a site, will return Null if it isn't one
+ site = siteService.getSite(maybeSite);
+ }
+ }
+ }
+ }
+ else
+ {
+ String error = "Unsupported template parameters found";
+ throw new WebScriptException(Status.STATUS_BAD_REQUEST, error);
+ }
+
+ // Have the real work done
+ return executeImpl(site, nodeRef, blog, req, json, status, cache);
+ }
+
+ protected abstract Map executeImpl(SiteInfo site,
+ NodeRef nodeRef, BlogPostInfo blog, WebScriptRequest req,
+ JSONObject json, Status status, Cache cache);
+}
diff --git a/src/main/java/org/alfresco/repo/web/scripts/blogs/BlogLibJs.java b/src/main/java/org/alfresco/repo/web/scripts/blogs/BlogLibJs.java
new file mode 100644
index 0000000000..4759c7fce2
--- /dev/null
+++ b/src/main/java/org/alfresco/repo/web/scripts/blogs/BlogLibJs.java
@@ -0,0 +1,71 @@
+/*
+ * #%L
+ * Alfresco Remote API
+ * %%
+ * Copyright (C) 2005 - 2016 Alfresco Software Limited
+ * %%
+ * This file is part of the Alfresco software.
+ * If the software was purchased under a paid Alfresco license, the terms of
+ * the paid license agreement will prevail. Otherwise, the software is
+ * provided under the following open source license terms:
+ *
+ * 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 .
+ * #L%
+ */
+package org.alfresco.repo.web.scripts.blogs;
+
+import java.io.Serializable;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.alfresco.model.BlogIntegrationModel;
+import org.alfresco.service.namespace.QName;
+import org.json.simple.JSONObject;
+
+/**
+ * This class is a port of a previous JavaScript library.
+ *
+ * @author Neil Mc Erlean (based on previous JavaScript)
+ * @since 4.0
+ * @deprecated This class should not be extended or reused as it may be refactored.
+ */
+public class BlogLibJs
+{
+ /**
+ * Fetches the blog properties from the json object and adds them to an array
+ * using the correct property names as indexes.
+ */
+ public static Map getBlogPropertiesArray(JSONObject json)
+ {
+ Map arr = new HashMap();
+
+ putJSONEntryInMap(json, arr, "blogType", BlogIntegrationModel.PROP_BLOG_IMPLEMENTATION);
+ putJSONEntryInMap(json, arr, "blogId", BlogIntegrationModel.PROP_ID);
+ putJSONEntryInMap(json, arr, "blogName", BlogIntegrationModel.PROP_NAME);
+ putJSONEntryInMap(json, arr, "blogDescription", BlogIntegrationModel.PROP_DESCRIPTION);
+ putJSONEntryInMap(json, arr, "blogUrl", BlogIntegrationModel.PROP_URL);
+ putJSONEntryInMap(json, arr, "username", BlogIntegrationModel.PROP_USER_NAME);
+ putJSONEntryInMap(json, arr, "password", BlogIntegrationModel.PROP_PASSWORD);
+ return arr;
+ }
+
+ private static void putJSONEntryInMap(JSONObject json,
+ Map arr, String jsonKey, QName mapKey)
+ {
+ if (json.containsKey(jsonKey))
+ {
+ arr.put(mapKey, (Serializable)json.get(jsonKey));
+ }
+ }
+}
diff --git a/src/main/java/org/alfresco/repo/web/scripts/blogs/BlogPostLibJs.java b/src/main/java/org/alfresco/repo/web/scripts/blogs/BlogPostLibJs.java
new file mode 100644
index 0000000000..7e818eeee4
--- /dev/null
+++ b/src/main/java/org/alfresco/repo/web/scripts/blogs/BlogPostLibJs.java
@@ -0,0 +1,132 @@
+/*
+ * #%L
+ * Alfresco Remote API
+ * %%
+ * Copyright (C) 2005 - 2016 Alfresco Software Limited
+ * %%
+ * This file is part of the Alfresco software.
+ * If the software was purchased under a paid Alfresco license, the terms of
+ * the paid license agreement will prevail. Otherwise, the software is
+ * provided under the following open source license terms:
+ *
+ * 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 .
+ * #L%
+ */
+package org.alfresco.repo.web.scripts.blogs;
+
+import java.io.Serializable;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.alfresco.model.BlogIntegrationModel;
+import org.alfresco.model.ContentModel;
+import org.alfresco.service.ServiceRegistry;
+import org.alfresco.service.cmr.repository.NodeRef;
+
+/**
+ * This class is a port of a previous JavaScript library.
+ *
+ * @author Neil Mc Erlean (based on previous JavaScript)
+ * @since 4.0
+ */
+public class BlogPostLibJs
+{
+ //FIXME It will be refactored when the other services are ported from JavaScript to Java.
+
+ /**
+ * Checks whether a blog configuration is available
+ * This should at some point also check whether the configuration is enabled.
+ *
+ * @param node the node that should be checked. Will check all parents if
+ * the node itself doesn't contain a configuration.
+ * @return {boolean} whether a configuration could be found.
+ */
+ public static boolean hasExternalBlogConfiguration(NodeRef node, ServiceRegistry services)
+ {
+ if (node == null)
+ {
+ return false;
+ }
+ else if (services.getNodeService().hasAspect(node, BlogIntegrationModel.ASPECT_BLOG_DETAILS))
+ {
+ return true;
+ }
+ else
+ {
+ return hasExternalBlogConfiguration(services.getNodeService().getPrimaryParent(node).getParentRef(), services);
+ }
+ }
+
+ public static Map getBlogPostData(NodeRef node, ServiceRegistry services)
+ {
+ Map data = new HashMap();
+ data.put("node", node);
+ String creator = (String)services.getNodeService().getProperty(node, ContentModel.PROP_CREATOR);
+ //ALF-18527
+ NodeRef person = services.getPersonService().getPersonOrNull(creator);
+ if (person != null)
+ {
+ data.put("author", person);
+ }
+
+ data.put("commentCount", CommentsLibJs.getCommentsCount(node, services));
+
+ // is the post published
+ Serializable published = services.getNodeService().getProperty(node, ContentModel.PROP_PUBLISHED);
+ boolean isPublished = published != null;
+ if (isPublished)
+ {
+ data.put("releasedDate", published);
+ }
+
+ // draft
+ data.put("isDraft", !isPublished);
+
+ // set the isUpdated flag
+ Date updatedDate = (Date) services.getNodeService().getProperty(node, ContentModel.PROP_UPDATED);
+ boolean isUpdated = updatedDate != null;
+ data.put("isUpdated", isUpdated);
+ if (isUpdated)
+ {
+ data.put("updatedDate", updatedDate);
+ }
+
+ // fetch standard created/modified dates
+ data.put("createdDate", services.getNodeService().getProperty(node, ContentModel.PROP_CREATED));
+ data.put("modifiedDate", services.getNodeService().getProperty(node, ContentModel.PROP_MODIFIED));
+
+ // does the external post require an update?
+ Date lastUpdate = (Date) services.getNodeService().getProperty(node, BlogIntegrationModel.PROP_LAST_UPDATE);
+ if (isPublished && lastUpdate != null)
+ {
+ // we either use the release or updated date
+ Date modifiedDate = (Date) data.get("releasedDate");
+
+ if (isUpdated)
+ {
+ modifiedDate = (Date) data.get("updatedDate");
+ }
+ data.put("outOfDate", modifiedDate.getTime() - lastUpdate.getTime() > 5000L);
+ }
+ else
+ {
+ data.put("outOfDate", false);
+ }
+
+ data.put("tags", services.getTaggingService().getTags(node));
+
+ return data;
+ }
+}
diff --git a/src/main/java/org/alfresco/repo/web/scripts/blogs/CommentsLibJs.java b/src/main/java/org/alfresco/repo/web/scripts/blogs/CommentsLibJs.java
new file mode 100644
index 0000000000..1da8eea028
--- /dev/null
+++ b/src/main/java/org/alfresco/repo/web/scripts/blogs/CommentsLibJs.java
@@ -0,0 +1,99 @@
+/*
+ * #%L
+ * Alfresco Remote API
+ * %%
+ * Copyright (C) 2005 - 2016 Alfresco Software Limited
+ * %%
+ * This file is part of the Alfresco software.
+ * If the software was purchased under a paid Alfresco license, the terms of
+ * the paid license agreement will prevail. Otherwise, the software is
+ * provided under the following open source license terms:
+ *
+ * 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 .
+ * #L%
+ */
+package org.alfresco.repo.web.scripts.blogs;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.alfresco.model.ContentModel;
+import org.alfresco.model.ForumModel;
+import org.alfresco.service.ServiceRegistry;
+import org.alfresco.service.cmr.repository.ChildAssociationRef;
+import org.alfresco.service.cmr.repository.NodeRef;
+import org.alfresco.service.namespace.NamespaceService;
+import org.alfresco.service.namespace.QName;
+import org.alfresco.service.namespace.RegexQNamePattern;
+
+/**
+ * This class is a port of a previous JavaScript library used by the blog webscript containers.
+ *
+ * @author Neil Mc Erlean (based on existing JavaScript code)
+ * @since 4.0
+ */
+class CommentsLibJs
+{
+ // TODO It will likely be refactored into the Blog REST API class framework.
+
+ private static final String COMMENTS_TOPIC_NAME = "Comments";
+
+ public static int getCommentsCount(NodeRef node, ServiceRegistry services)
+ {
+ return getComments(node, services).size();
+ }
+
+ /**
+ * Returns all comment nodes for a given node.
+ * @return an array of comments.
+ */
+ public static List getComments(NodeRef node, ServiceRegistry services)
+ {
+ List result = new ArrayList();
+
+ NodeRef commentsFolder = getCommentsFolder(node, services);
+ if (commentsFolder != null)
+ {
+ List children = services.getNodeService().getChildAssocs(commentsFolder, ContentModel.ASSOC_CONTAINS, RegexQNamePattern.MATCH_ALL);
+ if (!children.isEmpty())
+ {
+ result = children;
+ }
+ }
+
+ return result;
+ }
+
+ /**
+ * Returns the folder that contains all the comments.
+ *
+ * We currently use the fm:discussable aspect where we
+ * add a "Comments" topic to it.
+ */
+ public static NodeRef getCommentsFolder(NodeRef node, ServiceRegistry services)
+ {
+ //FIXME These methods are from the original JavaScript. Should use the (soon to arrive) CommentService.
+ NodeRef result = null;
+ if (services.getNodeService().hasAspect(node, ForumModel.ASPECT_DISCUSSABLE))
+ {
+ List forumFolders = services.getNodeService().getChildAssocs(node, ForumModel.ASSOC_DISCUSSION, RegexQNamePattern.MATCH_ALL);
+ // The JavaScript was retrieving the first child under this child-assoc so we'll do the same.
+ NodeRef forumFolder = forumFolders.get(0).getChildRef();
+
+ List topicFolder = services.getNodeService().getChildAssocs(forumFolder, ContentModel.ASSOC_CONTAINS, QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, COMMENTS_TOPIC_NAME));
+ result = topicFolder.isEmpty() ? null : topicFolder.get(0).getChildRef();
+ }
+ return result;
+ }
+}
diff --git a/src/main/java/org/alfresco/repo/web/scripts/blogs/blog/BlogGet.java b/src/main/java/org/alfresco/repo/web/scripts/blogs/blog/BlogGet.java
new file mode 100644
index 0000000000..6cf3bd9732
--- /dev/null
+++ b/src/main/java/org/alfresco/repo/web/scripts/blogs/blog/BlogGet.java
@@ -0,0 +1,81 @@
+/*
+ * #%L
+ * Alfresco Remote API
+ * %%
+ * Copyright (C) 2005 - 2016 Alfresco Software Limited
+ * %%
+ * This file is part of the Alfresco software.
+ * If the software was purchased under a paid Alfresco license, the terms of
+ * the paid license agreement will prevail. Otherwise, the software is
+ * provided under the following open source license terms:
+ *
+ * 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 .
+ * #L%
+ */
+package org.alfresco.repo.web.scripts.blogs.blog;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.alfresco.repo.web.scripts.blogs.AbstractBlogWebScript;
+import org.alfresco.service.cmr.blog.BlogPostInfo;
+import org.alfresco.service.cmr.repository.NodeRef;
+import org.alfresco.service.cmr.site.SiteInfo;
+import org.json.simple.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 blog.get web script.
+ *
+ * @author Neil Mc Erlean (based on existing JavaScript webscript controllers)
+ * @since 4.0
+ */
+public class BlogGet extends AbstractBlogWebScript
+{
+ @Override
+ protected Map executeImpl(SiteInfo site, NodeRef containerNodeRef,
+ BlogPostInfo blog, WebScriptRequest req, JSONObject json, Status status, Cache cache)
+ {
+ if (blog != null)
+ {
+ // They appear to have supplied a blog post itself...
+ // Oh well, let's hope for the best!
+ }
+
+ if (containerNodeRef == null && site != null)
+ {
+ // They want to know about a blog container on a
+ // site where nothing has lazy-created the container
+ // Give them info on the site for now, should be close enough!
+ containerNodeRef = site.getNodeRef();
+ }
+
+ if (containerNodeRef == null)
+ {
+ // Looks like they've asked for something that isn't there
+ throw new WebScriptException(Status.STATUS_NOT_FOUND, "Blog Not Found");
+ }
+
+ // Build the response
+ // (For now, we just supply the noderef, but when we have a
+ // proper blog details object we'll use that)
+ Map model = new HashMap();
+ model.put(ITEM, containerNodeRef);
+
+ return model;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/org/alfresco/repo/web/scripts/blogs/blog/BlogPut.java b/src/main/java/org/alfresco/repo/web/scripts/blogs/blog/BlogPut.java
new file mode 100644
index 0000000000..e3a01ebd2b
--- /dev/null
+++ b/src/main/java/org/alfresco/repo/web/scripts/blogs/blog/BlogPut.java
@@ -0,0 +1,104 @@
+/*
+ * #%L
+ * Alfresco Remote API
+ * %%
+ * Copyright (C) 2005 - 2016 Alfresco Software Limited
+ * %%
+ * This file is part of the Alfresco software.
+ * If the software was purchased under a paid Alfresco license, the terms of
+ * the paid license agreement will prevail. Otherwise, the software is
+ * provided under the following open source license terms:
+ *
+ * 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 .
+ * #L%
+ */
+package org.alfresco.repo.web.scripts.blogs.blog;
+
+import java.io.Serializable;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.alfresco.model.BlogIntegrationModel;
+import org.alfresco.repo.blog.BlogServiceImpl;
+import org.alfresco.repo.web.scripts.blogs.AbstractBlogWebScript;
+import org.alfresco.repo.web.scripts.blogs.BlogLibJs;
+import org.alfresco.service.cmr.blog.BlogPostInfo;
+import org.alfresco.service.cmr.repository.NodeRef;
+import org.alfresco.service.cmr.site.SiteInfo;
+import org.alfresco.service.namespace.QName;
+import org.json.simple.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 blog.put web script.
+ *
+ * TODO Push most of the logic from this into the BlogService
+ *
+ * @author Neil Mc Erlean (based on existing JavaScript webscript controllers)
+ * @since 4.0
+ */
+public class BlogPut extends AbstractBlogWebScript
+{
+ @Override
+ protected Map executeImpl(SiteInfo site, NodeRef containerNodeRef,
+ BlogPostInfo blog, WebScriptRequest req, JSONObject json, Status status, Cache cache)
+ {
+ if (blog != null)
+ {
+ // They appear to have supplied a blog post itself...
+ // Oh well, let's hope for the best!
+ throw new WebScriptException(Status.STATUS_BAD_REQUEST, "Blog post should not be updated via this web script.");
+ }
+
+ if (site != null && containerNodeRef == null)
+ {
+ // Force the lazy creation
+ // This is a bit icky, but it'll have to do for now...
+ containerNodeRef = siteService.createContainer(
+ site.getShortName(), BlogServiceImpl.BLOG_COMPONENT, null, null);
+ }
+
+ // Do the work
+ updateBlog(containerNodeRef, json);
+
+ // Record it as done
+ Map model = new HashMap();
+ model.put("item", containerNodeRef);
+
+ return model;
+ }
+
+ /**
+ * Creates a post inside the passed forum node.
+ */
+ @SuppressWarnings("deprecation")
+ private void updateBlog(NodeRef node, JSONObject json)
+ {
+ Map arr = BlogLibJs.getBlogPropertiesArray(json);
+
+ if (nodeService.hasAspect(node, BlogIntegrationModel.ASPECT_BLOG_DETAILS))
+ {
+ Map properties = nodeService.getProperties(node);
+ properties.putAll(arr);
+ nodeService.setProperties(node, properties);
+ }
+ else
+ {
+ nodeService.addAspect(node, BlogIntegrationModel.ASPECT_BLOG_DETAILS, arr);
+ }
+ }
+}
diff --git a/src/main/java/org/alfresco/repo/web/scripts/blogs/post/BlogPostDelete.java b/src/main/java/org/alfresco/repo/web/scripts/blogs/post/BlogPostDelete.java
new file mode 100644
index 0000000000..2c492101db
--- /dev/null
+++ b/src/main/java/org/alfresco/repo/web/scripts/blogs/post/BlogPostDelete.java
@@ -0,0 +1,82 @@
+/*
+ * #%L
+ * Alfresco Remote API
+ * %%
+ * Copyright (C) 2005 - 2016 Alfresco Software Limited
+ * %%
+ * This file is part of the Alfresco software.
+ * If the software was purchased under a paid Alfresco license, the terms of
+ * the paid license agreement will prevail. Otherwise, the software is
+ * provided under the following open source license terms:
+ *
+ * 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 .
+ * #L%
+ */
+package org.alfresco.repo.web.scripts.blogs.post;
+
+import java.text.MessageFormat;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.ResourceBundle;
+
+import org.alfresco.repo.web.scripts.blogs.AbstractBlogWebScript;
+import org.alfresco.service.cmr.blog.BlogPostInfo;
+import org.alfresco.service.cmr.repository.NodeRef;
+import org.alfresco.service.cmr.site.SiteInfo;
+import org.json.simple.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 blog-posts.get web script.
+ *
+ * @author Neil Mc Erlean (based on existing JavaScript webscript controllers)
+ * @since 4.0
+ */
+public class BlogPostDelete extends AbstractBlogWebScript
+{
+ protected static final String MSG_BLOG_DELETED = "blog-post.msg.deleted";
+
+ @Override
+ protected Map executeImpl(SiteInfo site, NodeRef nodeRef,
+ BlogPostInfo blog, WebScriptRequest req, JSONObject json, Status status, Cache cache)
+ {
+ final ResourceBundle rb = getResources();
+
+ if (blog == null)
+ {
+ throw new WebScriptException(Status.STATUS_NOT_FOUND, "Blog Post Not Found");
+ }
+
+ // TODO Get this from the BlogPostInfo Object
+ final boolean isDraftBlogPost = blogService.isDraftBlogPost(blog.getNodeRef());
+
+ // Have it deleted
+ blogService.deleteBlogPost(blog);
+
+ // If we're in a site, and it isn't a draft, add an activity
+ if (site != null && !isDraftBlogPost)
+ {
+ addActivityEntry("deleted", blog, site, req, json, nodeRef);
+ }
+
+ // Report it as deleted
+ Map model = new HashMap();
+ String message = rb.getString(MSG_BLOG_DELETED);
+ model.put("message",MessageFormat.format(message, blog.getNodeRef()));
+ return model;
+ }
+}
diff --git a/src/main/java/org/alfresco/repo/web/scripts/blogs/post/BlogPostGet.java b/src/main/java/org/alfresco/repo/web/scripts/blogs/post/BlogPostGet.java
new file mode 100644
index 0000000000..1e3f0f8151
--- /dev/null
+++ b/src/main/java/org/alfresco/repo/web/scripts/blogs/post/BlogPostGet.java
@@ -0,0 +1,88 @@
+/*
+ * #%L
+ * Alfresco Remote API
+ * %%
+ * Copyright (C) 2005 - 2016 Alfresco Software Limited
+ * %%
+ * This file is part of the Alfresco software.
+ * If the software was purchased under a paid Alfresco license, the terms of
+ * the paid license agreement will prevail. Otherwise, the software is
+ * provided under the following open source license terms:
+ *
+ * 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 .
+ * #L%
+ */
+package org.alfresco.repo.web.scripts.blogs.post;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.alfresco.repo.web.scripts.blogs.AbstractBlogWebScript;
+import org.alfresco.repo.web.scripts.blogs.BlogPostLibJs;
+import org.alfresco.service.cmr.blog.BlogPostInfo;
+import org.alfresco.service.cmr.repository.NodeRef;
+import org.alfresco.service.cmr.site.SiteInfo;
+import org.json.simple.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 blog-posts.get web script.
+ *
+ * @author Neil Mc Erlean (based on existing JavaScript webscript controllers)
+ * @since 4.0
+ */
+public class BlogPostGet extends AbstractBlogWebScript
+{
+ @Override
+ protected Map executeImpl(SiteInfo site, NodeRef nodeRef,
+ BlogPostInfo blog, WebScriptRequest req, JSONObject json, Status status, Cache cache)
+ {
+ if (blog == null)
+ {
+ throw new WebScriptException(Status.STATUS_NOT_FOUND, "Blog Post Not Found");
+ }
+
+ // Build the response
+ Map model = new HashMap();
+
+ // TODO Fetch this from the BlogPostInfo object
+ NodeRef node = blog.getNodeRef();
+ Map item = BlogPostLibJs.getBlogPostData(node, services);
+ model.put(ITEM, item);
+ model.put(POST, blog);
+
+ model.put("externalBlogConfig", BlogPostLibJs.hasExternalBlogConfiguration(node, services));
+
+ int contentLength = -1;
+ String arg = req.getParameter("contentLength");
+ if (arg != null)
+ {
+ try
+ {
+ contentLength = Integer.parseInt(arg);
+ }
+ catch (NumberFormatException ignored)
+ {
+ // Intentionally empty
+ }
+ }
+
+ model.put("contentLength", contentLength);
+
+ return model;
+ }
+}
diff --git a/src/main/java/org/alfresco/repo/web/scripts/blogs/posts/AbstractGetBlogWebScript.java b/src/main/java/org/alfresco/repo/web/scripts/blogs/posts/AbstractGetBlogWebScript.java
new file mode 100644
index 0000000000..2f9a8ec077
--- /dev/null
+++ b/src/main/java/org/alfresco/repo/web/scripts/blogs/posts/AbstractGetBlogWebScript.java
@@ -0,0 +1,229 @@
+/*
+ * #%L
+ * Alfresco Remote API
+ * %%
+ * Copyright (C) 2005 - 2016 Alfresco Software Limited
+ * %%
+ * This file is part of the Alfresco software.
+ * If the software was purchased under a paid Alfresco license, the terms of
+ * the paid license agreement will prevail. Otherwise, the software is
+ * provided under the following open source license terms:
+ *
+ * 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 .
+ * #L%
+ */
+package org.alfresco.repo.web.scripts.blogs.posts;
+
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.alfresco.model.ContentModel;
+import org.alfresco.query.PagingRequest;
+import org.alfresco.query.PagingResults;
+import org.alfresco.repo.web.scripts.blogs.AbstractBlogWebScript;
+import org.alfresco.repo.web.scripts.blogs.BlogPostLibJs;
+import org.alfresco.service.cmr.blog.BlogPostInfo;
+import org.alfresco.service.cmr.blog.BlogService.RangedDateProperty;
+import org.alfresco.service.cmr.repository.NodeRef;
+import org.alfresco.service.cmr.site.SiteInfo;
+import org.alfresco.util.Pair;
+import org.alfresco.util.ScriptPagingDetails;
+import org.alfresco.util.UrlUtil;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.json.simple.JSONObject;
+import org.springframework.extensions.surf.util.URLDecoder;
+import org.springframework.extensions.webscripts.Cache;
+import org.springframework.extensions.webscripts.Status;
+import org.springframework.extensions.webscripts.WebScriptRequest;
+
+/**
+ * @author Neil Mc Erlean (based on existing JavaScript webscript controllers)
+ * @since 4.0
+ */
+public abstract class AbstractGetBlogWebScript extends AbstractBlogWebScript
+{
+ private static final Log log = LogFactory.getLog(AbstractGetBlogWebScript.class);
+
+ @Override
+ protected Map executeImpl(SiteInfo site, NodeRef nonSiteContainer,
+ BlogPostInfo blog, WebScriptRequest req, JSONObject json, Status status, Cache cache)
+ {
+ Map model = new HashMap();
+
+ // process additional parameters.
+ PagingRequest pagingReq = parsePagingParams(req);
+ pagingReq.setRequestTotalCountMax(pagingReq.getSkipCount() + pagingReq.getRequestTotalCountMax());
+
+ // begin and end date.
+ // Legacy note: these dates are URL query parameters in int form.
+ Date fromDate = parseDateParam(req, "fromDate");
+ Date toDate = parseDateParam(req, "toDate");
+
+ String tag = req.getParameter("tag");
+ if (tag == null || tag.length() == 0)
+ {
+ tag = null;
+ }
+ else
+ {
+ // Tags can be full unicode strings, so decode
+ tag = URLDecoder.decode(tag);
+ }
+
+ // One webscript (blog-posts-new.get) uses a 'numdays' parameter as a 'fromDate'.
+ // This is a hacky solution to this special case. FIXME
+ if (this.getClass().equals(BlogPostsNewGet.class))
+ {
+ // Default is for 'now' minus seven days.
+ final long oneDayInMilliseconds = 24 * 60 * 60 * 1000;
+ final long sevenDaysInMilliseconds = 7 * oneDayInMilliseconds;
+ fromDate = new Date(System.currentTimeMillis() - sevenDaysInMilliseconds);
+
+ // But if there is a numdays parameter then that changes the fromDate
+ String numDays = req.getServiceMatch().getTemplateVars().get("numdays");
+ if (numDays != null)
+ {
+ Integer numDaysInt = Integer.parseInt(numDays);
+ fromDate = new Date(System.currentTimeMillis() - (numDaysInt * oneDayInMilliseconds));
+ }
+ }
+
+ // Fetch and assign the data
+ PagingResults blogPostList =
+ getBlogPostList(site, nonSiteContainer, fromDate, toDate, tag, pagingReq);
+
+ // We need the container for various bits
+ NodeRef container = nonSiteContainer;
+ if(container == null)
+ {
+ // Container mustn't exist yet
+ // Fake it with the site for permissions checking reasons
+ container = site.getNodeRef();
+ }
+
+ if (log.isDebugEnabled())
+ {
+ StringBuilder msg = new StringBuilder();
+ msg.append("Retrieved ").append(blogPostList.getPage().size()).append(" blog posts in page.");
+ log.debug(msg.toString());
+ }
+
+ createFtlModel(req, model, container, pagingReq, blogPostList);
+
+ return model;
+ }
+
+ protected void createFtlModel(WebScriptRequest req, Map model, NodeRef node, PagingRequest pagingReq,
+ PagingResults blogPostList)
+ {
+ Map blogPostsData = new HashMap();
+
+ final Pair totalResultCount = blogPostList.getTotalResultCount();
+ int total = blogPostList.getPage().size();
+ if (totalResultCount != null && totalResultCount.getFirst() != null)
+ {
+ total = totalResultCount.getFirst();
+ }
+ //FIXME What to do? null
+ blogPostsData.put("total", total);
+ blogPostsData.put("pageSize", pagingReq.getMaxItems());
+ blogPostsData.put("startIndex", pagingReq.getSkipCount());
+ blogPostsData.put("itemCount", blogPostList.getPage().size());
+
+ if (total == pagingReq.getRequestTotalCountMax())
+ {
+ blogPostsData.put("totalRecordsUpper", true);
+ }
+ else
+ {
+ blogPostsData.put("totalRecordsUpper", false);
+ }
+
+ List> blogPostDataSets = new ArrayList>(blogPostList.getPage().size());
+ for (BlogPostInfo postInfo : blogPostList.getPage())
+ {
+ Map data = BlogPostLibJs.getBlogPostData(postInfo.getNodeRef(), services);
+ blogPostDataSets.add(data);
+ }
+ blogPostsData.put("items", blogPostDataSets);
+
+ model.put("data", blogPostsData);
+
+ // fetch the contentLength param
+ String contentLengthStr = req.getParameter("contentLength");
+ int contentLength = contentLengthStr == null ? -1 : Integer.parseInt(contentLengthStr);
+ model.put("contentLength", contentLength);
+
+ // assign the blog node
+ model.put("blog", node);
+ model.put("externalBlogConfig", BlogPostLibJs.hasExternalBlogConfiguration(node, services));
+ }
+
+ private PagingRequest parsePagingParams(WebScriptRequest req)
+ {
+ return ScriptPagingDetails.buildPagingRequest(req, 1000);
+ }
+
+ private Date parseDateParam(WebScriptRequest req, String paramName)
+ {
+ Map templateVars = req.getServiceMatch().getTemplateVars();
+ String dateStr = templateVars.get(paramName);
+ if(dateStr == null)
+ {
+ // Try on the parameters instead
+ dateStr = req.getParameter(paramName);
+ }
+
+ // Parse if available
+ Date result = null;
+ if (dateStr != null)
+ {
+ result = new Date(Long.parseLong(dateStr));
+ }
+ return result;
+ }
+
+
+ /**
+ * Fetches all posts of the given blog
+ */
+ private PagingResults getBlogPostList(SiteInfo site, NodeRef nonSiteContainer,
+ Date fromDate, Date toDate, String tag, PagingRequest pagingReq)
+ {
+ // Currently we only support CannedQuery-based gets without tags:
+ if (tag == null || tag.trim().isEmpty())
+ {
+ return getBlogResultsImpl(site, nonSiteContainer, fromDate, toDate, pagingReq);
+ }
+ else
+ {
+ RangedDateProperty dateRange = new RangedDateProperty(fromDate, toDate, ContentModel.PROP_CREATED);
+ if(site != null)
+ {
+ return blogService.findBlogPosts(site.getShortName(), dateRange, tag, pagingReq);
+ }
+ else
+ {
+ return blogService.findBlogPosts(nonSiteContainer, dateRange, tag, pagingReq);
+ }
+ }
+ }
+
+ protected abstract PagingResults getBlogResultsImpl(
+ SiteInfo site, NodeRef nonSiteContainer, Date fromDate, Date toDate, PagingRequest pagingReq);
+}
diff --git a/src/main/java/org/alfresco/repo/web/scripts/blogs/posts/BlogPostsGet.java b/src/main/java/org/alfresco/repo/web/scripts/blogs/posts/BlogPostsGet.java
new file mode 100644
index 0000000000..8d38f0bdc8
--- /dev/null
+++ b/src/main/java/org/alfresco/repo/web/scripts/blogs/posts/BlogPostsGet.java
@@ -0,0 +1,61 @@
+/*
+ * #%L
+ * Alfresco Remote API
+ * %%
+ * Copyright (C) 2005 - 2016 Alfresco Software Limited
+ * %%
+ * This file is part of the Alfresco software.
+ * If the software was purchased under a paid Alfresco license, the terms of
+ * the paid license agreement will prevail. Otherwise, the software is
+ * provided under the following open source license terms:
+ *
+ * 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 .
+ * #L%
+ */
+package org.alfresco.repo.web.scripts.blogs.posts;
+
+import java.util.Date;
+
+import org.alfresco.query.EmptyPagingResults;
+import org.alfresco.query.PagingRequest;
+import org.alfresco.query.PagingResults;
+import org.alfresco.service.cmr.blog.BlogPostInfo;
+import org.alfresco.service.cmr.repository.NodeRef;
+import org.alfresco.service.cmr.site.SiteInfo;
+
+/**
+ * This class is the controller for the blog-posts.get web script.
+ *
+ * @author Neil Mc Erlean (based on existing JavaScript webscript controllers)
+ * @since 4.0
+ */
+public class BlogPostsGet extends AbstractGetBlogWebScript
+{
+ @SuppressWarnings("deprecation")
+ @Override
+ protected PagingResults getBlogResultsImpl(
+ SiteInfo site, NodeRef node, Date fromDate, Date toDate, PagingRequest pagingReq)
+ {
+ // As it uses deprecated methods, this bit can be a bit hacky...
+ if (node == null)
+ {
+ // Site based request, but no container exists yet
+ return new EmptyPagingResults();
+ }
+
+ // This intentionally uses the deprecated method in the foundation service.
+ // In fact the method is there specifically for this class.
+ return blogService.getMyDraftsAndAllPublished(node, fromDate, toDate, pagingReq);
+ }
+}
diff --git a/src/main/java/org/alfresco/repo/web/scripts/blogs/posts/BlogPostsMyDraftsGet.java b/src/main/java/org/alfresco/repo/web/scripts/blogs/posts/BlogPostsMyDraftsGet.java
new file mode 100644
index 0000000000..3152c8390c
--- /dev/null
+++ b/src/main/java/org/alfresco/repo/web/scripts/blogs/posts/BlogPostsMyDraftsGet.java
@@ -0,0 +1,59 @@
+/*
+ * #%L
+ * Alfresco Remote API
+ * %%
+ * Copyright (C) 2005 - 2016 Alfresco Software Limited
+ * %%
+ * This file is part of the Alfresco software.
+ * If the software was purchased under a paid Alfresco license, the terms of
+ * the paid license agreement will prevail. Otherwise, the software is
+ * provided under the following open source license terms:
+ *
+ * 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 .
+ * #L%
+ */
+package org.alfresco.repo.web.scripts.blogs.posts;
+
+import java.util.Date;
+
+import org.alfresco.query.PagingRequest;
+import org.alfresco.query.PagingResults;
+import org.alfresco.repo.security.authentication.AuthenticationUtil;
+import org.alfresco.service.cmr.blog.BlogPostInfo;
+import org.alfresco.service.cmr.repository.NodeRef;
+import org.alfresco.service.cmr.site.SiteInfo;
+
+/**
+ * This class is the controller for the blog-posts-mydrafts.get web script.
+ * Based on the original JavaScript webscript controller
+ *
+ * @author Neil Mc Erlean (based on existing JavaScript webscript controllers)
+ * @since 4.0
+ */
+public class BlogPostsMyDraftsGet extends AbstractGetBlogWebScript
+{
+ @Override
+ protected PagingResults getBlogResultsImpl(SiteInfo site, NodeRef node, Date fromDate, Date toDate, PagingRequest pagingReq)
+ {
+ String user = AuthenticationUtil.getFullyAuthenticatedUser();
+ if(site != null)
+ {
+ return blogService.getDrafts(site.getShortName(), user, pagingReq);
+ }
+ else
+ {
+ return blogService.getDrafts(node, user, pagingReq);
+ }
+ }
+}
diff --git a/src/main/java/org/alfresco/repo/web/scripts/blogs/posts/BlogPostsMyPublishedGet.java b/src/main/java/org/alfresco/repo/web/scripts/blogs/posts/BlogPostsMyPublishedGet.java
new file mode 100644
index 0000000000..d226a12fc4
--- /dev/null
+++ b/src/main/java/org/alfresco/repo/web/scripts/blogs/posts/BlogPostsMyPublishedGet.java
@@ -0,0 +1,59 @@
+/*
+ * #%L
+ * Alfresco Remote API
+ * %%
+ * Copyright (C) 2005 - 2016 Alfresco Software Limited
+ * %%
+ * This file is part of the Alfresco software.
+ * If the software was purchased under a paid Alfresco license, the terms of
+ * the paid license agreement will prevail. Otherwise, the software is
+ * provided under the following open source license terms:
+ *
+ * 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 .
+ * #L%
+ */
+package org.alfresco.repo.web.scripts.blogs.posts;
+
+import java.util.Date;
+
+import org.alfresco.query.PagingRequest;
+import org.alfresco.query.PagingResults;
+import org.alfresco.repo.security.authentication.AuthenticationUtil;
+import org.alfresco.service.cmr.blog.BlogPostInfo;
+import org.alfresco.service.cmr.repository.NodeRef;
+import org.alfresco.service.cmr.site.SiteInfo;
+
+/**
+ * This class is the controller for the blog-posts-mypublished.get web script.
+ *
+ * @author Neil Mc Erlean (based on existing JavaScript webscript controllers)
+ * @since 4.0
+ */
+public class BlogPostsMyPublishedGet extends AbstractGetBlogWebScript
+{
+ @Override
+ protected PagingResults getBlogResultsImpl(
+ SiteInfo site, NodeRef node, Date fromDate, Date toDate, PagingRequest pagingReq)
+ {
+ String fullyAuthenticatedUser = AuthenticationUtil.getFullyAuthenticatedUser();
+ if(site != null)
+ {
+ return blogService.getPublished(site.getShortName(), fromDate, toDate, fullyAuthenticatedUser, pagingReq);
+ }
+ else
+ {
+ return blogService.getPublished(node, fromDate, toDate, fullyAuthenticatedUser, pagingReq);
+ }
+ }
+}
diff --git a/src/main/java/org/alfresco/repo/web/scripts/blogs/posts/BlogPostsNewGet.java b/src/main/java/org/alfresco/repo/web/scripts/blogs/posts/BlogPostsNewGet.java
new file mode 100644
index 0000000000..2166418dcf
--- /dev/null
+++ b/src/main/java/org/alfresco/repo/web/scripts/blogs/posts/BlogPostsNewGet.java
@@ -0,0 +1,57 @@
+/*
+ * #%L
+ * Alfresco Remote API
+ * %%
+ * Copyright (C) 2005 - 2016 Alfresco Software Limited
+ * %%
+ * This file is part of the Alfresco software.
+ * If the software was purchased under a paid Alfresco license, the terms of
+ * the paid license agreement will prevail. Otherwise, the software is
+ * provided under the following open source license terms:
+ *
+ * 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 .
+ * #L%
+ */
+package org.alfresco.repo.web.scripts.blogs.posts;
+
+import java.util.Date;
+
+import org.alfresco.query.PagingRequest;
+import org.alfresco.query.PagingResults;
+import org.alfresco.service.cmr.blog.BlogPostInfo;
+import org.alfresco.service.cmr.repository.NodeRef;
+import org.alfresco.service.cmr.site.SiteInfo;
+
+/**
+ * This class is the controller for the blog-posts-publishedext.get web script.
+ *
+ * @author Neil Mc Erlean (based on existing JavaScript webscript controllers)
+ * @since 4.0
+ */
+public class BlogPostsNewGet extends AbstractGetBlogWebScript
+{
+ @Override
+ protected PagingResults getBlogResultsImpl(
+ SiteInfo site, NodeRef node, Date fromDate, Date toDate, PagingRequest pagingReq)
+ {
+ if(site != null)
+ {
+ return blogService.getPublished(site.getShortName(), fromDate, toDate, null, pagingReq);
+ }
+ else
+ {
+ return blogService.getPublished(node, fromDate, toDate, null, pagingReq);
+ }
+ }
+}
diff --git a/src/main/java/org/alfresco/repo/web/scripts/blogs/posts/BlogPostsPerMonthGet.java b/src/main/java/org/alfresco/repo/web/scripts/blogs/posts/BlogPostsPerMonthGet.java
new file mode 100644
index 0000000000..7b45916e9c
--- /dev/null
+++ b/src/main/java/org/alfresco/repo/web/scripts/blogs/posts/BlogPostsPerMonthGet.java
@@ -0,0 +1,170 @@
+/*
+ * #%L
+ * Alfresco Remote API
+ * %%
+ * Copyright (C) 2005 - 2016 Alfresco Software Limited
+ * %%
+ * This file is part of the Alfresco software.
+ * If the software was purchased under a paid Alfresco license, the terms of
+ * the paid license agreement will prevail. Otherwise, the software is
+ * provided under the following open source license terms:
+ *
+ * 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 .
+ * #L%
+ */
+package org.alfresco.repo.web.scripts.blogs.posts;
+
+import java.util.ArrayList;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.alfresco.model.ContentModel;
+import org.alfresco.query.PagingRequest;
+import org.alfresco.query.PagingResults;
+import org.alfresco.service.cmr.blog.BlogPostInfo;
+import org.alfresco.service.cmr.repository.NodeRef;
+import org.alfresco.service.cmr.site.SiteInfo;
+import org.springframework.extensions.webscripts.WebScriptRequest;
+
+/**
+ * This class is the controller for the blog-posts-mypublished.get web script.
+ *
+ * @author Neil Mc Erlean (based on existing JavaScript webscript controllers)
+ * @since 4.0
+ */
+public class BlogPostsPerMonthGet extends AbstractGetBlogWebScript
+{
+ @Override
+ protected PagingResults getBlogResultsImpl(
+ SiteInfo site, NodeRef node, Date fromDate, Date toDate, PagingRequest pagingReq)
+ {
+ if(site != null)
+ {
+ return blogService.getPublished(site.getShortName(), fromDate, toDate, null, pagingReq);
+ }
+ else
+ {
+ return blogService.getPublished(node, fromDate, toDate, null, pagingReq);
+ }
+ }
+
+ @Override
+ protected void createFtlModel(WebScriptRequest req, Map model, NodeRef node, PagingRequest pagingReq, PagingResults blogPostList)
+ {
+ model.put(DATA, getBlogPostMonths(blogPostList));
+ }
+
+
+ /**
+ * Ported from blog-posts-per-month.get.js
+ */
+ @SuppressWarnings("deprecation")
+ private Date getBeginOfMonthDate(Date date)
+ {
+ //TODO These date processing methods are copied almost verbatim from JavaScript to preserve behaviour.
+ // However they should be updated to use java.util.Calendar as the current implementation assumes a Gregorian calendar.
+ Calendar calendar = Calendar.getInstance();
+ calendar.set(date.getYear(), date.getMonth(), 1);
+ return calendar.getTime();
+ }
+
+ /**
+ * Returns the date representing the last second of a month (23:59:59)
+ * Ported from blog-posts-per-month.get.js
+ */
+ @SuppressWarnings("deprecation")
+ private Date getEndOfMonthDate(Date date)
+ {
+ Calendar calendar = Calendar.getInstance();
+ calendar.set(date.getYear(), date.getMonth(), date.getDay());
+ // In Gregorian calendar, this would be 31 for January, 30 for March, 28 or 29 for February.
+ int lastDayOfSpecifiedMonth = calendar.getActualMaximum(Calendar.DAY_OF_MONTH);
+ calendar.set(date.getYear(), date.getMonth(), lastDayOfSpecifiedMonth, 23, 59, 59);
+
+ return calendar.getTime();
+ }
+
+ /**
+ * Create an object containing information about the month specified by date.
+ * Ported from blog-posts-per-month.get.js
+ */
+ @SuppressWarnings("deprecation")
+ private Map getMonthDataObject(Date date)
+ {
+ Map data = new HashMap();
+ data.put("year", date.getYear() + 1900);
+ data.put("month", date.getMonth());
+ data.put("firstPostInMonth", date);
+ data.put("beginOfMonth", getBeginOfMonthDate(date));
+ data.put("endOfMonth", getEndOfMonthDate(date));
+ data.put("count", 1);
+
+ return data;
+ }
+
+ /**
+ * Fetches data for each month for which posts exist, plus the count of each.
+ * Note: If no posts could be found, this method will return the current month
+ * but with a count of posts equals zero.
+ * Ported from blog-posts-per-month.get.js
+ */
+ @SuppressWarnings("deprecation")
+ private List> getBlogPostMonths(PagingResults nodes)
+ {
+ // will hold the months information
+ List> data = new ArrayList>();
+
+ // do we have posts?
+ if (!nodes.getPage().isEmpty())
+ {
+ int currYear = -1;
+ int currMonth = -1;
+ Map currData = null;
+ for (int x = 0; x < nodes.getPage().size(); x++)
+ {
+ NodeRef node = nodes.getPage().get(x).getNodeRef();
+ Date date = (Date) nodeService.getProperty(node, ContentModel.PROP_PUBLISHED);
+
+ // is this a new month?
+ if (currYear != date.getYear() + 1900 || currMonth != date.getMonth())
+ {
+ currYear = date.getYear() + 1900;
+ currMonth = date.getMonth();
+ currData = getMonthDataObject(date);
+ data.add(currData);
+ }
+ // otherwise just increment the counter
+ else
+ {
+ Object countObj = currData.get("count");
+ Integer countInt = countObj == null ? 0 : (Integer)countObj;
+
+ currData.put("count", countInt + 1);
+ }
+ }
+ }
+ // if not, add the current month with count = 0
+ else
+ {
+ Map emptyData = getMonthDataObject(new Date());
+ emptyData.put("count", 0);
+ data.add(emptyData);
+ }
+
+ return data;
+ }
+}
diff --git a/src/main/java/org/alfresco/repo/web/scripts/blogs/posts/BlogPostsPost.java b/src/main/java/org/alfresco/repo/web/scripts/blogs/posts/BlogPostsPost.java
new file mode 100644
index 0000000000..0cd2843f7d
--- /dev/null
+++ b/src/main/java/org/alfresco/repo/web/scripts/blogs/posts/BlogPostsPost.java
@@ -0,0 +1,304 @@
+/*
+ * #%L
+ * Alfresco Remote API
+ * %%
+ * Copyright (C) 2005 - 2016 Alfresco Software Limited
+ * %%
+ * This file is part of the Alfresco software.
+ * If the software was purchased under a paid Alfresco license, the terms of
+ * the paid license agreement will prevail. Otherwise, the software is
+ * provided under the following open source license terms:
+ *
+ * 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 .
+ * #L%
+ */
+package org.alfresco.repo.web.scripts.blogs.posts;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.alfresco.repo.web.scripts.blogs.AbstractBlogWebScript;
+import org.alfresco.repo.web.scripts.blogs.BlogPostLibJs;
+import org.alfresco.service.cmr.blog.BlogPostInfo;
+import org.alfresco.service.cmr.repository.NodeRef;
+import org.alfresco.service.cmr.site.SiteInfo;
+import org.alfresco.service.cmr.tagging.TaggingService;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.json.simple.JSONArray;
+import org.json.simple.JSONObject;
+import org.springframework.extensions.webscripts.Cache;
+import org.springframework.extensions.webscripts.Status;
+import org.springframework.extensions.webscripts.WebScriptRequest;
+
+/**
+ * This class is the controller for the blog-posts.post web script.
+ *
+ * @author Neil Mc Erlean (based on existing JavaScript webscript controllers)
+ * @since 4.0
+ */
+public class BlogPostsPost extends AbstractBlogWebScript
+{
+ private static final Log log = LogFactory.getLog(BlogPostsPost.class);
+
+ // Injected services
+ private TaggingService taggingService;
+
+ public void setTaggingService(TaggingService taggingService)
+ {
+ this.taggingService = taggingService;
+ }
+
+ @Override
+ protected Map executeImpl(SiteInfo site, NodeRef nodeRef,
+ BlogPostInfo blog, WebScriptRequest req, JSONObject json, Status status, Cache cache)
+ {
+ Map model = new HashMap();
+
+ // If they're doing Path Based rather than Site Based, ensure
+ // that the Container is a Tag Scope
+ if(site == null && nodeRef != null)
+ {
+ ensureTagScope(nodeRef);
+ }
+
+ // Have the Blog Post created
+ JsonParams jsonPostParams = parsePostParams(json);
+ BlogPostInfo post = createBlogPost(jsonPostParams, site, nodeRef);
+
+ Map blogPostData = BlogPostLibJs.getBlogPostData(post.getNodeRef(), services);
+ model.put(ITEM, blogPostData);
+ model.put(EXTERNAL_BLOG_CONFIG, BlogPostLibJs.hasExternalBlogConfiguration(nodeRef, services));
+
+ boolean isDraft = blogPostData.get("isDraft") != null &&
+ ((Boolean)blogPostData.get("isDraft")).booleanValue();
+ if (jsonPostParams.getSite() != null &&
+ jsonPostParams.getContainer() != null &&
+ jsonPostParams.getPage() != null &&
+ !isDraft)
+ {
+ addActivityEntry("created", post, site, req, json, post.getNodeRef());
+ }
+
+ return model;
+ }
+
+ private JsonParams parsePostParams(JSONObject json)
+ {
+ JsonParams result = new JsonParams();
+ if (json.containsKey(TITLE))
+ {
+ result.setTitle((String)json.get(TITLE));
+ }
+ if (json.containsKey(CONTENT))
+ {
+ result.setContent((String)json.get(CONTENT));
+ }
+ if (json.containsKey(DRAFT))
+ {
+ Object draft = json.get(DRAFT);
+ if (draft instanceof Boolean)
+ {
+ result.setIsDraft((Boolean)draft);
+ }
+ else
+ {
+ result.setIsDraft( Boolean.parseBoolean((String)draft) );
+ }
+ }
+
+ // If there are no tags, this is a java.lang.String "".
+ // If there are any tags, it's a JSONArray of strings. One or more.
+ if (json.containsKey(TAGS))
+ {
+ Object tagsObj = json.get(TAGS);
+ List tags = new ArrayList();
+ if (tagsObj instanceof JSONArray)
+ {
+ JSONArray tagsJsonArray = (JSONArray)tagsObj;
+ for (int i = 0; i < tagsJsonArray.size(); i++)
+ {
+ tags.add( (String)tagsJsonArray.get(i) );
+ }
+ }
+ else
+ {
+ tags.add(tagsObj.toString());
+ }
+ result.setTags(tags);
+ }
+ if (json.containsKey(SITE))
+ {
+ result.setSite((String)json.get(SITE));
+ }
+ if (json.containsKey(PAGE))
+ {
+ result.setPage((String)json.get(PAGE));
+ }
+ if (json.containsKey(CONTAINER))
+ {
+ result.setContainer((String)json.get(CONTAINER));
+ }
+
+ return result;
+ }
+
+ /**
+ * Taken from JS
+ * @param node NodeRef
+ */
+ private void ensureTagScope(NodeRef node)
+ {
+ if (!taggingService.isTagScope(node))
+ {
+ taggingService.addTagScope(node);
+ }
+
+
+ // also check the parent (the site!)
+ NodeRef parent = nodeService.getPrimaryParent(node).getParentRef();
+ if (!taggingService.isTagScope(parent))
+ {
+ taggingService.addTagScope(parent);
+ }
+ }
+
+ /**
+ * Creates a blog post
+ */
+ private BlogPostInfo createBlogPost(JsonParams jsonParams, SiteInfo site, NodeRef blogNode)
+ {
+ String titleParam = jsonParams.getTitle() == null ? "" : jsonParams.getTitle();
+ String contentParam = jsonParams.getContent() == null ? "" : jsonParams.getContent();
+ boolean isDraftParam = jsonParams.getIsDraft();
+
+ if (log.isDebugEnabled())
+ {
+ StringBuilder msg = new StringBuilder();
+ msg.append("Creating blog-post '").append(titleParam).append("'");
+ if (isDraftParam)
+ {
+ msg.append(" DRAFT");
+ }
+ log.debug(msg.toString());
+ }
+
+ List tagsParam = new ArrayList();
+ if (jsonParams.getTags() != null)
+ {
+ tagsParam.addAll(jsonParams.getTags());
+ }
+
+ BlogPostInfo newPostNode;
+ if(site != null)
+ {
+ newPostNode = blogService.createBlogPost(
+ site.getShortName(), titleParam, contentParam, isDraftParam);
+ }
+ else
+ {
+ newPostNode = blogService.createBlogPost(
+ blogNode, titleParam, contentParam, isDraftParam);
+ }
+
+ // Ignore empty string tags
+ List nonEmptyTags = new ArrayList();
+ for (String tag : tagsParam)
+ {
+ if (!tag.trim().isEmpty())
+ {
+ nonEmptyTags.add(tag);
+ }
+ }
+ if (!nonEmptyTags.isEmpty())
+ {
+ taggingService.setTags(newPostNode.getNodeRef(), nonEmptyTags);
+ }
+
+ return newPostNode;
+ }
+
+ /**
+ * A simple POJO class for the parsed JSON from the POST body.
+ */
+ class JsonParams
+ {
+ private String title;
+ private String content;
+ private boolean isDraft = false;
+ private List tags;
+ private String site;
+ private String container;
+ private String page;
+
+ public String getTitle()
+ {
+ return title;
+ }
+ public void setTitle(String title)
+ {
+ this.title = title;
+ }
+ public String getContent()
+ {
+ return content;
+ }
+ public void setContent(String content)
+ {
+ this.content = content;
+ }
+ public boolean getIsDraft()
+ {
+ return isDraft;
+ }
+ public void setIsDraft(boolean isDraft)
+ {
+ this.isDraft = isDraft;
+ }
+ public List getTags()
+ {
+ return tags;
+ }
+ public void setTags(List tags)
+ {
+ this.tags = tags;
+ }
+ public String getSite()
+ {
+ return site;
+ }
+ public void setSite(String site)
+ {
+ this.site = site;
+ }
+ public String getContainer()
+ {
+ return container;
+ }
+ public void setContainer(String container)
+ {
+ this.container = container;
+ }
+ public String getPage()
+ {
+ return page;
+ }
+ public void setPage(String page)
+ {
+ this.page = page;
+ }
+ }
+}
diff --git a/src/main/java/org/alfresco/repo/web/scripts/discussion/AbstractDiscussionWebScript.java b/src/main/java/org/alfresco/repo/web/scripts/discussion/AbstractDiscussionWebScript.java
new file mode 100644
index 0000000000..11a4927e03
--- /dev/null
+++ b/src/main/java/org/alfresco/repo/web/scripts/discussion/AbstractDiscussionWebScript.java
@@ -0,0 +1,603 @@
+/*
+ * #%L
+ * Alfresco Remote API
+ * %%
+ * Copyright (C) 2005 - 2016 Alfresco Software Limited
+ * %%
+ * This file is part of the Alfresco software.
+ * If the software was purchased under a paid Alfresco license, the terms of
+ * the paid license agreement will prevail. Otherwise, the software is
+ * provided under the following open source license terms:
+ *
+ * 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 .
+ * #L%
+ */
+package org.alfresco.repo.web.scripts.discussion;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.alfresco.query.PagingRequest;
+import org.alfresco.query.PagingResults;
+import org.alfresco.repo.content.MimetypeMap;
+import org.alfresco.repo.discussion.DiscussionServiceImpl;
+import org.alfresco.repo.security.authentication.AuthenticationUtil;
+import org.alfresco.repo.site.SiteServiceImpl;
+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.AccessStatus;
+import org.alfresco.service.cmr.security.NoSuchPersonException;
+import org.alfresco.service.cmr.security.PermissionService;
+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.alfresco.util.ScriptPagingDetails;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.json.simple.JSONArray;
+import org.json.simple.JSONObject;
+import org.json.simple.parser.JSONParser;
+import org.json.simple.parser.ParseException;
+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);
+
+ protected static final String KEY_POSTDATA = "postData";
+ protected static final String KEY_IS_TOPIC_POST = "isTopicPost";
+ protected static final String KEY_TOPIC = "topic";
+ protected static final String KEY_POST = "post";
+ protected static final String KEY_CAN_EDIT = "canEdit";
+ protected static final String KEY_AUTHOR = "author";
+
+ // Injected services
+ protected NodeService nodeService;
+ protected SiteService siteService;
+ protected PersonService personService;
+ protected ActivityService activityService;
+ protected DiscussionService discussionService;
+ protected PermissionService permissionService;
+
+ public void setNodeService(NodeService nodeService)
+ {
+ this.nodeService = nodeService;
+ }
+
+ public void setSiteService(SiteService siteService)
+ {
+ this.siteService = siteService;
+ }
+
+ public void setPersonService(PersonService personService)
+ {
+ this.personService = personService;
+ }
+
+ public void setActivityService(ActivityService activityService)
+ {
+ this.activityService = activityService;
+ }
+
+ public void setDiscussionService(DiscussionService discussionService)
+ {
+ this.discussionService = discussionService;
+ }
+
+ public void setPermissionService(PermissionService permissionService)
+ {
+ this.permissionService = permissionService;
+ }
+
+
+ protected String getOrNull(JSONObject json, String key)
+ {
+ if (json.containsKey(key))
+ {
+ return (String)json.get(key);
+ }
+ return null;
+ }
+
+ /**
+ * Builds up a listing Paging request, based on the arguments
+ * specified in the URL
+ */
+ protected PagingRequest buildPagingRequest(WebScriptRequest req)
+ {
+ return new ScriptPagingDetails(req, MAX_QUERY_ENTRY_COUNT);
+ }
+
+ protected List getTags(JSONObject json)
+ {
+ List tags = null;
+ if (json.containsKey("tags"))
+ {
+ // Is it "tags":"" or "tags":[...] ?
+ if (json.get("tags") instanceof String)
+ {
+ // This is normally an empty string, skip
+ String tagsS = (String)json.get("tags");
+ if ("".equals(tagsS))
+ {
+ // No tags were given
+ return null;
+ }
+ else
+ {
+ // Log, and treat as empty
+ logger.warn("Unexpected tag data: " + tagsS);
+ return null;
+ }
+ }
+ else
+ {
+ tags = new ArrayList();
+ JSONArray jsTags = (JSONArray)json.get("tags");
+ for (int i=0; i 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);
+ }
+ }
+
+ /**
+ * Is the current user allowed to edit this post?
+ * In order to be deemed allowed, you first need write
+ * permissions on the underlying node of the post.
+ * You then also need to either be the cm:creator of
+ * the post node, or a site manager
+ */
+ protected boolean canUserEditPost(PostInfo post, SiteInfo site)
+ {
+ // Are they OK on the node?
+ AccessStatus canEdit = permissionService.hasPermission(post.getNodeRef(), PermissionService.WRITE);
+ if (canEdit == AccessStatus.ALLOWED)
+ {
+ // Only the creator and site managers may edit
+ String user = AuthenticationUtil.getFullyAuthenticatedUser();
+ if (post.getCreator().equals(user))
+ {
+ // It's their post
+ return true;
+ }
+ if (site != null)
+ {
+ String role = siteService.getMembersRole(site.getShortName(), user);
+ if (SiteServiceImpl.SITE_MANAGER.equals(role))
+ {
+ // Managers may edit
+ return true;
+ }
+ }
+ }
+
+ // If in doubt, you may not edit
+ return false;
+ }
+
+ protected Object buildPerson(String username)
+ {
+ // Empty string needed if the user can't be found
+ Object noSuchPersonResponse = "";
+
+ if (username == null || username.length() == 0)
+ {
+ return noSuchPersonResponse;
+ }
+
+ try
+ {
+ // Will turn into a Script Node needed of the person
+ NodeRef person = personService.getPerson(username);
+ return person;
+ }
+ catch(NoSuchPersonException e)
+ {
+ // This is normally caused by the person having been deleted
+ return noSuchPersonResponse;
+ }
+ }
+
+ /*
+ * Was topicpost.lib.js getReplyPostData
+ *
+ * TODO Switch the FTL to prefer the Info object rather than the ScriptNode
+ */
+ protected Map renderPost(PostInfo post, SiteInfo site)
+ {
+ Map item = new HashMap();
+ item.put(KEY_IS_TOPIC_POST, false);
+ item.put(KEY_POST, post.getNodeRef());
+ item.put(KEY_CAN_EDIT, canUserEditPost(post, site));
+ item.put(KEY_AUTHOR, buildPerson(post.getCreator()));
+ return item;
+ }
+
+ /*
+ * Was topicpost.lib.js getTopicPostData / getTopicPostDataFromTopicAndPosts
+ *
+ * TODO Switch the FTL to prefer the Info object rather than the ScriptNode
+ */
+ protected Map renderTopic(TopicInfo topic, SiteInfo site)
+ {
+ // Fetch the primary post
+ PostInfo primaryPost = discussionService.getPrimaryPost(topic);
+ if (primaryPost == null)
+ {
+ throw new WebScriptException(Status.STATUS_PRECONDITION_FAILED,
+ "First (primary) post was missing from the topic, can't fetch");
+ }
+
+ // Fetch the most recent reply
+ PostInfo mostRecentPost = discussionService.getMostRecentPost(topic);
+
+ // Find out how many replies there are
+ int numReplies;
+ if (mostRecentPost.getNodeRef().equals( primaryPost.getNodeRef() ))
+ {
+ // Only the one post in the topic
+ mostRecentPost = null;
+ numReplies = 0;
+ }
+ else
+ {
+ // Use this trick to get the number of posts in the topic,
+ // but without needing to get lots of data and objects
+ PagingRequest paging = new PagingRequest(1);
+ paging.setRequestTotalCountMax(MAX_QUERY_ENTRY_COUNT);
+ PagingResults posts = discussionService.listPosts(topic, paging);
+
+ // The primary post is in the list, so exclude from the reply count
+ numReplies = posts.getTotalResultCount().getFirst() - 1;
+ }
+
+ // Build the details
+ Map item = new HashMap();
+ item.put(KEY_IS_TOPIC_POST, true);
+ item.put(KEY_TOPIC, topic.getNodeRef());
+ item.put(KEY_POST, primaryPost.getNodeRef());
+ item.put(KEY_CAN_EDIT, canUserEditPost(primaryPost, site));
+ item.put(KEY_AUTHOR, buildPerson(topic.getCreator()));
+
+ // The reply count is one less than all posts (first is the primary one)
+ item.put("totalReplyCount", numReplies);
+
+ // Add the topic site
+ item.put("site", topic.getShortSiteName());
+
+ // We want details on the most recent post
+ if (mostRecentPost != null)
+ {
+ item.put("lastReply", mostRecentPost.getNodeRef());
+ item.put("lastReplyBy", buildPerson(mostRecentPost.getCreator()));
+ }
+
+ // Include the tags
+ item.put("tags", topic.getTags());
+
+ // All done
+ return item;
+ }
+
+ /*
+ * Renders out the list of topics
+ * TODO Fetch the post data in one go, rather than one at a time
+ */
+ protected Map renderTopics(PagingResults topics,
+ PagingRequest paging, SiteInfo site)
+ {
+ return renderTopics(topics.getPage(), topics.getTotalResultCount(), paging, site);
+ }
+ /*
+ * Renders out the list of topics
+ * TODO Fetch the post data in one go, rather than one at a time
+ */
+ protected Map renderTopics(List topics,
+ Pair size, PagingRequest paging, SiteInfo site)
+ {
+ Map model = new HashMap();
+
+ // Paging info
+ model.put("total", size.getFirst());
+ model.put("pageSize", paging.getMaxItems());
+ model.put("startIndex", paging.getSkipCount());
+ model.put("itemCount", topics.size());
+
+ // Data
+ List> items = new ArrayList>();
+ for (TopicInfo topic : topics)
+ {
+ // ACE-772 fix of incorrect display of topics into "My Discussions" dashlet.
+ // Into "My Discussions" dashlet forum topic will be displayed only if user is a member of that site.
+ if (null == site && null != topic.getShortSiteName())
+ {
+ String currentUser = AuthenticationUtil.getFullyAuthenticatedUser();
+ String siteShortName = topic.getShortSiteName();
+ boolean isSiteMember = siteService.isMember(siteShortName, currentUser);
+
+ if (isSiteMember)
+ {
+ items.add(renderTopic(topic, site));
+ }
+ }
+ // Display all topics on the forum of the site.
+ else
+ {
+ items.add(renderTopic(topic, site));
+ }
+ }
+ model.put("items", items);
+
+ // All done
+ return model;
+ }
+
+ protected Map buildCommonModel(SiteInfo site, TopicInfo topic,
+ PostInfo post, WebScriptRequest req)
+ {
+ // Build the common model parts
+ Map model = new HashMap();
+ model.put(KEY_TOPIC, topic);
+ model.put(KEY_POST, post);
+
+ // Capture the site details only if site based
+ if (site != null)
+ {
+ model.put("siteId", site.getShortName());
+ model.put("site", site);
+ }
+
+ // The limit on the length of the content to be returned
+ int contentLength = -1;
+ String contentLengthS = req.getParameter("contentLength");
+ if (contentLengthS != null)
+ {
+ try
+ {
+ contentLength = Integer.parseInt(contentLengthS);
+ }
+ catch (NumberFormatException e)
+ {
+ logger.info("Skipping invalid length " + contentLengthS);
+ }
+ }
+ model.put("contentLength", contentLength);
+
+ // All done
+ return model;
+ }
+
+ @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))
+ {
+ JSONParser parser = new JSONParser();
+ try
+ {
+ json = (JSONObject)parser.parse(req.getContent().getContent());
+ }
+ catch (IOException io)
+ {
+ throw new WebScriptException(Status.STATUS_BAD_REQUEST, "Invalid JSON: " + io.getMessage());
+ }
+ catch (ParseException pe)
+ {
+ throw new WebScriptException(Status.STATUS_BAD_REQUEST, "Invalid JSON: " + pe.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);
+
+ if (topic == null)
+ {
+ String error = "Could not find topic '" + name + "' for site '" +
+ site.getShortName() + "'";
+ throw new WebScriptException(Status.STATUS_NOT_FOUND, error);
+ }
+ nodeRef = topic.getNodeRef();
+ }
+ else
+ {
+ // The NodeRef is the container (if it exists)
+ if (siteService.hasContainer(siteName, DiscussionServiceImpl.DISCUSSION_COMPONENT))
+ {
+ nodeRef = siteService.getContainer(siteName, DiscussionServiceImpl.DISCUSSION_COMPONENT);
+ }
+ }
+ }
+ 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();
+ }
+
+ // See if it's actually attached to a site
+ if (topic != null)
+ {
+ NodeRef container = topic.getContainerNodeRef();
+ if (container != null)
+ {
+ NodeRef maybeSite = nodeService.getPrimaryParent(container).getParentRef();
+ if (maybeSite != null)
+ {
+ // Try to make it a site, will return Null if it isn't one
+ site = siteService.getSite(maybeSite);
+ }
+ }
+ }
+ }
+ 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/src/main/java/org/alfresco/repo/web/scripts/discussion/ForumPostDelete.java b/src/main/java/org/alfresco/repo/web/scripts/discussion/ForumPostDelete.java
new file mode 100644
index 0000000000..68f3350b0c
--- /dev/null
+++ b/src/main/java/org/alfresco/repo/web/scripts/discussion/ForumPostDelete.java
@@ -0,0 +1,125 @@
+/*
+ * #%L
+ * Alfresco Remote API
+ * %%
+ * Copyright (C) 2005 - 2016 Alfresco Software Limited
+ * %%
+ * This file is part of the Alfresco software.
+ * If the software was purchased under a paid Alfresco license, the terms of
+ * the paid license agreement will prevail. Otherwise, the software is
+ * provided under the following open source license terms:
+ *
+ * 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 .
+ * #L%
+ */
+package org.alfresco.repo.web.scripts.discussion;
+
+import java.text.MessageFormat;
+import java.util.Map;
+import java.util.ResourceBundle;
+
+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.simple.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 MSG_NODE_MARKED_REMOVED = "forum-post.msg.marked.removed";
+ private static final String MSG_NODE_DELETED = "forum-post.msg.deleted";
+ 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)
+ {
+ final ResourceBundle rb = getResources();
+
+ // Build the common model parts
+ Map model = buildCommonModel(site, topic, post, req);
+
+
+ // Are we deleting a topic, or a post in it?
+ String message = null;
+ if (post != null)
+ {
+ message = doDeletePost(topic, post, rb);
+ }
+ else if (topic != null)
+ {
+ message = doDeleteTopic(topic, site, req, json, rb);
+ }
+ 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, ResourceBundle rb)
+ {
+ // 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
+ String message = rb.getString(MSG_NODE_DELETED);
+
+ return MessageFormat.format(message, topic.getNodeRef());
+ }
+
+ /**
+ * 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, ResourceBundle rb)
+ {
+ // 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
+
+ String message = rb.getString(MSG_NODE_MARKED_REMOVED);
+ return MessageFormat.format(message, post.getNodeRef());
+ }
+}
diff --git a/src/main/java/org/alfresco/repo/web/scripts/discussion/ForumPostGet.java b/src/main/java/org/alfresco/repo/web/scripts/discussion/ForumPostGet.java
new file mode 100644
index 0000000000..03ee838c19
--- /dev/null
+++ b/src/main/java/org/alfresco/repo/web/scripts/discussion/ForumPostGet.java
@@ -0,0 +1,74 @@
+/*
+ * #%L
+ * Alfresco Remote API
+ * %%
+ * Copyright (C) 2005 - 2016 Alfresco Software Limited
+ * %%
+ * This file is part of the Alfresco software.
+ * If the software was purchased under a paid Alfresco license, the terms of
+ * the paid license agreement will prevail. Otherwise, the software is
+ * provided under the following open source license terms:
+ *
+ * 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 .
+ * #L%
+ */
+package org.alfresco.repo.web.scripts.discussion;
+
+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.simple.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 discussions page fetching forum-post.get webscript.
+ *
+ * @author Nick Burch
+ * @since 4.0
+ */
+public class ForumPostGet extends AbstractDiscussionWebScript
+{
+ @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 = buildCommonModel(site, topic, post, req);
+
+ // Did they want just one post, or the whole of the topic?
+ if (post != null)
+ {
+ model.put(KEY_POSTDATA, renderPost(post, site));
+ }
+ else if (topic != null)
+ {
+ model.put(KEY_POSTDATA, renderTopic(topic, site));
+ }
+ else
+ {
+ String error = "Node was of the wrong type, only Topic and Post are supported";
+ throw new WebScriptException(Status.STATUS_BAD_REQUEST, error);
+ }
+
+ // All done
+ return model;
+ }
+}
diff --git a/src/main/java/org/alfresco/repo/web/scripts/discussion/ForumPostPut.java b/src/main/java/org/alfresco/repo/web/scripts/discussion/ForumPostPut.java
new file mode 100644
index 0000000000..1b53e97b16
--- /dev/null
+++ b/src/main/java/org/alfresco/repo/web/scripts/discussion/ForumPostPut.java
@@ -0,0 +1,140 @@
+/*
+ * #%L
+ * Alfresco Remote API
+ * %%
+ * Copyright (C) 2005 - 2016 Alfresco Software Limited
+ * %%
+ * This file is part of the Alfresco software.
+ * If the software was purchased under a paid Alfresco license, the terms of
+ * the paid license agreement will prevail. Otherwise, the software is
+ * provided under the following open source license terms:
+ *
+ * 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 .
+ * #L%
+ */
+package org.alfresco.repo.web.scripts.discussion;
+
+import java.util.List;
+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.simple.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 discussions page editing forum-post.put webscript.
+ *
+ * @author Nick Burch
+ * @since 4.0
+ */
+public class ForumPostPut extends AbstractDiscussionWebScript
+{
+ @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 = buildCommonModel(site, topic, post, req);
+
+ // Did they want to change a reply or the whole topic?
+ if (post != null)
+ {
+ // Update the specified post
+ doUpdatePost(post, post.getTopic(), req, json);
+
+ // Add the activity entry for the reply change
+ addActivityEntry("reply", "updated", post.getTopic(), post, site, req, json);
+
+ // Build the JSON for just this post
+ model.put(KEY_POSTDATA, renderPost(post, site));
+ }
+ else if (topic != null)
+ {
+ // Update the primary post of the topic
+ post = discussionService.getPrimaryPost(topic);
+ if (post == null)
+ {
+ throw new WebScriptException(Status.STATUS_PRECONDITION_FAILED,
+ "First (primary) post was missing from the topic, can't fetch");
+ }
+ doUpdatePost(post, topic, req, json);
+
+ // Add the activity entry for the topic change
+ addActivityEntry("post", "updated", topic, null, site, req, json);
+
+ // Build the JSON for the whole topic
+ model.put(KEY_POSTDATA, renderTopic(topic, site));
+ }
+ else
+ {
+ String error = "Node was of the wrong type, only Topic and Post are supported";
+ throw new WebScriptException(Status.STATUS_BAD_REQUEST, error);
+ }
+
+ // All done
+ return model;
+ }
+
+ private void doUpdatePost(PostInfo post, TopicInfo topic, WebScriptRequest req,
+ JSONObject json)
+ {
+ boolean updateTopic = false;
+ // Fetch the details from the JSON
+
+ // Update the titles on the post and it's topic
+ if (json.containsKey("title"))
+ {
+ String title = (String)json.get("title");
+ post.setTitle(title);
+ if (title.length() > 0)
+ {
+ updateTopic = true;
+ topic.setTitle(title);
+ }
+ }
+
+ // Contents is on the post
+ if (json.containsKey("content"))
+ {
+ post.setContents((String)json.get("content"));
+ }
+
+ // Tags are on the topic
+ if (json.containsKey("tags"))
+ {
+ topic.getTags().clear();
+
+ List tags = getTags(json);
+ if (tags != null)
+ {
+ topic.getTags().addAll(tags);
+ }
+ updateTopic = true;
+ }
+
+ // Save the topic and the post
+ if (updateTopic == true)
+ {
+ discussionService.updateTopic(topic);
+ }
+ discussionService.updatePost(post);
+ }
+}
diff --git a/src/main/java/org/alfresco/repo/web/scripts/discussion/ForumPostRepliesGet.java b/src/main/java/org/alfresco/repo/web/scripts/discussion/ForumPostRepliesGet.java
new file mode 100644
index 0000000000..35718fc934
--- /dev/null
+++ b/src/main/java/org/alfresco/repo/web/scripts/discussion/ForumPostRepliesGet.java
@@ -0,0 +1,111 @@
+/*
+ * #%L
+ * Alfresco Remote API
+ * %%
+ * Copyright (C) 2005 - 2016 Alfresco Software Limited
+ * %%
+ * This file is part of the Alfresco software.
+ * If the software was purchased under a paid Alfresco license, the terms of
+ * the paid license agreement will prevail. Otherwise, the software is
+ * provided under the following open source license terms:
+ *
+ * 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 .
+ * #L%
+ */
+package org.alfresco.repo.web.scripts.discussion;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+import org.alfresco.service.cmr.discussion.PostInfo;
+import org.alfresco.service.cmr.discussion.PostWithReplies;
+import org.alfresco.service.cmr.discussion.TopicInfo;
+import org.alfresco.service.cmr.repository.NodeRef;
+import org.alfresco.service.cmr.site.SiteInfo;
+import org.json.simple.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 discussions page creating forum-post-replies.get webscript.
+ *
+ * @author Nick Burch
+ * @since 4.0
+ */
+public class ForumPostRepliesGet extends AbstractDiscussionWebScript
+{
+ @Override
+ protected Map executeImpl(SiteInfo site, NodeRef nodeRef,
+ TopicInfo topic, PostInfo post, WebScriptRequest req, JSONObject json,
+ Status status, Cache cache)
+ {
+ // How many levels did they want?
+ int levels = 1;
+ String levelsS = req.getParameter("levels");
+ if (levelsS != null)
+ {
+ try
+ {
+ levels = Integer.parseInt(levelsS);
+ }
+ catch (NumberFormatException e)
+ {
+ throw new WebScriptException(Status.STATUS_BAD_REQUEST, "Level depth parameter invalid");
+ }
+ }
+
+ // Fetch the replies
+ PostWithReplies replies;
+ if (post != null)
+ {
+ replies = discussionService.listPostReplies(post, levels);
+ }
+ else if (topic != null)
+ {
+ replies = discussionService.listPostReplies(topic, levels);
+ }
+ else
+ {
+ String error = "Node was of the wrong type, only Topic and Post are supported";
+ throw new WebScriptException(Status.STATUS_BAD_REQUEST, error);
+ }
+
+ // Build the common model parts
+ Map model = buildCommonModel(site, topic, post, req);
+
+ // Build the JSON for the replies
+ model.put("data", renderReplies(replies, site).get("children"));
+
+ // All done
+ return model;
+ }
+
+ private Map renderReplies(PostWithReplies replies, SiteInfo site)
+ {
+ Map reply = renderPost(replies.getPost(), site);
+ reply.put("childCount", replies.getReplies().size());
+
+ List> r = new ArrayList>();
+ for (PostWithReplies child : replies.getReplies())
+ {
+ r.add(renderReplies(child, site));
+ }
+ reply.put("children", r);
+
+ return reply;
+ }
+}
diff --git a/src/main/java/org/alfresco/repo/web/scripts/discussion/ForumPostRepliesPost.java b/src/main/java/org/alfresco/repo/web/scripts/discussion/ForumPostRepliesPost.java
new file mode 100644
index 0000000000..a0d3e38247
--- /dev/null
+++ b/src/main/java/org/alfresco/repo/web/scripts/discussion/ForumPostRepliesPost.java
@@ -0,0 +1,117 @@
+/*
+ * #%L
+ * Alfresco Remote API
+ * %%
+ * Copyright (C) 2005 - 2016 Alfresco Software Limited
+ * %%
+ * This file is part of the Alfresco software.
+ * If the software was purchased under a paid Alfresco license, the terms of
+ * the paid license agreement will prevail. Otherwise, the software is
+ * provided under the following open source license terms:
+ *
+ * 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 .
+ * #L%
+ */
+package org.alfresco.repo.web.scripts.discussion;
+
+import java.util.Map;
+
+import org.alfresco.model.ContentModel;
+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.simple.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 discussions page creating forum-post-replies.post webscript.
+ *
+ * @author Nick Burch
+ * @since 4.0
+ */
+public class ForumPostRepliesPost extends AbstractDiscussionWebScript
+{
+ @Override
+ protected Map executeImpl(SiteInfo site, NodeRef nodeRef,
+ TopicInfo topic, PostInfo post, WebScriptRequest req, JSONObject json,
+ Status status, Cache cache)
+ {
+ // If they're trying to create a reply to a topic, they actually
+ // mean to create the reply on the primary post
+ if (post == null)
+ {
+ post = discussionService.getPrimaryPost(topic);
+ if (post == null)
+ {
+ throw new WebScriptException(Status.STATUS_PRECONDITION_FAILED,
+ "First (primary) post was missing from the topic, can't fetch");
+ }
+ }
+ else if (topic == null)
+ {
+ String error = "Node was of the wrong type, only Topic and Post are supported";
+ throw new WebScriptException(Status.STATUS_BAD_REQUEST, error);
+ }
+
+ // Have the reply created
+ PostInfo reply = doCreatePost(post, topic, req, json);
+
+ // Add the activity entry for the reply change
+ addActivityEntry("reply", "created", topic, reply, site, req, json);
+
+ // Build the common model parts
+ Map model = buildCommonModel(site, topic, reply, req);
+
+ // Build the JSON for the new reply post
+ model.put(KEY_POSTDATA, renderPost(reply, site));
+
+ // All done
+ return model;
+ }
+
+ private PostInfo doCreatePost(PostInfo post, TopicInfo topic, WebScriptRequest req,
+ JSONObject json)
+ {
+ // Fetch the details from the JSON
+ String title = null;
+ if (json.containsKey("title"))
+ {
+ title = (String)json.get("title");
+ }
+
+ String contents = null;
+ if (json.containsKey("content"))
+ {
+ contents = (String)json.get("content");
+ }
+
+
+ // Create the reply
+ PostInfo reply = discussionService.createReply(post, contents);
+
+ // Set the title if needed (it normally isn't)
+ if (title != null && title.length() > 0)
+ {
+ nodeService.setProperty(reply.getNodeRef(), ContentModel.PROP_TITLE, title);
+ reply = discussionService.getPost(topic, reply.getSystemName());
+ }
+
+ // All done
+ return reply;
+ }
+}
diff --git a/src/main/java/org/alfresco/repo/web/scripts/discussion/ForumTopicPost.java b/src/main/java/org/alfresco/repo/web/scripts/discussion/ForumTopicPost.java
new file mode 100644
index 0000000000..6bbe4394d7
--- /dev/null
+++ b/src/main/java/org/alfresco/repo/web/scripts/discussion/ForumTopicPost.java
@@ -0,0 +1,110 @@
+/*
+ * #%L
+ * Alfresco Remote API
+ * %%
+ * Copyright (C) 2005 - 2016 Alfresco Software Limited
+ * %%
+ * This file is part of the Alfresco software.
+ * If the software was purchased under a paid Alfresco license, the terms of
+ * the paid license agreement will prevail. Otherwise, the software is
+ * provided under the following open source license terms:
+ *
+ * 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 .
+ * #L%
+ */
+package org.alfresco.repo.web.scripts.discussion;
+
+import java.util.List;
+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.simple.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 discussions page editing forum-posts.post webscript.
+ *
+ * @author Nick Burch
+ * @since 4.0
+ */
+public class ForumTopicPost extends AbstractDiscussionWebScript
+{
+ @Override
+ protected Map executeImpl(SiteInfo site, NodeRef nodeRef,
+ TopicInfo topic, PostInfo post, WebScriptRequest req, JSONObject json,
+ Status status, Cache cache)
+ {
+ // They shouldn't be adding to an existing Post or Topic
+ if (topic != null || post != null)
+ {
+ String error = "Can't create a new Topic inside an existing Topic or Post";
+ throw new WebScriptException(Status.STATUS_BAD_REQUEST, error);
+ }
+
+
+ // Grab the details of the new Topic and Post
+ String title = "";
+ String contents = "";
+ if (json.containsKey("title"))
+ {
+ title = (String)json.get("title");
+ }
+ if (json.containsKey("content"))
+ {
+ contents = (String)json.get("content");
+ }
+ List tags = getTags(json);
+
+
+ // Have the topic created
+ if (site != null)
+ {
+ topic = discussionService.createTopic(site.getShortName(), title);
+ }
+ else
+ {
+ topic = discussionService.createTopic(nodeRef, title);
+ }
+ if (tags != null && tags.size() > 0)
+ {
+ topic.getTags().clear();
+ topic.getTags().addAll(tags);
+ discussionService.updateTopic(topic);
+ }
+
+
+ // Have the primary post created
+ post = discussionService.createPost(topic, contents);
+
+
+ // Record the activity
+ addActivityEntry("post", "created", topic, post, site, req, json);
+
+
+ // Build the common model parts
+ Map model = buildCommonModel(site, topic, post, req);
+
+ // Build the JSON for the whole topic
+ model.put(KEY_POSTDATA, renderTopic(topic, site));
+
+ // All done
+ return model;
+ }
+}
diff --git a/src/main/java/org/alfresco/repo/web/scripts/discussion/ForumTopicsFilteredGet.java b/src/main/java/org/alfresco/repo/web/scripts/discussion/ForumTopicsFilteredGet.java
new file mode 100644
index 0000000000..a4e9c40649
--- /dev/null
+++ b/src/main/java/org/alfresco/repo/web/scripts/discussion/ForumTopicsFilteredGet.java
@@ -0,0 +1,414 @@
+/*
+ * #%L
+ * Alfresco Remote API
+ * %%
+ * Copyright (C) 2005 - 2016 Alfresco Software Limited
+ * %%
+ * This file is part of the Alfresco software.
+ * If the software was purchased under a paid Alfresco license, the terms of
+ * the paid license agreement will prevail. Otherwise, the software is
+ * provided under the following open source license terms:
+ *
+ * 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 .
+ * #L%
+ */
+package org.alfresco.repo.web.scripts.discussion;
+
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Calendar;
+import java.util.Comparator;
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeSet;
+
+import org.alfresco.query.EmptyPagingResults;
+import org.alfresco.query.PagingRequest;
+import org.alfresco.query.PagingResults;
+import org.alfresco.repo.discussion.TopicInfoImpl;
+import org.alfresco.repo.security.authentication.AuthenticationUtil;
+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.StoreRef;
+import org.alfresco.service.cmr.search.LimitBy;
+import org.alfresco.service.cmr.search.ResultSet;
+import org.alfresco.service.cmr.search.ResultSetRow;
+import org.alfresco.service.cmr.search.SearchParameters;
+import org.alfresco.service.cmr.search.SearchService;
+import org.alfresco.service.cmr.site.SiteInfo;
+import org.alfresco.util.ISO9075;
+import org.alfresco.util.Pair;
+import org.alfresco.util.ScriptPagingDetails;
+import org.json.simple.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;
+
+/**
+ * Gets topics matching the filters passed to it in the URL.
+ *
+ * topics = 'mine' (searches for posts by the user) or 'all' (ignores the author in the search)
+ * history = days in the past to search
+ * resultSize = the number of topics returned in the results
+ *
+ * @author Jamie Allison
+ */
+public class ForumTopicsFilteredGet extends AbstractDiscussionWebScript
+{
+ //Filter Defaults
+ protected static final String DEFAULT_TOPIC_AUTHOR = "mine";
+ protected static final int DEFAULT_TOPIC_LATEST_POST_DAYS_AGO = 1;
+ protected static final int DEFAULT_MAX_RESULTS = 10;
+
+ protected static final StoreRef SPACES_STORE = new StoreRef(StoreRef.PROTOCOL_WORKSPACE, "SpacesStore");
+
+ protected static final String SEARCH_QUERY = "TYPE:\"{http://www.alfresco.org/model/forum/1.0}post\""
+ + " AND PATH:\"/app:company_home/st:sites/%s/cm:discussions/*/*\""
+ + " AND @cm:created:[\"%s\" TO NOW]";
+
+ /** Spring-injected services */
+ private SearchService searchService;
+
+ /**
+ * Sets the searchService.
+ *
+ * @param searchService SearchService
+ */
+ public void setSearchService(SearchService searchService)
+ {
+ this.searchService = searchService;
+ }
+
+ /**
+ * Overrides AbstractDiscussionWebScript to allow a null site
+ *
+ * @param req WebScriptRequest
+ * @param status Status
+ * @param cache Cache
+ *
+ * @return Map
+ */
+ @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);
+ }
+
+ SiteInfo site = 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);
+ }
+ }
+
+ // Have the real work done
+ return executeImpl(site, null, null, null, req, null, null, null);
+ }
+
+ /**
+ * @param site SiteInfo
+ * @param nodeRef Not required. It is only included because it is overriding the parent class.
+ * @param topic Not required. It is only included because it is overriding the parent class.
+ * @param post Not required. It is only included because it is overriding the parent class.
+ * @param req WebScriptRequest
+ * @param status Not required. It is only included because it is overriding the parent class.
+ * @param cache Not required. It is only included because it is overriding the parent class.
+ *
+ * @return Map
+ */
+ @Override
+ protected Map executeImpl(SiteInfo site, NodeRef nodeRef, TopicInfo topic,
+ PostInfo post, WebScriptRequest req, JSONObject json, Status status, Cache cache)
+ {
+ // They shouldn't be trying to list of an existing Post or Topic
+ if (topic != null || post != null)
+ {
+ String error = "Can't list Topics inside an existing Topic or Post";
+ throw new WebScriptException(Status.STATUS_BAD_REQUEST, error);
+ }
+
+ // Set search filter to users topics or all topics
+ String pAuthor = req.getParameter("topics");
+ String author = DEFAULT_TOPIC_AUTHOR;
+ if (pAuthor != null)
+ {
+ author = pAuthor;
+ }
+ // Set the number of days in the past to search from
+ String pDaysAgo = req.getParameter("history");
+ int daysAgo = DEFAULT_TOPIC_LATEST_POST_DAYS_AGO;
+ if (pDaysAgo != null)
+ {
+ try
+ {
+ daysAgo = Integer.parseInt(pDaysAgo);
+ }
+ catch (NumberFormatException e)
+ {
+ //do nothing. history has already been preset to the default value.
+ }
+ }
+
+ // Get the complete search query
+ Pair searchQuery = getSearchQuery(site, author, daysAgo);
+
+ // Get the filtered topics
+ PagingRequest paging = buildPagingRequest(req);
+ PagingResults topics = doSearch(searchQuery, false, paging);
+
+ // Build the common model parts
+ Map model = buildCommonModel(site, topic, post, req);
+
+ // Have the topics rendered
+ model.put("data", renderTopics(topics, paging, site));
+
+ // All done
+ return model;
+ }
+
+ /**
+ * Do the actual search
+ *
+ * @param searchQuery Pair with query string in first and query language in second
+ * @param sortAscending boolean
+ * @param paging PagingRequest
+ */
+ protected PagingResults doSearch(Pair searchQuery, boolean sortAscending, PagingRequest paging)
+ {
+ ResultSet resultSet = null;
+ PagingResults pagedResults = new EmptyPagingResults();
+
+ String sortOn = "@{http://www.alfresco.org/model/content/1.0}created";
+
+ // Setup the search parameters
+ SearchParameters sp = new SearchParameters();
+ sp.addStore(SPACES_STORE);
+ sp.setQuery(searchQuery.getFirst());
+ sp.setLanguage(searchQuery.getSecond());
+ sp.addSort(sortOn, sortAscending);
+ if (paging.getMaxItems() > 0)
+ {
+ //Multiply maxItems by 10. This is to catch topics that have multiple replies and ensure that the maximum number of topics is shown.
+ sp.setLimit(paging.getMaxItems()*10);
+ sp.setLimitBy(LimitBy.FINAL_SIZE);
+ }
+ if (paging.getSkipCount() > 0)
+ {
+ sp.setSkipCount(paging.getSkipCount());
+ }
+
+ try
+ {
+ resultSet = searchService.query(sp);
+ pagedResults = wrap(resultSet, paging);
+ }
+ finally
+ {
+ try
+ {
+ resultSet.close();
+ }
+ catch(Exception e)
+ {
+ //do nothing
+ }
+ }
+
+ return pagedResults;
+ }
+
+ /**
+ * Build the search query from the passed in parameters and SEARCH_QUERY constant
+ *
+ * @param site SiteInfo
+ * @param author String
+ * @param daysAgo int
+ * @return Pair with the query string in first and query language in second
+ */
+ protected Pair getSearchQuery(SiteInfo site, String author, int daysAgo)
+ {
+ String search = String.format(SEARCH_QUERY,
+ (site != null ? "cm:" + ISO9075.encode(site.getShortName()) : "*"),
+ getDateXDaysAgo(daysAgo)
+ );
+
+ // If author equals 'mine' add cm:creator to the search query otherwise leave out
+ if(author.equals(DEFAULT_TOPIC_AUTHOR))
+ {
+ search += " AND @cm:creator:\"" + AuthenticationUtil.getFullyAuthenticatedUser() + "\"";
+ }
+
+ // Add the query string and language to the returned results
+ Pair searchQuery = new Pair(search, SearchService.LANGUAGE_FTS_ALFRESCO);
+
+ return searchQuery;
+ }
+
+ /**
+ * Get the date x days ago in the format 'yyyy-MM-dd'
+ *
+ * @param daysAgo int
+ * @return String
+ */
+ protected String getDateXDaysAgo(int daysAgo)
+ {
+ Calendar calendar = Calendar.getInstance();
+ calendar.add(Calendar.DAY_OF_MONTH, (daysAgo * -1));
+ SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
+
+ return sdf.format(calendar.getTime());
+ }
+
+ /**
+ * Builds up a listing Paging request, based on the arguments specified in the URL
+ *
+ * @param req WebScriptRequest
+ * @return PagingRequest
+ */
+ @Override
+ protected PagingRequest buildPagingRequest(WebScriptRequest req)
+ {
+ // Grab the number of topics to return
+ String pResultSize = req.getParameter("resultSize");
+ int resultSize = DEFAULT_MAX_RESULTS;
+ if (pResultSize != null)
+ {
+ try
+ {
+ resultSize = Integer.parseInt(pResultSize);
+ }
+ catch (NumberFormatException e)
+ {
+ //do nothing. ResultSize has already been preset to the default value.
+ }
+ }
+ return new ScriptPagingDetails(req, resultSize);
+ }
+
+ /**
+ * Wrap up search results as {@link TopicInfo} instances
+ *
+ * @param finalResults ResultSet
+ * @param paging PagingRequest
+ */
+ protected PagingResults wrap(final ResultSet finalResults, PagingRequest paging)
+ {
+ int maxItems = paging.getMaxItems();
+ Comparator lastPostDesc = new Comparator()
+ {
+ @Override
+ public int compare(TopicInfo t1, TopicInfo t2)
+ {
+ Date t1LastPostDate = t1.getCreatedAt();
+ if(discussionService.getMostRecentPost(t1) != null)
+ {
+ t1LastPostDate = discussionService.getMostRecentPost(t1).getCreatedAt();
+ }
+
+ Date t2LastPostDate = t2.getCreatedAt();
+ if(discussionService.getMostRecentPost(t2) != null)
+ {
+ t2LastPostDate = discussionService.getMostRecentPost(t2).getCreatedAt();
+ }
+ return t2LastPostDate.compareTo(t1LastPostDate);
+ }
+ };
+
+ final Set topics = new TreeSet(lastPostDesc);
+
+ for (ResultSetRow row : finalResults)
+ {
+ Pair pair = discussionService.getForNodeRef(row.getNodeRef());
+ TopicInfo topic = pair.getFirst();
+ if(topic != null)
+ {
+ String path = nodeService.getPath(topic.getNodeRef()).toDisplayPath(nodeService, permissionService);
+ String site = path.split("/")[3];
+ TopicInfoImpl tii = (TopicInfoImpl)topic;
+ tii.setShortSiteName(site);
+ topics.add(tii);
+
+ if(topics.size() >= maxItems)
+ {
+ break;
+ }
+ }
+ }
+
+ // Wrap
+ return new PagingResults()
+ {
+ @Override
+ public boolean hasMoreItems()
+ {
+ try
+ {
+ return finalResults.hasMore();
+ }
+ catch(UnsupportedOperationException e)
+ {
+ // Not all search results support paging
+ return false;
+ }
+ }
+
+ @Override
+ public Pair getTotalResultCount()
+ {
+ int skipCount = 0;
+ int itemsRemainingAfterThisPage = 0;
+ try
+ {
+ skipCount = finalResults.getStart();
+ }
+ catch(UnsupportedOperationException e) {}
+ try
+ {
+ itemsRemainingAfterThisPage = finalResults.length();
+ }
+ catch(UnsupportedOperationException e) {}
+
+ final int totalItemsInUnpagedResultSet = skipCount + itemsRemainingAfterThisPage;
+ return new Pair(totalItemsInUnpagedResultSet, totalItemsInUnpagedResultSet);
+ }
+
+ @Override
+ public List getPage()
+ {
+ return new ArrayList(topics);
+ }
+
+ @Override
+ public String getQueryExecutionId()
+ {
+ return null;
+ }
+ };
+ }
+}
diff --git a/src/main/java/org/alfresco/repo/web/scripts/discussion/ForumTopicsGet.java b/src/main/java/org/alfresco/repo/web/scripts/discussion/ForumTopicsGet.java
new file mode 100644
index 0000000000..662e54f1c4
--- /dev/null
+++ b/src/main/java/org/alfresco/repo/web/scripts/discussion/ForumTopicsGet.java
@@ -0,0 +1,121 @@
+/*
+ * #%L
+ * Alfresco Remote API
+ * %%
+ * Copyright (C) 2005 - 2016 Alfresco Software Limited
+ * %%
+ * This file is part of the Alfresco software.
+ * If the software was purchased under a paid Alfresco license, the terms of
+ * the paid license agreement will prevail. Otherwise, the software is
+ * provided under the following open source license terms:
+ *
+ * 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 .
+ * #L%
+ */
+package org.alfresco.repo.web.scripts.discussion;
+
+import java.util.Map;
+
+import org.alfresco.query.PagingRequest;
+import org.alfresco.query.PagingResults;
+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.simple.JSONObject;
+import org.springframework.extensions.surf.util.URLDecoder;
+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 discussions topics fetching forum-posts.get webscript.
+ *
+ * @author Nick Burch
+ * @since 4.0
+ */
+public class ForumTopicsGet extends AbstractDiscussionWebScript
+{
+ @Override
+ protected Map executeImpl(SiteInfo site, NodeRef nodeRef,
+ TopicInfo topic, PostInfo post, WebScriptRequest req, JSONObject json,
+ Status status, Cache cache)
+ {
+ // They shouldn't be trying to list of an existing Post or Topic
+ if (topic != null || post != null)
+ {
+ String error = "Can't list Topics inside an existing Topic or Post";
+ throw new WebScriptException(Status.STATUS_BAD_REQUEST, error);
+ }
+
+ // Do we need to list or search?
+ boolean tagSearch = false;
+ String tag = req.getParameter("tag");
+ if (tag != null && tag.length() > 0)
+ {
+ tagSearch = true;
+
+ // Tags can be full unicode strings, so decode
+ tag = URLDecoder.decode(tag);
+ }
+
+ // Get the topics
+ boolean oldestTopicsFirst = false;
+ PagingResults topics = null;
+ PagingRequest paging = buildPagingRequest(req);
+ if (tagSearch)
+ {
+ // Tag based is a search rather than a listing
+ if (site != null)
+ {
+ topics = discussionService.findTopics(site.getShortName(), null, tag, oldestTopicsFirst, paging);
+ }
+ else
+ {
+ topics = discussionService.findTopics(nodeRef, null, tag, oldestTopicsFirst, paging);
+ }
+ }
+ else
+ {
+ if (site != null)
+ {
+ topics = discussionService.listTopics(site.getShortName(), oldestTopicsFirst, paging);
+ }
+ else
+ {
+ topics = discussionService.listTopics(nodeRef, oldestTopicsFirst, buildPagingRequest(req));
+ }
+ }
+
+
+ // If they did a site based search, and the component hasn't
+ // been created yet, use the site for the permissions checking
+ if (site != null && nodeRef == null)
+ {
+ nodeRef = site.getNodeRef();
+ }
+
+
+ // Build the common model parts
+ Map model = buildCommonModel(site, topic, post, req);
+ model.put("forum", nodeRef);
+
+ // Have the topics rendered
+ model.put("data", renderTopics(topics, paging, site));
+
+ // All done
+ return model;
+ }
+}
diff --git a/src/main/java/org/alfresco/repo/web/scripts/discussion/ForumTopicsHotGet.java b/src/main/java/org/alfresco/repo/web/scripts/discussion/ForumTopicsHotGet.java
new file mode 100644
index 0000000000..579bf91a50
--- /dev/null
+++ b/src/main/java/org/alfresco/repo/web/scripts/discussion/ForumTopicsHotGet.java
@@ -0,0 +1,118 @@
+/*
+ * #%L
+ * Alfresco Remote API
+ * %%
+ * Copyright (C) 2005 - 2016 Alfresco Software Limited
+ * %%
+ * This file is part of the Alfresco software.
+ * If the software was purchased under a paid Alfresco license, the terms of
+ * the paid license agreement will prevail. Otherwise, the software is
+ * provided under the following open source license terms:
+ *
+ * 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 .
+ * #L%
+ */
+package org.alfresco.repo.web.scripts.discussion;
+
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+
+import org.alfresco.query.PagingRequest;
+import org.alfresco.query.PagingResults;
+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.alfresco.util.Pair;
+import org.json.simple.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 discussions topics fetching forum-posts-hot.get webscript.
+ *
+ * @author Nick Burch
+ * @since 4.0
+ */
+public class ForumTopicsHotGet extends AbstractDiscussionWebScript
+{
+ protected static final int RECENT_POSTS_PERIOD_DAYS = 30;
+ protected static final long ONE_DAY_MS = 24*60*60*1000;
+
+ @Override
+ protected Map executeImpl(SiteInfo site, NodeRef nodeRef,
+ TopicInfo topic, PostInfo post, WebScriptRequest req, JSONObject json,
+ Status status, Cache cache)
+ {
+ // They shouldn't be trying to list of an existing Post or Topic
+ if (topic != null || post != null)
+ {
+ String error = "Can't list Topics inside an existing Topic or Post";
+ throw new WebScriptException(Status.STATUS_BAD_REQUEST, error);
+ }
+
+ // Grab the date range to search over
+ String numDaysS = req.getParameter("numdays");
+ int numDays = RECENT_POSTS_PERIOD_DAYS;
+ if (numDaysS != null)
+ {
+ numDays = Integer.parseInt(numDaysS);
+ }
+
+ Date now = new Date();
+ Date since = new Date(now.getTime() - numDays*ONE_DAY_MS);
+
+ // Get the topics with recent replies
+ PagingResults> topicsAndCounts = null;
+ PagingRequest paging = buildPagingRequest(req);
+ if (site != null)
+ {
+ topicsAndCounts = discussionService.listHotTopics(site.getShortName(), since, paging);
+ }
+ else
+ {
+ topicsAndCounts = discussionService.listHotTopics(nodeRef, since, paging);
+ }
+
+ // For this, we actually only need the topics, not their counts too
+ List topics = new ArrayList();
+ for (Pair tc : topicsAndCounts.getPage())
+ {
+ topics.add(tc.getFirst());
+ }
+
+
+ // If they did a site based search, and the component hasn't
+ // been created yet, use the site for the permissions checking
+ if (site != null && nodeRef == null)
+ {
+ nodeRef = site.getNodeRef();
+ }
+
+
+ // Build the common model parts
+ Map model = buildCommonModel(site, topic, post, req);
+ model.put("forum", nodeRef);
+
+ // Have the topics rendered
+ model.put("data", renderTopics(topics, topicsAndCounts.getTotalResultCount(), paging, site));
+
+ // All done
+ return model;
+ }
+}
diff --git a/src/main/java/org/alfresco/repo/web/scripts/discussion/ForumTopicsMineGet.java b/src/main/java/org/alfresco/repo/web/scripts/discussion/ForumTopicsMineGet.java
new file mode 100644
index 0000000000..175b49042a
--- /dev/null
+++ b/src/main/java/org/alfresco/repo/web/scripts/discussion/ForumTopicsMineGet.java
@@ -0,0 +1,97 @@
+/*
+ * #%L
+ * Alfresco Remote API
+ * %%
+ * Copyright (C) 2005 - 2016 Alfresco Software Limited
+ * %%
+ * This file is part of the Alfresco software.
+ * If the software was purchased under a paid Alfresco license, the terms of
+ * the paid license agreement will prevail. Otherwise, the software is
+ * provided under the following open source license terms:
+ *
+ * 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 .
+ * #L%
+ */
+package org.alfresco.repo.web.scripts.discussion;
+
+import java.util.Map;
+
+import org.alfresco.query.PagingRequest;
+import org.alfresco.query.PagingResults;
+import org.alfresco.repo.security.authentication.AuthenticationUtil;
+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.simple.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 discussions topics fetching forum-posts-mine.get webscript.
+ *
+ * @author Nick Burch
+ * @since 4.0
+ */
+public class ForumTopicsMineGet extends AbstractDiscussionWebScript
+{
+ @Override
+ protected Map executeImpl(SiteInfo site, NodeRef nodeRef,
+ TopicInfo topic, PostInfo post, WebScriptRequest req, JSONObject json,
+ Status status, Cache cache)
+ {
+ // They shouldn't be trying to list of an existing Post or Topic
+ if (topic != null || post != null)
+ {
+ String error = "Can't list Topics inside an existing Topic or Post";
+ throw new WebScriptException(Status.STATUS_BAD_REQUEST, error);
+ }
+
+ // Grab the current user to list for
+ String username = AuthenticationUtil.getFullyAuthenticatedUser();
+
+ // Get the topics for the user, oldest first
+ PagingResults topics = null;
+ PagingRequest paging = buildPagingRequest(req);
+ if (site != null)
+ {
+ topics = discussionService.listTopics(site.getShortName(), username, true, paging);
+ }
+ else
+ {
+ topics = discussionService.listTopics(nodeRef, username, true, paging);
+ }
+
+
+ // If they did a site based search, and the component hasn't
+ // been created yet, use the site for the permissions checking
+ if (site != null && nodeRef == null)
+ {
+ nodeRef = site.getNodeRef();
+ }
+
+
+ // Build the common model parts
+ Map model = buildCommonModel(site, topic, post, req);
+ model.put("forum", nodeRef);
+
+ // Have the topics rendered
+ model.put("data", renderTopics(topics, paging, site));
+
+ // All done
+ return model;
+ }
+}
diff --git a/src/main/java/org/alfresco/repo/web/scripts/discussion/ForumTopicsRecentGet.java b/src/main/java/org/alfresco/repo/web/scripts/discussion/ForumTopicsRecentGet.java
new file mode 100644
index 0000000000..b9ea4a644d
--- /dev/null
+++ b/src/main/java/org/alfresco/repo/web/scripts/discussion/ForumTopicsRecentGet.java
@@ -0,0 +1,109 @@
+/*
+ * #%L
+ * Alfresco Remote API
+ * %%
+ * Copyright (C) 2005 - 2016 Alfresco Software Limited
+ * %%
+ * This file is part of the Alfresco software.
+ * If the software was purchased under a paid Alfresco license, the terms of
+ * the paid license agreement will prevail. Otherwise, the software is
+ * provided under the following open source license terms:
+ *
+ * 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 .
+ * #L%
+ */
+package org.alfresco.repo.web.scripts.discussion;
+
+import java.util.Date;
+import java.util.Map;
+
+import org.alfresco.query.PagingRequest;
+import org.alfresco.query.PagingResults;
+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.simple.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 discussions topics fetching forum-posts-new.get webscript.
+ *
+ * @author Nick Burch
+ * @since 4.0
+ */
+public class ForumTopicsRecentGet extends AbstractDiscussionWebScript
+{
+ protected static final int RECENT_SEARCH_PERIOD_DAYS = 7;
+ protected static final long ONE_DAY_MS = 24*60*60*1000;
+
+ @Override
+ protected Map executeImpl(SiteInfo site, NodeRef nodeRef,
+ TopicInfo topic, PostInfo post, WebScriptRequest req, JSONObject json,
+ Status status, Cache cache)
+ {
+ // They shouldn't be trying to list of an existing Post or Topic
+ if (topic != null || post != null)
+ {
+ String error = "Can't list Topics inside an existing Topic or Post";
+ throw new WebScriptException(Status.STATUS_BAD_REQUEST, error);
+ }
+
+ // Grab the date range to search over
+ String numDaysS = req.getParameter("numdays");
+ int numDays = RECENT_SEARCH_PERIOD_DAYS;
+ if (numDaysS != null)
+ {
+ numDays = Integer.parseInt(numDaysS);
+ }
+
+ Date now = new Date();
+ Date from = new Date(now.getTime() - numDays*ONE_DAY_MS);
+ Date to = new Date(now.getTime() + ONE_DAY_MS);
+
+ // Get the recent topics, newest first
+ PagingResults topics = null;
+ PagingRequest paging = buildPagingRequest(req);
+ if (site != null)
+ {
+ topics = discussionService.listTopics(site.getShortName(), from, to, false, paging);
+ }
+ else
+ {
+ topics = discussionService.listTopics(nodeRef, from, to, false, paging);
+ }
+
+
+ // If they did a site based search, and the component hasn't
+ // been created yet, use the site for the permissions checking
+ if (site != null && nodeRef == null)
+ {
+ nodeRef = site.getNodeRef();
+ }
+
+
+ // Build the common model parts
+ Map model = buildCommonModel(site, topic, post, req);
+ model.put("forum", nodeRef);
+
+ // Have the topics rendered
+ model.put("data", renderTopics(topics, paging, site));
+
+ // All done
+ return model;
+ }
+}
diff --git a/src/main/resources/alfresco/blogs/web-scripts-blog-context.xml b/src/main/resources/alfresco/blogs/web-scripts-blog-context.xml
new file mode 100644
index 0000000000..c2b0334acf
--- /dev/null
+++ b/src/main/resources/alfresco/blogs/web-scripts-blog-context.xml
@@ -0,0 +1,71 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/main/resources/alfresco/public-rest-context.xml b/src/main/resources/alfresco/public-rest-context.xml
index 064d5d31ae..d5500f0e1a 100644
--- a/src/main/resources/alfresco/public-rest-context.xml
+++ b/src/main/resources/alfresco/public-rest-context.xml
@@ -328,7 +328,16 @@
-
+
+
+
+
+
+ org.alfresco.discussions.reply-created
+
+
+
+
diff --git a/src/main/resources/alfresco/templates/activities/org/alfresco/blog/post-created.atomentry.ftl b/src/main/resources/alfresco/templates/activities/org/alfresco/blog/post-created.atomentry.ftl
new file mode 100644
index 0000000000..86fd85a509
--- /dev/null
+++ b/src/main/resources/alfresco/templates/activities/org/alfresco/blog/post-created.atomentry.ftl
@@ -0,0 +1,14 @@
+<#include "../slingshot-common.lib.ftl">
+
+ Blog post created: ${htmlTitle?xml}
+
+ http://www.alfresco.org/rss/atom/${id}
+ ${xmldate(date)}
+
+
+
+
+ ${userName?xml}
+ ${userId?xml}
+
+
\ No newline at end of file
diff --git a/src/main/resources/alfresco/templates/activities/org/alfresco/blog/post-deleted.atomentry.ftl b/src/main/resources/alfresco/templates/activities/org/alfresco/blog/post-deleted.atomentry.ftl
new file mode 100644
index 0000000000..81a56348ec
--- /dev/null
+++ b/src/main/resources/alfresco/templates/activities/org/alfresco/blog/post-deleted.atomentry.ftl
@@ -0,0 +1,14 @@
+<#include "../slingshot-common.lib.ftl">
+
+ Blog post deleted: ${htmlTitle?xml}
+
+ http://www.alfresco.org/rss/atom/${id}
+ ${xmldate(date)}
+
+
+
+
+ ${userName?xml}
+ ${userId?xml}
+
+
\ No newline at end of file
diff --git a/src/main/resources/alfresco/templates/activities/org/alfresco/blog/post-updated.atomentry.ftl b/src/main/resources/alfresco/templates/activities/org/alfresco/blog/post-updated.atomentry.ftl
new file mode 100644
index 0000000000..8c9cc18456
--- /dev/null
+++ b/src/main/resources/alfresco/templates/activities/org/alfresco/blog/post-updated.atomentry.ftl
@@ -0,0 +1,14 @@
+<#include "../slingshot-common.lib.ftl">
+
+ Blog post updated: ${htmlTitle?xml}
+
+ http://www.alfresco.org/rss/atom/${id}
+ ${xmldate(date)}
+
+
+
+
+ ${userName?xml}
+ ${userId?xml}
+
+
\ No newline at end of file
diff --git a/src/main/resources/alfresco/templates/activities/org/alfresco/discussions/post-created.atomentry.ftl b/src/main/resources/alfresco/templates/activities/org/alfresco/discussions/post-created.atomentry.ftl
new file mode 100644
index 0000000000..b6c4583005
--- /dev/null
+++ b/src/main/resources/alfresco/templates/activities/org/alfresco/discussions/post-created.atomentry.ftl
@@ -0,0 +1,14 @@
+<#include "../slingshot-common.lib.ftl">
+
+ New discussion: ${htmlTitle?xml}
+
+ http://www.alfresco.org/rss/atom/${id}
+ ${xmldate(date)}
+
+
+
+
+ ${userName?xml}
+ ${userId?xml}
+
+
\ No newline at end of file
diff --git a/src/main/resources/alfresco/templates/activities/org/alfresco/discussions/post-deleted.atomentry.ftl b/src/main/resources/alfresco/templates/activities/org/alfresco/discussions/post-deleted.atomentry.ftl
new file mode 100644
index 0000000000..013510cacb
--- /dev/null
+++ b/src/main/resources/alfresco/templates/activities/org/alfresco/discussions/post-deleted.atomentry.ftl
@@ -0,0 +1,14 @@
+<#include "../slingshot-common.lib.ftl">
+
+ Discussion deleted: ${htmlTitle?xml}
+
+ http://www.alfresco.org/rss/atom/${id}
+ ${xmldate(date)}
+
+
+
+
+ ${userName?xml}
+ ${userId?xml}
+
+
\ No newline at end of file
diff --git a/src/main/resources/alfresco/templates/activities/org/alfresco/discussions/post-updated.atomentry.ftl b/src/main/resources/alfresco/templates/activities/org/alfresco/discussions/post-updated.atomentry.ftl
new file mode 100644
index 0000000000..097dab3bdd
--- /dev/null
+++ b/src/main/resources/alfresco/templates/activities/org/alfresco/discussions/post-updated.atomentry.ftl
@@ -0,0 +1,14 @@
+<#include "../slingshot-common.lib.ftl">
+
+ Discussion updated: ${htmlTitle?xml}
+
+ http://www.alfresco.org/rss/atom/${id}
+ ${xmldate(date)}
+
+
+
+
+ ${userName?xml}
+ ${userId?xml}
+
+
\ No newline at end of file
diff --git a/src/main/resources/alfresco/templates/activities/org/alfresco/discussions/reply-created.atomentry.ftl b/src/main/resources/alfresco/templates/activities/org/alfresco/discussions/reply-created.atomentry.ftl
new file mode 100644
index 0000000000..2c062fcdb5
--- /dev/null
+++ b/src/main/resources/alfresco/templates/activities/org/alfresco/discussions/reply-created.atomentry.ftl
@@ -0,0 +1,14 @@
+<#include "../slingshot-common.lib.ftl">
+
+ Reply added to ${(htmlTitle!'')?html?xml}
+
+ http://www.alfresco.org/rss/atom/${id}
+ ${xmldate(date)}
+
+
+
+
+ ${userName?xml}
+ ${userId?xml}
+
+
\ No newline at end of file
diff --git a/src/main/resources/alfresco/templates/activities/org/alfresco/discussions/reply-updated.atomentry.ftl b/src/main/resources/alfresco/templates/activities/org/alfresco/discussions/reply-updated.atomentry.ftl
new file mode 100644
index 0000000000..25d4c3503c
--- /dev/null
+++ b/src/main/resources/alfresco/templates/activities/org/alfresco/discussions/reply-updated.atomentry.ftl
@@ -0,0 +1,14 @@
+<#include "../slingshot-common.lib.ftl">
+
+ Reply to ${(htmlTitle!'')?html?xml} updated
+
+ http://www.alfresco.org/rss/atom/${id}
+ ${xmldate(date)}
+
+
+
+
+ ${userName?xml}
+ ${userId?xml}
+
+
\ No newline at end of file
diff --git a/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/blogs/blog.lib.ftl b/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/blogs/blog.lib.ftl
new file mode 100644
index 0000000000..f5e782617c
--- /dev/null
+++ b/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/blogs/blog.lib.ftl
@@ -0,0 +1,31 @@
+<#--
+ This template renders the blog data object.
+-->
+<#macro blogJSON item>
+<#escape x as jsonUtils.encodeJSONString(x)>
+{
+ "qnamePath": "${item.qnamePath}",
+ "detailsUrl": "blog/node/${item.nodeRef?replace('://', '/')}",
+ "blogPostsUrl": "blog/node/${item.nodeRef?replace('://', '/')}/posts",
+ "type": "${item.properties["blg:blogImplementation"]!''}",
+ "id": "${item.properties["blg:id"]!'0'}",
+ "name": "${item.properties["blg:name"]!''}",
+ "description": "${item.properties["blg:description"]!''}",
+ "url": "${item.properties["blg:url"]!''}",
+ "username": "${item.properties["blg:userName"]!''}",
+ "password": "${item.properties["blg:password"]!''}",
+ "permissions":
+ {
+ <#if item.getParent()?? && item.getTypeShort() != "st:site" >
+ "create": ${(item.getParent()).hasPermission("CreateChildren")?string},
+ "edit": ${(item.getParent()).hasPermission("Write")?string},
+ "delete": ${(item.getParent()).hasPermission("Delete")?string}
+ <#else>
+ "create": ${item.hasPermission("CreateChildren")?string},
+ "edit": ${item.hasPermission("Write")?string},
+ "delete": ${item.hasPermission("Delete")?string}
+ #if>
+ }
+}
+#escape>
+#macro>
diff --git a/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/blogs/blog.lib.js b/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/blogs/blog.lib.js
new file mode 100644
index 0000000000..56f265d812
--- /dev/null
+++ b/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/blogs/blog.lib.js
@@ -0,0 +1,49 @@
+
+/** Name of the blog details aspect. */
+const BLOG_DETAILS_ASPECT = "blg:blogDetails";
+
+/**
+ * Fetches the blog properties from the json object and adds them to an array
+ * using the correct property names as indexes.
+ */
+function getBlogPropertiesArray()
+{
+ var arr = new Array();
+ if (json.has("blogType"))
+ {
+ arr["blg:blogImplementation"] = json.get("blogType");
+ }
+ if (json.has("blogId"))
+ {
+ arr["blg:id"] = json.get("blogId");
+ }
+ if (json.has("blogName"))
+ {
+ arr["blg:name"] = json.get("blogName");
+ }
+ if (json.has("blogDescription"))
+ {
+ arr["blg:description"] = json.get("blogDescription");
+ }
+ if (json.has("blogUrl"))
+ {
+ arr["blg:url"] = json.get("blogUrl");
+ }
+ if (json.has("username"))
+ {
+ arr["blg:userName"] = json.get("username");
+ }
+ if (json.has("password"))
+ {
+ arr["blg:password"] = json.get("password");
+ }
+ return arr;
+}
+
+/**
+ * Returns the data object of a blog node.
+ */
+function getBlogData(node)
+{
+ return node;
+}
diff --git a/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/blogs/blog/blog.get.desc.xml b/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/blogs/blog/blog.get.desc.xml
new file mode 100644
index 0000000000..318ccfff7d
--- /dev/null
+++ b/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/blogs/blog/blog.get.desc.xml
@@ -0,0 +1,11 @@
+
+ Get blog
+ Get the blog information.
+ /api/blog/site/{site}/{container}/{path}
+ /api/blog/site/{site}/{container}
+ /api/blog/node/{store_type}/{store_id}/{id}
+ argument
+ user
+ required
+ limited_support
+
diff --git a/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/blogs/blog/blog.get.json.ftl b/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/blogs/blog/blog.get.json.ftl
new file mode 100644
index 0000000000..cc1616611b
--- /dev/null
+++ b/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/blogs/blog/blog.get.json.ftl
@@ -0,0 +1,4 @@
+<#import "../blog.lib.ftl" as blogLib/>
+{
+ "item" : <@blogLib.blogJSON item=item />
+}
diff --git a/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/blogs/blog/blog.put.desc.xml b/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/blogs/blog/blog.put.desc.xml
new file mode 100644
index 0000000000..c77db3c4d3
--- /dev/null
+++ b/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/blogs/blog/blog.put.desc.xml
@@ -0,0 +1,11 @@
+
+ Put blog
+ Updates a blog.
+ /api/blog/site/{site}/{container}/{path}
+ /api/blog/site/{site}/{container}
+ /api/blog/node/{store_type}/{store_id}/{id}
+ argument
+ user
+ required
+ limited_support
+
\ No newline at end of file
diff --git a/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/blogs/blog/blog.put.json.ftl b/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/blogs/blog/blog.put.json.ftl
new file mode 100644
index 0000000000..cc1616611b
--- /dev/null
+++ b/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/blogs/blog/blog.put.json.ftl
@@ -0,0 +1,4 @@
+<#import "../blog.lib.ftl" as blogLib/>
+{
+ "item" : <@blogLib.blogJSON item=item />
+}
diff --git a/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/blogs/blogpost.lib.ftl b/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/blogs/blogpost.lib.ftl
new file mode 100644
index 0000000000..e56121b762
--- /dev/null
+++ b/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/blogs/blogpost.lib.ftl
@@ -0,0 +1,116 @@
+<#import "../generic-paged-results.lib.ftl" as gen/>
+
+<#-- Renders a person object. -->
+<#macro renderPerson person fieldName>
+<#escape x as jsonUtils.encodeJSONString(x)>
+ "${fieldName}":
+ {
+ <#if person.assocs["cm:avatar"]??>
+ "avatarRef": "${person.assocs["cm:avatar"][0].nodeRef?string}",
+ #if>
+ "username": "${person.properties["cm:userName"]}",
+ "firstName": "${person.properties["cm:firstName"]!""}",
+ "lastName": "${person.properties["cm:lastName"]!""}"
+ },
+#escape>
+#macro>
+
+
+<#macro addContent item>
+<#escape x as jsonUtils.encodeJSONString(x)>
+ <#assign safecontent=stringUtils.stripUnsafeHTML(item.node.content)>
+ <#if (contentLength?? && contentLength > -1 && (safecontent?length > contentLength))>
+ "content": "${safecontent?substring(0, contentLength)}",
+ <#else>
+ "content": "${safecontent}",
+ #if>
+#escape>
+#macro>
+
+<#--
+ This template renders a blog post.
+-->
+<#macro blogpostJSON item>
+<#escape x as jsonUtils.encodeJSONString(x)>
+{
+ "url": "blog/post/node/${item.node.nodeRef?replace('://','/')}",
+ "commentsUrl": "/node/${item.node.nodeRef?replace('://','/')}/comments",
+ "nodeRef": "${item.node.nodeRef}",
+ "name": "${item.node.properties.name!''}",
+ "title": "${item.node.properties.title!''}",
+ <@addContent item=item />
+ <#if item.author??>
+ <@renderPerson person=item.author fieldName="author" />
+ <#else>
+ "author":
+ {
+ "username": "${item.node.properties.creator}"
+ },
+ #if>
+ "createdOn": "${formatDateRFC822(item.createdDate)}",
+ "modifiedOn": "${formatDateRFC822(item.modifiedDate)}",
+ "permissions":
+ {
+ "edit": ${item.node.hasPermission("Write")?string},
+ "delete": ${item.node.hasPermission("Delete")?string}
+ },
+ "commentCount": ${item.commentCount?c},
+ "tags": [<#list item.tags as x>"${x}"<#if x_has_next>, #if>#list>],
+ <#-- draft vs internal published -->
+ "isDraft": ${item.isDraft?string},
+ <#if (! item.isDraft)>
+ "releasedOn": "${formatDateRFC822(item.releasedDate)}",
+ #if>
+ <#-- true if the post has been updated -->
+ "isUpdated": ${item.isUpdated?string},
+ <#if (item.isUpdated)>
+ "updatedOn": "${formatDateRFC822(item.updatedDate)}",
+ #if>
+ <#if (item.node.properties["blg:published"]?? && item.node.properties["blg:published"] == true)>
+ "publishedOn": "${formatDateRFC822(item.node.properties["blg:posted"])}",
+ "updatedOn": "${formatDateRFC822(item.node.properties["blg:lastUpdate"])}",
+ "postId": "${item.node.properties["blg:postId"]!''}",
+ "postLink": "${item.node.properties["blg:link"]!''}",
+ "outOfDate": ${item.outOfDate?string},
+ #if>
+ <#-- external publishing - last to make sure that we correctly end the response without a comma -->
+ "isPublished": ${(item.node.properties["blg:published"]!'false')?string}
+}
+#escape>
+#macro>
+
+<#macro renderPostList>
+{
+ "metadata":
+ {
+ "blogPermissions":
+ {
+ "create": ${blog.hasPermission("CreateChildren")?string},
+ "edit": ${blog.hasPermission("Write")?string},
+ "delete": ${blog.hasPermission("Delete")?string}
+ },
+ "externalBlogConfig": ${externalBlogConfig?string}
+ },
+ "totalRecordsUpper": ${data.totalRecordsUpper?string("true","false")},
+<@gen.pagedResults data=data ; item>
+ <@blogpostJSON item=item />
+@gen.pagedResults>
+}
+#macro>
+
+<#macro renderPost>
+{
+ "metadata":
+ {
+ "externalBlogConfig": ${externalBlogConfig?string}
+ },
+ "item": <@blogpostJSON item=item />
+}
+#macro>
+
+<#function formatDateRFC822 dateItem>
+ <# local temp=${.locale} -->
+ <#setting locale="en_US">
+ <#return dateItem?datetime?string("EEE, d MMM yyyy HH:mm:ss Z")>
+ <# setting locale=temp -->
+#function>
\ No newline at end of file
diff --git a/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/blogs/blogpost.lib.js b/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/blogs/blogpost.lib.js
new file mode 100644
index 0000000000..3fd174e74d
--- /dev/null
+++ b/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/blogs/blogpost.lib.js
@@ -0,0 +1,130 @@
+
+
+const ASPECT_SYNDICATION = "cm:syndication";
+const PROP_PUBLISHED = "cm:published";
+const PROP_UPDATED = "cm:updated";
+
+function setOrUpdateReleasedAndUpdatedDates(node)
+{
+ // make sure the syndication aspect has been added
+ if (!node.hasAspect(ASPECT_SYNDICATION))
+ {
+ node.addAspect(ASPECT_SYNDICATION, []);
+ }
+
+ // (re-)enable permission inheritance which got disable for draft posts
+ // only set if was previously draft - as only the owner/admin can do this
+ if (!node.inheritsPermissions())
+ {
+ // MNT-12082
+ node.removePermission("All", node.getOwner());
+ node.setInheritsPermissions(true);
+ }
+
+ // check whether the published date has been set
+ if (!node.properties[PROP_PUBLISHED])
+ {
+ // set the published date
+ node.properties[PROP_PUBLISHED] = new Date();
+ node.save();
+ }
+ else
+ {
+ // set/update the updated date
+ node.properties[PROP_UPDATED] = new Date();
+ node.save();
+ }
+}
+
+/**
+ * Returns the data of a blog post.
+ */
+function getBlogPostData(node)
+{
+ var data = {};
+ data.node = node;
+ data.author = people.getPerson(node.properties["cm:creator"]);
+ data.commentCount = getCommentsCount(node);
+
+ // is the post published
+ var isPublished = (node.properties[PROP_PUBLISHED] !== null);
+ if (isPublished)
+ {
+ data.releasedDate = node.properties[PROP_PUBLISHED];
+ }
+
+ // draft
+ data.isDraft = ! isPublished;
+
+ // set the isUpdated flag
+ var isUpdated = (node.properties[PROP_UPDATED] !== null);
+ data.isUpdated = isUpdated;
+ if (isUpdated)
+ {
+ data.updatedDate = node.properties[PROP_UPDATED];
+ }
+
+ // fetch standard created/modified dates
+ data.createdDate = node.properties["cm:created"];
+ data.modifiedDate = node.properties["cm:modified"];
+
+ // does the external post require an update?
+ if (isPublished && (node.properties["blg:lastUpdate"] !== null))
+ {
+ // we either use the release or updated date
+ var modifiedDate = data.releasedDate;
+ if (isUpdated)
+ {
+ modifiedDate = data.updatedDate;
+ }
+
+ if ((modifiedDate - node.properties["blg:lastUpdate"]) > 5000)
+ {
+ data.outOfDate = true;
+ }
+ else
+ {
+ data.outOfDate = false;
+ }
+ }
+ else
+ {
+ data.outOfDate = false;
+ }
+
+ // tags
+ if (node.tags !== null)
+ {
+ data.tags = node.tags;
+ }
+ else
+ {
+ data.tags = [];
+ }
+
+ return data;
+}
+
+/**
+ * Checks whether a blog configuration is available
+ * This should at some point also check whether the configuration is enabled.
+ *
+ * @param node the node that should be checked. Will check all parents if
+ * the node itself doesn't contain a configuration.
+ * @return {boolean} whether a configuration could be found.
+ */
+function hasExternalBlogConfiguration(node)
+{
+ if (node === null || !node.hasPermission("ReadProperties"))
+ {
+ return false;
+ }
+ else if (node.hasAspect("blg:blogDetails"))
+ {
+ return true;
+ }
+ else
+ {
+ return hasExternalBlogConfiguration(node.parent);
+ }
+}
\ No newline at end of file
diff --git a/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/blogs/post/blog-post.delete.desc.xml b/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/blogs/post/blog-post.delete.desc.xml
new file mode 100644
index 0000000000..2b6aeb36f7
--- /dev/null
+++ b/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/blogs/post/blog-post.delete.desc.xml
@@ -0,0 +1,10 @@
+
+ Delete blog post
+ Deletes a blog post.
+ /api/blog/post/site/{site}/{container}/{path}
+ /api/blog/post/node/{store_type}/{store_id}/{id}
+ argument
+ user
+ required
+ limited_support
+
\ No newline at end of file
diff --git a/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/blogs/post/blog-post.delete.json.ftl b/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/blogs/post/blog-post.delete.json.ftl
new file mode 100644
index 0000000000..9a22868515
--- /dev/null
+++ b/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/blogs/post/blog-post.delete.json.ftl
@@ -0,0 +1,3 @@
+{
+ "message" : "${message}"
+}
\ No newline at end of file
diff --git a/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/blogs/post/blog-post.delete.properties b/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/blogs/post/blog-post.delete.properties
new file mode 100644
index 0000000000..474e147054
--- /dev/null
+++ b/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/blogs/post/blog-post.delete.properties
@@ -0,0 +1 @@
+blog-post.msg.deleted=Blog {0} deleted
diff --git a/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/blogs/post/blog-post.delete_de.properties b/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/blogs/post/blog-post.delete_de.properties
new file mode 100755
index 0000000000..7ac89a6a3a
--- /dev/null
+++ b/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/blogs/post/blog-post.delete_de.properties
@@ -0,0 +1 @@
+blog-post.msg.deleted=Blog {0} gel\u00f6scht
diff --git a/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/blogs/post/blog-post.delete_es.properties b/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/blogs/post/blog-post.delete_es.properties
new file mode 100755
index 0000000000..5aadf01515
--- /dev/null
+++ b/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/blogs/post/blog-post.delete_es.properties
@@ -0,0 +1 @@
+blog-post.msg.deleted=Blog {0} eliminado
diff --git a/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/blogs/post/blog-post.delete_fr.properties b/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/blogs/post/blog-post.delete_fr.properties
new file mode 100755
index 0000000000..a9cc3794ee
--- /dev/null
+++ b/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/blogs/post/blog-post.delete_fr.properties
@@ -0,0 +1 @@
+blog-post.msg.deleted=Blog {0} supprim\u00e9
diff --git a/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/blogs/post/blog-post.delete_it.properties b/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/blogs/post/blog-post.delete_it.properties
new file mode 100755
index 0000000000..528bb1f41e
--- /dev/null
+++ b/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/blogs/post/blog-post.delete_it.properties
@@ -0,0 +1 @@
+blog-post.msg.deleted=Blog {0} eliminato
diff --git a/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/blogs/post/blog-post.delete_ja.properties b/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/blogs/post/blog-post.delete_ja.properties
new file mode 100755
index 0000000000..29448bdd3e
--- /dev/null
+++ b/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/blogs/post/blog-post.delete_ja.properties
@@ -0,0 +1 @@
+blog-post.msg.deleted=\u30d6\u30ed\u30b0 "{0}" \u304c\u524a\u9664\u3055\u308c\u307e\u3057\u305f
diff --git a/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/blogs/post/blog-post.delete_nb.properties b/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/blogs/post/blog-post.delete_nb.properties
new file mode 100755
index 0000000000..61ea5282e3
--- /dev/null
+++ b/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/blogs/post/blog-post.delete_nb.properties
@@ -0,0 +1 @@
+blog-post.msg.deleted=Bloggen {0} ble slettet
diff --git a/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/blogs/post/blog-post.delete_nl.properties b/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/blogs/post/blog-post.delete_nl.properties
new file mode 100755
index 0000000000..e391da0445
--- /dev/null
+++ b/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/blogs/post/blog-post.delete_nl.properties
@@ -0,0 +1 @@
+blog-post.msg.deleted=Blog {0} verwijderd
diff --git a/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/blogs/post/blog-post.delete_pt_BR.properties b/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/blogs/post/blog-post.delete_pt_BR.properties
new file mode 100644
index 0000000000..764ef85137
--- /dev/null
+++ b/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/blogs/post/blog-post.delete_pt_BR.properties
@@ -0,0 +1 @@
+blog-post.msg.deleted=Blog {0} exclu\u00eddo
diff --git a/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/blogs/post/blog-post.delete_ru.properties b/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/blogs/post/blog-post.delete_ru.properties
new file mode 100755
index 0000000000..d42a016a30
--- /dev/null
+++ b/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/blogs/post/blog-post.delete_ru.properties
@@ -0,0 +1 @@
+blog-post.msg.deleted=\u0411\u043b\u043e\u0433 {0} \u0443\u0434\u0430\u043b\u0435\u043d
diff --git a/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/blogs/post/blog-post.delete_zh_CN.properties b/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/blogs/post/blog-post.delete_zh_CN.properties
new file mode 100755
index 0000000000..6cd3c84da7
--- /dev/null
+++ b/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/blogs/post/blog-post.delete_zh_CN.properties
@@ -0,0 +1 @@
+blog-post.msg.deleted=\u5df2\u5220\u9664\u535a\u5ba2 {0}
diff --git a/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/blogs/post/blog-post.get.desc.xml b/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/blogs/post/blog-post.get.desc.xml
new file mode 100644
index 0000000000..1f95700778
--- /dev/null
+++ b/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/blogs/post/blog-post.get.desc.xml
@@ -0,0 +1,10 @@
+
+ Get all blogs
+ Gets all blogs.
+ /api/blog/post/site/{site}/{container}/{path}
+ /api/blog/post/node/{store_type}/{store_id}/{id}
+ argument
+ user
+ required
+ limited_support
+
diff --git a/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/blogs/post/blog-post.get.json.ftl b/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/blogs/post/blog-post.get.json.ftl
new file mode 100644
index 0000000000..709882b67f
--- /dev/null
+++ b/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/blogs/post/blog-post.get.json.ftl
@@ -0,0 +1,2 @@
+<#import "../blogpost.lib.ftl" as blogpostLib/>
+<@blogpostLib.renderPost />
\ No newline at end of file
diff --git a/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/blogs/post/blog-post.put.desc.xml b/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/blogs/post/blog-post.put.desc.xml
new file mode 100644
index 0000000000..d5e377c767
--- /dev/null
+++ b/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/blogs/post/blog-post.put.desc.xml
@@ -0,0 +1,10 @@
+
+ Update blog post
+ Updates a blog post.
+ /api/blog/post/site/{site}/{container}/{path}
+ /api/blog/post/node/{store_type}/{store_id}/{id}
+ argument
+ user
+ required
+ limited_support
+
\ No newline at end of file
diff --git a/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/blogs/post/blog-post.put.json.ftl b/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/blogs/post/blog-post.put.json.ftl
new file mode 100644
index 0000000000..709882b67f
--- /dev/null
+++ b/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/blogs/post/blog-post.put.json.ftl
@@ -0,0 +1,2 @@
+<#import "../blogpost.lib.ftl" as blogpostLib/>
+<@blogpostLib.renderPost />
\ No newline at end of file
diff --git a/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/blogs/post/blog-post.put.json.js b/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/blogs/post/blog-post.put.json.js
new file mode 100644
index 0000000000..4c52114aba
--- /dev/null
+++ b/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/blogs/post/blog-post.put.json.js
@@ -0,0 +1,91 @@
+
+
+
+/**
+ * Updates the draft mode part of the post
+ */
+function updateBlogPostDraftMode(postNode)
+{
+ // make sure the user doesn't try to put a non-draft
+ // post back into draft node
+ var currentDraft = (postNode.properties[PROP_PUBLISHED] == undefined);
+ var isDraft = json.has("draft") && json.get("draft").toString() == "true";
+
+ // requested draft, previously non-draft: throw an exception
+ if (isDraft && !currentDraft)
+ {
+ // set an error
+ status.setCode(status.STATUS_BAD_REQUEST, "Cannot put a published post back into draft mode");
+ return null;
+ }
+
+ if (!isDraft)
+ {
+ setOrUpdateReleasedAndUpdatedDates(postNode);
+ }
+}
+
+/**
+ * Updates a blog post node
+ */
+function updateBlogPost(postNode)
+{
+ // fetch the new data
+ var title = "";
+ if (json.has("title"))
+ {
+ title = json.get("title");
+ }
+ var content = "";
+ if (json.has("content"))
+ {
+ content = json.get("content");
+ }
+ var tags = [];
+ if (json.has("tags"))
+ {
+ // get the tags JSONArray and copy it into a real javascript array object
+ var tmp = json.get("tags");
+ for (var x=0; x < tmp.length(); x++)
+ {
+ tags.push(tmp.get(x));
+ }
+ }
+
+ // update the node
+ postNode.properties["cm:title"] = title;
+ postNode.mimetype = "text/html";
+ postNode.content = content
+ postNode.tags = tags;
+ postNode.save();
+
+ updateBlogPostDraftMode(postNode);
+}
+
+function main()
+{
+ // get requested node
+ var node = getRequestNode();
+ if (status.getCode() != status.STATUS_OK)
+ {
+ return;
+ }
+
+ // update blog post
+ updateBlogPost(node);
+
+ model.item = getBlogPostData(node);
+ model.externalBlogConfig = hasExternalBlogConfiguration(node);
+
+ if (json.has("site") && json.has("container") && json.has("page") && !model.item.isDraft)
+ {
+ var data =
+ {
+ title: model.item.node.properties.title,
+ page: json.get("page") + "?postId=" + model.item.node.properties.name
+ }
+ activities.postActivity("org.alfresco.blog.post-updated", json.get("site"), "blog", jsonUtils.toJSONString(data));
+ }
+}
+
+main();
diff --git a/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/blogs/posts/blog-posts-mydrafts.get.desc.xml b/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/blogs/posts/blog-posts-mydrafts.get.desc.xml
new file mode 100644
index 0000000000..592b003de7
--- /dev/null
+++ b/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/blogs/posts/blog-posts-mydrafts.get.desc.xml
@@ -0,0 +1,11 @@
+
+ Get blog posts (draft)
+ Gets all draft posts for a blog.
+ /api/blog/site/{site}/{container}/{path}/posts/mydrafts
+ /api/blog/site/{site}/{container}/posts/mydrafts
+ /api/blog/node/{store_type}/{store_id}/{id}/posts/mydrafts
+ argument
+ user
+ required
+ limited_support
+
diff --git a/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/blogs/posts/blog-posts-mydrafts.get.json.ftl b/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/blogs/posts/blog-posts-mydrafts.get.json.ftl
new file mode 100644
index 0000000000..8c931b77a3
--- /dev/null
+++ b/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/blogs/posts/blog-posts-mydrafts.get.json.ftl
@@ -0,0 +1,2 @@
+<#import "../blogpost.lib.ftl" as blogpostLib/>
+<@blogpostLib.renderPostList />
diff --git a/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/blogs/posts/blog-posts-mypublished.get.desc.xml b/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/blogs/posts/blog-posts-mypublished.get.desc.xml
new file mode 100644
index 0000000000..34a4911d09
--- /dev/null
+++ b/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/blogs/posts/blog-posts-mypublished.get.desc.xml
@@ -0,0 +1,11 @@
+
+ Get blog posts (published)
+ Gets all published posts for a blog.
+ /api/blog/site/{site}/{container}/{path}/posts/mypublished
+ /api/blog/site/{site}/{container}/posts/mypublished
+ /api/blog/node/{store_type}/{store_id}/{id}/posts/mypublished
+ argument
+ user
+ required
+ limited_support
+
diff --git a/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/blogs/posts/blog-posts-mypublished.get.json.ftl b/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/blogs/posts/blog-posts-mypublished.get.json.ftl
new file mode 100644
index 0000000000..b955ab5451
--- /dev/null
+++ b/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/blogs/posts/blog-posts-mypublished.get.json.ftl
@@ -0,0 +1,2 @@
+<#import "../blogpost.lib.ftl" as blogpostLib/>
+<@blogpostLib.renderPostList />
\ No newline at end of file
diff --git a/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/blogs/posts/blog-posts-new.get.desc.xml b/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/blogs/posts/blog-posts-new.get.desc.xml
new file mode 100644
index 0000000000..e113d6cd6b
--- /dev/null
+++ b/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/blogs/posts/blog-posts-new.get.desc.xml
@@ -0,0 +1,11 @@
+
+ Get blog posts for a number of days
+ Get all posts for a blog that were created within the specified number of days.
+ /api/blog/site/{site}/{container}/{path}/posts/new?numdays={numdays}
+ /api/blog/site/{site}/{container}/posts/new?numdays={numdays}
+ /api/blog/node/{store_type}/{store_id}/{id}/posts/new?numdays={numdays}
+ argument
+ user
+ required
+ limited_support
+
diff --git a/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/blogs/posts/blog-posts-new.get.json.ftl b/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/blogs/posts/blog-posts-new.get.json.ftl
new file mode 100644
index 0000000000..b955ab5451
--- /dev/null
+++ b/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/blogs/posts/blog-posts-new.get.json.ftl
@@ -0,0 +1,2 @@
+<#import "../blogpost.lib.ftl" as blogpostLib/>
+<@blogpostLib.renderPostList />
\ No newline at end of file
diff --git a/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/blogs/posts/blog-posts-per-month.get.desc.xml b/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/blogs/posts/blog-posts-per-month.get.desc.xml
new file mode 100644
index 0000000000..78989e4b9f
--- /dev/null
+++ b/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/blogs/posts/blog-posts-per-month.get.desc.xml
@@ -0,0 +1,11 @@
+
+ Get blog posts per month
+ Gets all months for which there are blog posts plus the number of posts in each.
+ /api/blog/site/{site}/{container}/{path}/postspermonth
+ /api/blog/site/{site}/{container}/postspermonth
+ /api/blog/node/{store_type}/{store_id}/{id}/postspermonth
+ argument
+ user
+ required
+ limited_support
+
diff --git a/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/blogs/posts/blog-posts-per-month.get.json.ftl b/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/blogs/posts/blog-posts-per-month.get.json.ftl
new file mode 100644
index 0000000000..decf31b8dd
--- /dev/null
+++ b/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/blogs/posts/blog-posts-per-month.get.json.ftl
@@ -0,0 +1,17 @@
+<#escape x as jsonUtils.encodeJSONString(x)>
+{
+ "items" : [
+ <#list data as item>
+ {
+ "firstPostInMonth" : "${item.firstPostInMonth?string("MMM dd yyyy HH:mm:ss 'GMT'Z '('zzz')'")}",
+ "beginOfMonth" : "${item.beginOfMonth?string("MMM dd yyyy HH:mm:ss 'GMT'Z '('zzz')'")}",
+ "endOfMonth" : "${item.endOfMonth?string("MMM dd yyyy HH:mm:ss 'GMT'Z '('zzz')'")}",
+ "year" : ${item.year?c},
+ "month" : ${item.month?c}, <#-- Note: January -->
+ "postCount" : ${item.count?c}
+ }
+ <#if item_has_next>,#if>
+ #list>
+ ]
+}
+#escape>
diff --git a/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/blogs/posts/blog-posts.get.desc.xml b/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/blogs/posts/blog-posts.get.desc.xml
new file mode 100644
index 0000000000..66475883ae
--- /dev/null
+++ b/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/blogs/posts/blog-posts.get.desc.xml
@@ -0,0 +1,11 @@
+
+ Get blog posts
+ Gets all posts for a blog.
+ /api/blog/site/{site}/{container}/{path}/posts
+ /api/blog/site/{site}/{container}/posts
+ /api/blog/node/{store_type}/{store_id}/{id}/posts
+ argument
+ user
+ required
+ limited_support
+
diff --git a/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/blogs/posts/blog-posts.get.json.ftl b/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/blogs/posts/blog-posts.get.json.ftl
new file mode 100644
index 0000000000..b955ab5451
--- /dev/null
+++ b/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/blogs/posts/blog-posts.get.json.ftl
@@ -0,0 +1,2 @@
+<#import "../blogpost.lib.ftl" as blogpostLib/>
+<@blogpostLib.renderPostList />
\ No newline at end of file
diff --git a/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/blogs/posts/blog-posts.post.desc.xml b/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/blogs/posts/blog-posts.post.desc.xml
new file mode 100644
index 0000000000..f29672a29f
--- /dev/null
+++ b/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/blogs/posts/blog-posts.post.desc.xml
@@ -0,0 +1,11 @@
+
+ Create blog post
+ Creates a new blog post.
+ /api/blog/site/{site}/{container}/{path}/posts
+ /api/blog/site/{site}/{container}/posts
+ /api/blog/node/{store_type}/{store_id}/{id}/posts
+ argument
+ user
+ required
+ limited_support
+
\ No newline at end of file
diff --git a/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/blogs/posts/blog-posts.post.json.ftl b/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/blogs/posts/blog-posts.post.json.ftl
new file mode 100644
index 0000000000..709882b67f
--- /dev/null
+++ b/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/blogs/posts/blog-posts.post.json.ftl
@@ -0,0 +1,2 @@
+<#import "../blogpost.lib.ftl" as blogpostLib/>
+<@blogpostLib.renderPost />
\ No newline at end of file
diff --git a/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/discussions/forum/forum-posts-filtered.get.desc.xml b/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/discussions/forum/forum-posts-filtered.get.desc.xml
new file mode 100644
index 0000000000..ea7bd1ff06
--- /dev/null
+++ b/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/discussions/forum/forum-posts-filtered.get.desc.xml
@@ -0,0 +1,10 @@
+
+ Get filtered forum posts
+ Get the discussion topics that match the filters.
+ /api/forum/site/{site}/discussions/posts/filtered
+ /api/forum/discussions/posts/filtered
+ argument
+ user
+ required
+ limited_support
+
diff --git a/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/discussions/forum/forum-posts-filtered.get.json.ftl b/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/discussions/forum/forum-posts-filtered.get.json.ftl
new file mode 100644
index 0000000000..f338b40df2
--- /dev/null
+++ b/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/discussions/forum/forum-posts-filtered.get.json.ftl
@@ -0,0 +1,19 @@
+<#import "../post.lib.ftl" as postLib/>
+<#import "../../generic-paged-results.lib.ftl" as gen/>
+{
+ "forumPermissions":
+ {
+ <#if forum??>
+ "create": ${forum.hasPermission("CreateChildren")?string},
+ "edit": ${forum.hasPermission("Write")?string},
+ "delete": ${forum.hasPermission("Delete")?string}
+ <#else>
+ "create": false,
+ "edit": false,
+ "delete": false
+ #if>
+ },
+<@gen.pagedResults data=data ; item>
+ <@postLib.postJSON postData=item />
+@gen.pagedResults>
+}
\ No newline at end of file
diff --git a/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/discussions/forum/forum-posts-hot.get.desc.xml b/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/discussions/forum/forum-posts-hot.get.desc.xml
new file mode 100644
index 0000000000..910f5fe038
--- /dev/null
+++ b/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/discussions/forum/forum-posts-hot.get.desc.xml
@@ -0,0 +1,11 @@
+
+ Get hot forum posts
+ Get the hot topics in the forum.
+ /api/forum/site/{site}/{container}/{path}/posts/hot
+ /api/forum/site/{site}/{container}/posts/hot
+ /api/forum/node/{store_type}/{store_id}/{id}/posts/hot
+ argument
+ user
+ required
+ limited_support
+
diff --git a/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/discussions/forum/forum-posts-hot.get.json.ftl b/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/discussions/forum/forum-posts-hot.get.json.ftl
new file mode 100644
index 0000000000..d60a684a3e
--- /dev/null
+++ b/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/discussions/forum/forum-posts-hot.get.json.ftl
@@ -0,0 +1,13 @@
+<#import "../post.lib.ftl" as postLib/>
+<#import "../../generic-paged-results.lib.ftl" as gen/>
+{
+ "forumPermissions":
+ {
+ "create": ${forum.hasPermission("CreateChildren")?string},
+ "edit": ${forum.hasPermission("Write")?string},
+ "delete": ${forum.hasPermission("Delete")?string}
+ },
+<@gen.pagedResults data=data ; item>
+ <@postLib.postJSON postData=item />
+@gen.pagedResults>
+}
diff --git a/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/discussions/forum/forum-posts-mine.get.desc.xml b/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/discussions/forum/forum-posts-mine.get.desc.xml
new file mode 100644
index 0000000000..ef849922f0
--- /dev/null
+++ b/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/discussions/forum/forum-posts-mine.get.desc.xml
@@ -0,0 +1,11 @@
+
+ Get my forum posts
+ Gets the forum posts created by the current user.
+ /api/forum/site/{site}/{container}/{path}/posts/myposts
+ /api/forum/site/{site}/{container}/posts/myposts
+ /api/forum/node/{store_type}/{store_id}/{id}/posts/myposts
+ argument
+ user
+ required
+ limited_support
+
diff --git a/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/discussions/forum/forum-posts-mine.get.json.ftl b/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/discussions/forum/forum-posts-mine.get.json.ftl
new file mode 100644
index 0000000000..d60a684a3e
--- /dev/null
+++ b/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/discussions/forum/forum-posts-mine.get.json.ftl
@@ -0,0 +1,13 @@
+<#import "../post.lib.ftl" as postLib/>
+<#import "../../generic-paged-results.lib.ftl" as gen/>
+{
+ "forumPermissions":
+ {
+ "create": ${forum.hasPermission("CreateChildren")?string},
+ "edit": ${forum.hasPermission("Write")?string},
+ "delete": ${forum.hasPermission("Delete")?string}
+ },
+<@gen.pagedResults data=data ; item>
+ <@postLib.postJSON postData=item />
+@gen.pagedResults>
+}
diff --git a/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/discussions/forum/forum-posts-new.get.desc.xml b/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/discussions/forum/forum-posts-new.get.desc.xml
new file mode 100644
index 0000000000..629218eb72
--- /dev/null
+++ b/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/discussions/forum/forum-posts-new.get.desc.xml
@@ -0,0 +1,11 @@
+
+ Get new forum posts
+ Gets the forum posts for the last specified number of days.
+ /api/forum/site/{site}/{container}/{path}/posts/new?numdays={numdays}
+ /api/forum/site/{site}/{container}/posts/new?numdays={numdays}
+ /api/forum/node/{store_type}/{store_id}/{id}/posts/new?numdays={numdays}
+ argument
+ user
+ required
+ limited_support
+
diff --git a/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/discussions/forum/forum-posts-new.get.json.ftl b/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/discussions/forum/forum-posts-new.get.json.ftl
new file mode 100644
index 0000000000..d60a684a3e
--- /dev/null
+++ b/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/discussions/forum/forum-posts-new.get.json.ftl
@@ -0,0 +1,13 @@
+<#import "../post.lib.ftl" as postLib/>
+<#import "../../generic-paged-results.lib.ftl" as gen/>
+{
+ "forumPermissions":
+ {
+ "create": ${forum.hasPermission("CreateChildren")?string},
+ "edit": ${forum.hasPermission("Write")?string},
+ "delete": ${forum.hasPermission("Delete")?string}
+ },
+<@gen.pagedResults data=data ; item>
+ <@postLib.postJSON postData=item />
+@gen.pagedResults>
+}
diff --git a/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/discussions/forum/forum-posts.get.desc.xml b/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/discussions/forum/forum-posts.get.desc.xml
new file mode 100644
index 0000000000..272aa32e82
--- /dev/null
+++ b/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/discussions/forum/forum-posts.get.desc.xml
@@ -0,0 +1,11 @@
+
+ Get forum posts
+ Gets the forum posts.
+ /api/forum/site/{site}/{container}/{path}/posts
+ /api/forum/site/{site}/{container}/posts
+ /api/forum/node/{store_type}/{store_id}/{id}/posts
+ argument
+ user
+ required
+ limited_support
+
\ No newline at end of file
diff --git a/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/discussions/forum/forum-posts.get.json.ftl b/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/discussions/forum/forum-posts.get.json.ftl
new file mode 100644
index 0000000000..6ba3e8ae02
--- /dev/null
+++ b/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/discussions/forum/forum-posts.get.json.ftl
@@ -0,0 +1,19 @@
+<#import "../post.lib.ftl" as postLib/>
+<#import "../../generic-paged-results.lib.ftl" as gen/>
+{
+ "forumPermissions":
+ {
+ <#if forum.getParent()?? && forum.getTypeShort() != "st:site" >
+ "create": ${(forum.getParent()).hasPermission("CreateChildren")?string},
+ "edit": ${(forum.getParent()).hasPermission("Write")?string},
+ "delete": ${(forum.getParent()).hasPermission("Delete")?string}
+ <#else>
+ "create": ${forum.hasPermission("CreateChildren")?string},
+ "edit": ${forum.hasPermission("Write")?string},
+ "delete": ${forum.hasPermission("Delete")?string}
+ #if>
+ },
+<@gen.pagedResults data=data ; item>
+ <@postLib.postJSON postData=item />
+@gen.pagedResults>
+}
diff --git a/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/discussions/forum/forum-posts.post.desc.xml b/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/discussions/forum/forum-posts.post.desc.xml
new file mode 100644
index 0000000000..94aed0bf27
--- /dev/null
+++ b/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/discussions/forum/forum-posts.post.desc.xml
@@ -0,0 +1,11 @@
+
+ Add forum post
+ Adds a post to a forum.
+ /api/forum/site/{site}/{container}/{path}/posts
+ /api/forum/site/{site}/{container}/posts
+ /api/forum/node/{store_type}/{store_id}/{id}/posts
+ argument
+ user
+ required
+ limited_support
+
\ No newline at end of file
diff --git a/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/discussions/forum/forum-posts.post.json.ftl b/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/discussions/forum/forum-posts.post.json.ftl
new file mode 100644
index 0000000000..a3cd1be869
--- /dev/null
+++ b/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/discussions/forum/forum-posts.post.json.ftl
@@ -0,0 +1,4 @@
+<#import "../post.lib.ftl" as postLib />
+{
+ "item": <@postLib.postJSON postData=postData />
+}
\ No newline at end of file
diff --git a/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/discussions/post.lib.ftl b/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/discussions/post.lib.ftl
new file mode 100644
index 0000000000..b48ca0a66d
--- /dev/null
+++ b/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/discussions/post.lib.ftl
@@ -0,0 +1,124 @@
+<#-- Renders a person object. -->
+<#macro renderPerson person fieldName>
+<#escape x as jsonUtils.encodeJSONString(x)>
+ "${fieldName}":
+ {
+ <#if person?has_content>
+ <#if person.assocs["cm:avatar"]??>
+ "avatarRef": "${person.assocs["cm:avatar"][0].nodeRef?string}",
+ #if>
+ "username": "${person.properties["cm:userName"]}",
+ "firstName": "${person.properties["cm:firstName"]!""}",
+ "lastName": "${person.properties["cm:lastName"]!""}"
+ #if>
+ },
+#escape>
+#macro>
+
+
+<#macro addContent post>
+<#escape x as jsonUtils.encodeJSONString(x)>
+ <#assign safecontent=stringUtils.stripUnsafeHTML(post.content)>
+ <#if (contentLength?? && contentLength > -1 && (safecontent?length > contentLength))>
+ "content": "${safecontent?substring(0, contentLength)}",
+ <#else>
+ "content": "${safecontent}",
+ #if>
+#escape>
+#macro>
+
+
+<#macro postJSON postData>
+{
+ <@postDataJSON postData=postData />
+}
+#macro>
+
+<#macro postDataJSON postData>
+<#escape x as jsonUtils.encodeJSONString(x)>
+ <#-- which node should be used for urls? which for the post data? -->
+ <#if postData.isTopicPost>
+ <#assign refNode=postData.topic />
+ <#else>
+ <#assign refNode=postData.post />
+ #if>
+
+ <#assign post=postData.post />
+
+ <#-- render topic post only data first -->
+ <#if postData.isTopicPost>
+ "name": "${postData.topic.name}",
+ "totalReplyCount": ${postData.totalReplyCount?c},
+ <#if postData.lastReply??>
+ "lastReplyOn": "${xmldate(postData.lastReply.properties.created)}",
+ <#if postData.lastReplyBy??>
+ <@renderPerson person=postData.lastReplyBy fieldName="lastReplyBy" />
+ <#else>
+ "lastReplyBy":
+ {
+ "username": ""
+ },
+ #if>
+ #if>
+ "tags": [<#list postData.tags as x>"${x}"<#if x_has_next>, #if>#list>],
+ "site": "${postData.site!""}",
+ <#else>
+ "name": "${post.name}",
+ #if>
+
+ <#-- data using refNode which might be the topic or the post node -->
+ "url": "/forum/post/node/${refNode.nodeRef.storeRef.protocol}/${refNode.nodeRef.storeRef.identifier}/${refNode.nodeRef.id}",
+ "repliesUrl": "/forum/post/node/${refNode.nodeRef.storeRef.protocol}/${refNode.nodeRef.storeRef.identifier}/${refNode.nodeRef.id}/replies",
+ "nodeRef": "${refNode.nodeRef}",
+
+ <#-- data coming from the post node -->
+ "title": "${(post.properties.title!"")}",
+ "createdOn": "${xmldate(post.properties.created)}",
+ "modifiedOn": "${xmldate(post.properties.modified)}",
+ <#if (post.properties["cm:updated"]??)>
+ "isUpdated": true,
+ "updatedOn": "${xmldate(post.properties["cm:updated"])}",
+ <#else>
+ "isUpdated": false,
+ #if>
+ <#if postData.author?? && postData.author?has_content>
+ <@renderPerson person=postData.author fieldName="author" />
+ <#else>
+ "author":
+ {
+ "username": "${post.properties["cm:creator"]}"
+ },
+ #if>
+ <@addContent post=post />
+ "replyCount": <#if post.sourceAssocs["cm:references"]??>${post.sourceAssocs["cm:references"]?size?c}<#else>0#if>,
+ "permissions":
+ {
+ "edit": ${postData.canEdit?string},
+ "reply": ${post.parent.hasPermission("CreateChildren")?string},
+ "delete": ${post.hasPermission("Delete")?string}
+ }
+#escape>
+#macro>
+
+
+<#-- Renders replies.
+ The difference is to a normal post is that the children might be
+ added inline in the returned JSON.
+-->
+<#macro repliesJSON data>
+{
+ <@postDataJSON postData=data />
+ <#if data.children?exists>
+ , "children": <@repliesRootJSON children=data.children />
+ #if>
+}
+#macro>
+
+<#macro repliesRootJSON children>
+[
+ <#list children as child>
+ <@repliesJSON data=child/>
+ <#if child_has_next>,#if>
+ #list>
+]
+#macro>
diff --git a/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/discussions/posts/forum-post-replies.get.desc.xml b/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/discussions/posts/forum-post-replies.get.desc.xml
new file mode 100644
index 0000000000..29b9fbdbce
--- /dev/null
+++ b/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/discussions/posts/forum-post-replies.get.desc.xml
@@ -0,0 +1,10 @@
+
+ Get forum post replies
+ Get the forum post replies.
+ /api/forum/post/site/{site}/{container}/{path}/replies
+ /api/forum/post/node/{store_type}/{store_id}/{id}/replies
+ argument
+ user
+ required
+ limited_support
+
diff --git a/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/discussions/posts/forum-post-replies.get.json.ftl b/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/discussions/posts/forum-post-replies.get.json.ftl
new file mode 100644
index 0000000000..2c4163b0d7
--- /dev/null
+++ b/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/discussions/posts/forum-post-replies.get.json.ftl
@@ -0,0 +1,4 @@
+<#import "../post.lib.ftl" as postLib />
+{
+ "items": <@postLib.repliesRootJSON children=data />
+}
diff --git a/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/discussions/posts/forum-post-replies.post.desc.xml b/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/discussions/posts/forum-post-replies.post.desc.xml
new file mode 100644
index 0000000000..68fabdd713
--- /dev/null
+++ b/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/discussions/posts/forum-post-replies.post.desc.xml
@@ -0,0 +1,9 @@
+
+ Add forum post reply
+ Adds a reply to a post.
+ /api/forum/post/node/{store_type}/{store_id}/{id}/replies
+ argument
+ user
+ required
+ limited_support
+
\ No newline at end of file
diff --git a/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/discussions/posts/forum-post-replies.post.json.ftl b/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/discussions/posts/forum-post-replies.post.json.ftl
new file mode 100644
index 0000000000..a3cd1be869
--- /dev/null
+++ b/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/discussions/posts/forum-post-replies.post.json.ftl
@@ -0,0 +1,4 @@
+<#import "../post.lib.ftl" as postLib />
+{
+ "item": <@postLib.postJSON postData=postData />
+}
\ No newline at end of file
diff --git a/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/discussions/posts/forum-post.delete.desc.xml b/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/discussions/posts/forum-post.delete.desc.xml
new file mode 100644
index 0000000000..b179fc4052
--- /dev/null
+++ b/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/discussions/posts/forum-post.delete.desc.xml
@@ -0,0 +1,10 @@
+
+ Delete topic
+ Deletes a topic.
+ /api/forum/post/site/{site}/{container}/{path}
+ /api/forum/post/node/{store_type}/{store_id}/{id}
+ argument
+ user
+ required
+ limited_support
+
\ No newline at end of file
diff --git a/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/discussions/posts/forum-post.delete.json.ftl b/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/discussions/posts/forum-post.delete.json.ftl
new file mode 100644
index 0000000000..9a22868515
--- /dev/null
+++ b/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/discussions/posts/forum-post.delete.json.ftl
@@ -0,0 +1,3 @@
+{
+ "message" : "${message}"
+}
\ No newline at end of file
diff --git a/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/discussions/posts/forum-post.delete.properties b/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/discussions/posts/forum-post.delete.properties
new file mode 100644
index 0000000000..062cb99eb9
--- /dev/null
+++ b/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/discussions/posts/forum-post.delete.properties
@@ -0,0 +1,2 @@
+forum-post.msg.deleted=Node {0} deleted
+forum-post.msg.marked.removed=Node {0} marked as removed
diff --git a/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/discussions/posts/forum-post.delete_de.properties b/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/discussions/posts/forum-post.delete_de.properties
new file mode 100755
index 0000000000..33122da30d
--- /dev/null
+++ b/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/discussions/posts/forum-post.delete_de.properties
@@ -0,0 +1,2 @@
+forum-post.msg.deleted=Node {0} gel\u00f6scht
+forum-post.msg.marked.removed=Node {0} als entfernt gekennzeichnet
diff --git a/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/discussions/posts/forum-post.delete_es.properties b/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/discussions/posts/forum-post.delete_es.properties
new file mode 100755
index 0000000000..b954e60984
--- /dev/null
+++ b/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/discussions/posts/forum-post.delete_es.properties
@@ -0,0 +1,2 @@
+forum-post.msg.deleted=Nodo {0} eliminado
+forum-post.msg.marked.removed=Nodo {0} marcado como eliminado
diff --git a/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/discussions/posts/forum-post.delete_fr.properties b/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/discussions/posts/forum-post.delete_fr.properties
new file mode 100755
index 0000000000..d97d6c2b19
--- /dev/null
+++ b/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/discussions/posts/forum-post.delete_fr.properties
@@ -0,0 +1,2 @@
+forum-post.msg.deleted=N\u0153ud {0} supprim\u00e9
+forum-post.msg.marked.removed=N\u0153ud {0} marqu\u00e9 comme supprim\u00e9
diff --git a/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/discussions/posts/forum-post.delete_it.properties b/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/discussions/posts/forum-post.delete_it.properties
new file mode 100755
index 0000000000..693eb2f438
--- /dev/null
+++ b/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/discussions/posts/forum-post.delete_it.properties
@@ -0,0 +1,2 @@
+forum-post.msg.deleted=Nodo {0} eliminato
+forum-post.msg.marked.removed=Nodo {0} contrassegnato come rimosso
diff --git a/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/discussions/posts/forum-post.delete_ja.properties b/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/discussions/posts/forum-post.delete_ja.properties
new file mode 100755
index 0000000000..d218214116
--- /dev/null
+++ b/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/discussions/posts/forum-post.delete_ja.properties
@@ -0,0 +1,2 @@
+forum-post.msg.deleted=\u30ce\u30fc\u30c9 ''{0}'' \u304c\u524a\u9664\u3055\u308c\u307e\u3057\u305f
+forum-post.msg.marked.removed=\u30ce\u30fc\u30c9 "{0}" \u304c\u524a\u9664\u6e08\u307f\u3068\u3057\u3066\u30de\u30fc\u30af\u3055\u308c\u307e\u3057\u305f
diff --git a/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/discussions/posts/forum-post.delete_nb.properties b/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/discussions/posts/forum-post.delete_nb.properties
new file mode 100755
index 0000000000..ea419fe8e4
--- /dev/null
+++ b/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/discussions/posts/forum-post.delete_nb.properties
@@ -0,0 +1,2 @@
+forum-post.msg.deleted=Slettet node {0}
+forum-post.msg.marked.removed=Node {0} er merket som slettet
diff --git a/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/discussions/posts/forum-post.delete_nl.properties b/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/discussions/posts/forum-post.delete_nl.properties
new file mode 100755
index 0000000000..8a0d1d645f
--- /dev/null
+++ b/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/discussions/posts/forum-post.delete_nl.properties
@@ -0,0 +1,2 @@
+forum-post.msg.deleted=Node {0} verwijderd
+forum-post.msg.marked.removed=Node {0} gemarkeerd als verwijderd
diff --git a/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/discussions/posts/forum-post.delete_pt_BR.properties b/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/discussions/posts/forum-post.delete_pt_BR.properties
new file mode 100644
index 0000000000..c1d8e916ff
--- /dev/null
+++ b/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/discussions/posts/forum-post.delete_pt_BR.properties
@@ -0,0 +1,2 @@
+forum-post.msg.deleted=N\u00f3 {0} exclu\u00eddo
+forum-post.msg.marked.removed=N\u00f3 {0} marcado como removido
diff --git a/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/discussions/posts/forum-post.delete_ru.properties b/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/discussions/posts/forum-post.delete_ru.properties
new file mode 100755
index 0000000000..4d6b255554
--- /dev/null
+++ b/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/discussions/posts/forum-post.delete_ru.properties
@@ -0,0 +1,2 @@
+forum-post.msg.deleted=\u0423\u0437\u0435\u043b {0} \u0443\u0434\u0430\u043b\u0435\u043d
+forum-post.msg.marked.removed=\u0423\u0437\u0435\u043b {0} \u043e\u0442\u043c\u0435\u0447\u0435\u043d \u043a\u0430\u043a \u0443\u0434\u0430\u043b\u0435\u043d\u043d\u044b\u0439
diff --git a/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/discussions/posts/forum-post.delete_zh_CN.properties b/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/discussions/posts/forum-post.delete_zh_CN.properties
new file mode 100755
index 0000000000..39bebedffd
--- /dev/null
+++ b/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/discussions/posts/forum-post.delete_zh_CN.properties
@@ -0,0 +1,2 @@
+forum-post.msg.deleted=\u5df2\u5220\u9664\u8282\u70b9 {0}
+forum-post.msg.marked.removed=\u8282\u70b9 {0} \u6807\u8bb0\u4e3a\u5df2\u79fb\u9664
diff --git a/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/discussions/posts/forum-post.get.desc.xml b/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/discussions/posts/forum-post.get.desc.xml
new file mode 100644
index 0000000000..f011745e9d
--- /dev/null
+++ b/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/discussions/posts/forum-post.get.desc.xml
@@ -0,0 +1,10 @@
+
+ Get topic details
+ Gets the details for a topic.
+ /api/forum/post/site/{site}/{container}/{path}
+ /api/forum/post/node/{store_type}/{store_id}/{id}
+ argument
+ user
+ required
+ limited_support
+
diff --git a/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/discussions/posts/forum-post.get.json.ftl b/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/discussions/posts/forum-post.get.json.ftl
new file mode 100644
index 0000000000..a3cd1be869
--- /dev/null
+++ b/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/discussions/posts/forum-post.get.json.ftl
@@ -0,0 +1,4 @@
+<#import "../post.lib.ftl" as postLib />
+{
+ "item": <@postLib.postJSON postData=postData />
+}
\ No newline at end of file
diff --git a/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/discussions/posts/forum-post.put.desc.xml b/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/discussions/posts/forum-post.put.desc.xml
new file mode 100644
index 0000000000..c04df8dfa7
--- /dev/null
+++ b/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/discussions/posts/forum-post.put.desc.xml
@@ -0,0 +1,10 @@
+
+ Update topic
+ Updates a topic.
+ /api/forum/post/site/{site}/{container}/{path}
+ /api/forum/post/node/{store_type}/{store_id}/{id}
+ argument
+ user
+ required
+ limited_support
+
\ No newline at end of file
diff --git a/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/discussions/posts/forum-post.put.json.ftl b/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/discussions/posts/forum-post.put.json.ftl
new file mode 100644
index 0000000000..a3cd1be869
--- /dev/null
+++ b/src/main/resources/alfresco/templates/webscripts/org/alfresco/repository/discussions/posts/forum-post.put.json.ftl
@@ -0,0 +1,4 @@
+<#import "../post.lib.ftl" as postLib />
+{
+ "item": <@postLib.postJSON postData=postData />
+}
\ No newline at end of file
diff --git a/src/main/resources/alfresco/web-scripts-application-context.xml b/src/main/resources/alfresco/web-scripts-application-context.xml
index 2076ba48c2..1cb6c41f62 100644
--- a/src/main/resources/alfresco/web-scripts-application-context.xml
+++ b/src/main/resources/alfresco/web-scripts-application-context.xml
@@ -891,6 +891,13 @@
parent="baseArchivedNodeWebScript">
+
+
+
+
+
+
+
@@ -1323,6 +1330,89 @@
parent="abstractLinksWebScript">
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/test/java/org/alfresco/AppContext04TestSuite.java b/src/test/java/org/alfresco/AppContext04TestSuite.java
index 730a25440a..7d4895f3ad 100644
--- a/src/test/java/org/alfresco/AppContext04TestSuite.java
+++ b/src/test/java/org/alfresco/AppContext04TestSuite.java
@@ -40,7 +40,9 @@ import org.junit.runners.Suite;
org.alfresco.repo.web.scripts.quickshare.QuickShareRestApiTest.class,
org.alfresco.repo.web.scripts.admin.AdminWebScriptTest.class,
org.alfresco.repo.web.scripts.audit.AuditWebScriptTest.class,
+ org.alfresco.repo.web.scripts.blogs.BlogServiceTest.class,
org.alfresco.repo.web.scripts.dictionary.DictionaryRestApiTest.class,
+ org.alfresco.repo.web.scripts.discussion.DiscussionRestApiTest.class,
org.alfresco.repo.web.scripts.activities.feed.control.FeedControlTest.class,
org.alfresco.repo.web.scripts.forms.FormRestApiGet_Test.class,
org.alfresco.repo.web.scripts.forms.FormRestApiJsonPost_Test.class,
diff --git a/src/test/java/org/alfresco/repo/web/scripts/ReadOnlyTransactionInGetRestApiTest.java b/src/test/java/org/alfresco/repo/web/scripts/ReadOnlyTransactionInGetRestApiTest.java
index a0dde35074..ebd98336ff 100644
--- a/src/test/java/org/alfresco/repo/web/scripts/ReadOnlyTransactionInGetRestApiTest.java
+++ b/src/test/java/org/alfresco/repo/web/scripts/ReadOnlyTransactionInGetRestApiTest.java
@@ -1,168 +1,188 @@
-/*
- * #%L
- * Alfresco Remote API
- * %%
- * Copyright (C) 2005 - 2016 Alfresco Software Limited
- * %%
- * This file is part of the Alfresco software.
- * If the software was purchased under a paid Alfresco license, the terms of
- * the paid license agreement will prevail. Otherwise, the software is
- * provided under the following open source license terms:
- *
- * 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 .
- * #L%
- */
-package org.alfresco.repo.web.scripts;
-
-import java.util.List;
-
-import org.alfresco.repo.node.archive.NodeArchiveService;
-import org.alfresco.repo.security.authentication.AuthenticationUtil;
-import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback;
-import org.alfresco.service.cmr.repository.ChildAssociationRef;
-import org.alfresco.service.cmr.repository.NodeRef;
-import org.alfresco.service.cmr.repository.NodeService;
-import org.alfresco.service.cmr.site.SiteInfo;
-import org.alfresco.service.cmr.site.SiteService;
-import org.alfresco.service.cmr.site.SiteVisibility;
-import org.alfresco.service.transaction.TransactionService;
-import org.springframework.context.ApplicationContext;
-import org.springframework.extensions.webscripts.Status;
-import org.springframework.extensions.webscripts.TestWebScriptServer.GetRequest;
-import org.springframework.extensions.webscripts.TestWebScriptServer.Response;
-
-/**
- * Set of tests that ensure GET REST APIs are run successfully in a read-only
- * transaction (ALF-10179).
- *
- * Some webscripts have a side effect of creating a "container" these tests
- * are to ensure this is handled gracefully in a way that allows the main
- * transaction to be declared as readonly for performance reasons.
- *
- * @author Gavin Cornwell
- * @since 4.0
- */
-public class ReadOnlyTransactionInGetRestApiTest extends BaseWebScriptTest
-{
- private static final String TEST_SITE_NAME = "readOnlyTestSite";
-
- private static final String URL_GET_SITE_LINKS = "/api/links/site/" + TEST_SITE_NAME + "/links?page=1&pageSize=10";
- private static final String URL_GET_SITE_LINK = "/api/links/link/site/" + TEST_SITE_NAME + "/links/123456789";
- private static final String URL_GET_SITE_TAGS = "/api/tagscopes/site/" + TEST_SITE_NAME + "/tags";
-
- private SiteService siteService;
- private NodeService nodeService;
- private TransactionService transactionService;
- private NodeArchiveService nodeArchiveService;
-
- private NodeRef testSiteNodeRef;
- private String testSiteNodeRefString;
-
- private boolean logEnabled = false;
-
- @Override
- protected void setUp() throws Exception
- {
- super.setUp();
- ApplicationContext appContext = getServer().getApplicationContext();
-
- this.siteService = (SiteService)appContext.getBean("SiteService");
- this.nodeService = (NodeService)appContext.getBean("NodeService");
- this.transactionService = (TransactionService)appContext.getBean("TransactionService");
- this.nodeArchiveService = (NodeArchiveService)getServer().getApplicationContext().getBean("nodeArchiveService");
-
- // set admin as current user
- AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getAdminUserName());
-
- // delete the test site if it's still hanging around from previous runs
- SiteInfo site = siteService.getSite(TEST_SITE_NAME);
- if (site != null)
- {
- siteService.deleteSite(TEST_SITE_NAME);
- nodeArchiveService.purgeArchivedNode(nodeArchiveService.getArchivedNode(site.getNodeRef()));
- }
-
- // create the test site, this should create a site but it won't have any containers created
- SiteInfo siteInfo = this.siteService.createSite("collaboration", TEST_SITE_NAME, "Read Only Test Site",
- "Test site for ReadOnlyTransactionRestApiTest", SiteVisibility.PUBLIC);
- this.testSiteNodeRef = siteInfo.getNodeRef();
- this.testSiteNodeRefString = this.testSiteNodeRef.toString().replace("://", "/");
-
- // ensure there are no containers present at the start of the test
- List children = nodeService.getChildAssocs(this.testSiteNodeRef);
- assertTrue("The test site should not have any containers", children.isEmpty());
- }
-
- /* (non-Javadoc)
- * @see junit.framework.TestCase#tearDown()
- */
- @Override
- protected void tearDown() throws Exception
- {
- super.tearDown();
-
- SiteInfo site = siteService.getSite(TEST_SITE_NAME);
- // use retrying transaction to delete the site
- this.transactionService.getRetryingTransactionHelper().doInTransaction(new RetryingTransactionCallback()
- {
- @Override
- public Void execute() throws Throwable
- {
- // delete the test site
- siteService.deleteSite(TEST_SITE_NAME);
-
- return null;
- }
- });
- nodeArchiveService.purgeArchivedNode(nodeArchiveService.getArchivedNode(site.getNodeRef()));
-
- AuthenticationUtil.clearCurrentSecurityContext();
- }
-
- public void testGetSiteLinks() throws Exception
- {
- Response response = sendRequest(new GetRequest(URL_GET_SITE_LINKS), 200);
- logResponse(response);
- assertEquals(Status.STATUS_OK, response.getStatus());
- }
-
- public void testGetSiteLink() throws Exception
- {
- Response response = sendRequest(new GetRequest(URL_GET_SITE_LINK), 404);
- logResponse(response);
- assertEquals(Status.STATUS_NOT_FOUND, response.getStatus());
- }
-
- public void testGetSiteTags() throws Exception
- {
- Response response = sendRequest(new GetRequest(URL_GET_SITE_TAGS), 200);
- logResponse(response);
- assertEquals(Status.STATUS_OK, response.getStatus());
- }
-
- private void logResponse(Response response)
- {
- if (this.logEnabled)
- {
- try
- {
- System.out.println(response.getContentAsString());
- }
- catch (Exception e)
- {
- System.err.println("Unable to log response: " + e.toString());
- }
- }
- }
-}
+/*
+ * #%L
+ * Alfresco Remote API
+ * %%
+ * Copyright (C) 2005 - 2016 Alfresco Software Limited
+ * %%
+ * This file is part of the Alfresco software.
+ * If the software was purchased under a paid Alfresco license, the terms of
+ * the paid license agreement will prevail. Otherwise, the software is
+ * provided under the following open source license terms:
+ *
+ * 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 .
+ * #L%
+ */
+package org.alfresco.repo.web.scripts;
+
+import java.util.List;
+
+import org.alfresco.repo.node.archive.NodeArchiveService;
+import org.alfresco.repo.security.authentication.AuthenticationUtil;
+import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback;
+import org.alfresco.service.cmr.repository.ChildAssociationRef;
+import org.alfresco.service.cmr.repository.NodeRef;
+import org.alfresco.service.cmr.repository.NodeService;
+import org.alfresco.service.cmr.site.SiteInfo;
+import org.alfresco.service.cmr.site.SiteService;
+import org.alfresco.service.cmr.site.SiteVisibility;
+import org.alfresco.service.transaction.TransactionService;
+import org.springframework.context.ApplicationContext;
+import org.springframework.extensions.webscripts.Status;
+import org.springframework.extensions.webscripts.TestWebScriptServer.GetRequest;
+import org.springframework.extensions.webscripts.TestWebScriptServer.Response;
+
+/**
+ * Set of tests that ensure GET REST APIs are run successfully in a read-only
+ * transaction (ALF-10179).
+ *
+ * Some webscripts have a side effect of creating a "container" these tests
+ * are to ensure this is handled gracefully in a way that allows the main
+ * transaction to be declared as readonly for performance reasons.
+ *
+ * @author Gavin Cornwell
+ * @since 4.0
+ */
+public class ReadOnlyTransactionInGetRestApiTest extends BaseWebScriptTest
+{
+ private static final String TEST_SITE_NAME = "readOnlyTestSite";
+
+
+ private static final String URL_GET_SITE_FORUM_POSTS = "/api/forum/site/" + TEST_SITE_NAME + "/discussions/posts";
+ private static final String URL_GET_SITE_BLOG = "/api/blog/site/" + TEST_SITE_NAME + "/blog";
+ private static final String URL_GET_SITE_LINKS = "/api/links/site/" + TEST_SITE_NAME + "/links?page=1&pageSize=10";
+ private static final String URL_GET_SITE_LINK = "/api/links/link/site/" + TEST_SITE_NAME + "/links/123456789";
+ private static final String URL_GET_SITE_TAGS = "/api/tagscopes/site/" + TEST_SITE_NAME + "/tags";
+
+ private SiteService siteService;
+ private NodeService nodeService;
+ private TransactionService transactionService;
+ private NodeArchiveService nodeArchiveService;
+
+ private NodeRef testSiteNodeRef;
+ private String testSiteNodeRefString;
+
+ private boolean logEnabled = false;
+
+ @Override
+ protected void setUp() throws Exception
+ {
+ super.setUp();
+ ApplicationContext appContext = getServer().getApplicationContext();
+
+ this.siteService = (SiteService)appContext.getBean("SiteService");
+ this.nodeService = (NodeService)appContext.getBean("NodeService");
+ this.transactionService = (TransactionService)appContext.getBean("TransactionService");
+ this.nodeArchiveService = (NodeArchiveService)getServer().getApplicationContext().getBean("nodeArchiveService");
+
+ // set admin as current user
+ AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getAdminUserName());
+
+ // delete the test site if it's still hanging around from previous runs
+ SiteInfo site = siteService.getSite(TEST_SITE_NAME);
+ if (site != null)
+ {
+ siteService.deleteSite(TEST_SITE_NAME);
+ nodeArchiveService.purgeArchivedNode(nodeArchiveService.getArchivedNode(site.getNodeRef()));
+ }
+
+ // create the test site, this should create a site but it won't have any containers created
+ SiteInfo siteInfo = this.siteService.createSite("collaboration", TEST_SITE_NAME, "Read Only Test Site",
+ "Test site for ReadOnlyTransactionRestApiTest", SiteVisibility.PUBLIC);
+ this.testSiteNodeRef = siteInfo.getNodeRef();
+ this.testSiteNodeRefString = this.testSiteNodeRef.toString().replace("://", "/");
+
+ // ensure there are no containers present at the start of the test
+ List children = nodeService.getChildAssocs(this.testSiteNodeRef);
+ assertTrue("The test site should not have any containers", children.isEmpty());
+ }
+
+ /* (non-Javadoc)
+ * @see junit.framework.TestCase#tearDown()
+ */
+ @Override
+ protected void tearDown() throws Exception
+ {
+ super.tearDown();
+
+ SiteInfo site = siteService.getSite(TEST_SITE_NAME);
+ // use retrying transaction to delete the site
+ this.transactionService.getRetryingTransactionHelper().doInTransaction(new RetryingTransactionCallback()
+ {
+ @Override
+ public Void execute() throws Throwable
+ {
+ // delete the test site
+ siteService.deleteSite(TEST_SITE_NAME);
+
+ return null;
+ }
+ });
+ nodeArchiveService.purgeArchivedNode(nodeArchiveService.getArchivedNode(site.getNodeRef()));
+
+ AuthenticationUtil.clearCurrentSecurityContext();
+ }
+
+ public void testGetSiteForumPosts() throws Exception
+ {
+ Response response = sendRequest(new GetRequest(URL_GET_SITE_FORUM_POSTS), 200);
+ logResponse(response);
+ assertEquals(Status.STATUS_OK, response.getStatus());
+ }
+
+ public void testGetSiteBlog() throws Exception
+ {
+ // TODO: Fixme - This REST API still requires a readwrite transaction to be successful
+ // Also add tests for all other blog GET REST APIs
+
+ Response response = sendRequest(new GetRequest(URL_GET_SITE_BLOG), 200);
+ logResponse(response);
+ assertEquals(Status.STATUS_OK, response.getStatus());
+ }
+
+ public void testGetSiteLinks() throws Exception
+ {
+ Response response = sendRequest(new GetRequest(URL_GET_SITE_LINKS), 200);
+ logResponse(response);
+ assertEquals(Status.STATUS_OK, response.getStatus());
+ }
+
+ public void testGetSiteLink() throws Exception
+ {
+ Response response = sendRequest(new GetRequest(URL_GET_SITE_LINK), 404);
+ logResponse(response);
+ assertEquals(Status.STATUS_NOT_FOUND, response.getStatus());
+ }
+
+ public void testGetSiteTags() throws Exception
+ {
+ Response response = sendRequest(new GetRequest(URL_GET_SITE_TAGS), 200);
+ logResponse(response);
+ assertEquals(Status.STATUS_OK, response.getStatus());
+ }
+
+ private void logResponse(Response response)
+ {
+ if (this.logEnabled)
+ {
+ try
+ {
+ System.out.println(response.getContentAsString());
+ }
+ catch (Exception e)
+ {
+ System.err.println("Unable to log response: " + e.toString());
+ }
+ }
+ }
+}
diff --git a/src/test/java/org/alfresco/repo/web/scripts/WebScriptTestSuite.java b/src/test/java/org/alfresco/repo/web/scripts/WebScriptTestSuite.java
index 6b66424c8c..a74d4d4a57 100644
--- a/src/test/java/org/alfresco/repo/web/scripts/WebScriptTestSuite.java
+++ b/src/test/java/org/alfresco/repo/web/scripts/WebScriptTestSuite.java
@@ -33,9 +33,11 @@ import org.alfresco.repo.web.scripts.action.RunningActionRestApiTest;
import org.alfresco.repo.web.scripts.activities.feed.control.FeedControlTest;
import org.alfresco.repo.web.scripts.admin.AdminWebScriptTest;
import org.alfresco.repo.web.scripts.audit.AuditWebScriptTest;
+import org.alfresco.repo.web.scripts.blogs.BlogServiceTest;
import org.alfresco.repo.web.scripts.comment.CommentsApiTest;
import org.alfresco.repo.web.scripts.custommodel.CustomModelImportTest;
import org.alfresco.repo.web.scripts.dictionary.DictionaryRestApiTest;
+import org.alfresco.repo.web.scripts.discussion.DiscussionRestApiTest;
import org.alfresco.repo.web.scripts.facet.FacetRestApiTest;
import org.alfresco.repo.web.scripts.forms.FormRestApiGet_Test;
import org.alfresco.repo.web.scripts.forms.FormRestApiJsonPost_Test;
@@ -78,7 +80,9 @@ public class WebScriptTestSuite extends TestSuite
suite.addTestSuite( QuickShareRestApiTest.class );
suite.addTestSuite( AdminWebScriptTest.class );
suite.addTestSuite( AuditWebScriptTest.class );
+ suite.addTestSuite( BlogServiceTest.class );
suite.addTestSuite( DictionaryRestApiTest.class );
+ suite.addTestSuite( DiscussionRestApiTest.class );
suite.addTestSuite( FeedControlTest.class );
suite.addTestSuite( FormRestApiGet_Test.class );
suite.addTestSuite( FormRestApiJsonPost_Test.class );
diff --git a/src/test/java/org/alfresco/repo/web/scripts/blogs/AllBlogTests.java b/src/test/java/org/alfresco/repo/web/scripts/blogs/AllBlogTests.java
new file mode 100644
index 0000000000..a6d725a5ec
--- /dev/null
+++ b/src/test/java/org/alfresco/repo/web/scripts/blogs/AllBlogTests.java
@@ -0,0 +1,49 @@
+/*
+ * #%L
+ * Alfresco Remote API
+ * %%
+ * Copyright (C) 2005 - 2016 Alfresco Software Limited
+ * %%
+ * This file is part of the Alfresco software.
+ * If the software was purchased under a paid Alfresco license, the terms of
+ * the paid license agreement will prevail. Otherwise, the software is
+ * provided under the following open source license terms:
+ *
+ * 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 .
+ * #L%
+ */
+package org.alfresco.repo.web.scripts.blogs;
+
+import org.alfresco.repo.blog.BlogServiceImplTest;
+import org.alfresco.service.cmr.blog.BlogService;
+import org.junit.runner.RunWith;
+import org.junit.runners.Suite;
+
+/**
+ * This class is a holder for the various test classes associated with the {@link BlogService}.
+ * It is not (at the time of writing) intended to be incorporated into the automatic build
+ * which will find the various test classes and run them individually.
+ *
+ * @author Neil Mc Erlean
+ * @since 4.0
+ */
+@RunWith(Suite.class)
+@Suite.SuiteClasses({
+ BlogServiceImplTest.class,
+ BlogServiceTest.class
+})
+public class AllBlogTests
+{
+ // Intentionally empty
+}
diff --git a/src/test/java/org/alfresco/repo/web/scripts/blogs/BlogServiceTest.java b/src/test/java/org/alfresco/repo/web/scripts/blogs/BlogServiceTest.java
new file mode 100644
index 0000000000..0bbc1f0080
--- /dev/null
+++ b/src/test/java/org/alfresco/repo/web/scripts/blogs/BlogServiceTest.java
@@ -0,0 +1,998 @@
+/*
+ * #%L
+ * Alfresco Remote API
+ * %%
+ * Copyright (C) 2005 - 2016 Alfresco Software Limited
+ * %%
+ * This file is part of the Alfresco software.
+ * If the software was purchased under a paid Alfresco license, the terms of
+ * the paid license agreement will prevail. Otherwise, the software is
+ * provided under the following open source license terms:
+ *
+ * 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 .
+ * #L%
+ */
+package org.alfresco.repo.web.scripts.blogs;
+
+import java.text.MessageFormat;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+import org.alfresco.model.ContentModel;
+import org.alfresco.repo.activities.feed.FeedGenerator;
+import org.alfresco.repo.activities.post.lookup.PostLookup;
+import org.alfresco.repo.management.subsystems.ChildApplicationContextFactory;
+import org.alfresco.repo.node.archive.NodeArchiveService;
+import org.alfresco.repo.security.authentication.AuthenticationComponent;
+import org.alfresco.repo.security.authentication.AuthenticationUtil;
+import org.alfresco.repo.site.SiteModel;
+import org.alfresco.repo.web.scripts.BaseWebScriptTest;
+import org.alfresco.service.cmr.activities.ActivityService;
+import org.alfresco.service.cmr.repository.NodeRef;
+import org.alfresco.service.cmr.security.MutableAuthenticationService;
+import org.alfresco.service.cmr.security.PersonService;
+import org.alfresco.service.cmr.site.SiteInfo;
+import org.alfresco.service.cmr.site.SiteService;
+import org.alfresco.service.cmr.site.SiteVisibility;
+import org.alfresco.util.GUID;
+import org.alfresco.util.PropertyMap;
+import org.alfresco.util.testing.category.LuceneTests;
+import org.alfresco.util.testing.category.RedundantTests;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.json.JSONArray;
+import org.json.JSONObject;
+import org.json.JSONStringer;
+import org.junit.Test;
+import org.junit.experimental.categories.Category;
+import org.springframework.context.ApplicationContext;
+import org.springframework.extensions.webscripts.Status;
+import org.springframework.extensions.webscripts.TestWebScriptServer.DeleteRequest;
+import org.springframework.extensions.webscripts.TestWebScriptServer.GetRequest;
+import org.springframework.extensions.webscripts.TestWebScriptServer.PostRequest;
+import org.springframework.extensions.webscripts.TestWebScriptServer.PutRequest;
+import org.springframework.extensions.webscripts.TestWebScriptServer.Response;
+
+/**
+ * Unit Test to test Blog Web Script API
+ *
+ * @author mruflin
+ */
+@Category(LuceneTests.class)
+public class BlogServiceTest extends BaseWebScriptTest
+{
+ @SuppressWarnings("unused")
+ private static Log logger = LogFactory.getLog(BlogServiceTest.class);
+
+ private MutableAuthenticationService authenticationService;
+ private AuthenticationComponent authenticationComponent;
+ private PersonService personService;
+ private SiteService siteService;
+ private NodeArchiveService nodeArchiveService;
+ private ActivityService activityService;
+ private FeedGenerator feedGenerator;
+ private PostLookup postLookup;
+
+ private static final String USER_ONE = "UserOneSecondToo";
+ private static final String USER_TWO = "UserTwoSecondToo";
+ private static final String SITE_SHORT_NAME_BLOG = "BlogSiteShortNameTest";
+ private static final String COMPONENT_BLOG = "blog";
+
+ private static final String URL_BLOG_POST = "/api/blog/post/site/" + SITE_SHORT_NAME_BLOG + "/" + COMPONENT_BLOG + "/";
+ private static final String URL_BLOG_CORE = "/api/blog/site/" + SITE_SHORT_NAME_BLOG + "/" + COMPONENT_BLOG;
+ private static final String URL_BLOG_POSTS = URL_BLOG_CORE + "/posts";
+ private static final String URL_MY_DRAFT_BLOG_POSTS = "/api/blog/site/" + SITE_SHORT_NAME_BLOG +
+ "/" + COMPONENT_BLOG + "/posts/mydrafts";
+ private static final String URL_MY_PUBLISHED_BLOG_POSTS = "/api/blog/site/" + SITE_SHORT_NAME_BLOG +
+ "/" + COMPONENT_BLOG + "/posts/mypublished";
+
+ private static final String URL_DELETE_COMMENT = "api/comment/node/{0}/{1}/{2}?site={3}&itemtitle={4}&page={5}";
+
+ private List posts;
+ private List drafts;
+
+
+ // General methods
+
+ @Override
+ protected void setUp() throws Exception
+ {
+ super.setUp();
+
+ this.authenticationService = (MutableAuthenticationService)getServer().getApplicationContext().getBean("AuthenticationService");
+ this.authenticationComponent = (AuthenticationComponent)getServer().getApplicationContext().getBean("authenticationComponent");
+ this.personService = (PersonService)getServer().getApplicationContext().getBean("PersonService");
+ this.siteService = (SiteService)getServer().getApplicationContext().getBean("SiteService");
+ this.nodeArchiveService = (NodeArchiveService)getServer().getApplicationContext().getBean("nodeArchiveService");
+ this.activityService = (ActivityService)getServer().getApplicationContext().getBean("activityService");
+ ChildApplicationContextFactory activitiesFeed = (ChildApplicationContextFactory)getServer().getApplicationContext().getBean("ActivitiesFeed");
+ ApplicationContext activitiesFeedCtx = activitiesFeed.getApplicationContext();
+ this.feedGenerator = (FeedGenerator)activitiesFeedCtx.getBean("feedGenerator");
+ this.postLookup = (PostLookup)activitiesFeedCtx.getBean("postLookup");
+
+ // Authenticate as user
+ this.authenticationComponent.setCurrentUser(AuthenticationUtil.getAdminUserName());
+
+ // Create test site
+ // - only create the site if it doesn't already exist
+ SiteInfo siteInfo = this.siteService.getSite(SITE_SHORT_NAME_BLOG);
+ if (siteInfo == null)
+ {
+ this.siteService.createSite("BlogSitePreset", SITE_SHORT_NAME_BLOG, "BlogSiteTitle", "BlogSiteDescription", SiteVisibility.PUBLIC);
+ }
+
+ // Create users
+ createUser(USER_ONE, SiteModel.SITE_COLLABORATOR, SITE_SHORT_NAME_BLOG);
+ createUser(USER_TWO, SiteModel.SITE_COLLABORATOR, SITE_SHORT_NAME_BLOG);
+
+ // Blank our lists used to track things the test creates
+ posts = new ArrayList(5);
+ drafts = new ArrayList(5);
+
+ // Do tests as inviter user
+ this.authenticationComponent.setCurrentUser(USER_ONE);
+ }
+
+ @Override
+ protected void tearDown() throws Exception
+ {
+ super.tearDown();
+
+ // admin user required to delete things
+ this.authenticationComponent.setCurrentUser(AuthenticationUtil.getAdminUserName());
+
+ SiteInfo siteInfo = this.siteService.getSite(SITE_SHORT_NAME_BLOG);
+ if (siteInfo != null)
+ {
+ // delete invite site
+ siteService.deleteSite(SITE_SHORT_NAME_BLOG);
+ nodeArchiveService.purgeArchivedNode(nodeArchiveService.getArchivedNode(siteInfo.getNodeRef()));
+ }
+
+
+ // delete the users
+ personService.deletePerson(USER_ONE);
+ if (this.authenticationService.authenticationExists(USER_ONE))
+ {
+ this.authenticationService.deleteAuthentication(USER_ONE);
+ }
+
+ personService.deletePerson(USER_TWO);
+ if (this.authenticationService.authenticationExists(USER_TWO))
+ {
+ this.authenticationService.deleteAuthentication(USER_TWO);
+ }
+ }
+
+ private void createUser(String userName, String role, String siteMembership)
+ {
+ // if user with given user name doesn't already exist then create user
+ if (this.authenticationService.authenticationExists(userName) == false)
+ {
+ // create user
+ this.authenticationService.createAuthentication(userName, "password".toCharArray());
+
+ // create person properties
+ PropertyMap personProps = new PropertyMap();
+ personProps.put(ContentModel.PROP_USERNAME, userName);
+ personProps.put(ContentModel.PROP_FIRSTNAME, "FirstName123");
+ personProps.put(ContentModel.PROP_LASTNAME, "LastName123");
+ personProps.put(ContentModel.PROP_EMAIL, "FirstName123.LastName123@email.com");
+ personProps.put(ContentModel.PROP_JOBTITLE, "JobTitle123");
+ personProps.put(ContentModel.PROP_JOBTITLE, "Organisation123");
+
+ // create person node for user
+ this.personService.createPerson(personProps);
+ }
+
+ // add the user as a member with the given role
+ this.siteService.setMembership(siteMembership, userName, role);
+ }
+
+
+ // Test helper methods
+
+ private JSONObject getRequestObject(String title, String content, String[] tags, boolean isDraft)
+ throws Exception
+ {
+ JSONObject post = new JSONObject();
+ if (title != null)
+ {
+ post.put("title", title);
+ }
+ if (content != null)
+ {
+ post.put("content", content);
+ }
+ if (tags != null)
+ {
+ JSONArray arr = new JSONArray();
+ for (String s : tags)
+ {
+ arr.put(s);
+ }
+ post.put("tags", arr);
+ }
+ post.put("draft", isDraft);
+ return post;
+ }
+
+ private JSONObject createPost(String title, String content, String[] tags, boolean isDraft, int expectedStatus)
+ throws Exception
+ {
+ JSONObject post = getRequestObject(title, content, tags, isDraft);
+ Response response = sendRequest(new PostRequest(URL_BLOG_POSTS, post.toString(), "application/json"), expectedStatus);
+
+ if (expectedStatus != 200)
+ {
+ return null;
+ }
+
+ //logger.debug(response.getContentAsString());
+ JSONObject result = new JSONObject(response.getContentAsString());
+ JSONObject item = result.getJSONObject("item");
+ if (isDraft)
+ {
+ this.drafts.add(item.getString("name"));
+ }
+ else
+ {
+ this.posts.add(item.getString("name"));
+ }
+ return item;
+ }
+
+ private JSONObject updatePost(String name, String title, String content, String[] tags, boolean isDraft, int expectedStatus)
+ throws Exception
+ {
+ JSONObject post = getRequestObject(title, content, tags, isDraft);
+ Response response = sendRequest(new PutRequest(URL_BLOG_POST + name, post.toString(), "application/json"), expectedStatus);
+
+ if (expectedStatus != 200)
+ {
+ return null;
+ }
+
+ JSONObject result = new JSONObject(response.getContentAsString());
+ return result.getJSONObject("item");
+ }
+
+ private JSONObject getPost(String name, int expectedStatus)
+ throws Exception
+ {
+ Response response = sendRequest(new GetRequest(URL_BLOG_POST + name), expectedStatus);
+ if (expectedStatus == 200)
+ {
+ JSONObject result = new JSONObject(response.getContentAsString());
+ return result.getJSONObject("item");
+ }
+ else
+ {
+ return null;
+ }
+ }
+
+ private String getCommentsUrl(String nodeRef)
+ {
+ return "/api/node/" + nodeRef.replace("://", "/") + "/comments";
+ }
+
+ private String getCommentUrl(String nodeRef)
+ {
+ return "/api/comment/node/" + nodeRef.replace("://", "/");
+ }
+
+ private String getDeleteCommentUrl(NodeRef commentNodeRef)
+ {
+ String itemTitle = "Test Title";
+ String page = "document-details";
+
+ String URL = MessageFormat.format(URL_DELETE_COMMENT, new Object[] { commentNodeRef.getStoreRef().getProtocol(),
+ commentNodeRef.getStoreRef().getIdentifier(), commentNodeRef.getId(), SITE_SHORT_NAME_BLOG, itemTitle, page});
+ return URL;
+ }
+
+ private JSONObject createComment(String nodeRef, String title, String content, int expectedStatus)
+ throws Exception
+ {
+ JSONObject comment = new JSONObject();
+ comment.put("title", title);
+ comment.put("content", content);
+ comment.put("site", SITE_SHORT_NAME_BLOG);
+ Response response = sendRequest(new PostRequest(getCommentsUrl(nodeRef), comment.toString(), "application/json"), expectedStatus);
+
+ if (expectedStatus != 200)
+ {
+ return null;
+ }
+
+ //logger.debug("Comment created: " + response.getContentAsString());
+ JSONObject result = new JSONObject(response.getContentAsString());
+ return result.getJSONObject("item");
+ }
+
+ private JSONObject updateComment(String nodeRef, String title, String content, int expectedStatus)
+ throws Exception
+ {
+ JSONObject comment = new JSONObject();
+ comment.put("title", title);
+ comment.put("content", content);
+ Response response = sendRequest(new PutRequest(getCommentUrl(nodeRef), comment.toString(), "application/json"), expectedStatus);
+
+ if (expectedStatus != 200)
+ {
+ return null;
+ }
+
+ //logger.debug("Comment updated: " + response.getContentAsString());
+ JSONObject result = new JSONObject(response.getContentAsString());
+ return result.getJSONObject("item");
+ }
+
+
+ // Tests
+
+ public void testCreateDraftPost() throws Exception
+ {
+ String title = "test";
+ String content = "test";
+ JSONObject item = createPost(title, content, null, true, 200);
+
+ // check that the values
+ assertEquals(title, item.get("title"));
+ assertEquals(content, item.get("content"));
+ assertEquals(true, item.get("isDraft"));
+
+ // check that other user doesn't have access to the draft
+ this.authenticationComponent.setCurrentUser(USER_TWO);
+ getPost(item.getString("name"), 404);
+ this.authenticationComponent.setCurrentUser(USER_ONE);
+
+ // Now we'll GET my-drafts to ensure that the post is there.
+ Response response = sendRequest(new GetRequest(URL_MY_DRAFT_BLOG_POSTS), 200);
+ JSONObject result = new JSONObject(response.getContentAsString());
+ assertTrue("Wrong number of posts", result.length() > 0);
+ }
+
+ /**
+ * @since 4.0
+ */
+ @Test
+ @Category({LuceneTests.class, RedundantTests.class})
+ public void testCreateDraftPostWithTagsAndComment() throws Exception
+ {
+ String[] tags = new String[]{"foo", "bar"};
+ String title = "test";
+ String content = "test";
+ JSONObject item = createPost(title, content, tags, true, 200);
+
+ // check that the values
+ assertEquals(title, item.get("title"));
+ assertEquals(content, item.get("content"));
+ assertEquals(true, item.get("isDraft"));
+ JSONArray reportedTags = (JSONArray)item.get("tags");
+ assertEquals("Tags size was wrong.", 2, reportedTags.length());
+ List recoveredTagsList = Arrays.asList(new String[]{reportedTags.getString(0), reportedTags.getString(1)});
+ assertEquals("Tags were wrong.", Arrays.asList(tags), recoveredTagsList);
+
+ // comment on the blog post.
+ NodeRef blogPostNode = new NodeRef(item.getString("nodeRef"));
+ // Currently (mid-Swift dev) there is no Java CommentService, so we have to post a comment via the REST API.
+ String commentsPostUrl = "/api/node/" + blogPostNode.getStoreRef().getProtocol() +
+ "/" + blogPostNode.getStoreRef().getIdentifier() + "/" +
+ blogPostNode.getId() + "/comments";
+
+ String jsonToPost = new JSONStringer().object()
+ .key("title").value("Commented blog title")
+ .key("content").value("Some content.")
+ .endObject().toString();
+
+ Response response = sendRequest(new PostRequest(commentsPostUrl, jsonToPost, "application/json"), 200);
+
+ // check that other user doesn't have access to the draft
+ this.authenticationComponent.setCurrentUser(USER_TWO);
+ getPost(item.getString("name"), 404);
+ this.authenticationComponent.setCurrentUser(USER_ONE);
+
+ // Now we'll GET my-drafts to ensure that the post is there.
+ response = sendRequest(new GetRequest(URL_MY_DRAFT_BLOG_POSTS), 200);
+ JSONObject result = new JSONObject(response.getContentAsString());
+
+ // Ensure it reports the tag correctly on GET.
+ JSONArray items = result.getJSONArray("items");
+ JSONArray tagsArray = items.getJSONObject(0).getJSONArray("tags");
+ assertEquals("Wrong number of tags", 2, tagsArray.length());
+ assertEquals("Tag wrong", tags[0], tagsArray.getString(0));
+ assertEquals("Tag wrong", tags[1], tagsArray.getString(1));
+
+ // Ensure the comment count is accurate
+ assertEquals("Wrong comment count", 1, items.getJSONObject(0).getInt("commentCount"));
+
+ // and that there is content at the commentsURL.
+ String commentsUrl = "/api" + items.getJSONObject(0).getString("commentsUrl");
+ response = sendRequest(new GetRequest(commentsUrl), 200);
+
+
+ // Now get blog-post by tag.
+ // 1. No such tag
+ response = sendRequest(new GetRequest(URL_BLOG_POSTS + "?tag=NOSUCHTAG"), 200);
+ result = new JSONObject(response.getContentAsString());
+
+ assertEquals(0, result.getInt("total"));
+
+ // tag created above
+ response = sendRequest(new GetRequest(URL_BLOG_POSTS + "?tag=foo"), 200);
+ result = new JSONObject(response.getContentAsString());
+
+ assertEquals(1, result.getInt("total"));
+
+ //TODO More assertions on recovered node.
+ }
+
+ public void testCreatePublishedPost() throws Exception
+ {
+ String title = "published";
+ String content = "content";
+
+ JSONObject item = createPost(title, content, null, false, 200);
+ final String postName = item.getString("name");
+
+ // check the values
+ assertEquals(title, item.get("title"));
+ assertEquals(content, item.get("content"));
+ assertEquals(false, item.get("isDraft"));
+
+ // check that user two has access to it as well
+ this.authenticationComponent.setCurrentUser(USER_TWO);
+ getPost(item.getString("name"), 200);
+ this.authenticationComponent.setCurrentUser(USER_ONE);
+
+ // Now we'll GET my-published to ensure that the post is there.
+ Response response = sendRequest(new GetRequest(URL_MY_PUBLISHED_BLOG_POSTS), 200);
+ JSONObject result = new JSONObject(response.getContentAsString());
+
+ // we should have posts.size + drafts.size together
+ assertEquals(this.posts.size() + this.drafts.size(), result.getInt("total"));
+
+ // Finally, we'll delete the blog-post to test the REST DELETE call.
+ response = sendRequest(new DeleteRequest(URL_BLOG_POST + postName), 200);
+
+ }
+
+ public void testCreateEmptyPost() throws Exception
+ {
+ JSONObject item = createPost(null, null, null, false, 200);
+
+ // check the values
+ assertEquals("", item.get("title"));
+ assertEquals("", item.get("content"));
+ assertEquals(false, item.get("isDraft"));
+
+ // check that user two has access to it as well
+ this.authenticationComponent.setCurrentUser(USER_TWO);
+ getPost(item.getString("name"), 200);
+ this.authenticationComponent.setCurrentUser(USER_ONE);
+ }
+
+ public void testUpdated() throws Exception
+ {
+ JSONObject item = createPost("test", "test", null, false, 200);
+ String name = item.getString("name");
+ assertEquals(false, item.getBoolean("isUpdated"));
+
+ item = updatePost(name, "new title", "new content", null, false, 200);
+ assertEquals(true, item.getBoolean("isUpdated"));
+ assertEquals("new title", item.getString("title"));
+ assertEquals("new content", item.getString("content"));
+ }
+
+ public void testUpdateWithEmptyValues() throws Exception
+ {
+ JSONObject item = createPost("test", "test", null, false, 200);
+ String name = item.getString("name");
+ assertEquals(false, item.getBoolean("isUpdated"));
+
+ item = updatePost(item.getString("name"), null, null, null, false, 200);
+ assertEquals("", item.getString("title"));
+ assertEquals("", item.getString("content"));
+ }
+
+ public void testPublishThroughUpdate() throws Exception
+ {
+ JSONObject item = createPost("test", "test", null, true, 200);
+ String name = item.getString("name");
+ assertEquals(true, item.getBoolean("isDraft"));
+
+ // check that user two does not have access
+ this.authenticationComponent.setCurrentUser(USER_TWO);
+ getPost(name, 404);
+ this.authenticationComponent.setCurrentUser(USER_ONE);
+
+ item = updatePost(name, "new title", "new content", null, false, 200);
+ assertEquals("new title", item.getString("title"));
+ assertEquals("new content", item.getString("content"));
+ assertEquals(false, item.getBoolean("isDraft"));
+
+ // check that user two does have access
+ this.authenticationComponent.setCurrentUser(USER_TWO);
+ getPost(name, 200);
+ this.authenticationComponent.setCurrentUser(USER_ONE);
+ }
+
+ public void testCannotDoUnpublish() throws Exception
+ {
+ JSONObject item = createPost("test", "test", null, false, 200);
+ String name = item.getString("name");
+ assertEquals(false, item.getBoolean("isDraft"));
+
+ item = updatePost(name, "new title", "new content", null, true, 400); // should return bad request
+ }
+
+ public void testGetAll() throws Exception
+ {
+ String url = URL_BLOG_POSTS;
+ Response response = sendRequest(new GetRequest(url), 200);
+ JSONObject result = new JSONObject(response.getContentAsString());
+ JSONObject blog;
+
+ // We shouldn't have any posts at this point
+ assertEquals(0, this.posts.size());
+ assertEquals(0, this.drafts.size());
+
+ assertEquals(0, result.getInt("total"));
+ assertEquals(0, result.getInt("startIndex"));
+ assertEquals(0, result.getInt("itemCount"));
+ assertEquals(0, result.getJSONArray("items").length());
+
+ // Check that the permissions are correct
+ JSONObject metadata = result.getJSONObject("metadata");
+ JSONObject perms = metadata.getJSONObject("blogPermissions");
+ assertEquals(false, metadata.getBoolean("externalBlogConfig"));
+ assertEquals(false, perms.getBoolean("delete")); // No container yet
+ assertEquals(true, perms.getBoolean("edit"));
+ assertEquals(true, perms.getBoolean("create"));
+
+
+ // Create a draft and a full post
+ String TITLE_1 = "Published";
+ String TITLE_2 = "Draft";
+ String TITLE_3 = "Another Published";
+ createPost(TITLE_1, "Stuff", null, false, Status.STATUS_OK);
+ createPost(TITLE_2, "Draft Stuff", null, true, Status.STATUS_OK);
+
+ // Check now
+ response = sendRequest(new GetRequest(url), 200);
+ result = new JSONObject(response.getContentAsString());
+ assertEquals(2, result.getInt("total"));
+ assertEquals(0, result.getInt("startIndex"));
+ assertEquals(2, result.getInt("itemCount"));
+ assertEquals(2, result.getJSONArray("items").length());
+
+ // Check the core permissions
+ metadata = result.getJSONObject("metadata");
+ perms = metadata.getJSONObject("blogPermissions");
+ assertEquals(false, metadata.getBoolean("externalBlogConfig"));
+ assertEquals(true, perms.getBoolean("delete")); // On the container itself
+ assertEquals(true, perms.getBoolean("edit"));
+ assertEquals(true, perms.getBoolean("create"));
+
+ // Check each one in detail, they'll come back Published
+ // then draft (newest first within that)
+ blog = result.getJSONArray("items").getJSONObject(0);
+ assertEquals(TITLE_1, blog.get("title"));
+ assertEquals(false, blog.getBoolean("isDraft"));
+ perms = blog.getJSONObject("permissions");
+ assertEquals(true, perms.getBoolean("delete"));
+ assertEquals(true, perms.getBoolean("edit"));
+
+ blog = result.getJSONArray("items").getJSONObject(1);
+ assertEquals(TITLE_2, blog.get("title"));
+ assertEquals(true, blog.getBoolean("isDraft"));
+ perms = blog.getJSONObject("permissions");
+ assertEquals(true, perms.getBoolean("delete"));
+ assertEquals(true, perms.getBoolean("edit"));
+
+
+ // Add a third post
+ createPost(TITLE_3, "Still Stuff", null, false, Status.STATUS_OK);
+
+ response = sendRequest(new GetRequest(url), 200);
+ result = new JSONObject(response.getContentAsString());
+ assertEquals(3, result.getInt("total"));
+ assertEquals(0, result.getInt("startIndex"));
+ assertEquals(3, result.getInt("itemCount"));
+ assertEquals(3, result.getJSONArray("items").length());
+
+ // Published then draft, newest first
+ blog = result.getJSONArray("items").getJSONObject(0);
+ assertEquals(TITLE_3, blog.get("title"));
+ blog = result.getJSONArray("items").getJSONObject(1);
+ assertEquals(TITLE_1, blog.get("title"));
+ blog = result.getJSONArray("items").getJSONObject(2);
+ assertEquals(TITLE_2, blog.get("title"));
+
+
+ // Ensure that paging behaves properly
+ response = sendRequest(new GetRequest(url + "?pageSize=2&startIndex=0"), 200);
+ result = new JSONObject(response.getContentAsString());
+ assertEquals(3, result.getInt("total"));
+ assertEquals(0, result.getInt("startIndex"));
+ assertEquals(2, result.getInt("itemCount"));
+ assertEquals(2, result.getJSONArray("items").length());
+
+ assertEquals(TITLE_3, result.getJSONArray("items").getJSONObject(0).get("title"));
+ assertEquals(TITLE_1, result.getJSONArray("items").getJSONObject(1).get("title"));
+
+
+ response = sendRequest(new GetRequest(url + "?pageSize=2&startIndex=1"), 200);
+ result = new JSONObject(response.getContentAsString());
+ assertEquals(3, result.getInt("total"));
+ assertEquals(1, result.getInt("startIndex"));
+ assertEquals(2, result.getInt("itemCount"));
+ assertEquals(2, result.getJSONArray("items").length());
+
+ assertEquals(TITLE_1, result.getJSONArray("items").getJSONObject(0).get("title"));
+ assertEquals(TITLE_2, result.getJSONArray("items").getJSONObject(1).get("title"));
+
+
+ response = sendRequest(new GetRequest(url + "?pageSize=2&startIndex=2"), 200);
+ result = new JSONObject(response.getContentAsString());
+ assertEquals(3, result.getInt("total"));
+ assertEquals(2, result.getInt("startIndex"));
+ assertEquals(1, result.getInt("itemCount"));
+ assertEquals(1, result.getJSONArray("items").length());
+
+ assertEquals(TITLE_2, result.getJSONArray("items").getJSONObject(0).get("title"));
+
+
+ // Switch user, check that permissions are correct
+ // (Drafts won't be seen)
+ this.authenticationComponent.setCurrentUser(USER_TWO);
+
+ response = sendRequest(new GetRequest(url), 200);
+ result = new JSONObject(response.getContentAsString());
+ assertEquals(2, result.getInt("total"));
+ assertEquals(0, result.getInt("startIndex"));
+ assertEquals(2, result.getInt("itemCount"));
+
+ assertEquals(2, result.getJSONArray("items").length());
+ blog = result.getJSONArray("items").getJSONObject(0);
+ assertEquals(TITLE_3, blog.get("title"));
+ assertEquals(false, blog.getBoolean("isDraft"));
+ perms = blog.getJSONObject("permissions");
+ assertEquals(false, perms.getBoolean("delete"));
+ assertEquals(true, perms.getBoolean("edit"));
+
+ blog = result.getJSONArray("items").getJSONObject(1);
+ assertEquals(TITLE_1, blog.get("title"));
+ assertEquals(false, blog.getBoolean("isDraft"));
+ perms = blog.getJSONObject("permissions");
+ assertEquals(false, perms.getBoolean("delete"));
+ assertEquals(true, perms.getBoolean("edit"));
+ }
+
+ public void testGetNew() throws Exception
+ {
+ String url = URL_BLOG_POSTS + "/new";
+ Response response = sendRequest(new GetRequest(url), 200);
+ JSONObject result = new JSONObject(response.getContentAsString());
+
+ // we should have posts.size
+ assertEquals(this.posts.size(), result.getInt("total"));
+ }
+
+ public void testGetDrafts() throws Exception
+ {
+ String url = URL_BLOG_POSTS + "/mydrafts";
+ Response response = sendRequest(new GetRequest(URL_BLOG_POSTS), 200);
+ JSONObject result = new JSONObject(response.getContentAsString());
+
+ // we should have drafts.size resultss
+ assertEquals(this.drafts.size(), result.getInt("total"));
+
+ // the second user should have zero
+ this.authenticationComponent.setCurrentUser(USER_TWO);
+ response = sendRequest(new GetRequest(url), 200);
+ result = new JSONObject(response.getContentAsString());
+ assertEquals(0, result.getInt("total"));
+ this.authenticationComponent.setCurrentUser(USER_ONE);
+
+ }
+
+ public void testMyPublished() throws Exception
+ {
+ String url = URL_BLOG_POSTS + "/mypublished";
+ Response response = sendRequest(new GetRequest(url), 200);
+ JSONObject result = new JSONObject(response.getContentAsString());
+
+ // we should have posts.size results
+ assertEquals(this.drafts.size(), result.getInt("total"));
+
+ // the second user should have zero
+ this.authenticationComponent.setCurrentUser(USER_TWO);
+ response = sendRequest(new GetRequest(url), 200);
+ result = new JSONObject(response.getContentAsString());
+ assertEquals(0, result.getInt("total"));
+ this.authenticationComponent.setCurrentUser(USER_ONE);
+ }
+
+ public void testComments() throws Exception
+ {
+ JSONObject item = createPost("test", "test", null, false, 200);
+ String name = item.getString("name");
+ String nodeRef = item.getString("nodeRef");
+
+ JSONObject commentOne = createComment(nodeRef, "comment", "content", 200);
+ JSONObject commentTwo = createComment(nodeRef, "comment", "content", 200);
+
+ // fetch the comments
+ Response response = sendRequest(new GetRequest(getCommentsUrl(nodeRef)), 200);
+ JSONObject result = new JSONObject(response.getContentAsString());
+ assertEquals(2, result.getInt("total"));
+
+ // add another one
+ JSONObject commentThree = createComment(nodeRef, "comment", "content", 200);
+
+ response = sendRequest(new GetRequest(getCommentsUrl(nodeRef)), 200);
+ result = new JSONObject(response.getContentAsString());
+ assertEquals(3, result.getInt("total"));
+
+ // delete the last comment
+ response = sendRequest(new DeleteRequest(getCommentUrl(commentThree.getString("nodeRef"))), 200);
+
+ response = sendRequest(new GetRequest(getCommentsUrl(nodeRef)), 200);
+ result = new JSONObject(response.getContentAsString());
+ assertEquals(2, result.getInt("total"));
+
+ JSONObject commentTwoUpdated = updateComment(commentTwo.getString("nodeRef"), "new title", "new content", 200);
+ assertEquals("new title", commentTwoUpdated.getString("title"));
+ assertEquals("new content", commentTwoUpdated.getString("content"));
+ }
+
+ /**
+ * REPO-828 (MNT-16401)
+ * @throws Exception
+ */
+ public void testDeleteCommentPostActivity() throws Exception
+ {
+ this.authenticationComponent.setCurrentUser(USER_ONE);
+ JSONObject item = createPost("testActivity", "test", null, false, 200);
+ assertNotNull(item);
+ postLookup.execute();
+ feedGenerator.execute();
+ int activityNumStart = activityService.getSiteFeedEntries(SITE_SHORT_NAME_BLOG).size();
+ String nodeRef = item.getString("nodeRef");
+ JSONObject commentOne = createComment(nodeRef, "comment", "content", 200);
+ assertNotNull(item);
+ postLookup.execute();
+ feedGenerator.execute();
+ int activityNumNext = activityService.getSiteFeedEntries(SITE_SHORT_NAME_BLOG).size();
+ assertEquals("The activity feeds were not generated after adding a comment", activityNumStart + 1, activityNumNext);
+ activityNumStart = activityNumNext;
+ NodeRef commentNodeRef = new NodeRef(commentOne.getString("nodeRef"));
+ Response resp = sendRequest(new DeleteRequest(getDeleteCommentUrl(commentNodeRef)), 200);
+ assertTrue(resp.getStatus() == 200);
+ postLookup.execute();
+ feedGenerator.execute();
+ activityNumNext = activityService.getSiteFeedEntries(SITE_SHORT_NAME_BLOG).size();
+ assertEquals("The activity feeds were not generated after deleting a comment", activityNumStart + 1, activityNumNext);
+ }
+
+ /**
+ * You can attach information to the blog container relating
+ * to integration with external blogs.
+ * This tests that feature
+ */
+ public void testBlogIntegration() throws Exception
+ {
+ // Try to fetch the details on a new site
+ Response response = sendRequest(new GetRequest(URL_BLOG_CORE), 200);
+ String json = response.getContentAsString();
+ JSONObject result = new JSONObject(json);
+
+ assertEquals("No item in:\n"+json, true, result.has("item"));
+ JSONObject item = result.getJSONObject("item");
+
+ assertEquals("Missing key in: " + item, true, item.has("qnamePath"));
+ assertEquals("Missing key in: " + item, true, item.has("detailsUrl"));
+ assertEquals("Missing key in: " + item, true, item.has("blogPostsUrl"));
+
+ // Blog properties are empty to start
+ assertEquals("", item.getString("type"));
+ assertEquals("", item.getString("name"));
+ assertEquals("", item.getString("description"));
+ assertEquals("", item.getString("url"));
+ assertEquals("", item.getString("username"));
+ assertEquals("", item.getString("password"));
+
+
+ // Have it updated
+ JSONObject blog = new JSONObject();
+ blog.put("blogType", "wordpress");
+ blog.put("blogName", "A Blog!");
+ blog.put("username", "guest");
+ sendRequest(new PutRequest(URL_BLOG_CORE, blog.toString(), "application/json"), Status.STATUS_OK);
+
+ // Check again now
+ response = sendRequest(new GetRequest(URL_BLOG_CORE), 200);
+ json = response.getContentAsString();
+ result = new JSONObject(json);
+
+ assertEquals("No item in:\n"+json, true, result.has("item"));
+ item = result.getJSONObject("item");
+
+ assertEquals("Missing key in: " + item, true, item.has("qnamePath"));
+ assertEquals("Missing key in: " + item, true, item.has("detailsUrl"));
+ assertEquals("Missing key in: " + item, true, item.has("blogPostsUrl"));
+
+ // Blog properties should now be set
+ assertEquals("wordpress", item.getString("type"));
+ assertEquals("A Blog!", item.getString("name"));
+ assertEquals("", item.getString("description"));
+ assertEquals("", item.getString("url"));
+ assertEquals("guest", item.getString("username"));
+ assertEquals("", item.getString("password"));
+ }
+
+ /**
+ * Does some stress tests.
+ *
+ * Currently observed errors:
+ * 1. [repo.action.AsynchronousActionExecutionQueueImpl] Failed to execute asynchronous action: Action[ id=485211db-f117-4976-9530-ab861a19f563, node=null ]
+ * org.alfresco.repo.security.permissions.AccessDeniedException: Access Denied. You do not have the appropriate permissions to perform this operation.
+ *
+ * 2. JSONException, but with root cause being
+ * get(assocs) failed on instance of org.alfresco.repo.template.TemplateNode
+ * The problematic instruction:
+ * ----------
+ * ==> if person.assocs["cm:avatar"]?? [on line 4, column 7 in org/alfresco/repository/blogs/blogpost.lib.ftl]
+ *
+ * @throws Exception
+ */
+ public void _testTagsStressTest() throws Exception
+ {
+ final List exceptions = Collections.synchronizedList(new ArrayList());
+ List threads = new ArrayList();
+
+ System.err.println("Creating and starting threads...");
+ for (int x=0; x < 3; x++)
+ {
+ Thread t = new Thread(new Runnable()
+ {
+ public void run()
+ {
+ // set the correct user
+ authenticationComponent.setCurrentUser(USER_ONE);
+
+ // now do some requests
+ try
+ {
+ for (int y=0; y < 3; y++)
+ {
+ off_testPostTags();
+ off_testClearTags();
+ }
+ System.err.println("------------- SUCCEEDED ---------------");
+ } catch (Exception e)
+ {
+ System.err.println("------------- ERROR ---------------");
+ exceptions.add(e);
+ e.printStackTrace();
+ return;
+ }
+ }});
+
+ threads.add(t);
+ t.start();
+ }
+ /*for (Thread t : threads)
+ {
+ t.start();
+ }*/
+
+ for (Thread t : threads)
+ {
+ t.join();
+ }
+
+ System.err.println("------------- STACK TRACES ---------------");
+ for (Exception e : exceptions)
+ {
+ e.printStackTrace();
+ }
+ System.err.println("------------- STACK TRACES END ---------------");
+ if (exceptions.size() > 0)
+ {
+ throw exceptions.get(0);
+ }
+ }
+
+ public void off_testPostTags() throws Exception
+ {
+ String[] tags = { "first", "test" };
+ JSONObject item = createPost("tagtest", "tagtest", tags, false, 200);
+ assertEquals(2, item.getJSONArray("tags").length());
+ assertEquals("first", item.getJSONArray("tags").get(0));
+ assertEquals("test", item.getJSONArray("tags").get(1));
+
+ item = updatePost(item.getString("name"), null, null, new String[] { "First", "Test", "Second" }, false, 200);
+ assertEquals(3, item.getJSONArray("tags").length());
+ assertEquals("first", item.getJSONArray("tags").get(0));
+ assertEquals("test", item.getJSONArray("tags").get(1));
+ assertEquals("second", item.getJSONArray("tags").get(2));
+ }
+
+ public void off_testClearTags() throws Exception
+ {
+ String[] tags = { "abc", "def"};
+ JSONObject item = createPost("tagtest", "tagtest", tags, false, 200);
+ assertEquals(2, item.getJSONArray("tags").length());
+
+ item = updatePost(item.getString("name"), null, null, new String[0], false, 200);
+ assertEquals(0, item.getJSONArray("tags").length());
+ }
+
+ /**
+ * Test for MNT-11964
+ * @throws Exception
+ */
+ public void testBlogPermission() throws Exception
+ {
+ this.authenticationComponent.setCurrentUser(AuthenticationUtil.getAdminUserName());
+
+ String siteName = SITE_SHORT_NAME_BLOG + GUID.generate();
+ this.siteService.createSite("BlogSitePreset", siteName, "BlogSiteTitle", "BlogSiteDescription", SiteVisibility.PUBLIC);
+
+ String userName = USER_ONE + GUID.generate();
+ createUser(userName, SiteModel.SITE_COLLABORATOR, siteName);
+
+ // Check permissions for admin
+ checkBlogPermissions(siteName);
+
+ // Check permissions for user
+ this.authenticationComponent.setCurrentUser(userName);
+ checkBlogPermissions(siteName);
+
+ // Cleanup
+ this.authenticationComponent.setCurrentUser(AuthenticationUtil.getAdminUserName());
+ this.siteService.deleteSite(siteName);
+
+ // Create a new site as user
+ this.authenticationComponent.setCurrentUser(userName);
+ siteName = SITE_SHORT_NAME_BLOG + GUID.generate();
+ this.siteService.createSite("BlogSitePreset", siteName, "BlogSiteTitle", "BlogSiteDescription", SiteVisibility.PUBLIC);
+
+ // Check permissions for user
+ checkBlogPermissions(siteName);
+
+ // Check permissions for admin
+ this.authenticationComponent.setCurrentUser(AuthenticationUtil.getAdminUserName());
+ checkBlogPermissions(siteName);
+
+ // Cleanup
+ this.siteService.deleteSite(siteName);
+ this.personService.deletePerson(userName);
+ }
+
+ private void checkBlogPermissions(String siteName) throws Exception
+ {
+ String url = "/api/blog/site/" + siteName + "/" + COMPONENT_BLOG;
+ Response response = sendRequest(new GetRequest(url), 200);
+ JSONObject result = new JSONObject(response.getContentAsString());
+
+ assertTrue("The user sould have permission to create a new blog.", result.getJSONObject("item").getJSONObject("permissions").getBoolean("create"));
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/org/alfresco/repo/web/scripts/calendar/CalendarRestApiTest.java b/src/test/java/org/alfresco/repo/web/scripts/calendar/CalendarRestApiTest.java
index 1c048226e0..ec71468b2a 100644
--- a/src/test/java/org/alfresco/repo/web/scripts/calendar/CalendarRestApiTest.java
+++ b/src/test/java/org/alfresco/repo/web/scripts/calendar/CalendarRestApiTest.java
@@ -1,28 +1,28 @@
-/*
- * #%L
- * Alfresco Remote API
- * %%
- * Copyright (C) 2005 - 2016 Alfresco Software Limited
- * %%
- * This file is part of the Alfresco software.
- * If the software was purchased under a paid Alfresco license, the terms of
- * the paid license agreement will prevail. Otherwise, the software is
- * provided under the following open source license terms:
- *
- * 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 .
- * #L%
- */
+/*
+ * #%L
+ * Alfresco Remote API
+ * %%
+ * Copyright (C) 2005 - 2016 Alfresco Software Limited
+ * %%
+ * This file is part of the Alfresco software.
+ * If the software was purchased under a paid Alfresco license, the terms of
+ * the paid license agreement will prevail. Otherwise, the software is
+ * provided under the following open source license terms:
+ *
+ * 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 .
+ * #L%
+ */
package org.alfresco.repo.web.scripts.calendar;
import java.util.ArrayList;
@@ -120,7 +120,7 @@ public class CalendarRestApiTest extends BaseWebScriptTest
SiteInfo siteInfo = this.siteService.getSite(SITE_SHORT_NAME_CALENDAR);
if (siteInfo == null)
{
- this.siteService.createSite("CalendarSitePreset", SITE_SHORT_NAME_CALENDAR, "CalendarSiteTitle", "CalendarSiteDescription", SiteVisibility.PUBLIC);
+ this.siteService.createSite("CalendarSitePreset", SITE_SHORT_NAME_CALENDAR, "CalendarSiteTitle", "BlogSiteDescription", SiteVisibility.PUBLIC);
}
// Ensure the calendar container is there
diff --git a/src/test/java/org/alfresco/repo/web/scripts/comment/CommentsApiTest.java b/src/test/java/org/alfresco/repo/web/scripts/comment/CommentsApiTest.java
index 254e0c0ae9..31fcbdd8b5 100644
--- a/src/test/java/org/alfresco/repo/web/scripts/comment/CommentsApiTest.java
+++ b/src/test/java/org/alfresco/repo/web/scripts/comment/CommentsApiTest.java
@@ -78,7 +78,7 @@ import org.springframework.extensions.webscripts.WebScriptException;
/**
* TODO: Fix the loose transaction handling.
- * TODO: Rationalise with other v0 Comment REST API tests. See also ACE-5437.
+ * TODO: Rationalise with other v0 Comment REST API tests (eg. see BlogServiceTest ... etc). See also ACE-5437.
*/
public class CommentsApiTest extends BaseWebScriptTest
{
diff --git a/src/test/java/org/alfresco/repo/web/scripts/discussion/DiscussionRestApiTest.java b/src/test/java/org/alfresco/repo/web/scripts/discussion/DiscussionRestApiTest.java
new file mode 100644
index 0000000000..2a178643fb
--- /dev/null
+++ b/src/test/java/org/alfresco/repo/web/scripts/discussion/DiscussionRestApiTest.java
@@ -0,0 +1,1306 @@
+/*
+ * #%L
+ * Alfresco Remote API
+ * %%
+ * Copyright (C) 2005 - 2016 Alfresco Software Limited
+ * %%
+ * This file is part of the Alfresco software.
+ * If the software was purchased under a paid Alfresco license, the terms of
+ * the paid license agreement will prevail. Otherwise, the software is
+ * provided under the following open source license terms:
+ *
+ * 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 .
+ * #L%
+ */
+package org.alfresco.repo.web.scripts.discussion;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import javax.transaction.UserTransaction;
+
+import org.alfresco.model.ContentModel;
+import org.alfresco.model.ForumModel;
+import org.alfresco.repo.node.archive.NodeArchiveService;
+import org.alfresco.repo.policy.BehaviourFilter;
+import org.alfresco.repo.security.authentication.AuthenticationComponent;
+import org.alfresco.repo.security.authentication.AuthenticationUtil;
+import org.alfresco.repo.site.SiteModel;
+import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback;
+import org.alfresco.repo.web.scripts.BaseWebScriptTest;
+import org.alfresco.service.cmr.repository.ChildAssociationRef;
+import org.alfresco.service.cmr.repository.NodeRef;
+import org.alfresco.service.cmr.repository.NodeService;
+import org.alfresco.service.cmr.security.MutableAuthenticationService;
+import org.alfresco.service.cmr.security.PermissionService;
+import org.alfresco.service.cmr.security.PersonService;
+import org.alfresco.service.cmr.site.SiteInfo;
+import org.alfresco.service.cmr.site.SiteService;
+import org.alfresco.service.cmr.site.SiteVisibility;
+import org.alfresco.service.namespace.QName;
+import org.alfresco.service.namespace.RegexQNamePattern;
+import org.alfresco.service.transaction.TransactionService;
+import org.alfresco.util.GUID;
+import org.alfresco.util.PropertyMap;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.json.JSONArray;
+import org.json.JSONObject;
+import org.springframework.extensions.webscripts.Status;
+import org.springframework.extensions.webscripts.TestWebScriptServer.DeleteRequest;
+import org.springframework.extensions.webscripts.TestWebScriptServer.GetRequest;
+import org.springframework.extensions.webscripts.TestWebScriptServer.PostRequest;
+import org.springframework.extensions.webscripts.TestWebScriptServer.PutRequest;
+import org.springframework.extensions.webscripts.TestWebScriptServer.Response;
+
+/**
+ * Unit Test to test Discussions Web Script API
+ */
+public class DiscussionRestApiTest extends BaseWebScriptTest
+{
+ @SuppressWarnings("unused")
+ private static Log logger = LogFactory.getLog(DiscussionRestApiTest.class);
+
+ private static final String DELETED_REPLY_POST_MARKER = "[[deleted]]";
+
+ private MutableAuthenticationService authenticationService;
+ private AuthenticationComponent authenticationComponent;
+ private TransactionService transactionService;
+ private BehaviourFilter policyBehaviourFilter;
+ private PermissionService permissionService;
+ private PersonService personService;
+ private SiteService siteService;
+ private NodeService nodeService;
+ private NodeService internalNodeService;
+ private NodeArchiveService nodeArchiveService;
+
+ private static final String USER_ONE = "UserOneThird";
+ private static final String USER_TWO = "UserTwoThird";
+ private static final String SITE_SHORT_NAME_DISCUSSION = "DiscussionSiteShortNameThree";
+ private static final String COMPONENT_DISCUSSION = "discussions";
+
+ private static final String URL_FORUM_SITE_POST = "/api/forum/post/site/" + SITE_SHORT_NAME_DISCUSSION + "/" + COMPONENT_DISCUSSION + "/";
+ private static final String URL_FORUM_SITE_POSTS = "/api/forum/site/" + SITE_SHORT_NAME_DISCUSSION + "/" + COMPONENT_DISCUSSION + "/posts";
+ private static final String URL_FORUM_NODE_POST_BASE = "/api/forum/post/node/"; // Plus node id
+ private static final String URL_FORUM_NODE_POSTS_BASE = "/api/forum/node/"; // Plus node id + /posts
+
+ private List posts = new ArrayList(5);
+ private NodeRef FORUM_NODE;
+
+
+ // General methods
+
+ @Override
+ protected void setUp() throws Exception
+ {
+ super.setUp();
+
+ this.authenticationService = (MutableAuthenticationService)getServer().getApplicationContext().getBean("AuthenticationService");
+ this.authenticationComponent = (AuthenticationComponent)getServer().getApplicationContext().getBean("authenticationComponent");
+ this.policyBehaviourFilter = (BehaviourFilter)getServer().getApplicationContext().getBean("policyBehaviourFilter");
+ this.transactionService = (TransactionService)getServer().getApplicationContext().getBean("transactionService");
+ this.permissionService = (PermissionService)getServer().getApplicationContext().getBean("PermissionService");
+ this.personService = (PersonService)getServer().getApplicationContext().getBean("PersonService");
+ this.siteService = (SiteService)getServer().getApplicationContext().getBean("SiteService");
+ this.nodeService = (NodeService)getServer().getApplicationContext().getBean("NodeService");
+ this.internalNodeService = (NodeService)getServer().getApplicationContext().getBean("nodeService");
+ this.nodeArchiveService = (NodeArchiveService)getServer().getApplicationContext().getBean("nodeArchiveService");
+
+ // Authenticate as user
+ this.authenticationComponent.setCurrentUser(AuthenticationUtil.getAdminUserName());
+
+ // Create test site
+ // - only create the site if it doesn't already exist
+ SiteInfo siteInfo = this.siteService.getSite(SITE_SHORT_NAME_DISCUSSION);
+ if (siteInfo == null)
+ {
+ siteInfo = this.siteService.createSite("DiscussionSitePreset", SITE_SHORT_NAME_DISCUSSION,
+ "DiscussionSiteTitle", "DiscussionSiteDescription", SiteVisibility.PUBLIC);
+ }
+ final NodeRef siteNodeRef = siteInfo.getNodeRef();
+
+ // Create the forum
+ final String forumNodeName = "TestForum";
+ FORUM_NODE = nodeService.getChildByName(siteInfo.getNodeRef(), ContentModel.ASSOC_CONTAINS, forumNodeName);
+ if (FORUM_NODE == null)
+ {
+ FORUM_NODE = transactionService.getRetryingTransactionHelper().doInTransaction(new RetryingTransactionCallback() {
+ @Override
+ public NodeRef execute() throws Throwable {
+ Map props = new HashMap(5);
+ props.put(ContentModel.PROP_NAME, forumNodeName);
+ props.put(ContentModel.PROP_TITLE, forumNodeName);
+
+ return nodeService.createNode(
+ siteNodeRef, ContentModel.ASSOC_CONTAINS,
+ QName.createQName(forumNodeName), ForumModel.TYPE_FORUM, props
+ ).getChildRef();
+ }
+ });
+ }
+
+ // Create users
+ createUser(USER_ONE, SiteModel.SITE_COLLABORATOR, SITE_SHORT_NAME_DISCUSSION);
+ createUser(USER_TWO, SiteModel.SITE_CONTRIBUTOR, SITE_SHORT_NAME_DISCUSSION);
+
+ // Do tests as inviter user
+ this.authenticationComponent.setCurrentUser(USER_ONE);
+ }
+
+ @Override
+ protected void tearDown() throws Exception
+ {
+ super.tearDown();
+
+ // admin user required to delete user
+ this.authenticationComponent.setCurrentUser(AuthenticationUtil.getAdminUserName());
+
+ // delete the discussions users
+ if(personService.personExists(USER_ONE))
+ {
+ personService.deletePerson(USER_ONE);
+ }
+ if (this.authenticationService.authenticationExists(USER_ONE))
+ {
+ this.authenticationService.deleteAuthentication(USER_ONE);
+ }
+
+ if(personService.personExists(USER_TWO))
+ {
+ personService.deletePerson(USER_TWO);
+ }
+ if (this.authenticationService.authenticationExists(USER_TWO))
+ {
+ this.authenticationService.deleteAuthentication(USER_TWO);
+ }
+
+ SiteInfo siteInfo = this.siteService.getSite(SITE_SHORT_NAME_DISCUSSION);
+ if (siteInfo != null)
+ {
+ // delete discussions test site
+ RetryingTransactionCallback deleteCallback = new RetryingTransactionCallback()
+ {
+ @Override
+ public Void execute() throws Throwable
+ {
+ siteService.deleteSite(SITE_SHORT_NAME_DISCUSSION);
+ return null;
+ }
+ };
+ transactionService.getRetryingTransactionHelper().doInTransaction(deleteCallback);
+ nodeArchiveService.purgeArchivedNode(nodeArchiveService.getArchivedNode(siteInfo.getNodeRef()));
+ }
+ }
+
+ private void createUser(String userName, String role, String siteName)
+ {
+ // if user with given user name doesn't already exist then create user
+ if (!this.authenticationService.authenticationExists(userName))
+ {
+ // create user
+ this.authenticationService.createAuthentication(userName, "password".toCharArray());
+ }
+
+ if (!this.personService.personExists(userName))
+ {
+ // create person properties
+ PropertyMap personProps = new PropertyMap();
+ personProps.put(ContentModel.PROP_USERNAME, userName);
+ personProps.put(ContentModel.PROP_FIRSTNAME, "FirstName123");
+ personProps.put(ContentModel.PROP_LASTNAME, "LastName123");
+ personProps.put(ContentModel.PROP_EMAIL, "FirstName123.LastName123@email.com");
+ personProps.put(ContentModel.PROP_JOBTITLE, "JobTitle123");
+ personProps.put(ContentModel.PROP_JOBTITLE, "Organisation123");
+
+ // create person node for user
+ this.personService.createPerson(personProps);
+ }
+
+ // add the user as a member with the given role
+ this.siteService.setMembership(siteName, userName, role);
+
+ // Give the test user access to the test node
+ // They need to be able to read it, and create children of it
+ permissionService.setPermission(FORUM_NODE, userName, PermissionService.READ, true);
+ permissionService.setPermission(FORUM_NODE, userName, PermissionService.CREATE_CHILDREN, true);
+ }
+
+
+ // -----------------------------------------------------
+ // Test helper methods
+ // -----------------------------------------------------
+
+ /**
+ * Creates a new topic+post in the test site
+ */
+ private JSONObject createSitePost(String title, String content, int expectedStatus)
+ throws Exception
+ {
+ return doCreatePost(URL_FORUM_SITE_POSTS, title, content, expectedStatus);
+ }
+
+ /**
+ * Creates a new topic+post under the given node
+ */
+ private JSONObject createNodePost(NodeRef nodeRef, String title, String content,
+ int expectedStatus) throws Exception
+ {
+ return doCreatePost(getPostsUrl(nodeRef), title, content, expectedStatus);
+ }
+
+ private JSONObject doCreatePost(String url, String title, String content,
+ int expectedStatus) throws Exception
+ {
+ JSONObject post = new JSONObject();
+ post.put("title", title);
+ post.put("content", content);
+ Response response = sendRequest(new PostRequest(url, post.toString(), "application/json"), expectedStatus);
+
+ if (expectedStatus != Status.STATUS_OK)
+ {
+ return null;
+ }
+
+ JSONObject result = new JSONObject(response.getContentAsString());
+ JSONObject item = result.getJSONObject("item");
+ posts.add(item.getString("name"));
+ return item;
+ }
+
+ private JSONObject updatePost(NodeRef nodeRef, String title, String content,
+ int expectedStatus) throws Exception
+ {
+ return doUpdatePost(getPostUrl(nodeRef), title, content, expectedStatus);
+ }
+
+ private JSONObject updatePost(String name, String title, String content,
+ int expectedStatus) throws Exception
+ {
+ return doUpdatePost(URL_FORUM_SITE_POST + name, title, content, expectedStatus);
+ }
+
+ private JSONObject doUpdatePost(String url, String title, String content,
+ int expectedStatus) throws Exception
+ {
+ JSONObject post = new JSONObject();
+ post.put("title", title);
+ post.put("content", content);
+ Response response = sendRequest(new PutRequest(url, post.toString(), "application/json"), expectedStatus);
+
+ if (expectedStatus != Status.STATUS_OK)
+ {
+ return null;
+ }
+
+ JSONObject result = new JSONObject(response.getContentAsString());
+ return result.getJSONObject("item");
+ }
+
+ private JSONObject getPost(String name, int expectedStatus) throws Exception
+ {
+ return doGetPost(URL_FORUM_SITE_POST + name, expectedStatus);
+ }
+
+ private JSONObject getPost(NodeRef nodeRef, int expectedStatus) throws Exception
+ {
+ return doGetPost(getPostUrl(nodeRef), expectedStatus);
+ }
+
+ private JSONObject doGetPost(String url, int expectedStatus) throws Exception
+ {
+ Response response = sendRequest(new GetRequest(url), expectedStatus);
+ if (expectedStatus == Status.STATUS_OK)
+ {
+ JSONObject result = new JSONObject(response.getContentAsString());
+ return result.getJSONObject("item");
+ }
+ else
+ {
+ return null;
+ }
+ }
+
+ private JSONObject getReplies(String name, int expectedStatus) throws Exception
+ {
+ return doGetReplies(getRepliesUrl(name), expectedStatus);
+ }
+
+ private JSONObject getReplies(NodeRef nodeRef, int expectedStatus) throws Exception
+ {
+ return doGetReplies(getRepliesUrl(nodeRef), expectedStatus);
+ }
+
+ private JSONObject doGetReplies(String url, int expectedStatus) throws Exception
+ {
+ Response response = sendRequest(new GetRequest(url), expectedStatus);
+ if (expectedStatus == Status.STATUS_OK)
+ {
+ JSONObject result = new JSONObject(response.getContentAsString());
+ return result;
+ }
+ else
+ {
+ return null;
+ }
+ }
+
+ private JSONObject getPosts(String type, int expectedStatus) throws Exception
+ {
+ return doGetPosts(URL_FORUM_SITE_POSTS, type, expectedStatus);
+ }
+
+ private JSONObject getPosts(NodeRef nodeRef, String type, int expectedStatus) throws Exception
+ {
+ return doGetPosts(getPostsUrl(nodeRef), type, expectedStatus);
+ }
+
+ private JSONObject doGetPosts(String baseUrl, String type, int expectedStatus) throws Exception
+ {
+ String url = null;
+ if (type == null)
+ {
+ url = baseUrl;
+ }
+ else if (type == "limit")
+ {
+ url = baseUrl + "?pageSize=1";
+ }
+ else if (type == "hot")
+ {
+ url = baseUrl + "/hot";
+ }
+ else if (type == "mine")
+ {
+ url = baseUrl + "/myposts";
+ }
+ else if (type.startsWith("new"))
+ {
+ url = baseUrl + "/" + type;
+ }
+ else
+ {
+ throw new IllegalArgumentException("Invalid search type " + type);
+ }
+
+ Response response = sendRequest(new GetRequest(url), expectedStatus);
+ if (expectedStatus == Status.STATUS_OK)
+ {
+ JSONObject result = new JSONObject(response.getContentAsString());
+ return result;
+ }
+ else
+ {
+ return null;
+ }
+ }
+
+ private JSONObject deletePost(String name, int expectedStatus) throws Exception
+ {
+ return doDeletePost(URL_FORUM_SITE_POST + name, expectedStatus);
+ }
+
+ private JSONObject deletePost(NodeRef nodeRef, int expectedStatus) throws Exception
+ {
+ return doDeletePost(getPostUrl(nodeRef), expectedStatus);
+ }
+
+ private JSONObject doDeletePost(String url, int expectedStatus) throws Exception
+ {
+ Response response = sendRequest(new DeleteRequest(url), Status.STATUS_OK);
+ if (expectedStatus == Status.STATUS_OK)
+ {
+ return new JSONObject(response.getContentAsString());
+ }
+ else
+ {
+ return null;
+ }
+ }
+
+ private String getRepliesUrl(NodeRef nodeRef)
+ {
+ return getPostUrl(nodeRef) + "/replies";
+ }
+
+ private String getRepliesUrl(String postName)
+ {
+ return URL_FORUM_SITE_POST + postName + "/replies";
+ }
+
+ private String getPostUrl(NodeRef nodeRef)
+ {
+ return URL_FORUM_NODE_POST_BASE + nodeRef.toString().replace("://", "/");
+ }
+
+ private String getPostsUrl(NodeRef nodeRef)
+ {
+ return URL_FORUM_NODE_POSTS_BASE + nodeRef.toString().replace("://", "/") + "/posts";
+ }
+
+ private JSONObject createReply(NodeRef nodeRef, String title, String content, int expectedStatus)
+ throws Exception
+ {
+ JSONObject reply = new JSONObject();
+ reply.put("title", title);
+ reply.put("content", content);
+ Response response = sendRequest(new PostRequest(getRepliesUrl(nodeRef), reply.toString(), "application/json"), expectedStatus);
+
+ if (expectedStatus != 200)
+ {
+ return null;
+ }
+
+ JSONObject result = new JSONObject(response.getContentAsString());
+ return result.getJSONObject("item");
+ }
+
+ private JSONObject updateComment(NodeRef nodeRef, String title, String content,
+ int expectedStatus) throws Exception
+ {
+ JSONObject comment = new JSONObject();
+ comment.put("title", title);
+ comment.put("content", content);
+ Response response = sendRequest(new PutRequest(getPostUrl(nodeRef), comment.toString(), "application/json"), expectedStatus);
+
+ if (expectedStatus != Status.STATUS_OK)
+ {
+ return null;
+ }
+
+ //logger.debug("Comment updated: " + response.getContentAsString());
+ JSONObject result = new JSONObject(response.getContentAsString());
+ return result.getJSONObject("item");
+ }
+
+ /**
+ * Monkeys with the created and published dates on a topic+posts
+ */
+ private void pushCreatedDateBack(NodeRef node, int daysAgo) throws Exception
+ {
+ Date created = (Date)nodeService.getProperty(node, ContentModel.PROP_CREATED);
+ Date newCreated = new Date(created.getTime() - daysAgo*24*60*60*1000);
+ Date published = (Date)nodeService.getProperty(node, ContentModel.PROP_PUBLISHED);
+ if(published == null) published = created;
+ Date newPublished = new Date(published.getTime() - daysAgo*24*60*60*1000);
+
+ UserTransaction txn = transactionService.getUserTransaction();
+ txn.begin();
+
+ this.policyBehaviourFilter.disableBehaviour(ContentModel.ASPECT_AUDITABLE);
+ internalNodeService.setProperty(node, ContentModel.PROP_CREATED, newCreated);
+ internalNodeService.setProperty(node, ContentModel.PROP_MODIFIED, newCreated);
+ internalNodeService.setProperty(node, ContentModel.PROP_PUBLISHED, newPublished);
+ this.policyBehaviourFilter.enableBehaviour(ContentModel.ASPECT_AUDITABLE);
+
+ txn.commit();
+
+ // Now chance something else on the node to have it re-indexed
+ nodeService.setProperty(node, ContentModel.PROP_CREATED, newCreated);
+ nodeService.setProperty(node, ContentModel.PROP_MODIFIED, newCreated);
+ nodeService.setProperty(node, ContentModel.PROP_PUBLISHED, newPublished);
+ nodeService.setProperty(node, ContentModel.PROP_DESCRIPTION, "Forced change");
+
+ // Finally change any children (eg if updating a topic, do the posts)
+ for(ChildAssociationRef ref : nodeService.getChildAssocs(node))
+ {
+ pushCreatedDateBack(ref.getChildRef(), daysAgo);
+ }
+ }
+
+ // -----------------------------------------------------
+ // Tests
+ // -----------------------------------------------------
+
+ public void testCreateForumPost() throws Exception
+ {
+ String title = "test";
+ String content = "test";
+ JSONObject item = createSitePost(title, content, Status.STATUS_OK);
+
+ // Check that the values in the response are correct
+ assertEquals(title, item.get("title"));
+ assertEquals(content, item.get("content"));
+ assertEquals(0, item.get("replyCount"));
+ assertEquals("Invalid JSON " + item, true, item.has("createdOn"));
+ assertEquals("Invalid JSON " + item, true, item.has("modifiedOn"));
+ assertEquals("Invalid JSON " + item, true, item.has("author"));
+ assertEquals("Invalid JSON " + item, true, item.has("permissions"));
+ assertEquals("Invalid JSON " + item, true, item.has("url"));
+ assertEquals("Invalid JSON " + item, true, item.has("repliesUrl"));
+ assertEquals("Invalid JSON " + item, true, item.has("nodeRef"));
+
+ // Save some details
+ String name = item.getString("name");
+ NodeRef nodeRef = new NodeRef(item.getString("nodeRef"));
+
+
+ // Fetch the post by name and check
+ item = getPost(name, Status.STATUS_OK);
+
+ assertEquals(title, item.get("title"));
+ assertEquals(content, item.get("content"));
+ assertEquals(0, item.get("replyCount"));
+ assertEquals("Invalid JSON " + item, true, item.has("createdOn"));
+ assertEquals("Invalid JSON " + item, true, item.has("modifiedOn"));
+ assertEquals("Invalid JSON " + item, true, item.has("author"));
+ assertEquals("Invalid JSON " + item, true, item.has("permissions"));
+ assertEquals("Invalid JSON " + item, true, item.has("url"));
+ assertEquals("Invalid JSON " + item, true, item.has("repliesUrl"));
+ assertEquals("Invalid JSON " + item, true, item.has("nodeRef"));
+
+
+ // Fetch the post by noderef and check
+ item = getPost(nodeRef, Status.STATUS_OK);
+
+ assertEquals(title, item.get("title"));
+ assertEquals(content, item.get("content"));
+ assertEquals(0, item.get("replyCount"));
+ assertEquals("Invalid JSON " + item, true, item.has("createdOn"));
+ assertEquals("Invalid JSON " + item, true, item.has("modifiedOn"));
+ assertEquals("Invalid JSON " + item, true, item.has("author"));
+ assertEquals("Invalid JSON " + item, true, item.has("permissions"));
+ assertEquals("Invalid JSON " + item, true, item.has("url"));
+ assertEquals("Invalid JSON " + item, true, item.has("repliesUrl"));
+ assertEquals("Invalid JSON " + item, true, item.has("nodeRef"));
+
+
+ // Create another post, this time by noderef
+ title = "By Node Title";
+ content = "By Node Content";
+ item = createNodePost(FORUM_NODE, title, content, Status.STATUS_OK);
+
+ assertEquals(title, item.get("title"));
+ assertEquals(content, item.get("content"));
+ assertEquals(0, item.get("replyCount"));
+
+ // Check it by noderef
+ nodeRef = new NodeRef(item.getString("nodeRef"));
+ item = getPost(nodeRef, Status.STATUS_OK);
+
+ assertEquals(title, item.get("title"));
+ assertEquals(content, item.get("content"));
+ assertEquals(0, item.get("replyCount"));
+ }
+
+ public void testUpdateForumPost() throws Exception
+ {
+ String title = "test";
+ String content = "test";
+ JSONObject item = createSitePost(title, content, 200);
+
+ // check that the values
+ assertEquals(title, item.get("title"));
+ assertEquals(content, item.get("content"));
+ assertEquals(false, item.getBoolean("isUpdated"));
+
+ assertEquals(true, item.has("name"));
+ String name = item.getString("name");
+ assertEquals(true, item.has("nodeRef"));
+ NodeRef nodeRef = new NodeRef(item.getString("nodeRef"));
+
+ // fetch the post by name
+ item = getPost(item.getString("name"), 200);
+ assertEquals(title, item.get("title"));
+ assertEquals(content, item.get("content"));
+ assertEquals(false, item.getBoolean("isUpdated"));
+
+ // Fetch the post by noderef
+ item = getPost(nodeRef, 200);
+ assertEquals(title, item.get("title"));
+ assertEquals(content, item.get("content"));
+ assertEquals(false, item.getBoolean("isUpdated"));
+
+
+ // Update it by name
+ String title2 = "updated test";
+ String content2 = "test updated";
+ item = updatePost(name, title2, content2, 200);
+
+ // Check the response
+ assertEquals(title2, item.get("title"));
+ assertEquals(content2, item.get("content"));
+ assertEquals(name, item.get("name"));
+ assertEquals(nodeRef.toString(), item.get("nodeRef"));
+ assertEquals(true, item.getBoolean("isUpdated"));
+
+ // Fetch and check
+ item = getPost(nodeRef, 200);
+ assertEquals(title2, item.get("title"));
+ assertEquals(content2, item.get("content"));
+ assertEquals(name, item.get("name"));
+ assertEquals(nodeRef.toString(), item.get("nodeRef"));
+ assertEquals(true, item.getBoolean("isUpdated"));
+
+
+ // Update it again, this time by noderef
+ String title3 = "updated 3 test";
+ String content3 = "test 3 updated";
+ item = updatePost(nodeRef, title3, content3, 200);
+
+ // Check that the values returned are correct
+ assertEquals(title3, item.get("title"));
+ assertEquals(content3, item.get("content"));
+ assertEquals(name, item.get("name"));
+ assertEquals(nodeRef.toString(), item.get("nodeRef"));
+ assertEquals(true, item.getBoolean("isUpdated"));
+
+ // Fetch and re-check
+ item = getPost(nodeRef, 200);
+ assertEquals(title3, item.get("title"));
+ assertEquals(content3, item.get("content"));
+ assertEquals(name, item.get("name"));
+ assertEquals(nodeRef.toString(), item.get("nodeRef"));
+ assertEquals(true, item.getBoolean("isUpdated"));
+ }
+
+ /**
+ * Tests that the permissions details included with topics and
+ * posts are correct
+ */
+ public void testPermissions() throws Exception
+ {
+ // Create a post, and check the details on it
+ JSONObject item = createSitePost("test", "test", Status.STATUS_OK);
+ String name = item.getString("name");
+
+ JSONObject perms = item.getJSONObject("permissions");
+ assertEquals(true, perms.getBoolean("edit"));
+ assertEquals(true, perms.getBoolean("reply"));
+ assertEquals(true, perms.getBoolean("delete"));
+
+ // Check on a fetch too
+ item = getPost(name, Status.STATUS_OK);
+ perms = item.getJSONObject("permissions");
+ assertEquals(true, perms.getBoolean("edit"));
+ assertEquals(true, perms.getBoolean("reply"));
+ assertEquals(true, perms.getBoolean("delete"));
+
+
+ // Switch to another user, see what they see
+ this.authenticationComponent.setCurrentUser(USER_TWO);
+
+ item = getPost(name, Status.STATUS_OK);
+ perms = item.getJSONObject("permissions");
+ assertEquals(false, perms.getBoolean("edit"));
+ assertEquals(true, perms.getBoolean("reply"));
+ assertEquals(false, perms.getBoolean("delete"));
+
+
+ // Remove the user from the site, see the change
+ this.siteService.removeMembership(SITE_SHORT_NAME_DISCUSSION, USER_TWO);
+
+ item = getPost(name, Status.STATUS_OK);
+ perms = item.getJSONObject("permissions");
+ assertEquals(false, perms.getBoolean("edit"));
+ assertEquals(false, perms.getBoolean("reply"));
+ assertEquals(false, perms.getBoolean("delete"));
+
+
+ // Make the site private, will vanish
+ SiteInfo siteInfo = siteService.getSite(SITE_SHORT_NAME_DISCUSSION);
+ siteInfo.setVisibility(SiteVisibility.PRIVATE);
+ this.authenticationComponent.setCurrentUser(AuthenticationUtil.getAdminUserName());
+ this.siteService.updateSite(siteInfo);
+ this.authenticationComponent.setCurrentUser(USER_TWO);
+
+ // On a private site we're not a member of, shouldn't be visable at all
+ getPost(name, Status.STATUS_NOT_FOUND);
+ }
+
+ /**
+ * ALF-1973 - If the user who added a reply has been deleted, don't break
+ */
+ public void testViewReplyByDeletedUser() throws Exception
+ {
+ // Create a post
+ JSONObject item = createSitePost("test", "test", Status.STATUS_OK);
+ String name = item.getString("name");
+ NodeRef topicNodeRef = new NodeRef(item.getString("nodeRef"));
+
+ // Now create a reply as a different user
+ this.authenticationComponent.setCurrentUser(USER_TWO);
+ createReply(topicNodeRef, "Reply", "By the other user", Status.STATUS_OK);
+
+ // Should see the reply
+ item = getReplies(name, Status.STATUS_OK);
+ assertEquals(1, item.getJSONArray("items").length());
+
+ // Delete the user, check that the reply still shows
+ this.authenticationComponent.setCurrentUser(AuthenticationUtil.getAdminUserName());
+ personService.deletePerson(USER_TWO);
+ this.authenticationComponent.setCurrentUser(USER_ONE);
+
+ item = getReplies(name, Status.STATUS_OK);
+ assertEquals(1, item.getJSONArray("items").length());
+ }
+
+ public void testAddReply() throws Exception
+ {
+ // Create a root post
+ JSONObject item = createSitePost("test", "test", Status.STATUS_OK);
+ String topicName = item.getString("name");
+ NodeRef topicNodeRef = new NodeRef(item.getString("nodeRef"));
+
+ // Add a reply
+ JSONObject reply = createReply(topicNodeRef, "test", "test", Status.STATUS_OK);
+ NodeRef replyNodeRef = new NodeRef(reply.getString("nodeRef"));
+ assertEquals("test", reply.getString("title"));
+ assertEquals("test", reply.getString("content"));
+
+ // Add a reply to the reply
+ JSONObject reply2 = createReply(replyNodeRef, "test2", "test2", 200);
+ NodeRef reply2NodeRef = new NodeRef(reply2.getString("nodeRef"));
+ assertEquals("test2", reply2.getString("title"));
+ assertEquals("test2", reply2.getString("content"));
+
+
+ // Check things were correctly setup. These should all be siblings
+ // of each other, with relations between the replies
+ assertEquals(ForumModel.TYPE_TOPIC, nodeService.getType(topicNodeRef));
+ assertEquals(ForumModel.TYPE_POST, nodeService.getType(replyNodeRef));
+ assertEquals(ForumModel.TYPE_POST, nodeService.getType(reply2NodeRef));
+ assertEquals(topicNodeRef, nodeService.getPrimaryParent(replyNodeRef).getParentRef());
+ assertEquals(topicNodeRef, nodeService.getPrimaryParent(reply2NodeRef).getParentRef());
+
+ // Reply 2 should have an assoc to Reply 1
+ assertEquals(0, nodeService.getSourceAssocs(reply2NodeRef, RegexQNamePattern.MATCH_ALL).size());
+ assertEquals(1, nodeService.getTargetAssocs(reply2NodeRef, RegexQNamePattern.MATCH_ALL).size());
+ assertEquals(replyNodeRef, nodeService.getTargetAssocs(reply2NodeRef, RegexQNamePattern.MATCH_ALL).get(0).getTargetRef());
+
+ assertEquals(1, nodeService.getSourceAssocs(replyNodeRef, RegexQNamePattern.MATCH_ALL).size());
+ assertEquals(1, nodeService.getTargetAssocs(replyNodeRef, RegexQNamePattern.MATCH_ALL).size());
+ assertEquals(reply2NodeRef, nodeService.getSourceAssocs(replyNodeRef, RegexQNamePattern.MATCH_ALL).get(0).getSourceRef());
+
+
+ // Fetch all replies for the post
+ JSONObject result = getReplies(topicNodeRef, Status.STATUS_OK);
+ // check the number of replies
+ assertEquals(1, result.getJSONArray("items").length());
+
+ // Check the replies by name too
+ result = getReplies(topicName, Status.STATUS_OK);
+ assertEquals(1, result.getJSONArray("items").length());
+
+
+ // Fetch the top level post again, and check the counts there
+ // That post should have one direct reply, and one reply to it's reply
+ item = getPost(topicName, Status.STATUS_OK);
+ assertEquals(2, item.getInt("totalReplyCount"));
+ assertEquals(1, item.getInt("replyCount"));
+ }
+
+ public void testUpdateReply() throws Exception
+ {
+ // Create a root post
+ JSONObject item = createSitePost("test", "test", Status.STATUS_OK);
+ String postName = item.getString("name");
+ NodeRef postNodeRef = new NodeRef(item.getString("nodeRef"));
+ assertEquals("test", item.getString("title"));
+ assertEquals("test", item.getString("content"));
+ assertEquals(false, item.getBoolean("isUpdated"));
+
+
+ // Add a reply to it
+ JSONObject reply = createReply(postNodeRef, "rtest", "rtest", Status.STATUS_OK);
+ NodeRef replyNodeRef = new NodeRef(reply.getString("nodeRef"));
+ assertEquals("rtest", reply.getString("title"));
+ assertEquals("rtest", reply.getString("content"));
+ assertEquals(false, reply.getBoolean("isUpdated"));
+
+
+ // Now update the reply
+ JSONObject reply2 = updatePost(replyNodeRef, "test2", "test2", Status.STATUS_OK);
+ assertEquals("test2", reply2.getString("title"));
+ assertEquals("test2", reply2.getString("content"));
+ assertEquals(true, reply2.getBoolean("isUpdated"));
+
+ // Fetch it to check
+ reply2 = getPost(replyNodeRef, Status.STATUS_OK);
+ assertEquals("test2", reply2.getString("title"));
+ assertEquals("test2", reply2.getString("content"));
+ assertEquals(true, reply2.getBoolean("isUpdated"));
+
+
+ // Ensure the original post wasn't changed
+ item = getPost(postName, Status.STATUS_OK);
+ assertEquals("test", item.getString("title"));
+ assertEquals("test", item.getString("content"));
+ assertEquals(false, item.getBoolean("isUpdated"));
+ }
+
+ public void testDeleteToplevelPost() throws Exception
+ {
+ // Create two posts
+ JSONObject item1 = createSitePost("test1", "test1", Status.STATUS_OK);
+ JSONObject item2 = createSitePost("test2", "test2", Status.STATUS_OK);
+ String name1 = item1.getString("name");
+ NodeRef nodeRef1 = new NodeRef(item1.getString("nodeRef"));
+ NodeRef nodeRef2 = new NodeRef(item2.getString("nodeRef"));
+
+ // The node references returned correspond to the topics
+ assertEquals(ForumModel.TYPE_TOPIC, nodeService.getType(nodeRef1));
+ assertEquals(ForumModel.TYPE_TOPIC, nodeService.getType(nodeRef2));
+
+
+ // Delete one post by name
+ deletePost(name1, Status.STATUS_OK);
+
+ // Check it went
+ getPost(name1, Status.STATUS_NOT_FOUND);
+
+
+ // Delete the other post by noderef
+ deletePost(nodeRef2, Status.STATUS_OK);
+
+ // Check it went
+ getPost(nodeRef2, Status.STATUS_NOT_FOUND);
+
+
+ // Check all the nodes have gone
+ assertEquals(false, nodeService.exists(nodeRef1));
+ assertEquals(false, nodeService.exists(nodeRef2));
+ }
+
+ public void testDeleteReplyPost() throws Exception
+ {
+ // Create a root post
+ JSONObject item = createSitePost("test", "test", Status.STATUS_OK);
+ String postName = item.getString("name");
+ NodeRef postNodeRef = new NodeRef(item.getString("nodeRef"));
+
+ // It doesn't have any replies yet
+ assertEquals(0, item.getInt("totalReplyCount"));
+ assertEquals(0, item.getInt("replyCount"));
+
+
+ // Add a reply
+ JSONObject reply = createReply(postNodeRef, "testR", "testR", Status.STATUS_OK);
+ NodeRef replyNodeRef = new NodeRef(reply.getString("nodeRef"));
+ String replyName = reply.getString("name");
+ assertEquals("testR", reply.getString("title"));
+ assertEquals("testR", reply.getString("content"));
+
+ // Fetch the reply and check
+ reply = getPost(replyNodeRef, Status.STATUS_OK);
+ assertEquals("testR", reply.getString("title"));
+ assertEquals("testR", reply.getString("content"));
+
+ // Note - you can't fetch a reply by name, only by noderef
+ // It only works for primary posts as they share the topic name
+ getPost(replyName, Status.STATUS_NOT_FOUND);
+
+
+ // Check the main post, ensure the replies show up
+ item = getPost(postName, Status.STATUS_OK);
+ assertEquals(1, item.getInt("totalReplyCount"));
+ assertEquals(1, item.getInt("replyCount"));
+
+
+ // Delete the reply
+ deletePost(replyNodeRef, Status.STATUS_OK);
+
+ // These nodes don't really get deleted at the moment
+ // Due to threading, we just add special marker text
+ // TODO Really we should probably delete posts with no attached replies
+ reply = getPost(replyNodeRef, Status.STATUS_OK);
+ assertEquals(DELETED_REPLY_POST_MARKER, reply.get("title"));
+ assertEquals(DELETED_REPLY_POST_MARKER, reply.get("content"));
+
+
+ // Fetch the top level post again, replies stay because they
+ // haven't really been deleted...
+ // TODO Really we should probably delete posts with no attached replies
+ item = getPost(postName, Status.STATUS_OK);
+ assertEquals(1, item.getInt("totalReplyCount"));
+ assertEquals(1, item.getInt("replyCount"));
+ }
+
+ /**
+ * Test for the various listings:
+ * All, New, Hot (Most Active), Mine
+ */
+ public void testListings() throws Exception
+ {
+ JSONObject result;
+ JSONObject item;
+
+
+ // Check all of the listings, none should have anything yet
+ result = getPosts(null, Status.STATUS_OK);
+ assertEquals(0, result.getInt("total"));
+ assertEquals(0, result.getInt("itemCount"));
+ assertEquals(0, result.getJSONArray("items").length());
+
+ result = getPosts("hot", Status.STATUS_OK);
+ assertEquals(0, result.getInt("total"));
+ assertEquals(0, result.getInt("itemCount"));
+ assertEquals(0, result.getJSONArray("items").length());
+
+ result = getPosts("mine", Status.STATUS_OK);
+ assertEquals(0, result.getInt("total"));
+ assertEquals(0, result.getInt("itemCount"));
+ assertEquals(0, result.getJSONArray("items").length());
+
+ result = getPosts("new?numdays=100", Status.STATUS_OK);
+ assertEquals(0, result.getInt("total"));
+ assertEquals(0, result.getInt("itemCount"));
+ assertEquals(0, result.getJSONArray("items").length());
+
+
+ // Check with a noderef too
+ result = getPosts(FORUM_NODE, null, Status.STATUS_OK);
+ assertEquals(0, result.getInt("total"));
+ assertEquals(0, result.getInt("itemCount"));
+ assertEquals(0, result.getJSONArray("items").length());
+
+ result = getPosts(FORUM_NODE, "hot", Status.STATUS_OK);
+ assertEquals(0, result.getInt("total"));
+ assertEquals(0, result.getInt("itemCount"));
+ assertEquals(0, result.getJSONArray("items").length());
+
+ result = getPosts(FORUM_NODE, "mine", Status.STATUS_OK);
+ assertEquals(0, result.getInt("total"));
+ assertEquals(0, result.getInt("itemCount"));
+ assertEquals(0, result.getJSONArray("items").length());
+
+ result = getPosts(FORUM_NODE, "new?numdays=100", Status.STATUS_OK);
+ assertEquals(0, result.getInt("total"));
+ assertEquals(0, result.getInt("itemCount"));
+ assertEquals(0, result.getJSONArray("items").length());
+
+
+ // Now add a few topics with replies
+ // Some of these will be created as different users
+ item = createSitePost("SiteTitle1", "Content", Status.STATUS_OK);
+ NodeRef siteTopic1 = new NodeRef(item.getString("nodeRef"));
+ this.authenticationComponent.setCurrentUser(USER_TWO);
+ item = createSitePost("SiteTitle2", "Content", Status.STATUS_OK);
+ NodeRef siteTopic2 = new NodeRef(item.getString("nodeRef"));
+
+ item = createNodePost(FORUM_NODE, "NodeTitle1", "Content", Status.STATUS_OK);
+ NodeRef nodeTopic1 = new NodeRef(item.getString("nodeRef"));
+ this.authenticationComponent.setCurrentUser(USER_ONE);
+ item = createNodePost(FORUM_NODE, "NodeTitle2", "Content", Status.STATUS_OK);
+ NodeRef nodeTopic2 = new NodeRef(item.getString("nodeRef"));
+ item = createNodePost(FORUM_NODE, "NodeTitle3", "Content", Status.STATUS_OK);
+ NodeRef nodeTopic3 = new NodeRef(item.getString("nodeRef"));
+
+ item = createReply(siteTopic1, "Reply1a", "Content", Status.STATUS_OK);
+ NodeRef siteReply1A = new NodeRef(item.getString("nodeRef"));
+ item = createReply(siteTopic1, "Reply1b", "Content", Status.STATUS_OK);
+ NodeRef siteReply1B = new NodeRef(item.getString("nodeRef"));
+
+ this.authenticationComponent.setCurrentUser(USER_TWO);
+ item = createReply(siteTopic2, "Reply2a", "Content", Status.STATUS_OK);
+ NodeRef siteReply2A = new NodeRef(item.getString("nodeRef"));
+ item = createReply(siteTopic2, "Reply2b", "Content", Status.STATUS_OK);
+ NodeRef siteReply2B = new NodeRef(item.getString("nodeRef"));
+ item = createReply(siteTopic2, "Reply2c", "Content", Status.STATUS_OK);
+ NodeRef siteReply2C = new NodeRef(item.getString("nodeRef"));
+
+ item = createReply(siteReply2A, "Reply2aa", "Content", Status.STATUS_OK);
+ NodeRef siteReply2AA = new NodeRef(item.getString("nodeRef"));
+ item = createReply(siteReply2A, "Reply2ab", "Content", Status.STATUS_OK);
+ NodeRef siteReply2AB = new NodeRef(item.getString("nodeRef"));
+ this.authenticationComponent.setCurrentUser(USER_ONE);
+ item = createReply(siteReply2AA, "Reply2aaa", "Content", Status.STATUS_OK);
+ NodeRef siteReply2AAA = new NodeRef(item.getString("nodeRef"));
+
+ item = createReply(nodeTopic1, "ReplyN1a", "Content", Status.STATUS_OK);
+ NodeRef nodeReply1A = new NodeRef(item.getString("nodeRef"));
+ item = createReply(nodeReply1A, "ReplyN1aa", "Content", Status.STATUS_OK);
+ NodeRef nodeReply1AA = new NodeRef(item.getString("nodeRef"));
+ item = createReply(nodeReply1AA, "ReplyN1aaa", "Content", Status.STATUS_OK);
+ NodeRef nodeReply1AAA = new NodeRef(item.getString("nodeRef"));
+
+
+ // Check for totals
+ // We should get all the topics
+ result = getPosts(null, Status.STATUS_OK);
+ assertEquals(2, result.getInt("total"));
+ assertEquals(2, result.getInt("itemCount"));
+ assertEquals(2, result.getJSONArray("items").length());
+ assertEquals("SiteTitle1", result.getJSONArray("items").getJSONObject(1).getString("title"));
+ assertEquals("SiteTitle2", result.getJSONArray("items").getJSONObject(0).getString("title"));
+ assertEquals(2, result.getJSONArray("items").getJSONObject(1).getInt("replyCount"));
+ assertEquals(3, result.getJSONArray("items").getJSONObject(0).getInt("replyCount"));
+
+ result = getPosts(FORUM_NODE, null, Status.STATUS_OK);
+ assertEquals(3, result.getInt("total"));
+ assertEquals(3, result.getInt("itemCount"));
+ assertEquals(3, result.getJSONArray("items").length());
+ assertEquals("NodeTitle1", result.getJSONArray("items").getJSONObject(2).getString("title"));
+ assertEquals("NodeTitle2", result.getJSONArray("items").getJSONObject(1).getString("title"));
+ assertEquals("NodeTitle3", result.getJSONArray("items").getJSONObject(0).getString("title"));
+ assertEquals(1, result.getJSONArray("items").getJSONObject(2).getInt("replyCount"));
+ assertEquals(0, result.getJSONArray("items").getJSONObject(1).getInt("replyCount"));
+ assertEquals(0, result.getJSONArray("items").getJSONObject(0).getInt("replyCount"));
+
+
+ // Check for "mine"
+ // User 1 has Site 1, and Nodes 2 + 3
+ result = getPosts("mine", Status.STATUS_OK);
+ assertEquals(1, result.getInt("total"));
+ assertEquals(1, result.getInt("itemCount"));
+ assertEquals(1, result.getJSONArray("items").length());
+ assertEquals("SiteTitle1", result.getJSONArray("items").getJSONObject(0).getString("title"));
+ assertEquals(2, result.getJSONArray("items").getJSONObject(0).getInt("replyCount"));
+
+ result = getPosts(FORUM_NODE, "mine", Status.STATUS_OK);
+ assertEquals(2, result.getInt("total"));
+ assertEquals(2, result.getInt("itemCount"));
+ assertEquals(2, result.getJSONArray("items").length());
+ assertEquals("NodeTitle2", result.getJSONArray("items").getJSONObject(0).getString("title"));
+ assertEquals("NodeTitle3", result.getJSONArray("items").getJSONObject(1).getString("title"));
+ assertEquals(0, result.getJSONArray("items").getJSONObject(0).getInt("replyCount"));
+ assertEquals(0, result.getJSONArray("items").getJSONObject(1).getInt("replyCount"));
+
+
+ // Check for recent (new)
+ // We should get all the topics, with the newest one first (rather than last as with others)
+ result = getPosts("new?numdays=2", Status.STATUS_OK);
+ assertEquals(2, result.getInt("total"));
+ assertEquals(2, result.getInt("itemCount"));
+ assertEquals(2, result.getJSONArray("items").length());
+ assertEquals("SiteTitle2", result.getJSONArray("items").getJSONObject(0).getString("title"));
+ assertEquals("SiteTitle1", result.getJSONArray("items").getJSONObject(1).getString("title"));
+ assertEquals(3, result.getJSONArray("items").getJSONObject(0).getInt("replyCount"));
+ assertEquals(2, result.getJSONArray("items").getJSONObject(1).getInt("replyCount"));
+
+ result = getPosts(FORUM_NODE, "new?numdays=2", Status.STATUS_OK);
+ assertEquals(3, result.getInt("total"));
+ assertEquals(3, result.getInt("itemCount"));
+ assertEquals(3, result.getJSONArray("items").length());
+ assertEquals("NodeTitle3", result.getJSONArray("items").getJSONObject(0).getString("title"));
+ assertEquals("NodeTitle2", result.getJSONArray("items").getJSONObject(1).getString("title"));
+ assertEquals("NodeTitle1", result.getJSONArray("items").getJSONObject(2).getString("title"));
+ assertEquals(0, result.getJSONArray("items").getJSONObject(0).getInt("replyCount"));
+ assertEquals(0, result.getJSONArray("items").getJSONObject(1).getInt("replyCount"));
+ assertEquals(1, result.getJSONArray("items").getJSONObject(2).getInt("replyCount"));
+
+
+ // Check for hot
+ // Will only show topics with replies. Sorting is by replies, not date
+ result = getPosts("hot", Status.STATUS_OK);
+ assertEquals(2, result.getInt("total"));
+ assertEquals(2, result.getInt("itemCount"));
+ assertEquals(2, result.getJSONArray("items").length());
+ assertEquals("SiteTitle2", result.getJSONArray("items").getJSONObject(0).getString("title"));
+ assertEquals("SiteTitle1", result.getJSONArray("items").getJSONObject(1).getString("title"));
+ assertEquals(3, result.getJSONArray("items").getJSONObject(0).getInt("replyCount"));
+ assertEquals(2, result.getJSONArray("items").getJSONObject(1).getInt("replyCount"));
+
+ result = getPosts(FORUM_NODE, "hot", Status.STATUS_OK);
+ assertEquals(1, result.getInt("total"));
+ assertEquals(1, result.getInt("itemCount"));
+ assertEquals(1, result.getJSONArray("items").length());
+ assertEquals("NodeTitle1", result.getJSONArray("items").getJSONObject(0).getString("title"));
+ assertEquals(1, result.getJSONArray("items").getJSONObject(0).getInt("replyCount"));
+
+
+ // Shift some of the posts into the past
+ // (Update the created and published dates)
+ pushCreatedDateBack(siteTopic1, 10);
+ pushCreatedDateBack(siteReply1B, -2); // Make it newer
+
+ pushCreatedDateBack(nodeTopic2, 10);
+ pushCreatedDateBack(nodeTopic3, 4);
+ pushCreatedDateBack(nodeReply1AAA, -1); // Make it newer
+
+
+ // Re-check totals, only ordering changes
+ result = getPosts(null, Status.STATUS_OK);
+ assertEquals(2, result.getInt("total"));
+ assertEquals(2, result.getInt("itemCount"));
+ assertEquals(2, result.getJSONArray("items").length());
+ assertEquals("SiteTitle1", result.getJSONArray("items").getJSONObject(1).getString("title"));
+ assertEquals("SiteTitle2", result.getJSONArray("items").getJSONObject(0).getString("title"));
+ assertEquals(2, result.getJSONArray("items").getJSONObject(1).getInt("replyCount"));
+ assertEquals(3, result.getJSONArray("items").getJSONObject(0).getInt("replyCount"));
+
+ result = getPosts(FORUM_NODE, null, Status.STATUS_OK);
+ assertEquals(3, result.getInt("total"));
+ assertEquals(3, result.getInt("itemCount"));
+ assertEquals(3, result.getJSONArray("items").length());
+ assertEquals("NodeTitle2", result.getJSONArray("items").getJSONObject(2).getString("title"));
+ assertEquals("NodeTitle3", result.getJSONArray("items").getJSONObject(1).getString("title"));
+ assertEquals("NodeTitle1", result.getJSONArray("items").getJSONObject(0).getString("title"));
+ assertEquals(0, result.getJSONArray("items").getJSONObject(2).getInt("replyCount"));
+ assertEquals(0, result.getJSONArray("items").getJSONObject(1).getInt("replyCount"));
+ assertEquals(1, result.getJSONArray("items").getJSONObject(0).getInt("replyCount"));
+
+
+ // Re-check recent, old ones vanish
+ result = getPosts("new?numdays=2", Status.STATUS_OK);
+ assertEquals(1, result.getInt("total"));
+ assertEquals(1, result.getInt("itemCount"));
+ assertEquals(1, result.getJSONArray("items").length());
+ assertEquals("SiteTitle2", result.getJSONArray("items").getJSONObject(0).getString("title"));
+ assertEquals(3, result.getJSONArray("items").getJSONObject(0).getInt("replyCount"));
+
+ result = getPosts(FORUM_NODE, "new?numdays=6", Status.STATUS_OK);
+ assertEquals(2, result.getInt("total"));
+ assertEquals(2, result.getInt("itemCount"));
+ assertEquals(2, result.getJSONArray("items").length());
+ assertEquals("NodeTitle1", result.getJSONArray("items").getJSONObject(0).getString("title"));
+ assertEquals("NodeTitle3", result.getJSONArray("items").getJSONObject(1).getString("title"));
+ assertEquals(1, result.getJSONArray("items").getJSONObject(0).getInt("replyCount"));
+ assertEquals(0, result.getJSONArray("items").getJSONObject(1).getInt("replyCount"));
+
+ result = getPosts(FORUM_NODE, "new?numdays=2", Status.STATUS_OK);
+ assertEquals(1, result.getInt("total"));
+ assertEquals(1, result.getInt("itemCount"));
+ assertEquals(1, result.getJSONArray("items").length());
+ assertEquals("NodeTitle1", result.getJSONArray("items").getJSONObject(0).getString("title"));
+ assertEquals(1, result.getJSONArray("items").getJSONObject(0).getInt("replyCount"));
+
+
+ // Re-check "mine", no change except ordering
+ result = getPosts("mine", Status.STATUS_OK);
+ assertEquals(1, result.getInt("total"));
+ assertEquals(1, result.getInt("itemCount"));
+ assertEquals(1, result.getJSONArray("items").length());
+ assertEquals("SiteTitle1", result.getJSONArray("items").getJSONObject(0).getString("title"));
+ assertEquals(2, result.getJSONArray("items").getJSONObject(0).getInt("replyCount"));
+
+ result = getPosts(FORUM_NODE, "mine", Status.STATUS_OK);
+ assertEquals(2, result.getInt("total"));
+ assertEquals(2, result.getInt("itemCount"));
+ assertEquals(2, result.getJSONArray("items").length());
+ assertEquals("NodeTitle2", result.getJSONArray("items").getJSONObject(0).getString("title"));
+ assertEquals("NodeTitle3", result.getJSONArray("items").getJSONObject(1).getString("title"));
+ assertEquals(0, result.getJSONArray("items").getJSONObject(0).getInt("replyCount"));
+ assertEquals(0, result.getJSONArray("items").getJSONObject(1).getInt("replyCount"));
+
+
+ // Re-check hot, some old ones vanish
+ result = getPosts("hot", Status.STATUS_OK);
+ assertEquals(2, result.getInt("total"));
+ assertEquals(2, result.getInt("itemCount"));
+ assertEquals(2, result.getJSONArray("items").length());
+ assertEquals("SiteTitle2", result.getJSONArray("items").getJSONObject(0).getString("title"));
+ assertEquals("SiteTitle1", result.getJSONArray("items").getJSONObject(1).getString("title"));
+ assertEquals(3, result.getJSONArray("items").getJSONObject(0).getInt("replyCount"));
+ assertEquals(2, result.getJSONArray("items").getJSONObject(1).getInt("replyCount"));
+
+ result = getPosts(FORUM_NODE, "hot", Status.STATUS_OK);
+ assertEquals(1, result.getInt("total"));
+ assertEquals(1, result.getInt("itemCount"));
+ assertEquals(1, result.getJSONArray("items").length());
+ assertEquals("NodeTitle1", result.getJSONArray("items").getJSONObject(0).getString("title"));
+ assertEquals(1, result.getJSONArray("items").getJSONObject(0).getInt("replyCount"));
+
+
+ // Check paging
+ result = getPosts("limit", Status.STATUS_OK);
+ assertEquals(2, result.getInt("total"));
+ assertEquals(1, result.getInt("itemCount"));
+ assertEquals(1, result.getJSONArray("items").length());
+ assertEquals("SiteTitle2", result.getJSONArray("items").getJSONObject(0).getString("title"));
+ assertEquals(3, result.getJSONArray("items").getJSONObject(0).getInt("replyCount"));
+
+ result = getPosts(FORUM_NODE, "limit", Status.STATUS_OK);
+ assertEquals(3, result.getInt("total"));
+ assertEquals(1, result.getInt("itemCount"));
+ assertEquals(1, result.getJSONArray("items").length());
+ assertEquals("NodeTitle1", result.getJSONArray("items").getJSONObject(0).getString("title"));
+ assertEquals(1, result.getJSONArray("items").getJSONObject(0).getInt("replyCount"));
+ }
+
+ /**
+ * https://issues.alfresco.com/jira/browse/ALF-17443 reports that site contributors are unable
+ * to edit replies that they have made.
+ */
+ public void testContributorCanEditReply() throws Exception
+ {
+ authenticationComponent.setCurrentUser(USER_ONE);
+ JSONObject post = createSitePost("Can contributors edit replies?", "The title says it all", Status.STATUS_OK);
+ NodeRef postNodeRef = new NodeRef(post.getString("nodeRef"));
+
+ authenticationComponent.setCurrentUser(USER_TWO);
+ JSONObject reply = createReply(postNodeRef, "", "Let's see.", Status.STATUS_OK);
+ NodeRef replyNodeRef = new NodeRef(reply.getString("nodeRef"));
+ updateComment(replyNodeRef, "", "Yes I can", Status.STATUS_OK);
+
+ authenticationComponent.setCurrentUser(USER_ONE);
+
+ post = getPost(postNodeRef, Status.STATUS_OK);
+ assertEquals("Can contributors edit replies?", post.getString("title"));
+ assertEquals("The title says it all", post.getString("content"));
+ assertEquals(1, post.getInt("replyCount"));
+
+ JSONObject replies = getReplies(postNodeRef, Status.STATUS_OK);
+ JSONArray items = replies.getJSONArray("items");
+ assertEquals(1, items.length());
+
+ reply = items.getJSONObject(0);
+ assertEquals("Yes I can", reply.getString("content"));
+
+ }
+
+ /**
+ * Test for MNT-11964
+ * @throws Exception
+ */
+ public void testCreateForumPermission() throws Exception
+ {
+ this.authenticationComponent.setCurrentUser(AuthenticationUtil.getAdminUserName());
+
+ String siteName = SITE_SHORT_NAME_DISCUSSION + GUID.generate();
+ this.siteService.createSite("ForumSitePreset", siteName, "SiteTitle", "SiteDescription", SiteVisibility.PUBLIC);
+
+ String userName = USER_ONE + GUID.generate();
+ createUser(userName, SiteModel.SITE_COLLABORATOR, siteName);
+
+ // Check permissions for admin
+ checkForumPermissions(siteName);
+
+ // Check permissions for user
+ this.authenticationComponent.setCurrentUser(userName);
+ checkForumPermissions(siteName);
+
+ // Cleanup
+ this.authenticationComponent.setCurrentUser(AuthenticationUtil.getAdminUserName());
+ this.siteService.deleteSite(siteName);
+
+ // Create a new site as user
+ this.authenticationComponent.setCurrentUser(userName);
+ siteName = SITE_SHORT_NAME_DISCUSSION + GUID.generate();
+ this.siteService.createSite("BlogSitePreset", siteName, "SiteTitle", "SiteDescription", SiteVisibility.PUBLIC);
+
+ // Check permissions for user
+ checkForumPermissions(siteName);
+
+ // Check permissions for admin
+ this.authenticationComponent.setCurrentUser(AuthenticationUtil.getAdminUserName());
+ checkForumPermissions(siteName);
+
+ // Cleanup
+ this.siteService.deleteSite(siteName);
+ this.personService.deletePerson(userName);
+ }
+
+ private void checkForumPermissions(String siteName) throws Exception
+ {
+ String url = "/api/forum/site/" + siteName + "/" + COMPONENT_DISCUSSION + "/posts";
+ Response response = sendRequest(new GetRequest(url), 200);
+ JSONObject result = new JSONObject(response.getContentAsString());
+
+ assertTrue("The user sould have permission to create a new discussion.", result.getJSONObject("forumPermissions").getBoolean("create"));
+ }
+}