com.alkacon.opencms.v8.documentcenter.CategoryTree.java Source code

Java tutorial

Introduction

Here is the source code for com.alkacon.opencms.v8.documentcenter.CategoryTree.java

Source

/*
 * File   : $Source: /alkacon/cvs/alkacon/com.alkacon.opencms.documentcenter/src/com/alkacon/opencms/documentcenter/CategoryTree.java,v $
 * Date   : $Date: 2010/03/19 15:31:14 $
 * Version: $Revision: 1.3 $
 *
 * This file is part of the Alkacon OpenCms Add-On Module Package
 *
 * Copyright (c) 2010 Alkacon Software GmbH (http://www.alkacon.com)
 *
 * The Alkacon OpenCms Add-On Module Package is free software: 
 * you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * The Alkacon OpenCms Add-On Module Package 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 General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with the Alkacon OpenCms Add-On Module Package.  
 * If not, see http://www.gnu.org/licenses/.
 *
 * For further information about Alkacon Software GmbH, please see the
 * company website: http://www.alkacon.com.
 *
 * For further information about OpenCms, please see the
 * project website: http://www.opencms.org.
 */

package com.alkacon.opencms.v8.documentcenter;

import org.opencms.file.CmsFolder;
import org.opencms.file.CmsObject;
import org.opencms.file.CmsPropertyDefinition;
import org.opencms.file.CmsResource;
import org.opencms.file.CmsUser;
import org.opencms.main.CmsException;
import org.opencms.main.CmsLog;
import org.opencms.util.CmsStringUtil;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.StringTokenizer;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;

import org.apache.commons.logging.Log;

/**
 * A Java bean to do a query for VFS documents.<p>
 * 
 * The category tree is internally stored by a map of adjacency lists
 * containing Lgt category objects keyed by their parent categories/folders.<p>
 * 
 * @author Thomas Weckert 
 * 
 * @version $Revision: 1.3 $ 
 * 
 * @since 6.0.0 
 */
public class CategoryTree {

    /** The form action to trigger a search for new documents. */
    public static final String C_ACTION_SEARCH = "search";

    /** The form action to toggle the category tree. */
    public static final String C_ACTION_TOGGLE_TREE = "toggleTree";

    /** The default max. tree depth of the category tree. */
    public static final int C_DEFAULT_MAX_TREE_DEPTH = 2;

    /** The sep.-char. to build lists. */
    public static final String C_LIST_SEPARATOR = ",";

    /** The category toggle +/- in the tree. */
    public static final String C_PARAM_TOGGLE_CATEGORY = "toggleCategory";

    /** The mode +/- how to tree was toggled. */
    public static final String C_PARAM_TOGGLE_MODE = "toggleMode";

    /** The category property. */
    public static final String C_PROP_CATEGORY = "category";

    /** The property key to read the max. folder/category depth of the tree. */
    public static final String C_PROPERTY_MAX_TREE_DEPTH = "categoryTreeMaxDepth";

    /** The key to get the opened categories from the user info hash. */
    public static final String C_USER_INFO_OPENED_CATEGORIES = "opened_categories";

    /** The key to get the selected categories from the user info hash. */
    public static final String C_USER_INFO_SELECTED_CATEGORIES = "selected_categories";

    /** A separator for property prefixes. */
    private static final String C_PREFIX_SEPARATOR = ":";

    /** The log object for this class. */
    private static final Log LOG = CmsLog.getLog(CategoryTree.class);

    /** The action submitted from the form what to do next. */
    private String m_action;

    /** A boolean flag that saves if the user checked the "select all categories" checkbox in the form. */
    private boolean m_allSelected;

    /** The current user's Cms object. */
    private CmsObject m_cms;

    /** The max. folder/category depth of the tree. */
    private int m_maxTreeDepth;

    /** The folder names of the categories opened in the tree. */
    private List<String> m_openedCategoryList;

    /** The name of the property definition for the localized title, e.g. "Title_en". */
    private String m_propertyTitle;

    /** The request used tu initialize the Category Tree. */
    private HttpServletRequest m_request;

