org.alfresco.web.bean.forums.ForumsBean.java Source code

Java tutorial

Introduction

Here is the source code for org.alfresco.web.bean.forums.ForumsBean.java

Source

/*
 * #%L
 * Alfresco Repository WAR Community
 * %%
 * 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.web.bean.forums;

import java.io.IOException;
import java.io.StringWriter;
import java.io.Writer;
import java.text.MessageFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.UUID;

import javax.faces.component.UIComponent;
import javax.faces.context.FacesContext;
import javax.faces.context.ResponseWriter;
import javax.faces.event.ActionEvent;
import javax.transaction.UserTransaction;

import org.alfresco.error.AlfrescoRuntimeException;
import org.alfresco.model.ContentModel;
import org.alfresco.model.ForumModel;
import org.alfresco.service.cmr.dictionary.DictionaryService;
import org.alfresco.service.cmr.dictionary.TypeDefinition;
import org.alfresco.service.cmr.repository.AssociationRef;
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.InvalidNodeRefException;
import org.alfresco.service.cmr.repository.NodeRef;
import org.alfresco.service.cmr.repository.NodeService;
import org.alfresco.service.cmr.search.QueryParameterDefinition;
import org.alfresco.service.cmr.search.SearchService;
import org.alfresco.service.namespace.NamespaceService;
import org.alfresco.service.namespace.QName;
import org.alfresco.service.namespace.RegexQNamePattern;
import org.alfresco.web.app.Application;
import org.alfresco.web.app.context.IContextListener;
import org.alfresco.web.app.context.UIContextService;
import org.alfresco.web.bean.BrowseBean;
import org.alfresco.web.bean.NavigationBean;
import org.alfresco.web.bean.repository.MapNode;
import org.alfresco.web.bean.repository.Node;
import org.alfresco.web.bean.repository.NodePropertyResolver;
import org.alfresco.web.bean.repository.Repository;
import org.alfresco.web.config.ViewsConfigElement;
import org.alfresco.web.ui.common.Utils;
import org.alfresco.web.ui.common.component.UIActionLink;
import org.alfresco.web.ui.common.component.UIModeList;
import org.alfresco.web.ui.common.component.data.UIColumn;
import org.alfresco.web.ui.common.component.data.UIRichList;
import org.alfresco.web.ui.common.renderer.data.IRichListRenderer;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

/**
 * Bean providing properties and behaviour for the forums screens.
 * 
 * @author gavinc
 */
public class ForumsBean implements IContextListener {
    private static final long serialVersionUID = 7066410060288061436L;

    private static Log logger = LogFactory.getLog(ForumsBean.class);
    private static final String PAGE_NAME_FORUMS = "forums";
    private static final String PAGE_NAME_FORUM = "forum";
    private static final String PAGE_NAME_TOPIC = "topic";

    /** The NodeService to be used by the bean */
    transient private NodeService nodeService;

    /** The ContentService to be used by the bean */
    transient private ContentService contentService;

    /** The DictionaryService bean reference */
    transient private DictionaryService dictionaryService;

    /** The SearchService bean reference. */
    transient private SearchService searchService;

    /** The NamespaceService bean reference. */
    transient private NamespaceService namespaceService;

    /** The browse bean */
    protected BrowseBean browseBean;

    /** The NavigationBean bean reference */
    protected NavigationBean navigator;

    /** Views configuration object */
    protected ViewsConfigElement viewsConfig = null;

    /** Component references */
    protected UIRichList forumsRichList;
    protected UIRichList forumRichList;
    protected UIRichList topicRichList;

    /** Node lists */
    private List<Node> forums;
    private List<Node> topics;
    private List<Node> posts;

    /** The current forums view mode - set to a well known IRichListRenderer identifier */
    private String forumsViewMode;

    /** The current forums view page size */
    private int forumsPageSize;

    /** The current forum view mode - set to a well known IRichListRenderer identifier */
    private String forumViewMode;

    /** The current forum view page size */
    private int forumPageSize;

    /** The current topic view mode - set to a well known IRichListRenderer identifier */
    private String topicViewMode;

    /** The current topic view page size */
    private int topicPageSize;

    // ------------------------------------------------------------------------------
    // Construction 

    /**
     * Default Constructor
     */
    public ForumsBean() {
        UIContextService.getInstance(FacesContext.getCurrentInstance()).registerBean(this);

        initFromClientConfig();
    }

    // ------------------------------------------------------------------------------
    // Bean property getters and setters 

    /**
     * @param nodeService The NodeService to set.
     */
    public void setNodeService(NodeService nodeService) {
        this.nodeService = nodeService;
    }

    protected NodeService getNodeService() {
        if (nodeService == null) {
            nodeService = Repository.getServiceRegistry(FacesContext.getCurrentInstance()).getNodeService();
        }
        return nodeService;
    }

    /**
     * Sets the content service to use
     * 
     * @param contentService The ContentService
     */
    public void setContentService(ContentService contentService) {
        this.contentService = contentService;
    }

