org.alfresco.repo.discussion.DiscussionServiceImpl.java Source code

Java tutorial

Introduction

Here is the source code for org.alfresco.repo.discussion.DiscussionServiceImpl.java

Source

/*
 * #%L
 * Alfresco Repository
 * %%
 * 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 <http://www.gnu.org/licenses/>.
 * #L%
 */
package org.alfresco.repo.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 org.alfresco.model.ContentModel;
import org.alfresco.model.ForumModel;
import org.alfresco.query.CannedQueryFactory;
import org.alfresco.query.CannedQueryResults;
import org.alfresco.query.CannedQuerySortDetails;
import org.alfresco.query.EmptyCannedQueryResults;
import org.alfresco.query.EmptyPagingResults;
import org.alfresco.query.PagingRequest;
import org.alfresco.query.PagingResults;
import org.alfresco.repo.content.MimetypeMap;
import org.alfresco.repo.discussion.cannedqueries.GetDiscussionTopcisWithPostsCannedQuery;
import org.alfresco.repo.discussion.cannedqueries.GetDiscussionTopcisWithPostsCannedQueryFactory;
import org.alfresco.repo.discussion.cannedqueries.NodeWithChildrenEntity;
import org.alfresco.repo.domain.node.NodeDAO;
import org.alfresco.repo.node.getchildren.GetChildrenAuditableCannedQuery;
import org.alfresco.repo.node.getchildren.GetChildrenAuditableCannedQueryFactory;
import org.alfresco.repo.node.getchildren.GetChildrenWithTargetAssocsAuditableCannedQuery;
import org.alfresco.repo.node.getchildren.GetChildrenWithTargetAssocsAuditableCannedQueryFactory;
import org.alfresco.repo.query.NodeBackedEntity;
import org.alfresco.repo.query.NodeWithTargetsEntity;
import org.alfresco.repo.query.NodeWithTargetsEntity.TargetAndTypeId;
import org.alfresco.repo.security.authentication.AuthenticationUtil;
import org.alfresco.repo.site.SiteServiceImpl;
import org.alfresco.repo.tenant.TenantService;
import org.alfresco.service.cmr.discussion.DiscussionService;
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.ChildAssociationRef;
import org.alfresco.service.cmr.repository.ContentReader;
import org.alfresco.service.cmr.repository.ContentService;
import org.alfresco.service.cmr.repository.ContentWriter;
import org.alfresco.service.cmr.repository.NodeRef;
import org.alfresco.service.cmr.repository.NodeService;
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.SiteService;
import org.alfresco.service.cmr.tagging.TaggingService;
import org.alfresco.service.namespace.NamespaceService;
import org.alfresco.service.namespace.QName;
import org.alfresco.service.transaction.TransactionService;
import org.alfresco.util.ISO9075;
import org.alfresco.util.Pair;
import org.alfresco.util.registry.NamedObjectRegistry;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

/**
 * @author Nick Burch (based on existing webscript controllers in the REST API)
 * @since 4.0
 */
public class DiscussionServiceImpl implements DiscussionService {
    public static final String DISCUSSION_COMPONENT = "discussions";

    protected static final String CANNED_QUERY_GET_CHILDREN = "discussionGetChildrenCannedQueryFactory";
    protected static final String CANNED_QUERY_GET_CHILDREN_TARGETS = "discussionGetChildrenWithTargetAssocsAuditableCannedQueryFactory";
    protected static final String CANNED_QUERY_GET_TOPICS_WITH_POSTS = "discussionGetDiscussionTopcisWithPostsCannedQueryFactory";
    protected static final int MAX_REPLIES_FETCH_SIZE = 1000;

    /**
     * The logger
     */
    private static Log logger = LogFactory.getLog(DiscussionServiceImpl.class);

    private NodeDAO nodeDAO;
    private NodeService nodeService;
    private SiteService siteService;
    private SearchService searchService;
    private ContentService contentService;
    private TaggingService taggingService;
    private NamespaceService namespaceService;
    private TransactionService transactionService;
    private NamedObjectRegistry<CannedQueryFactory<? extends Object>> cannedQueryRegistry;
    private TenantService tenantService;

    public void setNodeDAO(NodeDAO nodeDAO) {
        this.nodeDAO = nodeDAO;
    }

    public void setNodeService(NodeService nodeService) {
        this.nodeService = nodeService;
    }

    public void setSiteService(SiteService siteService) {
        this.siteService = siteService;
    }

    public void setSearchService(SearchService searchService) {
        this.searchService = searchService;
    }