    /** The root folder from where the tree is built. */
    private String m_rootFolder;

    /** The folder names of the categories selected in the tree. */
    private List<String> m_selectedCategoryList;

    /** The map to stroe the category tree. */
    private Map<String, List<CmsCategory>> m_treeMap;

    /**
     * Creates a new category tree.<p>
     * @param cms the current user's Cms object
     * @param request the Http request
     * @param rootFolder the root folder from where the category tree is built (usually "/")
     * @param maxTreeDepth the max. category/folder depth of the tree
     */
    public CategoryTree(CmsObject cms, HttpServletRequest request, String rootFolder, String maxTreeDepth) {

        String value = null;
        String mode = null;
        String category = null;

        m_cms = cms;
        m_rootFolder = rootFolder;
        m_request = request;
        m_propertyTitle = CmsPropertyDefinition.PROPERTY_TITLE + "_"
                + cms.getRequestContext().getLocale().toString();

        if (maxTreeDepth != null) {
            try {
                m_maxTreeDepth = Integer.parseInt(maxTreeDepth);
            } catch (Exception e) {

                if (LOG.isErrorEnabled()) {
                    LOG.error(
                            "Error: maxTreeDepth is not a string representing an integer! Using default max. tree depth "
                                    + C_DEFAULT_MAX_TREE_DEPTH,
                            e);
                }
                m_maxTreeDepth = C_DEFAULT_MAX_TREE_DEPTH;
            }
        } else {

            m_maxTreeDepth = C_DEFAULT_MAX_TREE_DEPTH;

            if (LOG.isErrorEnabled()) {
                LOG.error("Error: maxTreeDepth is null! Using default max. tree depth " + C_DEFAULT_MAX_TREE_DEPTH);
            }
        }

        // save all required request params.

        value = request.getParameter("action");
        if (value != null) {
            m_action = value;
        }

        value = request.getParameter("openedCategories");
        if (value != null) {
            m_openedCategoryList = commaStringToList(value, C_LIST_SEPARATOR);
        } else {
            m_openedCategoryList = new ArrayList<String>();
        }

        value = request.getParameter("categorylist");
        if (value != null) {
            m_selectedCategoryList = commaStringToList(value, C_LIST_SEPARATOR);
        } else {
            m_selectedCategoryList = new ArrayList<String>();
        }

        value = request.getParameter("all");
        if (value != null) {
            m_allSelected = true;
        } else {
            m_allSelected = false;
        }

        if (LOG.isDebugEnabled()) {
            LOG.debug("################ INPUT VALUES FOR CATEGORY TREE");
            LOG.debug("action : " + m_action);
            LOG.debug("opened categories : " + m_openedCategoryList.toString());
            LOG.debug("selected categories : " + m_selectedCategoryList.toString());
            LOG.debug("root folder : " + m_rootFolder);
            LOG.debug("max. tree depth : " + m_maxTreeDepth);
        }

        // initialize everything by switching the submitted form action

        if (m_action != null) {

            if (C_ACTION_TOGGLE_TREE.equalsIgnoreCase(m_action)) {

                // the user toggled the tree...

                mode = request.getParameter(C_PARAM_TOGGLE_MODE);
                category = request.getParameter(C_PARAM_TOGGLE_CATEGORY);

                if ((mode != null) && (category != null)) {

                    if ("+".equalsIgnoreCase(mode)) {

                        // the user opened a sub-tree
                        openCategory(category);
                        saveTreeInfo();
                    } else if ("-".equalsIgnoreCase(mode)) {

                        // the user closed a sub-tree
                        closeCategory(category);
                        saveTreeInfo();
                    } else if (LOG.isErrorEnabled()) {

                        LOG.debug("Unknown toggle mode " + mode + " submitted!");
                    }
                }
            } else if ("redirect_a".equalsIgnoreCase(m_action) || C_ACTION_SEARCH.equalsIgnoreCase(m_action)
                    || ("searchText".equalsIgnoreCase(m_action))) {

                // the user triggered a search for new documents...
                saveTreeInfo();
            }
        }
    }