    protected ContentService getContentService() {
        if (contentService == null) {
            contentService = Repository.getServiceRegistry(FacesContext.getCurrentInstance()).getContentService();
        }
        return contentService;
    }

    /**
     * @param dictionaryService The DictionaryService to set.
     */
    public void setDictionaryService(DictionaryService dictionaryService) {
        this.dictionaryService = dictionaryService;
    }

    protected DictionaryService getDictionaryService() {
        if (dictionaryService == null) {
            dictionaryService = Repository.getServiceRegistry(FacesContext.getCurrentInstance())
                    .getDictionaryService();
        }
        return dictionaryService;
    }

    /**
     * @param searchService The SearchService to set.
     */
    public void setSearchService(SearchService searchService) {
        this.searchService = searchService;
    }

    protected SearchService getSearchService() {
        if (searchService == null) {
            searchService = Repository.getServiceRegistry(FacesContext.getCurrentInstance()).getSearchService();
        }
        return searchService;
    }

    /**
     * @param namespaceService The NamespaceService to set.
     */
    public void setNamespaceService(NamespaceService namespaceService) {
        this.namespaceService = namespaceService;
    }

    protected NamespaceService getNamespaceService() {
        if (namespaceService == null) {
            namespaceService = Repository.getServiceRegistry(FacesContext.getCurrentInstance())
                    .getNamespaceService();
        }
        return namespaceService;
    }

    /**
     * Sets the BrowseBean instance to use to retrieve the current document
     * 
     * @param browseBean BrowseBean instance
     */
    public void setBrowseBean(BrowseBean browseBean) {
        this.browseBean = browseBean;
    }

    /**
     * @param navigator The NavigationBean to set.
     */
    public void setNavigator(NavigationBean navigator) {
        this.navigator = navigator;
    }

    /**
     * @param forumsRichList The forumsRichList to set.
     */
    public void setForumsRichList(UIRichList forumsRichList) {
        this.forumsRichList = forumsRichList;
        if (this.forumsRichList != null) {
            // set the initial sort column and direction
            this.forumsRichList.setInitialSortColumn(this.viewsConfig.getDefaultSortColumn(PAGE_NAME_FORUMS));
            this.forumsRichList.setInitialSortDescending(this.viewsConfig.hasDescendingSort(PAGE_NAME_FORUMS));

            // ETWOONE-183 & ETWOONE-339. For URL addressability of forums spaces
            this.forumsRichList.setRefreshOnBind(true);
        }
    }

    /**
     * @return Returns the forumsRichList.
     */
    public UIRichList getForumsRichList() {
        return this.forumsRichList;
    }

    /**
     * @return Returns the forums View mode. See UIRichList
     */
    public String getForumsViewMode() {
        return this.forumsViewMode;
    }

    /**
     * @param forumsViewMode      The forums View mode to set. See UIRichList.
     */
    public void setForumsViewMode(String forumsViewMode) {
        this.forumsViewMode = forumsViewMode;
    }

    /**
     * @return Returns the forumsPageSize.
     */
    public int getForumsPageSize() {
        return this.forumsPageSize;
    }

    /**
     * @param forumsPageSize The forumsPageSize to set.
     */
    public void setForumsPageSize(int forumsPageSize) {
        this.forumsPageSize = forumsPageSize;
    }

    /**
     * @param topicRichList The topicRichList to set.
     */
    public void setTopicRichList(UIRichList topicRichList) {
        this.topicRichList = topicRichList;

        if (this.topicRichList != null) {
            // set the initial sort column and direction
            this.topicRichList.setInitialSortColumn(this.viewsConfig.getDefaultSortColumn(PAGE_NAME_TOPIC));
            this.topicRichList.setInitialSortDescending(this.viewsConfig.hasDescendingSort(PAGE_NAME_TOPIC));

            // ETWOONE-183. For URL addressability of topics:
            this.topicRichList.setRefreshOnBind(true);
        }
    }

    /**
     * @return Returns the topicRichList.
     */
    public UIRichList getTopicRichList() {
        return this.topicRichList;
    }

    /**
     * @return Returns the topics View mode. See UIRichList
     */
    public String getTopicViewMode() {
        return this.topicViewMode;
    }

    /**
     * @param topicViewMode      The topic View mode to set. See UIRichList.
     */
    public void setTopicViewMode(String topicViewMode) {
        this.topicViewMode = topicViewMode;
    }

    /**
     * @return Returns the topicsPageSize.
     */
    public int getTopicPageSize() {
        return this.topicPageSize;
    }

    /**
     * @param topicPageSize The topicPageSize to set.
     */
    public void setTopicPageSize(int topicPageSize) {
        this.topicPageSize = topicPageSize;
    }

    /**
     * @param forumRichList The forumRichList to set.
     */
    public void setForumRichList(UIRichList forumRichList) {
        this.forumRichList = forumRichList;

        if (this.forumRichList != null) {
            // set the initial sort column and direction
            this.forumRichList.setInitialSortColumn(this.viewsConfig.getDefaultSortColumn(PAGE_NAME_FORUM));
            this.forumRichList.setInitialSortDescending(this.viewsConfig.hasDescendingSort(PAGE_NAME_FORUM));

            // ETWOONE-183 & ETWOONE-339. For URL addressability of forum spaces
            this.forumRichList.setRefreshOnBind(true);
        }
    }