    public void setContentService(ContentService contentService) {
        this.contentService = contentService;
    }

    public void setTaggingService(TaggingService taggingService) {
        this.taggingService = taggingService;
    }

    public void setNamespaceService(NamespaceService namespaceService) {
        this.namespaceService = namespaceService;
    }

    public void setTransactionService(TransactionService transactionService) {
        this.transactionService = transactionService;
    }

    /**
     * Set the registry of {@link CannedQueryFactory canned queries}
     */
    public void setCannedQueryRegistry(
            NamedObjectRegistry<CannedQueryFactory<? extends Object>> cannedQueryRegistry) {
        this.cannedQueryRegistry = cannedQueryRegistry;
    }

    /**
     * Set the {@link TenantService}
     */
    public void setTenantService(TenantService tenantService) {
        this.tenantService = tenantService;
    }

    /**
     * Fetches the Discussions Container on a site, creating as required if requested.
     */
    protected NodeRef getSiteDiscussionsContainer(final String siteShortName, boolean create) {
        return SiteServiceImpl.getSiteContainer(siteShortName, DISCUSSION_COMPONENT, create, siteService,
                transactionService, taggingService);
    }

    private String generateName() {
        // Generate a unique name
        // (Should be unique, but will retry for a new one if not)
        String name = "post-" + (new Date()).getTime() + "_" + Math.round(Math.random() * 10000);
        return name;
    }

    private TopicInfo buildTopic(NodeRef nodeRef, NodeRef container, String name) {
        TopicInfoImpl topic = new TopicInfoImpl(nodeRef, container, name);

        // Grab all the properties, we need the bulk of them anyway
        Map<QName, Serializable> props = nodeService.getProperties(nodeRef);

        // Start with the auditable properties
        topic.setCreator((String) props.get(ContentModel.PROP_CREATOR));
        topic.setModifier((String) props.get(ContentModel.PROP_MODIFIER));
        topic.setCreatedAt((Date) props.get(ContentModel.PROP_CREATED));
        topic.setModifiedAt((Date) props.get(ContentModel.PROP_MODIFIED));

        // Now do the discussion ones
        // Title is special - in older cases from share it was set on the
        //  First Post but not on the Topic
        String title = (String) props.get(ContentModel.PROP_TITLE);
        if (title == null) {
            // Try the first child
            PostInfo primaryPost = getPrimaryPost(topic);
            if (primaryPost != null) {
                title = primaryPost.getTitle();
            }
        }
        topic.setTitle(title);

        // Finally tags
        topic.setTags(taggingService.getTags(nodeRef));

        // All done
        return topic;
    }

    private PostInfo buildPost(NodeRef nodeRef, TopicInfo topic, String name, String preLoadedContents) {
        PostInfoImpl post = new PostInfoImpl(nodeRef, name, topic);

        // Grab all the properties, we need the bulk of them anyway
        Map<QName, Serializable> props = nodeService.getProperties(nodeRef);

        // Start with the auditable properties
        post.setCreator((String) props.get(ContentModel.PROP_CREATOR));
        post.setModifier((String) props.get(ContentModel.PROP_MODIFIER));
        post.setCreatedAt((Date) props.get(ContentModel.PROP_CREATED));
        post.setModifiedAt((Date) props.get(ContentModel.PROP_MODIFIED));
        post.setUpdatedAt((Date) props.get(ContentModel.PROP_UPDATED));

        // Now do the discussion ones
        post.setTitle((String) props.get(ContentModel.PROP_TITLE));

        // Finally, do the content
        String contents = preLoadedContents;
        if (contents == null) {
            ContentReader reader = contentService.getReader(nodeRef, ContentModel.PROP_CONTENT);
            if (reader != null) {
                contents = reader.getContentString();
            }
        }
        post.setContents(contents);

        // All done
        return post;
    }

    @Override
    public TopicInfo getTopic(String siteShortName, String topicName) {
        NodeRef container = getSiteDiscussionsContainer(siteShortName, false);
        if (container == null) {
            // No discussions
            return null;
        }

        // We can now fetch by parent nodeRef
        return getTopic(container, topicName);
    }

    @Override
    public TopicInfo getTopic(NodeRef parentNodeRef, String topicName) {
        NodeRef topic = nodeService.getChildByName(parentNodeRef, ContentModel.ASSOC_CONTAINS, topicName);
        if (topic != null) {
            return buildTopic(topic, parentNodeRef, topicName);
        }
        return null;
    }