    /**
     * Turns comma-separated string into a List of strings.<p>
     * 
     * @param str the string to be split
     * @param sep a separator character
     * @return a List of tokens
     */
    public static List<String> commaStringToList(String str, String sep) {

        List<String> result = null;
        StringTokenizer tokenizer = null;

        if (str != null) {

            tokenizer = new StringTokenizer(str, sep);
            result = new ArrayList<String>(tokenizer.countTokens());

            while (tokenizer.hasMoreTokens()) {
                result.add(tokenizer.nextToken());
            }

            Collections.sort(result);
        } else {

            result = new ArrayList<String>();
        }

        return result;
    }

    /**
     * Cuts off a prefix separated by <code>:</code> from the "real" property value.<p>
     * 
     * @param value a property value
     * @return the property value without the prefix
     */
    public static String cutPrefix(String value) {

        int index = value.indexOf(C_PREFIX_SEPARATOR);
        if (index != -1) {
            return value.substring(index + 1);
        }

        return value;
    }

    /**
     * Returns the tree info for the specified key from the current user's info hash.<p>
     * @param cms the CmsObject
     * @param key the key
     * @param request the current request
     * 
     * @return the tree info for the specified key
     * @see #C_USER_INFO_OPENED_CATEGORIES
     * @see #C_USER_INFO_SELECTED_CATEGORIES
     */
    public static String getTreeInfo(CmsObject cms, String key, HttpServletRequest request) {

        key = cms.getRequestContext().addSiteRoot(cms.getRequestContext().getUri()) + "_" + key;

        // try to read the tree settings fomr the request
        String value;
        value = (String) request.getSession(true).getAttribute(key);

        // if nothing was found in the session, try to read some infoes from the user
        if (value == null) {
            value = (String) cms.getRequestContext().getCurrentUser().getAdditionalInfo(key);
        }

        if (LOG.isDebugEnabled()) {
            LOG.debug("retrieving additional info for user " + cms.getRequestContext().getCurrentUser().getName()
                    + " with key: " + key + ", value: " + value);
        }

        return value;
    }

    /**
     * Turns a List of strings into a comma-separated string.<p>
     * 
     * @param list the list
     * @param sep the separator
     * @return a comma-separated string
     */
    public static String listToCommaString(List<String> list, String sep) {

        StringBuffer buf = new StringBuffer();

        for (int i = 0, n = list.size(); i < n; i++) {

            buf.append(list.get(i));
            if (i < (n - 1)) {
                buf.append(sep);
            }
        }

        return buf.toString();
    }

    /**
     * Builds the HTML of the category tree.<p>
     * @param categoryTree a List with the categories currently visible in the tree
     * @param optCheckboxArgs optional arguments to be included in the checkbox tags, or null
     * @param optAnchorArgs optional arguments to be included in the anchor tags to toggle the tree, or null
     * @param indent a HTML fragment (e.g. a span tag) to indent sub-elements in the tree
     * @return the HTML of the category tree
     */
    public String buildCategoryTree(List<CmsCategory> categoryTree, String optCheckboxArgs, String optAnchorArgs,
            String indent) {

        StringBuffer buf = new StringBuffer();
        CmsCategory category = null;
        String resourceName = null;
        String toggleMode = null;
        int counter = 0;
        int pathLevel = 0;
        int pathLevelRootFolder = CmsResource.getPathLevel(m_rootFolder) + 1;

        // build the HTML of the category tree
        for (int i = 0, n = categoryTree.size(); i < n; i++) {

            category = categoryTree.get(i);

            // check if this is a main category
            boolean mainCat = false;
            if (category.getPosition() != null) {
                mainCat = category.getPosition().indexOf(".") <= 0;
            }

            resourceName = category.getCmsResource();

            if ((resourceName != null) && !"".equalsIgnoreCase(resourceName)) {

                toggleMode = m_openedCategoryList.contains(category.getCmsResource()) ? "-" : "+";

                pathLevel = CmsResource.getPathLevel(category.getCmsResource()) - pathLevelRootFolder;
                for (int j = 0; j < pathLevel; j++) {
                    buf.append(indent);
                }

                buf.append("<input type=\"checkbox\" name=\"cat").append(counter).append("\"");
                buf.append(" id=\"cat").append(counter).append("\"");
                buf.append(" value=\"").append(resourceName).append("\"");

                if (m_selectedCategoryList.contains(resourceName)) {
                    buf.append(" checked=\"checked\"");
                }

                if (optCheckboxArgs != null) {
                    buf.append(" ").append(optCheckboxArgs);
                }

                buf.append("/>&nbsp;");
                buf.append("<a");

                if (optAnchorArgs != null) {
                    buf.append(" ").append(optAnchorArgs);
                }

                buf.append(" href=\"#\" onClick=\"toggleTree(" + categoryTree.size() + ",'" + resourceName + "','")
                        .append(toggleMode).append("')\">");
                buf.append("[").append(toggleMode).append("]</a>");
                buf.append("&nbsp;");
                if (mainCat) {
                    buf.append("<span class=\"treetopcategory\">");
                }
                buf.append("<label for=\"").append("cat").append(counter).append("\">");
                buf.append(category.getTitle());
                buf.append("</label>");
                if (mainCat) {
                    buf.append("</span>");
                }
                buf.append("<br/>\n");
                counter++;
            }

        }

        return buf.toString();
    }