    /**
     * @return Returns the forumRichList.
     */
    public UIRichList getForumRichList() {
        return this.forumRichList;
    }

    /**
     * @return Returns the forum View mode. See UIRichList
     */
    public String getForumViewMode() {
        return this.forumViewMode;
    }

    /**
     * @param forumViewMode      The forum View mode to set. See UIRichList.
     */
    public void setForumViewMode(String forumViewMode) {
        this.forumViewMode = forumViewMode;
    }

    /**
     * @return Returns the forumPageSize.
     */
    public int getForumPageSize() {
        return this.forumPageSize;
    }

    /**
     * @param forumPageSize The forumPageSize to set.
     */
    public void setForumPageSize(int forumPageSize) {
        this.forumPageSize = forumPageSize;
    }

    public List<Node> getForums() {
        if (this.forums == null) {
            getNodes();
        }

        return this.forums;
    }

    public List<Node> getTopics() {
        if (this.topics == null) {
            getNodes();
        }

        return this.topics;
    }

    public List<Node> getPosts() {
        if (this.posts == null) {
            getNodes();
        }

        return this.posts;
    }

    private void getNodes() {
        long startTime = 0;
        if (logger.isDebugEnabled())
            startTime = System.currentTimeMillis();

        UserTransaction tx = null;
        try {
            FacesContext context = FacesContext.getCurrentInstance();
            tx = Repository.getUserTransaction(context, true);
            tx.begin();

            // get the current space from NavigationBean
            String parentNodeId = this.navigator.getCurrentNodeId();

            NodeRef parentRef;
            if (parentNodeId == null) {
                // no specific parent node specified - use the root node
                parentRef = this.getNodeService().getRootNode(Repository.getStoreRef());
            } else {
                // build a NodeRef for the specified Id and our store
                parentRef = new NodeRef(Repository.getStoreRef(), parentNodeId);
            }

            List<ChildAssociationRef> childRefs = this.getNodeService().getChildAssocs(parentRef,
                    ContentModel.ASSOC_CONTAINS, RegexQNamePattern.MATCH_ALL);
            this.forums = new ArrayList<Node>(childRefs.size());
            this.topics = new ArrayList<Node>(childRefs.size());
            this.posts = new ArrayList<Node>(childRefs.size());

            for (ChildAssociationRef ref : childRefs) {
                // create our Node representation from the NodeRef
                NodeRef nodeRef = ref.getChildRef();

                if (this.getNodeService().exists(nodeRef)) {
                    // find it's type so we can see if it's a node we are interested in
                    QName type = this.getNodeService().getType(nodeRef);

                    // make sure the type is defined in the data dictionary
                    TypeDefinition typeDef = this.getDictionaryService().getType(type);

                    if (typeDef != null) {
                        // extract forums, forum, topic and post types

                        if (this.getDictionaryService().isSubClass(type,
                                ContentModel.TYPE_SYSTEM_FOLDER) == false) {
                            if (this.getDictionaryService().isSubClass(type, ForumModel.TYPE_FORUMS)
                                    || this.getDictionaryService().isSubClass(type, ForumModel.TYPE_FORUM)) {
                                // create our Node representation
                                MapNode node = new MapNode(nodeRef, this.getNodeService(), true);
                                node.addPropertyResolver("icon", this.browseBean.resolverSpaceIcon);
                                node.addPropertyResolver("smallIcon", this.browseBean.resolverSmallIcon);

                                this.forums.add(node);
                            }
                            if (this.getDictionaryService().isSubClass(type, ForumModel.TYPE_TOPIC)) {
                                // create our Node representation
                                MapNode node = new MapNode(nodeRef, this.getNodeService(), true);
                                node.addPropertyResolver("icon", this.browseBean.resolverSpaceIcon);
                                node.addPropertyResolver("smallIcon", this.browseBean.resolverSmallIcon);
                                node.addPropertyResolver("replies", this.resolverReplies);

                                this.topics.add(node);
                            } else if (this.getDictionaryService().isSubClass(type, ForumModel.TYPE_POST)) {
                                // create our Node representation
                                MapNode node = new MapNode(nodeRef, this.getNodeService(), true);

                                this.browseBean.setupCommonBindingProperties(node);
                                node.addPropertyResolver("smallIcon", this.browseBean.resolverSmallIcon);
                                node.addPropertyResolver("message", this.resolverContent);
                                node.addPropertyResolver("replyTo", this.resolverReplyTo);

                                this.posts.add(node);
                            }
                        }
                    } else {
                        if (logger.isWarnEnabled())
                            logger.warn("Found invalid object in database: id = " + nodeRef + ", type = " + type);
                    }
                }
            }

            // commit the transaction
            tx.commit();
        } catch (InvalidNodeRefException refErr) {
            Utils.addErrorMessage(MessageFormat.format(
                    Application.getMessage(FacesContext.getCurrentInstance(), Repository.ERROR_NODEREF),
                    new Object[] { refErr.getNodeRef() }));
            this.forums = Collections.<Node>emptyList();
            this.topics = Collections.<Node>emptyList();
            this.posts = Collections.<Node>emptyList();
            try {
                if (tx != null) {
                    tx.rollback();
                }
            } catch (Exception tex) {
            }
        } catch (Throwable err) {
            Utils.addErrorMessage(MessageFormat.format(
                    Application.getMessage(FacesContext.getCurrentInstance(), Repository.ERROR_GENERIC),
                    err.getMessage()), err);
            this.forums = Collections.<Node>emptyList();
            this.topics = Collections.<Node>emptyList();
            this.posts = Collections.<Node>emptyList();
            try {
                if (tx != null) {
                    tx.rollback();
                }
            } catch (Exception tex) {
            }
        }

        if (logger.isDebugEnabled()) {
            long endTime = System.currentTimeMillis();
            logger.debug("Time to query and build forums nodes: " + (endTime - startTime) + "ms");
        }
    }