    @Override
    public PostInfo getPost(TopicInfo topic, String postName) {
        // Sanity check what we were given
        if (topic.getNodeRef() == null) {
            throw new IllegalArgumentException("Can't get posts for a topic that was never persisted!");
        }

        // Fetch
        NodeRef post = nodeService.getChildByName(topic.getNodeRef(), ContentModel.ASSOC_CONTAINS, postName);
        if (post != null) {
            return buildPost(post, topic, postName, null);
        }
        return null;
    }

    @Override
    public TopicInfo createTopic(final String siteShortName, final String title) {
        // Grab the location to store in
        NodeRef container = getSiteDiscussionsContainer(siteShortName, true);

        // Add by Parent NodeRef
        return createTopic(container, title);
    }

    @Override
    public TopicInfo createTopic(final NodeRef parentNodeRef, final String title) {
        // Build the name
        String name = generateName();

        // Get the properties for the node
        Map<QName, Serializable> props = new HashMap<QName, Serializable>();
        props.put(ContentModel.PROP_NAME, name);
        props.put(ContentModel.PROP_TITLE, title);

        // Build the node
        NodeRef nodeRef = nodeService.createNode(parentNodeRef, ContentModel.ASSOC_CONTAINS,
                QName.createQName(name), ForumModel.TYPE_TOPIC, props).getChildRef();

        // We require that the parent container of the topic
        //  is always a tag scope. This should always be the case
        //  for site containers, but perhaps not others
        if (!taggingService.isTagScope(parentNodeRef)) {
            AuthenticationUtil.runAs(new AuthenticationUtil.RunAsWork<Void>() {
                public Void doWork() throws Exception {
                    // Add the tag scope aspect
                    taggingService.addTagScope(parentNodeRef);
                    return null;
                }
            }, AuthenticationUtil.getSystemUserName());
        }

        // Generate the wrapping object for it
        // Build it that way, so creator and created date come through
        return buildTopic(nodeRef, parentNodeRef, name);
    }

    @Override
    public PostInfo createPost(TopicInfo topic, String contents) {
        // Sanity check what we were given
        if (topic.getNodeRef() == null) {
            throw new IllegalArgumentException("Can't create posts for a topic that was never persisted!");
        }

        // Are we going to be the primary post?
        boolean isPrimary = false;
        if (getPrimaryPost(topic) == null) {
            isPrimary = true;
        }

        // Decide on the name. If this is the first post in a topic,
        //  it should share the topic's name, otherwise needs a new one
        String name = generateName();
        if (isPrimary) {
            name = topic.getSystemName();
        }

        // Create the properties for the node
        Map<QName, Serializable> props = new HashMap<QName, Serializable>();
        props.put(ContentModel.PROP_NAME, name);

        // TODO Remove this shortly, when the webscripts have been
        //  fixed up to avoid their current broken-ness
        props.put(ContentModel.PROP_PUBLISHED, new Date());

        // Do we want a title? By default, primary posts share a title
        //  with the topic, but replies are title-free
        if (isPrimary) {
            props.put(ContentModel.PROP_TITLE, topic.getTitle());
        }

        // Build the node
        NodeRef nodeRef = nodeService.createNode(topic.getNodeRef(), ContentModel.ASSOC_CONTAINS,
                QName.createQName(name), ForumModel.TYPE_POST, props).getChildRef();

        // Store the content
        ContentWriter writer = contentService.getWriter(nodeRef, ContentModel.PROP_CONTENT, true);
        writer.setEncoding("UTF-8");
        writer.setMimetype(MimetypeMap.MIMETYPE_HTML);
        writer.putContent(contents);

        // Generate the wrapping object for it
        // Build it that way, so creator and created date come through
        return buildPost(nodeRef, topic, name, contents);
    }

    @Override
    public PostInfo createReply(PostInfo parentPost, String contents) {
        // Sanity check what we were given
        if (parentPost.getNodeRef() == null) {
            throw new IllegalArgumentException("Can't reply to a post that was never persisted");
        }
        if (parentPost.getTopic() == null) {
            throw new IllegalArgumentException("Can't reply to a post with no attached topic");
        }

        // Have the post created
        PostInfo reply = createPost(parentPost.getTopic(), contents);

        // Now make it a reply
        nodeService.createAssociation(reply.getNodeRef(), parentPost.getNodeRef(), ContentModel.ASSOC_REFERENCES);

        // All done
        return reply;
    }