    /**
     * Returns a list of all categories sorted ascending by their position in the 
     * category tree.<p>
     * 
     * The tree contains the main-categories, plus the sub-categories of all opened 
     * categories.<p>
     * 
     * @return a list of all categories
     */
    public List<CmsCategory> getCategoryTree() {

        List<CmsCategory> result = null;
        CmsCategory category = null;
        String parentFolder = null;
        List<CmsCategory> treeList = null;
        boolean addToResult = false;

        try {
            // create a new tree map
            m_treeMap = Collections.synchronizedMap(new HashMap<String, List<CmsCategory>>());

            // add all selected categories to the tree        
            addSelectedCategories();

            // add all opened categories to the tree
            addOpenedCategories();

            // add all folders having the "category" property set to tree
            addCategoryFolders();

            // turn the tree map into a List in DFS order
            treeList = toList(new CmsCategory("", "", m_rootFolder));
            if (treeList == null) {
                // the tree is empty...
                return Collections.emptyList();
            } else {
                // tree[0] is the root folder and can be skipped from the result tree
                if (treeList.size() > 1) {
                    treeList = treeList.subList(1, treeList.size());
                }
            }

            // build the final result tree. this is to include only sub-trees
            // in the result that either have a parent folder being opened, 
            // or a "sibling" node in the tree that is selected
            result = new ArrayList<CmsCategory>();
            for (int i = 0, n = treeList.size(); i < n; i++) {

                category = treeList.get(i);
                parentFolder = CmsResource.getParentFolder(category.getCmsResource());

                // add the category to the result if it is a main category
                addToResult = (category.getDepth() == 0);
                // or if it is a sub-folder/category of an opened category
                addToResult |= m_openedCategoryList.contains(parentFolder);
                // and if it's folder depth is below the max. folder depth
                addToResult &= CmsResource.getPathLevel(category.getCmsResource()) <= m_maxTreeDepth;

                if (addToResult) {
                    result.add(category);
                }
            }
        } catch (Exception e) {

            if (LOG.isErrorEnabled()) {
                LOG.error("Error building category tree", e);
            }
        }

        return result;
    }

    /**
     * Returns the comma-separated string of opened categories.<p>
     * 
     * @return the comma-separated string of opened categories
     */
    public String getOpenedCategories() {

        if ((m_openedCategoryList == null) || (m_openedCategoryList.size() == 0)) {
            return "";
        }

        return listToCommaString(m_openedCategoryList, C_LIST_SEPARATOR);
    }