    /**
     * Returns the HTML to represent a bubble rendition of the text of the the
     * forum article being replied to.
     * 
     * @return The HTML for the bubble
     */
    public String getReplyBubbleHTML() {
        try {
            // if the forum being replied to was a new post show the orange bubble
            // with the user on the left otherwise show the yellow bubble with the
            // user on the right.
            StringWriter writer = new StringWriter();

            FacesContext context = FacesContext.getCurrentInstance();
            Node replyToNode = this.browseBean.getDocument();
            boolean isReplyPost = this.getNodeService().hasAspect(replyToNode.getNodeRef(),
                    ContentModel.ASPECT_REFERENCING);
            String contextPath = context.getExternalContext().getRequestContextPath();
            String colour = isReplyPost ? "yellow" : "orange";
            String bgColour = isReplyPost ? "#FFF5A3" : "#FCC75E";

            // build the HTML to represent the user that posted the article being replied to
            StringBuilder replyPosterHTML = new StringBuilder("<td valign='top'>");
            replyPosterHTML.append("<img src='");
            replyPosterHTML.append(contextPath);
            replyPosterHTML.append("/images/icons/user_large.gif' /><br>");
            replyPosterHTML.append((String) replyToNode.getProperties().get("creator"));
            replyPosterHTML.append("</td>");

            // start the table
            writer.write("<table border='0' cellpadding='0' cellspacing='0' width='100%'><tr>");

            if (isReplyPost) {
                renderReplyContentHTML(context, replyToNode, writer, contextPath, colour, bgColour);
                writer.write(replyPosterHTML.toString());
            } else {
                writer.write(replyPosterHTML.toString());
                renderReplyContentHTML(context, replyToNode, writer, contextPath, colour, bgColour);
            }

            // finish the table
            writer.write("</tr></table>");

            return writer.toString();
        } catch (IOException ioe) {
            throw new AlfrescoRuntimeException("Failed to render reply bubble HTML", ioe);
        }
    }

    // ------------------------------------------------------------------------------
    // IContextListener implementation 

    /**
     * @see org.alfresco.web.app.context.IContextListener#contextUpdated()
     */
    public void contextUpdated() {
        if (logger.isDebugEnabled())
            logger.debug("Invalidating forums components...");

        // clear the value for the list components - will cause re-bind to it's data and refresh
        if (this.forumsRichList != null) {
            this.forumsRichList.setValue(null);
            if (this.forumsRichList.getInitialSortColumn() == null) {
                // set the initial sort column and direction
                this.forumsRichList.setInitialSortColumn(this.viewsConfig.getDefaultSortColumn(PAGE_NAME_FORUMS));
                this.forumsRichList.setInitialSortDescending(this.viewsConfig.hasDescendingSort(PAGE_NAME_FORUMS));
            }
        }

        if (this.forumRichList != null) {
            this.forumRichList.setValue(null);
            if (this.forumRichList.getInitialSortColumn() == null) {
                // set the initial sort column and direction
                this.forumRichList.setInitialSortColumn(this.viewsConfig.getDefaultSortColumn(PAGE_NAME_FORUM));
                this.forumRichList.setInitialSortDescending(this.viewsConfig.hasDescendingSort(PAGE_NAME_FORUM));
            }
        }

        if (this.topicRichList != null) {
            this.topicRichList.setValue(null);
            if (this.topicRichList.getInitialSortColumn() == null) {
                // set the initial sort column and direction
                this.topicRichList.setInitialSortColumn(this.viewsConfig.getDefaultSortColumn(PAGE_NAME_TOPIC));
                this.topicRichList.setInitialSortDescending(this.viewsConfig.hasDescendingSort(PAGE_NAME_TOPIC));
            }
        }

        // reset the lists
        this.forums = null;
        this.topics = null;
        this.posts = null;
    }