    @Override
    public TopicInfo updateTopic(TopicInfo topic) {
        // Sanity check what we were given
        if (topic.getNodeRef() == null) {
            throw new IllegalArgumentException(
                    "Can't update a topic that was never persisted, call create instead");
        }

        // Update the properties
        NodeRef nodeRef = topic.getNodeRef();
        nodeService.setProperty(nodeRef, ContentModel.PROP_TITLE, topic.getTitle());

        // Now do the tags
        taggingService.setTags(nodeRef, topic.getTags());

        // All done
        return topic;
    }

    @Override
    public PostInfo updatePost(PostInfo post) {
        // Sanity check what we were given
        if (post.getNodeRef() == null) {
            throw new IllegalArgumentException("Can't update a post that was never persisted, call create instead");
        }

        // Update the properties
        NodeRef nodeRef = post.getNodeRef();
        nodeService.setProperty(nodeRef, ContentModel.PROP_TITLE, post.getTitle());

        // Change the content
        ContentWriter writer = contentService.getWriter(nodeRef, ContentModel.PROP_CONTENT, true);
        writer.setEncoding("UTF-8");
        writer.putContent(post.getContents());

        // Mark it as having been updated
        Date updatedAt = new Date();
        nodeService.setProperty(nodeRef, ContentModel.PROP_UPDATED, updatedAt);
        if (post instanceof PostInfoImpl) {
            ((PostInfoImpl) post).setUpdatedAt(updatedAt);
            ((PostInfoImpl) post).setModifiedAt(updatedAt);
        } else {
            // Re-create to get the updated date
            post = buildPost(nodeRef, post.getTopic(), post.getSystemName(), post.getContents());
        }

        // All done
        return post;
    }

    @Override
    public void deleteTopic(TopicInfo topic) {
        if (topic.getNodeRef() == null) {
            throw new IllegalArgumentException("Can't delete a topic that was never persisted");
        }

        nodeService.deleteNode(topic.getNodeRef());
    }

    @Override
    public void deletePost(PostInfo post) {
        if (post.getNodeRef() == null) {
            throw new IllegalArgumentException("Can't delete a post that was never persisted");
        }

        nodeService.deleteNode(post.getNodeRef());
    }

    @Override
    public Pair<TopicInfo, PostInfo> getForNodeRef(NodeRef nodeRef) {
        QName type = nodeService.getType(nodeRef);
        TopicInfo topic = null;
        PostInfo post = null;

        if (type.equals(ForumModel.TYPE_TOPIC)) {
            ChildAssociationRef ref = nodeService.getPrimaryParent(nodeRef);
            String topicName = ref.getQName().getLocalName();
            topic = getTopic(ref.getParentRef(), topicName);
        } else if (type.equals(ForumModel.TYPE_POST)) {
            ChildAssociationRef toTopic = nodeService.getPrimaryParent(nodeRef);
            String postName = toTopic.getQName().getLocalName();
            NodeRef topicNodeRef = toTopic.getParentRef();

            ChildAssociationRef toParent = nodeService.getPrimaryParent(topicNodeRef);
            String topicName = toParent.getQName().getLocalName();
            topic = getTopic(toParent.getParentRef(), topicName);
            post = getPost(topic, postName);
        } else {
            logger.debug("Invalid type " + type + " found");
            return null;
        }

        // Return what we found
        return new Pair<TopicInfo, PostInfo>(topic, post);
    }

    @Override
    public PostInfo getPrimaryPost(TopicInfo topic) {
        // First up, see if there is a post with the same name as the topic
        // (That's the normal Share case)
        PostInfo post = getPost(topic, topic.getSystemName());
        if (post != null) {
            return post;
        }

        // Cater for the explorer case, we want the first child
        List<ChildAssociationRef> children = nodeService.getChildAssocs(topic.getNodeRef());
        if (children.size() == 0) {
            // No child posts yet
            return null;
        }

        // We want the first one in the list
        NodeRef postNodeRef = children.get(0).getChildRef();
        String postName = children.get(0).getQName().getLocalName();
        return buildPost(postNodeRef, topic, postName, null);
    }

    @Override
    public PostInfo getMostRecentPost(TopicInfo topic) {
        // Do a listing at the Node level, ordered by created date
        //  to get the most recent nodes
        PagingRequest paging = new PagingRequest(0, 1);
        CannedQueryResults<NodeBackedEntity> results = listEntries(topic.getNodeRef(), ForumModel.TYPE_POST, null,
                null, null, false, paging);

        // Bail if the topic lacks posts
        if (results.getPage().size() == 0) {
            // No posts in the topic
            return null;
        }

        // Grab the newest node
        NodeBackedEntity node = results.getPage().get(0);

        // Wrap and return
        return buildPost(tenantService.getBaseName(node.getNodeRef()), topic, node.getName(), null);
    }