    /**
     * Returns "true" if all categories should be pre-selected in the form.<p>
     * 
     * This string can be used to trigger a JavaScript in the form.<p>
     * 
     * @return "true" if all categories should be pre-selected in the form
     */
    public String getPreSelectAllCategories() {

        // all categories should be pre-selected either if the user checked the
        // "select all categories" checkbox", or if the user openes the form for
        // the first time "action=null" and he doesn't have a tree of selected
        // categories saved in his user info hash.

        if (m_allSelected || (((m_action == null) || "".equalsIgnoreCase(m_action))
                && (m_selectedCategoryList.size() == 0))) {
            return "true";
        }

        return "false";
    }

    /**
     * Returns the root folder from where the tree is built.<p>
     * 
     * @return the root folder from where the tree is built
     */
    public String getRootFolder() {

        return m_rootFolder;
    }

    /**
     * Returns the comma-separated string of selected categories.<p>
     * 
     * @return the comma-separated string of selected categories
     */
    public String getSelectedCategories() {

        if ((m_selectedCategoryList == null) || (m_selectedCategoryList.size() == 0)) {
            return "";
        }

        return listToCommaString(m_selectedCategoryList, C_LIST_SEPARATOR);
    }

    /**
     * Adds a category to the tree if it is not already contained in the tree.<p>
     * 
     * @param parent the parent folder of the category
     * @param category the category
     */
    protected void addCategory(String parent, CmsCategory category) {

        // get the child list of the folder specified by the parent ID
        List<CmsCategory> children = m_treeMap.get(parent);

        if (children == null) {

            // the child is obviously the first child of it's parent folder
            children = new ArrayList<CmsCategory>();

            // add the new child list as a sub-tree
            m_treeMap.put(parent, children);
        }

        if (!children.contains(category)) {
            // add the child to the child list
            children.add(category);
        }
    }

    /**
     * Adds all folders having the "category" property set to the tree .<p>
     */
    protected void addCategoryFolders() {

        List<CmsResource> resources = null;
        CmsResource resource = null;
        String position = null;
        String title = null;
        String resourceName = null;
        CmsCategory category = null;
        String parentFolder = null;

        try {

            // read all resources having the "category" property set from current root folder
            resources = m_cms.readResourcesWithProperty(m_rootFolder, C_PROP_CATEGORY);
        } catch (CmsException e) {

            if (LOG.isErrorEnabled()) {
                LOG.error("Error reading resources with property " + C_PROP_CATEGORY, e);
            }

            resources = Collections.emptyList();
        }

        // turn the resources into a map of LgtCategory objects 
        // keyed by their parent folder in the category tree        
        for (int i = 0, n = resources.size(); i < n; i++) {
            resource = resources.get(i);

            try {

                resourceName = m_cms.getRequestContext().removeSiteRoot(resource.getRootPath());
                position = m_cms.readPropertyObject(resourceName, C_PROP_CATEGORY, false).getValue();
                if ((position != null) && !"".equalsIgnoreCase(position)) {
                    position = CategoryTree.cutPrefix(position);
                    title = readTitle(resourceName);
                    category = new CmsCategory(title, position, resourceName);

                    parentFolder = CmsResource.getParentFolder(resourceName);
                    if (parentFolder.startsWith(m_rootFolder)) {

                        // add only folders starting with the root folder
                        // because we want a tree beginning from the root folder                    
                        addCategory(parentFolder, category);
                    }
                }
            } catch (CmsException e) {
                if (LOG.isWarnEnabled()) {
                    LOG.warn("Error reading properties of resource " + resource.getRootPath(), e);
                } else if (LOG.isErrorEnabled()) {
                    LOG.error("Error reading properties of resource " + resource.getRootPath());
                }
            }
        }
    }