    /**
     * @see org.alfresco.web.app.context.IContextListener#areaChanged()
     */
    public void areaChanged() {
        // nothing to do
    }

    /**
     * @see org.alfresco.web.app.context.IContextListener#spaceChanged()
     */
    public void spaceChanged() {
        // nothing to do
    }

    // ------------------------------------------------------------------------------
    // Navigation action event handlers 

    /**
     * Change the current forums view mode based on user selection
     * 
     * @param event      ActionEvent
     */
    public void forumsViewModeChanged(ActionEvent event) {
        UIModeList viewList = (UIModeList) event.getComponent();

        // get the view mode ID
        String viewMode = viewList.getValue().toString();

        // push the view mode into the lists
        setForumsViewMode(viewMode);

        // get the default for the forum page
        this.forumsPageSize = this.viewsConfig.getDefaultPageSize(PAGE_NAME_FORUMS, this.forumsViewMode);

        if (logger.isDebugEnabled())
            logger.debug("Set default forums page size to: " + this.forumsPageSize);
    }

    /**
     * Change the current forum view mode based on user selection
     * 
     * @param event      ActionEvent
     */
    public void forumViewModeChanged(ActionEvent event) {
        UIModeList viewList = (UIModeList) event.getComponent();

        // get the view mode ID
        String viewMode = viewList.getValue().toString();

        // push the view mode into the lists
        setForumViewMode(viewMode);

        // get the default for the forum page
        this.forumPageSize = this.viewsConfig.getDefaultPageSize(PAGE_NAME_FORUM, this.forumViewMode);

        if (logger.isDebugEnabled())
            logger.debug("Set default forum page size to: " + this.forumPageSize);
    }

    /**
     * Change the current topic view mode based on user selection
     * 
     * @param event      ActionEvent
     */
    public void topicViewModeChanged(ActionEvent event) {
        UIModeList viewList = (UIModeList) event.getComponent();

        // get the view mode ID
        String viewMode = viewList.getValue().toString();

        // push the view mode into the lists
        setTopicViewMode(viewMode);

        // change the default page size if necessary
        this.topicPageSize = this.viewsConfig.getDefaultPageSize(PAGE_NAME_TOPIC, this.topicViewMode);

        if (logger.isDebugEnabled())
            logger.debug("Set default topic page size to: " + this.topicPageSize);
    }

    /**
     * Event handler called when a user wants to view or participate 
     * in a discussion on an object
     * 
     * @param event ActionEvent
     */
    public void discuss(ActionEvent event) {
        UIActionLink link = (UIActionLink) event.getComponent();
        Map<String, String> params = link.getParameterMap();
        String id = params.get("id");
        if (id == null || id.length() == 0) {
            throw new AlfrescoRuntimeException("discuss called without an id");
        }

        FacesContext context = FacesContext.getCurrentInstance();

        NodeRef nodeRef = new NodeRef(Repository.getStoreRef(), id);

        if (this.getNodeService().hasAspect(nodeRef, ForumModel.ASPECT_DISCUSSABLE) == false) {
            throw new AlfrescoRuntimeException("discuss called for an object that does not have a discussion!");
        }

        // as the node has the discussable aspect there must be a discussions child assoc
        List<ChildAssociationRef> children = this.getNodeService().getChildAssocs(nodeRef,
                ForumModel.ASSOC_DISCUSSION, RegexQNamePattern.MATCH_ALL);

        // there should only be one child, retrieve it if there is
        if (children.size() == 1) {
            // show the forum for the discussion
            NodeRef forumNodeRef = children.get(0).getChildRef();
            this.browseBean.clickSpace(forumNodeRef);
            context.getApplication().getNavigationHandler().handleNavigation(context, null, "showForum");
        } else {
            // this should never happen as the action evaluator should stop the action
            // from displaying, just in case print a warning to the console
            logger.warn("Node has the discussable aspect but does not have 1 child, it has " + children.size()
                    + " children!");
        }
    }

    // ------------------------------------------------------------------------------
    // Property Resolvers

    public NodePropertyResolver resolverReplies = new NodePropertyResolver() {
        private static final long serialVersionUID = -4800772273246202885L;

        public Object get(Node node) {
            // query for the number of posts within the given node
            String repliesXPath = "./*[(subtypeOf('" + ForumModel.TYPE_POST + "'))]";
            List<NodeRef> replies = getSearchService().selectNodes(node.getNodeRef(), repliesXPath,
                    new QueryParameterDefinition[] {}, getNamespaceService(), false);

            // reduce the count by 1 as one of the posts will be the initial post
            int noReplies = replies.size() - 1;

            if (noReplies < 0) {
                noReplies = 0;
            }

            return new Integer(noReplies);
        }
    };

    public NodePropertyResolver resolverContent = new NodePropertyResolver() {
        private static final long serialVersionUID = -2575377410105460440L;

        public Object get(Node node) {
            String content = null;

            // get the content property from the node and retrieve the 
            // full content as a string (obviously should only be used
            // for small amounts of content)
            ContentReader reader = getContentService().getReader(node.getNodeRef(), ContentModel.PROP_CONTENT);

            if (reader != null) {
                content = Utils.stripUnsafeHTMLTags(reader.getContentString());
            }

            return content;
        }
    };

