diff --git a/config/alfresco/templates/webscripts/org/alfresco/repository/discussions/forum/forum-posts-filtered.get.desc.xml b/config/alfresco/templates/webscripts/org/alfresco/repository/discussions/forum/forum-posts-filtered.get.desc.xml new file mode 100644 index 0000000000..d7daea840d --- /dev/null +++ b/config/alfresco/templates/webscripts/org/alfresco/repository/discussions/forum/forum-posts-filtered.get.desc.xml @@ -0,0 +1,9 @@ + + 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 + diff --git a/config/alfresco/templates/webscripts/org/alfresco/repository/discussions/forum/forum-posts-filtered.get.json.ftl b/config/alfresco/templates/webscripts/org/alfresco/repository/discussions/forum/forum-posts-filtered.get.json.ftl new file mode 100644 index 0000000000..f338b40df2 --- /dev/null +++ b/config/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 + + }, +<@gen.pagedResults data=data ; item> + <@postLib.postJSON postData=item /> + +} \ No newline at end of file diff --git a/config/alfresco/templates/webscripts/org/alfresco/repository/discussions/post.lib.ftl b/config/alfresco/templates/webscripts/org/alfresco/repository/discussions/post.lib.ftl index c477c4ceb5..e39ec2ee71 100644 --- a/config/alfresco/templates/webscripts/org/alfresco/repository/discussions/post.lib.ftl +++ b/config/alfresco/templates/webscripts/org/alfresco/repository/discussions/post.lib.ftl @@ -61,6 +61,7 @@ "tags": [<#list postData.tags as x>"${x}"<#if x_has_next>, ], + "site": "${postData.site!""}", <#else> "name": "${post.name}", diff --git a/config/alfresco/web-scripts-application-context.xml b/config/alfresco/web-scripts-application-context.xml index 4b08a8b53c..3ca598456c 100644 --- a/config/alfresco/web-scripts-application-context.xml +++ b/config/alfresco/web-scripts-application-context.xml @@ -1732,6 +1732,13 @@ parent="abstractDiscussionWebScript"> + + + + + diff --git a/source/java/org/alfresco/repo/web/scripts/discussion/AbstractDiscussionWebScript.java b/source/java/org/alfresco/repo/web/scripts/discussion/AbstractDiscussionWebScript.java index ec23051254..2fe83357ac 100644 --- a/source/java/org/alfresco/repo/web/scripts/discussion/AbstractDiscussionWebScript.java +++ b/source/java/org/alfresco/repo/web/scripts/discussion/AbstractDiscussionWebScript.java @@ -358,6 +358,9 @@ public abstract class AbstractDiscussionWebScript extends DeclarativeWebScript // 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) { diff --git a/source/java/org/alfresco/repo/web/scripts/discussion/ForumTopicsFilteredGet.java b/source/java/org/alfresco/repo/web/scripts/discussion/ForumTopicsFilteredGet.java new file mode 100644 index 0000000000..5b15169802 --- /dev/null +++ b/source/java/org/alfresco/repo/web/scripts/discussion/ForumTopicsFilteredGet.java @@ -0,0 +1,408 @@ +/* + * Copyright (C) 2005-2012 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.repo.web.scripts.discussion; + +import java.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.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 + */ + public void setSearchService(SearchService searchService) + { + this.searchService = searchService; + } + + /** + * Overrides AbstractDiscussionWebScript to allow a null site + * + * @param req + * @param status + * @param cache + * + * @return + */ + @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 + * @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 + * @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 + */ + @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 + * @param paging + * @return + */ + 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 + * @param author + * @param daysAgo + * @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:" + 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 + * @return + */ + 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 + * @return + */ + @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 + * @param paging + * @return + */ + 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; + } + }; + } +}