    /**
     * Adds all currently opened categories and their sub-categories/folders to the tree.<p>
     */
    protected void addOpenedCategories() {

        String folder = null;
        String subFolder = null;
        String parentFolder = null;
        String position = null;
        String title = null;
        CmsCategory category = null;
        CmsCategory subCategory = null;
        List<String> subCategories = null;
        String openedCategories = null;

        // check if the user has a list of opened categories saved in his info hash
        openedCategories = getTreeInfo(m_cms, C_USER_INFO_OPENED_CATEGORIES, m_request);
        if ((openedCategories != null) && !"".equalsIgnoreCase(openedCategories)) {
            m_openedCategoryList = commaStringToList(openedCategories, C_LIST_SEPARATOR);
        } else {
            m_openedCategoryList = new ArrayList<String>();
        }

        for (int i = 0, n = m_openedCategoryList.size(); i < n; i++) {

            try {

                folder = m_openedCategoryList.get(i);
                parentFolder = CmsResource.getParentFolder(folder);
                position = m_cms.readPropertyObject(folder, C_PROP_CATEGORY, false).getValue();
                if ((position != null) && !"".equalsIgnoreCase(position)) {
                    position = CategoryTree.cutPrefix(position);
                    title = readTitle(folder);
                    category = new CmsCategory(title, position, folder);

                    if (folder.startsWith(m_rootFolder) && !m_rootFolder.equalsIgnoreCase(folder)) {

                        // add the opened category itself to the tree
                        addCategory(parentFolder, category);

                        // add all sub-categories/folders to the tree
                        subCategories = getSubCategories(folder);
                        for (int j = 0, m = subCategories.size(); j < m; j++) {

                            subFolder = subCategories.get(j);
                            position = m_cms.readPropertyObject(subFolder, C_PROP_CATEGORY, false).getValue();
                            if ((position != null) && !"".equalsIgnoreCase(position)) {
                                position = CategoryTree.cutPrefix(position);
                                title = readTitle(subFolder);
                                subCategory = new CmsCategory(title, position, subFolder);
                                addCategory(folder, subCategory);
                            }
                        }
                    }
                }
            } catch (CmsException e) {
                if (LOG.isErrorEnabled()) {
                    LOG.error("Error reading properties of resource " + folder, e);
                }
            }
        }
    }

    /**
     * Adds all selected categories/folders and their parent categories/folders to the tree.<p>
     */
    protected void addSelectedCategories() {

        String selectedCategories = null;
        String folder = null;
        List<String> subFolders = null;
        String subFolder = null;
        String title = null;
        String position = null;
        CmsCategory category = null;

        // check if the user has a list of selected categories saved in his info hash
        selectedCategories = getTreeInfo(m_cms, C_USER_INFO_SELECTED_CATEGORIES, m_request);
        if ((selectedCategories != null) && !"".equalsIgnoreCase(selectedCategories)) {
            m_selectedCategoryList = commaStringToList(selectedCategories, C_LIST_SEPARATOR);
        } else {
            m_selectedCategoryList = new ArrayList<String>();
        }

        // add all parent categories/folders of the selected categories to the tree
        for (int i = 0, n = m_selectedCategoryList.size(); i < n; i++) {

            folder = m_selectedCategoryList.get(i);
            while (!m_rootFolder.equalsIgnoreCase(folder)) {

                folder = CmsResource.getParentFolder(folder);
                if (m_rootFolder.equalsIgnoreCase(folder)) {

                    // continue if the parent folder is the root folder
                    // because we want a tree beginning from the root folder
                    continue;
                }

                try {

                    // add all sub-folders of the current folder to the tree map
                    subFolders = getSubCategories(folder);
                    for (int j = 0, m = subFolders.size(); j < m; j++) {

                        subFolder = subFolders.get(j);
                        position = m_cms.readPropertyObject(subFolder, C_PROP_CATEGORY, false).getValue();
                        if ((position != null) && !"".equalsIgnoreCase(position)) {
                            position = CategoryTree.cutPrefix(position);
                            title = readTitle(subFolder);
                            category = new CmsCategory(title, position, subFolder);

                            addCategory(folder, category);
                        }

                    }
                } catch (CmsException e) {

                    if (LOG.isErrorEnabled()) {
                        LOG.error("Error reading sub-folders of " + folder, e);
                    }
                }
            }
        }
    }

    /**
     * Removes all categories from the tree.<p>
     */
    protected synchronized void clear() {

        List<CmsCategory> children = null;
        String parent = null;
        List<String> list = null;

        if (m_treeMap == null) {
            return;
        }

        // iterate over all adjacency lists to clear them
        list = new ArrayList<String>(m_treeMap.keySet());
        for (int i = 0, n = list.size(); i < n; i++) {
            parent = list.get(i);

            // clear the adjacency list of the current parent
            children = m_treeMap.get(parent);
            children.clear();

            // remove the parent from the tree
            m_treeMap.remove(parent);
        }

        // clear the tree map again (robustness)
        m_treeMap.clear();
    }