    public NodePropertyResolver resolverReplyTo = new NodePropertyResolver() {
        private static final long serialVersionUID = 2614901755220899360L;

        public Object get(Node node) {
            // determine if this node is a reply to another post, if so find
            // the creator of the original poster
            String replyTo = null;

            List<AssociationRef> assocs = getNodeService().getTargetAssocs(node.getNodeRef(),
                    ContentModel.ASSOC_REFERENCES);

            // there should only be one association, if there is more than one
            // just get the first one
            if (assocs.size() > 0) {
                AssociationRef assoc = assocs.get(0);
                NodeRef target = assoc.getTargetRef();
                Node targetNode = new Node(target);
                replyTo = (String) targetNode.getProperties().get("creator");
            }

            return replyTo;
        }
    };

    /**
     * Creates a file name for the message being posted
     * 
     * @return The file name for the post
     */
    public static String createPostFileName() {
        StringBuilder name = new StringBuilder("posted-");

        // add a timestamp
        SimpleDateFormat dateFormat = new SimpleDateFormat("dd-MM-yyyy-HH-mm-ss");
        name.append(dateFormat.format(new Date()));

        // add Universal Unique Identifier
        // fix bugs ETWOONE-196 and ETWOONE-203
        name.append("-" + UUID.randomUUID());

        // add the HTML file extension
        name.append(".html");

        return name.toString();
    }

    // ------------------------------------------------------------------------------
    // Helpers

    /**
     * Initialise default values from client configuration
     */
    private void initFromClientConfig() {
        // TODO - review implications of these default values for dynamic/MT client
        this.viewsConfig = (ViewsConfigElement) Application.getConfigService(FacesContext.getCurrentInstance())
                .getConfig("Views").getConfigElement(ViewsConfigElement.CONFIG_ELEMENT_ID);

        // get the defaults for the forums page
        this.forumsViewMode = this.viewsConfig.getDefaultView(PAGE_NAME_FORUMS);
        this.forumsPageSize = this.viewsConfig.getDefaultPageSize(PAGE_NAME_FORUMS, this.forumsViewMode);

        // get the default for the forum page
        this.forumViewMode = this.viewsConfig.getDefaultView(PAGE_NAME_FORUM);
        this.forumPageSize = this.viewsConfig.getDefaultPageSize(PAGE_NAME_FORUM, this.forumViewMode);

        // get the default for the topic page
        this.topicViewMode = this.viewsConfig.getDefaultView(PAGE_NAME_TOPIC);
        this.topicPageSize = this.viewsConfig.getDefaultPageSize(PAGE_NAME_TOPIC, this.topicViewMode);

        if (logger.isDebugEnabled()) {
            logger.debug("Set default forums view mode to: " + this.forumsViewMode);
            logger.debug("Set default forums page size to: " + this.forumsPageSize);
            logger.debug("Set default forum view mode to: " + this.forumViewMode);
            logger.debug("Set default forum page size to: " + this.forumPageSize);
            logger.debug("Set default topic view mode to: " + this.topicViewMode);
            logger.debug("Set default topic page size to: " + this.topicPageSize);
        }
    }

    protected void renderReplyContentHTML(FacesContext context, Node replyToNode, StringWriter writer,
            String contextPath, String colour, String bgColour) throws IOException {
        // get the content of the article being replied to
        String replyContent = "";
        ContentReader reader = this.getContentService().getReader(replyToNode.getNodeRef(),
                ContentModel.PROP_CONTENT);
        if (reader != null) {
            replyContent = Utils.stripUnsafeHTMLTags(reader.getContentString());
        }

        // get the date of the article being replied to
        String postedDate = Utils.getDateTimeFormat(context).format(replyToNode.getProperties().get("created"));

        // generate the HTML
        writer.write("<td width='100%'>");
        TopicBubbleViewRenderer.renderBubbleTop(writer, contextPath, colour, bgColour);
        writer.write("<span class='mainSubTitle'>");
        writer.write(Application.getMessage(context, "posted"));
        writer.write(":&nbsp</span>");
        writer.write(postedDate);
        TopicBubbleViewRenderer.renderBubbleMiddle(writer, contextPath, colour);
        writer.write(replyContent);
        TopicBubbleViewRenderer.renderBubbleBottom(writer, contextPath, colour);
        writer.write("</td>");
    }

    /**
     * Class to implement a bubble view for the RichList component used in the topics screen
     * 
     * @author gavinc
     */
    public static class TopicBubbleViewRenderer implements IRichListRenderer {
        private static final long serialVersionUID = -6641033880549363822L;

        public static final String VIEWMODEID = "bubble";

        public String getViewModeID() {
            return VIEWMODEID;
        }

        public void renderListBefore(FacesContext context, UIRichList richList, UIColumn[] columns)
                throws IOException {
            // nothing to do
        }