    @Override
    public PagingResults<TopicInfo> listTopics(String siteShortName, boolean sortAscending, PagingRequest paging) {
        NodeRef container = getSiteDiscussionsContainer(siteShortName, false);
        if (container == null) {
            // No topics
            return new EmptyPagingResults<TopicInfo>();
        }

        // We can now fetch by parent nodeRef
        return listTopics(container, sortAscending, paging);
    }

    @Override
    public PagingResults<TopicInfo> listTopics(NodeRef nodeRef, boolean sortAscending, PagingRequest paging) {
        // Do the listing, oldest first
        CannedQueryResults<NodeBackedEntity> nodes = listEntries(nodeRef, ForumModel.TYPE_TOPIC, null, null, null,
                sortAscending, paging);

        // Wrap and return
        return wrap(nodes, nodeRef);
    }

    @Override
    public PagingResults<TopicInfo> listTopics(String siteShortName, String username, boolean sortAscending,
            PagingRequest paging) {
        NodeRef container = getSiteDiscussionsContainer(siteShortName, false);
        if (container == null) {
            // No topics
            return new EmptyPagingResults<TopicInfo>();
        }

        // We can now fetch by parent nodeRef
        return listTopics(container, username, sortAscending, paging);
    }

    @Override
    public PagingResults<TopicInfo> listTopics(NodeRef nodeRef, String username, boolean sortAscending,
            PagingRequest paging) {
        // Do the listing, oldest first
        CannedQueryResults<NodeBackedEntity> nodes = listEntries(nodeRef, ForumModel.TYPE_TOPIC, username, null,
                null, sortAscending, paging);

        // Wrap and return
        return wrap(nodes, nodeRef);
    }

    @Override
    public PagingResults<TopicInfo> listTopics(String siteShortName, Date from, Date to, boolean sortAscending,
            PagingRequest paging) {
        NodeRef container = getSiteDiscussionsContainer(siteShortName, false);
        if (container == null) {
            // No topics
            return new EmptyPagingResults<TopicInfo>();
        }

        // We can now fetch by parent nodeRef
        return listTopics(container, from, to, sortAscending, paging);
    }

    @Override
    public PagingResults<TopicInfo> listTopics(NodeRef nodeRef, Date from, Date to, boolean sortAscending,
            PagingRequest paging) {
        // Do the listing, with the sort order as requested
        CannedQueryResults<NodeBackedEntity> nodes = listEntries(nodeRef, ForumModel.TYPE_TOPIC, null, from, to,
                sortAscending, paging);

        // Wrap and return
        return wrap(nodes, nodeRef);
    }

    @Override
    public PagingResults<Pair<TopicInfo, Integer>> listHotTopics(String siteShortName, Date since,
            PagingRequest paging) {
        NodeRef container = getSiteDiscussionsContainer(siteShortName, false);
        if (container == null) {
            // No topics
            return new EmptyPagingResults<Pair<TopicInfo, Integer>>();
        }

        // We can now fetch by parent nodeRef
        return listHotTopics(container, since, paging);
    }

    @Override
    public PagingResults<Pair<TopicInfo, Integer>> listHotTopics(NodeRef nodeRef, Date since,
            PagingRequest paging) {
        // Do the query
        GetDiscussionTopcisWithPostsCannedQueryFactory getCQFactory = (GetDiscussionTopcisWithPostsCannedQueryFactory) cannedQueryRegistry
                .getNamedObject(CANNED_QUERY_GET_TOPICS_WITH_POSTS);
        GetDiscussionTopcisWithPostsCannedQuery cq = (GetDiscussionTopcisWithPostsCannedQuery) getCQFactory
                .getCannedQuery(nodeRef, null, since, true, null, paging);

        // Execute the canned query
        CannedQueryResults<NodeWithChildrenEntity> results = cq.execute();

        // Wrap and return
        return wrapWithCount(results, nodeRef);
    }

    @Override
    public PagingResults<TopicInfo> findTopics(String siteShortName, String username, String tag,
            boolean sortAscending, PagingRequest paging) {
        NodeRef container = getSiteDiscussionsContainer(siteShortName, false);
        if (container == null) {
            // No topics
            return new EmptyPagingResults<TopicInfo>();
        }

        // We can now search by parent nodeRef
        return findTopics(container, username, tag, sortAscending, paging);
    }