    /**
     * Closes a category in the tree.<p>
     * 
     * @param closedCategoryFolder the category to be closed
     */
    protected void closeCategory(String closedCategoryFolder) {

        String category = null;
        List<String> tmpList = null;

        // update the list of opened categories
        tmpList = new ArrayList<String>();
        for (int i = 0, n = m_openedCategoryList.size(); i < n; i++) {

            category = m_openedCategoryList.get(i);
            if (!category.startsWith(closedCategoryFolder)) {
                tmpList.add(category);
            }
        }
        m_openedCategoryList = tmpList;

        // update the list of selected categories
        tmpList = new ArrayList<String>();
        for (int i = 0, n = m_selectedCategoryList.size(); i < n; i++) {

            category = m_selectedCategoryList.get(i);
            if (closedCategoryFolder.equalsIgnoreCase(category) || !category.startsWith(closedCategoryFolder)) {
                tmpList.add(category);
            }
        }
        m_selectedCategoryList = tmpList;
    }

    /**
     * @see java.lang.Object#finalize()
     */
    @Override
    protected void finalize() throws Throwable {

        try {

            clear();
            m_treeMap = null;
        } catch (Throwable t) {
            // ignore
        }

        super.finalize();
    }

    /**
     * Returns all sub-categories/folders of a specified category.<p>
     * 
     * LGT case: the first 2 folder levels are categories. Thus, for the first 2 folder 
     * levels only folders having the "category" property set should be visible in the
     * tree. Down from the 2nd folder level, all folders should be visible in the tree.<p>
     * 
     * Overwrite this method if you want to make folders visible in the tree from
     * a folder level different than the LGT case here.<p>
     * 
     * @param parentFolder the parent category folder
     * @return a List of all sub-categories/folders
     */
    protected List<String> getSubCategories(String parentFolder) {

        String title = null;
        String position = null;
        List<CmsResource> list = null;
        CmsCategory parentCategory = null;
        String subFolder = null;
        List<String> result = new ArrayList<String>();

        try {

            // turn the parent folder into a category 
            position = m_cms.readPropertyObject(parentFolder, C_PROP_CATEGORY, false).getValue();
            position = CategoryTree.cutPrefix(position);
            title = m_cms.readPropertyObject(parentFolder, CmsPropertyDefinition.PROPERTY_TITLE, false).getValue();
            parentCategory = new CmsCategory(title, position, parentFolder);

            list = m_cms.getSubFolders(parentFolder);

            if (parentCategory.getDepth() == 0) {

                // add only folders having the "category" property set if the
                // parent folder is a main category in the tree

                for (int i = 0, n = list.size(); i < n; i++) {

                    subFolder = m_cms.getRequestContext().removeSiteRoot(((CmsFolder) list.get(i)).getRootPath());
                    position = m_cms.readPropertyObject(subFolder, C_PROP_CATEGORY, false).getValue();

                    if ((position != null) && !"".equalsIgnoreCase(position)) {
                        position = CategoryTree.cutPrefix(position);
                        result.add(subFolder);
                    }
                }
            } else {

                for (int i = 0, n = list.size(); i < n; i++) {

                    subFolder = m_cms.getRequestContext().removeSiteRoot(((CmsFolder) list.get(i)).getRootPath());
                    result.add(subFolder);
                }
            }
        } catch (CmsException e) {

            if (LOG.isErrorEnabled()) {
                LOG.error("Error reading properties", e);
            }
        }

        return result;
    }