        public void renderListRow(FacesContext context, UIRichList richList, UIColumn[] columns, Object row)
                throws IOException {
            ResponseWriter out = context.getResponseWriter();

            // find primary column (which must exist) and the actions column (which doesn't 
            // have to exist)
            UIColumn primaryColumn = null;
            UIColumn actionsColumn = null;
            for (int i = 0; i < columns.length; i++) {
                if (columns[i].isRendered()) {
                    if (columns[i].getPrimary()) {
                        primaryColumn = columns[i];
                    } else if (columns[i].getActions()) {
                        actionsColumn = columns[i];
                    }
                }
            }

            if (primaryColumn == null) {
                if (logger.isWarnEnabled())
                    logger.warn("No primary column found for RichList definition: " + richList.getId());
            }

            out.write("<tr>");

            Node node = (Node) row;
            if (node.getProperties().get("replyTo") == null) {
                renderNewPostBubble(context, out, node, primaryColumn, actionsColumn, columns);
            } else {
                renderReplyToBubble(context, out, node, primaryColumn, actionsColumn, columns);
            }

            out.write("</tr>");

            // add a little padding
            out.write("<tr><td><div style='padding:3px'></div></td></tr>");
        }

        public void renderListAfter(FacesContext context, UIRichList richList, UIColumn[] columns)
                throws IOException {
            ResponseWriter out = context.getResponseWriter();

            out.write("<tr><td colspan='99' align='center'>");
            for (Iterator i = richList.getChildren().iterator(); i.hasNext(); /**/) {
                // output all remaining child components that are not UIColumn
                UIComponent child = (UIComponent) i.next();
                if (child instanceof UIColumn == false) {
                    Utils.encodeRecursive(context, child);
                }
            }
            out.write("</td></tr>");
        }

        /**
         * Renders the top part of the bubble i.e. before the header
         * 
         * @param out The writer to output to
         * @param contextPath Context path of the application
         * @param colour The colour of the bubble
         * @param titleBgColour Background colour of the header area
         */
        public static void renderBubbleTop(Writer out, String contextPath, String colour, String titleBgColour)
                throws IOException {
            out.write("<table border='0' cellpadding='0' cellspacing='0' width='100%'><tr>");
            out.write("<td style='background: url(");
            out.write(contextPath);
            out.write("/images/parts/");
            out.write(colour);
            out.write("header_1.gif) no-repeat #FFFFFF;' width='24' height='24'></td>");
            out.write("<td style='background: url(");
            out.write(contextPath);
            out.write("/images/parts/");
            out.write(colour);
            out.write("header_2.gif) repeat-x ");
            out.write(titleBgColour);
            out.write("'>");
        }

        /**
         * Renders the middle part of the bubble i.e. after the header and before the body
         * 
         * @param out The writer to output to
         * @param contextPath Context path of the application
         * @param colour The colour of the bubble
         */
        public static void renderBubbleMiddle(Writer out, String contextPath, String colour) throws IOException {
            out.write("</td>");
            out.write("<td style='background: url(");
            out.write(contextPath);
            out.write("/images/parts/");
            out.write(colour);
            out.write("header_3.gif) no-repeat #FFFFFF;' width='24' height='24'></td>");
            out.write("</tr><tr>");
            out.write("<td width='24' height='13'><img width='24' height='13' alt='' src='");
            out.write(contextPath);
            out.write("/images/parts/");
            out.write(colour);
            out.write("body_1.gif' /></td>");
            out.write("<td background='");
            out.write(contextPath);
            out.write("/images/parts/");
            out.write(colour);
            out.write("body_2.gif'><img width='21' height='13' alt='' src='");
            out.write(contextPath);
            out.write("/images/parts/");
            out.write(colour);
            out.write("body_2.gif' /></td>");
            out.write("<td width='24' height='13'><img width='24' height='13' alt='' src='");
            out.write(contextPath);
            out.write("/images/parts/");
            out.write(colour);
            out.write("body_3.gif' /></td>");
            out.write("</tr><tr>");
            out.write("<td width='24' height='13' background='");
            out.write(contextPath);
            out.write("/images/parts/");
            out.write(colour);
            out.write("body_4.gif'><img width='24' height='4' alt='' src='");
            out.write(contextPath);
            out.write("/images/parts/");
            out.write(colour);
            out.write("body_4.gif' /></td>");
            out.write("<td>");
        }