    @Override
    public PagingResults<TopicInfo> findTopics(NodeRef nodeRef, String username, String tag, boolean sortAscending,
            PagingRequest paging) {
        // Build the query
        StringBuilder luceneQuery = new StringBuilder();
        luceneQuery.append(" +TYPE:\"" + ForumModel.TYPE_TOPIC + "\"");
        luceneQuery.append(" +PATH:\"" + nodeService.getPath(nodeRef).toPrefixString(namespaceService) + "/*\"");

        if (username != null) {
            luceneQuery.append(" +@cm\\:creator:\"" + username + "\"");
        }
        if (tag != null) {
            luceneQuery.append(" +PATH:\"/cm:taggable/cm:" + ISO9075.encode(tag) + "/member\"");
        }

        String sortOn = "@{http://www.alfresco.org/model/content/1.0}created";

        // Query
        SearchParameters sp = new SearchParameters();
        sp.addStore(nodeRef.getStoreRef());
        sp.setLanguage(SearchService.LANGUAGE_LUCENE);
        sp.setQuery(luceneQuery.toString());
        sp.addSort(sortOn, sortAscending);
        if (paging.getSkipCount() > 0) {
            sp.setSkipCount(paging.getSkipCount());
        }

        // Build the results
        PagingResults<TopicInfo> pagedResults = new EmptyPagingResults<TopicInfo>();
        ResultSet results = null;

        try {
            results = searchService.query(sp);
            pagedResults = wrap(results, nodeRef, paging);
        } finally {
            if (results != null) {
                results.close();
            }
        }

        return pagedResults;
    }

    @Override
    public PagingResults<PostInfo> listPosts(TopicInfo topic, PagingRequest paging) {
        // Do the listing, oldest first
        CannedQueryResults<NodeBackedEntity> nodes = listEntries(topic.getNodeRef(), ForumModel.TYPE_POST, null,
                null, null, true, paging);

        // Wrap and return
        return wrap(nodes, topic);
    }

    @Override
    public PostWithReplies listPostReplies(TopicInfo topic, int levels) {
        PostInfo primaryPost = getPrimaryPost(topic);
        if (primaryPost == null) {
            return null;
        }
        return listPostReplies(primaryPost, levels);
    }

    @Override
    public PostWithReplies listPostReplies(PostInfo primaryPost, int levels) {
        // Grab the factory
        GetChildrenWithTargetAssocsAuditableCannedQueryFactory cqFactory = (GetChildrenWithTargetAssocsAuditableCannedQueryFactory) cannedQueryRegistry
                .getNamedObject(CANNED_QUERY_GET_CHILDREN_TARGETS);

        // Sort by date
        CannedQuerySortDetails sorting = cqFactory.createDateAscendingCQSortDetails();

        // Run the canned query
        GetChildrenWithTargetAssocsAuditableCannedQuery cq = (GetChildrenWithTargetAssocsAuditableCannedQuery) cqFactory
                .getCannedQuery(primaryPost.getTopic().getNodeRef(), ForumModel.TYPE_POST,
                        ContentModel.ASSOC_REFERENCES, sorting, new PagingRequest(MAX_REPLIES_FETCH_SIZE));

        // Execute the canned query
        CannedQueryResults<NodeWithTargetsEntity> results = cq.execute();

        // Prepare to invert
        Map<Long, NodeRef> idToNodeRef = new HashMap<Long, NodeRef>();
        for (NodeWithTargetsEntity e : results.getPage()) {
            idToNodeRef.put(e.getId(), tenantService.getBaseName(e.getNodeRef()));
        }

        Map<NodeRef, List<NodeWithTargetsEntity>> idToReplies = new HashMap<NodeRef, List<NodeWithTargetsEntity>>();
        for (NodeWithTargetsEntity e : results.getPage()) {
            for (TargetAndTypeId idP : e.getTargetIds()) {
                Long id = idP.getTargetId();
                NodeRef nodeRef = idToNodeRef.get(id);
                if (nodeRef == null) {
                    // References a node outside of this topic
                    continue;
                }
                nodeRef = tenantService.getBaseName(nodeRef);
                if (id.equals(e.getId())) {
                    // Self reference
                    continue;
                }
                if (!idToReplies.containsKey(nodeRef)) {
                    idToReplies.put(nodeRef, new ArrayList<NodeWithTargetsEntity>());
                }
                idToReplies.get(nodeRef).add(e);
            }
        }

        // Grab the list of NodeRefs to pre-load, and pre-load them
        List<NodeRef> preLoad = new ArrayList<NodeRef>();
        calculateRepliesPreLoad(primaryPost.getNodeRef(), preLoad, idToReplies, levels);
        nodeDAO.cacheNodes(preLoad);

        // Wrap
        return wrap(primaryPost, idToReplies, levels);
    }