    /**
     * Opens a category in the tree.<p>
     * 
     * @param openedCategoryFolder the category to be opened
     */
    protected void openCategory(String openedCategoryFolder) {

        List<String> subCategories = null;

        // the user opened a sub-tree
        if (!m_openedCategoryList.contains(openedCategoryFolder)) {
            m_openedCategoryList.add(openedCategoryFolder);

            if (m_selectedCategoryList.contains(openedCategoryFolder)) {

                // add all sub-categories of the opened category
                // if the opened category is currently selected
                subCategories = getSubCategories(openedCategoryFolder);
                m_selectedCategoryList.addAll(subCategories);
            }
        }
    }

    /**
     * Reads the value of the title property of the specified resource.<p>
     * 
     * First reads the localized title, if this is not found, the common title.<p>
     * 
     * @param resourceName the resource name to look up the property
     * @return the value of the title property of the specified resource
     */
    protected String readTitle(String resourceName) {

        String title = "";
        try {
            try {
                // first read the title property for the current locale
                title = m_cms.readPropertyObject(resourceName, m_propertyTitle, false).getValue();
            } catch (CmsException exc) {
                // ignore, property might not exist
            }
            if (CmsStringUtil.isEmptyOrWhitespaceOnly(title)) {
                // localized title not found, read the usual title
                title = m_cms.readPropertyObject(resourceName, CmsPropertyDefinition.PROPERTY_TITLE, false)
                        .getValue();
            }
        } catch (CmsException e) {
            // ignore
        }
        if (CmsStringUtil.isEmptyOrWhitespaceOnly(title)) {
            // no title property found at all, use the resource name as title
            title = CmsResource.getName(resourceName);
        }
        return title;
    }

    /**
     * Saves the tree info (opened/selected categories) in the current user's info hash.<p>
     */
    protected void saveTreeInfo() {

        String prefix = m_cms.getRequestContext().addSiteRoot(m_cms.getRequestContext().getUri()) + "_";

        // save the categories that the user selected in his info hash
        saveUserInfo(prefix + C_USER_INFO_SELECTED_CATEGORIES,
                listToCommaString(m_selectedCategoryList, C_LIST_SEPARATOR));

        // save the categories that the user selected in his info hash
        saveUserInfo(prefix + C_USER_INFO_OPENED_CATEGORIES,
                listToCommaString(m_openedCategoryList, C_LIST_SEPARATOR));
    }

    /**
     * Saves a key/value pair in the additional info hash of the current user.<p>
     * 
     * @param key the key
     * @param value the value
     */
    protected void saveUserInfo(String key, String value) {

        CmsUser user = null;

        if (LOG.isDebugEnabled()) {
            LOG.debug("saving additional info for user " + m_cms.getRequestContext().getCurrentUser().getName()
                    + " with key: " + key + ", value: " + value);
        }

        if (value == null) {

            return;
        }

        try {
            // store the current information about the tree stauts in the session
            HttpSession session = m_request.getSession(true);
            session.setAttribute(key, value);

            // if the user is not the guest user, store the settings in the user, too
            user = m_cms.getRequestContext().getCurrentUser();
            if (!user.isGuestUser()) {
                user.setAdditionalInfo(key, value);
                m_cms.writeUser(user);
            }
        } catch (CmsException e) {

            if (LOG.isErrorEnabled()) {
                LOG.error("Error saving additional info for user " + user.getName() + " with key: " + key
                        + ", value: " + value, e);
            }
        }
    }

    /**
     * Recursively builds a DFS list of a sub-tree beginning 
     * from the specified parent category.<p>
     * 
     * @param parent the category from where the sub-tree is built
     * @return a List of categories in the sub tree in DFS order
     */
    protected List<CmsCategory> toList(CmsCategory parent) {

        List<CmsCategory> result = null;
        List<CmsCategory> children = null;
        CmsCategory category = null;

        if (parent == null) {

            return null;
        }

        result = new ArrayList<CmsCategory>();
        result.add(parent);

        // get the adjacency list with the child objects of the current parent ID
        children = m_treeMap.get(parent.getCmsResource());
        if (children == null) {

            return result;
        }

        // add the sub-tree of the current parent resource to the result list
        Collections.sort(children);
        for (int i = 0, n = children.size(); i < n; i++) {

            category = children.get(i);
            result.addAll(toList(category));
        }

        return result;
    }

}