        /**
         * Renders the bottom part of the bubble i.e. after the body
         * 
         * @param out The writer to output to
         * @param contextPath Context path of the application
         * @param colour The colour of the bubble
         */
        public static void renderBubbleBottom(Writer out, String contextPath, String colour) throws IOException {
            out.write("</td>");
            out.write("<td width='24' height='13' background='");
            out.write(contextPath);
            out.write("/images/parts/");
            out.write(colour);
            out.write("body_6.gif'><img width='24' height='3' alt='' src='");
            out.write(contextPath);
            out.write("/images/parts/");
            out.write(colour);
            out.write("body_6.gif' /></td>");
            out.write("</tr><tr>");
            out.write("<td width='24' height='13'><img width='24' height='13' alt='' src='");
            out.write(contextPath);
            out.write("/images/parts/");
            out.write(colour);
            out.write("body_7.gif' /></td>");
            out.write("<td background='");
            out.write(contextPath);
            out.write("/images/parts/");
            out.write(colour);
            out.write("body_8.gif'><img width='20' height='13' alt='' src='");
            out.write(contextPath);
            out.write("/images/parts/");
            out.write(colour);
            out.write("body_8.gif' /></td>");
            out.write("<td width='24' height='13'><img width='24' height='13' alt='' src='");
            out.write(contextPath);
            out.write("/images/parts/");
            out.write(colour);
            out.write("body_9.gif' /></td>");
            out.write("</tr></table>");
        }

        /**
         * Renders the new post speech bubble
         * 
         * @param context Faces context
         * @param out The response writer
         * @param node The Node for the row being rendered
         * @param primaryColumn The primary column containing the message content
         * @param actionsColumn The actions column containing all the actions
         * @param columns All configured columns
         */
        private void renderNewPostBubble(FacesContext context, ResponseWriter out, Node node,
                UIColumn primaryColumn, UIColumn actionsColumn, UIColumn[] columns) throws IOException {
            String contextPath = context.getExternalContext().getRequestContextPath();
            String colour = "orange";

            out.write("<td><table border='0' cellpadding='0' cellspacing='0' width='100%'><tr>");
            out.write("<td valign='top'><img src='");
            out.write(contextPath);
            out.write("/images/icons/user_large.gif'/><br>");
            out.write((String) node.getProperties().get("creator"));
            out.write("</td><td width='100%'>");

            renderBubbleTop(out, contextPath, colour, "#FCC75E");
            renderHeaderContents(context, out, primaryColumn, actionsColumn, columns);
            renderBubbleMiddle(out, contextPath, colour);
            renderBodyContents(context, primaryColumn);
            renderBubbleBottom(out, contextPath, colour);

            out.write("</td><td><div style='width:32px;'></div></td></tr></table></td>");
        }

        /**
         * Renders the reply to post speech bubble
         * 
         * @param context Faces context
         * @param out The response writer
         * @param node The Node for the row being rendered
         * @param primaryColumn The primary column containing the message content
         * @param actionsColumn The actions column containing all the actions
         * @param columns All configured columns
         */
        private void renderReplyToBubble(FacesContext context, ResponseWriter out, Node node,
                UIColumn primaryColumn, UIColumn actionsColumn, UIColumn[] columns) throws IOException {
            String contextPath = context.getExternalContext().getRequestContextPath();
            String colour = "yellow";

            out.write("<td width='100%'><table border='0' cellpadding='0' cellspacing='0' width='100%'><tr>");
            out.write("<td><div style='width:32px;'></div></td><td width='100%'>");

            renderBubbleTop(out, contextPath, colour, "#FFF5A3");
            renderHeaderContents(context, out, primaryColumn, actionsColumn, columns);
            renderBubbleMiddle(out, contextPath, colour);
            renderBodyContents(context, primaryColumn);
            renderBubbleBottom(out, contextPath, colour);

            out.write("</td><td valign='top'><img src='");
            out.write(contextPath);
            out.write("/images/icons/user_large.gif'/><br>");
            out.write((String) node.getProperties().get("creator"));
            out.write("</td></tr></table></td>");
        }

        private void renderHeaderContents(FacesContext context, ResponseWriter out, UIColumn primaryColumn,
                UIColumn actionsColumn, UIColumn[] columns) throws IOException {
            // render the header area with the configured columns
            out.write("<table width='100%' cellpadding='2' cellspacing='2' border='0'><tr>");

            for (int i = 0; i < columns.length; i++) {
                UIColumn column = columns[i];

                if (column.isRendered() == true && column.getPrimary() == false && column.getActions() == false) {
                    // render the column header as the label
                    UIComponent header = column.getFacet("header");
                    if (header != null) {
                        out.write("<td><nobr><b>");
                        Utils.encodeRecursive(context, header);
                        out.write("</b></nobr></td>");
                    }

                    // render the contents of the column
                    if (column.getChildCount() != 0) {
                        out.write("<td><nobr>");
                        Utils.encodeRecursive(context, column);
                        out.write("</nobr></td>");
                    }
                }
            }

            // render the actions column
            out.write("<td align='right' width='100%'><nobr>");
            if (actionsColumn != null && actionsColumn.getChildCount() != 0) {
                Utils.encodeRecursive(context, actionsColumn);
            }
            out.write("</nobr></td></tr></table>");
        }

        /**
         * Renders the body contents for the bubble using the given primary coumn 
         * 
         * @param context Faces context
         * @param primaryColumn The primary column holding the message text
         */
        private void renderBodyContents(FacesContext context, UIColumn primaryColumn) throws IOException {
            // render the primary column
            if (primaryColumn != null && primaryColumn.getChildCount() != 0) {
                Utils.encodeRecursive(context, primaryColumn);
            }
        }
    }
}