    private void calculateRepliesPreLoad(NodeRef nodeRef, List<NodeRef> preLoad,
            Map<NodeRef, List<NodeWithTargetsEntity>> idToReplies, int levels) {
        preLoad.add(nodeRef);
        if (levels > 0) {
            List<NodeWithTargetsEntity> replies = idToReplies.get(nodeRef);
            if (replies != null && replies.size() > 0) {
                for (NodeWithTargetsEntity entity : replies) {
                    calculateRepliesPreLoad(tenantService.getBaseName(entity.getNodeRef()), preLoad, idToReplies,
                            levels - 1);
                }
            }
        }
    }

    private PostWithReplies wrap(PostInfo post, Map<NodeRef, List<NodeWithTargetsEntity>> idToReplies, int levels) {
        List<PostWithReplies> replies = new ArrayList<PostWithReplies>();
        if (levels > 0) {
            List<NodeWithTargetsEntity> replyEntities = idToReplies.get(post.getNodeRef());
            if (replyEntities != null && replyEntities.size() > 0) {
                for (NodeWithTargetsEntity entity : replyEntities) {
                    PostInfo replyPost = buildPost(tenantService.getBaseName(entity.getNodeRef()), post.getTopic(),
                            entity.getName(), null);
                    replies.add(wrap(replyPost, idToReplies, levels - 1));
                }
            }
        }
        return new PostWithReplies(post, replies);
    }

    /**
     * Finds nodes in the specified parent container, with the given
     *  type, optionally filtered by creator
     */
    private CannedQueryResults<NodeBackedEntity> listEntries(NodeRef parent, QName nodeType, String creatorUsername,
            Date from, Date to, boolean oldestFirst, PagingRequest paging) {
        // The Canned Query system doesn't allow for zero sized pages
        // If they asked for that (bits of share sometimes do), bail out now
        if (paging != null && paging.getMaxItems() == 0) {
            return new EmptyCannedQueryResults<NodeBackedEntity>(null);
        }

        // Grab the factory
        GetChildrenAuditableCannedQueryFactory getChildrenCannedQueryFactory = (GetChildrenAuditableCannedQueryFactory) cannedQueryRegistry
                .getNamedObject(CANNED_QUERY_GET_CHILDREN);

        // Do the sorting, newest first or last by created date (as requested)
        CannedQuerySortDetails sorting = null;
        if (oldestFirst) {
            sorting = getChildrenCannedQueryFactory.createDateAscendingCQSortDetails();
        } else {
            sorting = getChildrenCannedQueryFactory.createDateDescendingCQSortDetails();
        }

        // Run the canned query
        GetChildrenAuditableCannedQuery cq = (GetChildrenAuditableCannedQuery) getChildrenCannedQueryFactory
                .getCannedQuery(parent, nodeType, creatorUsername, from, to, null, null, null, sorting, paging);

        // Execute the canned query
        CannedQueryResults<NodeBackedEntity> results = cq.execute();

        // Return for wrapping
        return results;
    }

    /**
     * Our class to wrap up search results as {@link TopicInfo} instances
     */
    private PagingResults<TopicInfo> wrap(final ResultSet finalLuceneResults, final NodeRef container,
            final PagingRequest paging) {
        final List<TopicInfo> topics = new ArrayList<TopicInfo>();
        int cnt = 1;
        for (ResultSetRow row : finalLuceneResults) {
            TopicInfo topic = buildTopic(row.getNodeRef(), container, row.getQName().getLocalName());
            topics.add(topic);
            cnt++;
            if (paging.getMaxItems() > 0 && cnt > paging.getMaxItems()) {
                break;
            }
        }

        // Wrap
        return new PagingResults<TopicInfo>() {
            @Override
            public boolean hasMoreItems() {
                try {
                    return finalLuceneResults.hasMore();
                } catch (UnsupportedOperationException e) {
                    // Not all lucene results support paging
                    return false;
                }
            }

            @Override
            public Pair<Integer, Integer> getTotalResultCount() {
                int skipCount = 0;
                int itemsRemainingAfterThisPage = 0;
                try {
                    skipCount = finalLuceneResults.getStart();
                } catch (UnsupportedOperationException e) {
                    skipCount = paging.getSkipCount();
                }
                try {
                    itemsRemainingAfterThisPage = finalLuceneResults.length();
                } catch (UnsupportedOperationException e) {
                }

                final int totalItemsInUnpagedResultSet = skipCount + itemsRemainingAfterThisPage;
                return new Pair<Integer, Integer>(totalItemsInUnpagedResultSet, totalItemsInUnpagedResultSet);
            }

            @Override
            public List<TopicInfo> getPage() {
                return topics;
            }

            @Override
            public String getQueryExecutionId() {
                return null;
            }
        };
    }

    /**
     * Our class to wrap up paged results of NodeBackedEntities as
     *  {@link TopicInfo} instances
     */
    private PagingResults<TopicInfo> wrap(final PagingResults<NodeBackedEntity> results, final NodeRef container) {
        // Pre-load the nodes before we create them
        List<Long> ids = new ArrayList<Long>();
        for (NodeBackedEntity node : results.getPage()) {
            ids.add(node.getId());
        }
        nodeDAO.cacheNodesById(ids);

        // Wrap
        return new PagingResults<TopicInfo>() {
            @Override
            public String getQueryExecutionId() {
                return results.getQueryExecutionId();
            }

            @Override
            public List<TopicInfo> getPage() {
                List<TopicInfo> topics = new ArrayList<TopicInfo>();
                for (NodeBackedEntity node : results.getPage()) {
                    NodeRef nodeRef = node.getNodeRef();
                    String name = node.getName();
                    topics.add(buildTopic(nodeRef, container, name));
                }
                return topics;
            }

            @Override
            public boolean hasMoreItems() {
                return results.hasMoreItems();
            }

            @Override
            public Pair<Integer, Integer> getTotalResultCount() {
                return results.getTotalResultCount();
            }
        };
    }

    /**
     * Our class to wrap up paged results of NodeBackedEntities as
     *  {@link PostInfo} instances
     */
    private PagingResults<PostInfo> wrap(final PagingResults<NodeBackedEntity> results, final TopicInfo topic) {
        // Pre-load the nodes before we create them
        List<Long> ids = new ArrayList<Long>();
        for (NodeBackedEntity node : results.getPage()) {
            ids.add(node.getId());
        }
        nodeDAO.cacheNodesById(ids);

        // Wrap
        return new PagingResults<PostInfo>() {
            @Override
            public String getQueryExecutionId() {
                return results.getQueryExecutionId();
            }

            @Override
            public List<PostInfo> getPage() {
                List<PostInfo> posts = new ArrayList<PostInfo>();
                for (NodeBackedEntity node : results.getPage()) {
                    NodeRef nodeRef = node.getNodeRef();
                    String name = node.getName();
                    posts.add(buildPost(nodeRef, topic, name, null));
                }
                return posts;
            }

            @Override
            public boolean hasMoreItems() {
                return results.hasMoreItems();
            }

            @Override
            public Pair<Integer, Integer> getTotalResultCount() {
                return results.getTotalResultCount();
            }
        };
    }

    /**
     * Our class to wrap up paged results of NodeWithChildrenEntity as
     *  {@link TopicInfo} instances
     */
    private PagingResults<Pair<TopicInfo, Integer>> wrapWithCount(
            final PagingResults<NodeWithChildrenEntity> results, final NodeRef container) {
        // Pre-load the nodes before we create them
        List<Long> ids = new ArrayList<Long>();
        for (NodeBackedEntity node : results.getPage()) {
            ids.add(node.getId());
        }
        nodeDAO.cacheNodesById(ids);

        // Wrap
        return new PagingResults<Pair<TopicInfo, Integer>>() {
            @Override
            public String getQueryExecutionId() {
                return results.getQueryExecutionId();
            }

            @Override
            public List<Pair<TopicInfo, Integer>> getPage() {
                List<Pair<TopicInfo, Integer>> topics = new ArrayList<Pair<TopicInfo, Integer>>();
                for (NodeWithChildrenEntity node : results.getPage()) {
                    NodeRef nodeRef = node.getNodeRef();
                    String name = node.getName();
                    int count = node.getChildren().size();

                    TopicInfo topic = buildTopic(nodeRef, container, name);
                    topics.add(new Pair<TopicInfo, Integer>(topic, count));
                }
                return topics;
            }

            @Override
            public boolean hasMoreItems() {
                return results.hasMoreItems();
            }

            @Override
            public Pair<Integer, Integer> getTotalResultCount() {
                return results.getTotalResultCount();
            }
        };
    }
}