org.alfresco.web.bean.wcm.FilePickerBean.java Source code

Java tutorial

Introduction

Here is the source code for org.alfresco.web.bean.wcm.FilePickerBean.java

Source

/*
 * Copyright (C) 2005-2010 Alfresco Software Limited.
 *
 * This file is part of Alfresco
 *
 * Alfresco is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * Alfresco is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with Alfresco. If not, see <http://www.gnu.org/licenses/>. 
 */
package org.alfresco.web.bean.wcm;

import java.io.InputStream;
import java.io.Serializable;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Pattern;

import javax.faces.context.ExternalContext;
import javax.faces.context.FacesContext;
import javax.faces.context.ResponseWriter;
import javax.servlet.http.HttpServletRequest;

import org.alfresco.error.AlfrescoRuntimeException;
import org.alfresco.model.ContentModel;
import org.alfresco.model.WCMModel;
import org.alfresco.repo.avm.AVMNodeConverter;
import org.alfresco.repo.content.MimetypeMap;
import org.alfresco.repo.domain.PropertyValue;
import org.alfresco.repo.search.SearcherException;
import org.alfresco.repo.security.permissions.AccessDeniedException;
import org.alfresco.service.cmr.avm.AVMNodeDescriptor;
import org.alfresco.service.cmr.avm.AVMService;
import org.alfresco.service.cmr.dictionary.DataTypeDefinition;
import org.alfresco.service.cmr.dictionary.DictionaryService;
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.NodeRef;
import org.alfresco.service.cmr.repository.NodeService;
import org.alfresco.service.cmr.repository.StoreRef;
import org.alfresco.service.cmr.search.ResultSet;
import org.alfresco.service.cmr.search.ResultSetRow;
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.util.Pair;
import org.alfresco.web.app.Application;
import org.alfresco.web.app.servlet.ajax.InvokeCommand;
import org.alfresco.web.bean.FileUploadBean;
import org.alfresco.web.bean.repository.Node;
import org.alfresco.web.bean.repository.Repository;
import org.alfresco.util.XMLUtil;
import org.apache.commons.fileupload.FileItem;
import org.apache.commons.fileupload.disk.DiskFileItemFactory;
import org.apache.commons.fileupload.servlet.ServletFileUpload;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.DocumentHelper;
import org.dom4j.Element;
import org.dom4j.XPath;
import org.dom4j.io.SAXReader;
import org.springframework.util.FileCopyUtils;

/**
 * Bean for interacting with the file picker widget using ajax requests.
 */
public class FilePickerBean implements Serializable {
    private static final Log LOGGER = LogFactory.getLog(FilePickerBean.class);

    private static final String CONFIGURED_SEARCH_QUERY_XPATH = "/search/query";
    private static final String CDATA_START_DELIM = "![CDATA[";
    private static final String CDATA_END_DELIM = "]]";

    // parameter names
    private static final String PARAM_FOLDER_RESTRICTION = "folderRestriction";
    private static final String PARAM_CONFIGURED_SEARCH_NAME = "configSearchName";
    private static final String PARAM_SELECTABLE_TYPES = "selectableTypes";
    private static final String PARAM_FILTER_MIME_TYPES = "filterMimetypes";
    private static final String PARAM_CURRENT_PATH = "currentPath";
    private static final String EXTERNAL_PROTOCOL_REGEXP = "^.*:.*";

    private final Set<NodeRef> uploads = new HashSet<NodeRef>();

    // property instance variables
    private AVMBrowseBean avmBrowseBean;
    transient private AVMService avmService;
    transient private NamespaceService namespaceService;
    transient private SearchService searchService;
    transient private NodeService nodeService;
    transient private DictionaryService dictionaryService;
    transient private ContentService contentService;

    // cached reference to the public saved searches folder
    private NodeRef publicSearchesRef = null;

    public FilePickerBean() {
    }

    public void clearUploadedFiles() {
        this.uploads.clear();
    }

    public NodeRef[] getUploadedFiles() {
        return (NodeRef[]) this.uploads.toArray(new NodeRef[this.uploads.size()]);
    }

    /**
     * Set avmBrowseBean property for this bean
     * 
     * @param avmBrowseBean
     *           the AVMBrowseBean object to pass into this property
     */
    public void setAvmBrowseBean(final AVMBrowseBean avmBrowseBean) {
        this.avmBrowseBean = avmBrowseBean;
    }

    /**
     * Get avmBrowseBean property for this bean
     * 
     * @return avmBrowseBean property value for this bean
     */
    public AVMBrowseBean getAvmBrowseBean() {
        return this.avmBrowseBean;
    }

    /**
     * Set avmService property for this bean
     * 
     * @param avmService
     *           the avmService object to pass into this property
     */
    public void setAvmService(final AVMService avmService) {
        this.avmService = avmService;
    }

    /**
     * Get avmService property for this bean
     * 
     * @return avmService property value for this bean
     */
    public AVMService getAvmService() {
        if (this.avmService == null) {
            this.avmService = Repository.getServiceRegistry(FacesContext.getCurrentInstance()).getAVMService();
        }
        return this.avmService;
    }

    /**
     * Set nodeService property value for this bean
     * 
     * @param nodeService
     *           the NodeService object to pass into this property
     */
    public void setNodeService(final NodeService nodeService) {
        this.nodeService = nodeService;
    }

    /**
     * Get nodeService property for this bean
     * 
     * @return nodeService property value for this bean
     */
    public NodeService getNodeService() {
        if (this.nodeService == null) {
            this.nodeService = Repository.getServiceRegistry(FacesContext.getCurrentInstance()).getNodeService();
        }
        return this.nodeService;
    }

    /**
     * Set searchService property for this bean
     * 
     * @param searchService
     *           the SearchService object to pass into this property
     */
    public void setSearchService(final SearchService searchService) {
        this.searchService = searchService;
    }

    /**
     * Get SearchService property for this bean
     * 
     * @return searchService property value for this bean
     */
    public SearchService getSearchService() {
        if (this.searchService == null) {
            this.searchService = Repository.getServiceRegistry(FacesContext.getCurrentInstance())
                    .getSearchService();
        }
        return this.searchService;
    }

    /**
     * Set dictionaryService property value for this bean
     * 
     * @param dictionaryService
     *           the DictionaryService object to pass into this property
     */
    public void setDictionaryService(final DictionaryService dictionaryService) {
        this.dictionaryService = dictionaryService;
    }

    /**
     * Get dictionaryService property for this bean
     * 
     * @return dictionaryService property value for this bean
     */
    public DictionaryService getDictionaryService() {
        if (this.dictionaryService == null) {
            this.dictionaryService = Repository.getServiceRegistry(FacesContext.getCurrentInstance())
                    .getDictionaryService();
        }
        return this.dictionaryService;
    }

    /**
     * Set namespaceService property for this bean
     * 
     * @param namespaceService
     *           the NamespaceService object to pass into this property
     */
    public void setNamespaceService(final NamespaceService namespaceService) {
        this.namespaceService = namespaceService;
    }

    /**
     * Get the namespaceService property value for this bean
     * 
     * @return namespaceService property value for this bean
     */
    public NamespaceService getNamespaceService() {
        if (this.namespaceService == null) {
            this.namespaceService = Repository.getServiceRegistry(FacesContext.getCurrentInstance())
                    .getNamespaceService();
        }
        return this.namespaceService;
    }

    /**
     * Set contentService property
     * 
     * @param contentService
     *           the ContentService object to pass into this property
     */
    public void setContentService(final ContentService contentService) {
        this.contentService = contentService;
    }

    /**
     * Get contentService property value for this bean
     * 
     * @return contentService property value for this bean
     */
    public ContentService getContentService() {
        if (this.contentService == null) {
            this.contentService = Repository.getServiceRegistry(FacesContext.getCurrentInstance())
                    .getContentService();
        }
        return this.contentService;
    }

    /**
     * Provides data for a file picker widget.
     */
    @InvokeCommand.ResponseMimetype(value = MimetypeMap.MIMETYPE_XML)
    public void getFilePickerData() throws Exception {
        final FacesContext facesContext = FacesContext.getCurrentInstance();
        final ExternalContext externalContext = facesContext.getExternalContext();

        // get configured search name parameter value
        String configSearchName = null;
        String[] configSearchNameParam = (String[]) externalContext.getRequestParameterValuesMap()
                .get(PARAM_CONFIGURED_SEARCH_NAME);
        if ((configSearchNameParam != null) && (configSearchNameParam.length != 0)) {
            configSearchName = configSearchNameParam[0];
        }

        // get selectableTypes parameter value
        final QName[] selectableTypes = this.getSelectableTypes(
                (String[]) externalContext.getRequestParameterValuesMap().get(PARAM_SELECTABLE_TYPES));

        // / get filterMimetypes parameter value
        final Pattern[] filterMimetypes = this.getFilterMimetypes(
                (String[]) externalContext.getRequestParameterValuesMap().get(PARAM_FILTER_MIME_TYPES));

        // get 'folderRestriction' parameter value
        // expecting a relative AVM folder path to be held in this parameter
        // (relative to web project webapp root)
        String folderPathRestriction = null;
        String[] folderPathRestrictionParam = (String[]) externalContext.getRequestParameterValuesMap()
                .get(PARAM_FOLDER_RESTRICTION);
        if ((folderPathRestrictionParam != null) && (folderPathRestrictionParam.length != 0)) {
            folderPathRestriction = folderPathRestrictionParam[0];

            // remove leading '/' or '\' (if present) from path restriction
            if ((folderPathRestriction.charAt(0) == '/') || (folderPathRestriction.charAt(0) == '\\')) {
                folderPathRestriction = folderPathRestriction.substring(1);
            }
        }

        // ###
        // the following section sets file picker current path (the folder that
        // file picker
        // is opened at when selected/changed in the form)

        String currentPath = null;

        // get current path request parameter
        String currentPathReqParam = (String) externalContext.getRequestParameterMap().get(PARAM_CURRENT_PATH);

        // create file picker data XML document to return
        // and append file picker data element to it
        final org.w3c.dom.Document filePickerDataDoc = XMLUtil.newDocument();
        final org.w3c.dom.Element filePickerDataElement = filePickerDataDoc.createElement("file-picker-data");
        filePickerDataDoc.appendChild(filePickerDataElement);

        // if current path request parameter null then set current path to the
        // current AVM path
        if (currentPathReqParam == null) {
            currentPath = this.getCurrentAVMPath();
        }

        // Fix for ALF-3764. We cannot select an external protocol link (i.e. http://example.net by filepicker.
        // Such links should be typed manually in the text box.
        if (currentPathReqParam.matches(EXTERNAL_PROTOCOL_REGEXP)) {
            currentPath = this.getCurrentAVMPath();
            filePickerDataElement.setAttribute("error",
                    Application.getMessage(facesContext, "error_external_protocol_support"));
        }
        // else set current path to current path request parameter converted to
        // AVM preview
        // store path
        else {
            final String previewStorePath = AVMUtil.getCorrespondingPathInPreviewStore(this.getCurrentAVMPath());
            currentPath = AVMUtil.buildPath(previewStorePath, currentPathReqParam,
                    AVMUtil.PathRelation.WEBAPP_RELATIVE);
        }

        // if folder path restriction (relative path) is set,
        // then calculate the absolute restriction path from the root
        // of the webapp and set the current path to that
        if ((folderPathRestriction != null) && (folderPathRestriction.length() != 0)) {
            currentPath = AVMUtil.getWebappPath(currentPath) + "/" + folderPathRestriction;
        }

        if (LOGGER.isDebugEnabled()) {
            LOGGER.debug(this + ".getFilePickerData(path = " + currentPath + ", folderRestriction = "
                    + folderPathRestriction + ", selectableTypes = [" + StringUtils.join(selectableTypes, ",")
                    + "], filterMimetypes = [" + StringUtils.join(filterMimetypes, ",") + "])");
        }

        // make sure that there is a node associated with current path
        // if not, set an applicable error message as an attribute on
        // the file picker data element
        final AVMNodeDescriptor currentNode = this.getAvmService().lookup(-1, currentPath);

        // if the current node is null (path held in current path variable is
        // invalid), then add an error attribute to the file picker data
        if (currentNode == null) {
            currentPath = AVMUtil.getWebappRelativePath(currentPath);

            filePickerDataElement.setAttribute("error",
                    MessageFormat.format(Application.getMessage(facesContext, "error_not_found"),
                            "'" + currentPath.substring(currentPath.lastIndexOf("/") + 1, currentPath.length())
                                    + "'",
                            (currentPath.lastIndexOf("/") == 0 ? "/"
                                    : currentPath.substring(0, currentPath.lastIndexOf("/")))));

            // If folder restriction has been set, since the derived
            // current path is invalid just set it to null
            if ((folderPathRestriction != null) && (folderPathRestriction.length() != 0)) {
                currentPath = null;
            } else
            // otherwise folder restriction has not been set, so it should be safe
            // to fall back to setting the current path to the current AVM path
            // (as was the behaviour before the folder restriction feature
            // was added)
            {
                currentPath = this.getCurrentAVMPath();
            }
        }
        // else current node is not null (path held in current path variable is
        // valid)
        else {
            // if node for current path points to a file instead of a directory,
            // then make sure that the current path points to
            // just the directory part of the path
            if (!currentNode.isDirectory()) {
                currentPath = AVMNodeConverter.SplitBase(currentPath)[0];
            }
        }

        // create current-node element representing node for current path
        // and append it to the file picker data element
        org.w3c.dom.Element currentNodeElement = filePickerDataDoc.createElement("current-node");
        if (currentPath == null) {
            currentNodeElement.setAttribute("avmPath", "");
            currentNodeElement.setAttribute("webappRelativePath", "");
        } else {
            currentNodeElement.setAttribute("avmPath", currentPath);
            currentNodeElement.setAttribute("webappRelativePath", AVMUtil.getWebappRelativePath(currentPath));
        }
        currentNodeElement.setAttribute("type", "directory");
        currentNodeElement.setAttribute("image", "/images/icons/space_small.gif");

        filePickerDataElement.appendChild(currentNodeElement);

        // if configured search name supplied (i.e. neither null nor empty),
        // then get configured search node matching given name
        // and add the nodes from the search result to the 
        // file-picker-data element
        if ((configSearchName != null) && (configSearchName.length() != 0)) {
            // get node reference for named configured search
            NodeRef configuredSearchNodeRef = getConfiguredSearches(configSearchName);

            // if configured search node ref is null, then there is no
            // configured search matching the name in the 'config search name'
            // parameter, so add error message as attribute to file-picker-data
            // element
            if (configuredSearchNodeRef == null) {
                filePickerDataElement.setAttribute("error", MessageFormat
                        .format(Application.getMessage(facesContext, "error_search_not_exist"), configSearchName));
            } else
            // else node ref for named configured search is not null, then
            // add content nodes from search results as child elements of 
            // the file picker data element.
            {
                try {
                    addSearchResultNodes(filePickerDataDoc, filePickerDataElement, configuredSearchNodeRef,
                            selectableTypes, facesContext);
                }
                // if searcher exception thrown whilst getting search results,
                // then add error message as attribute to file-picker-data element
                catch (SearcherException e) {
                    filePickerDataElement.setAttribute("error",
                            MessageFormat.format(
                                    Application.getMessage(facesContext, "error_retrieving_search_results"),
                                    configSearchName, e.getMessage()));
                }
            }
        } else {
            // add elements for child nodes of current path to file picker
            // data element if current path is not null
            if (currentPath != null) {
                addPathChildNodesToElement(filePickerDataDoc, filePickerDataElement, currentPath, selectableTypes,
                        filterMimetypes, facesContext);
            }
        }

        final ResponseWriter out = facesContext.getResponseWriter();
        XMLUtil.print(filePickerDataDoc, out);
    }

    @InvokeCommand.ResponseMimetype(value = MimetypeMap.MIMETYPE_HTML)
    public void uploadFile() throws Exception {
        LOGGER.debug(this + ".uploadFile()");
        final FacesContext facesContext = FacesContext.getCurrentInstance();
        final ExternalContext externalContext = facesContext.getExternalContext();
        final HttpServletRequest request = (HttpServletRequest) externalContext.getRequest();

        final ServletFileUpload upload = new ServletFileUpload(new DiskFileItemFactory());
        upload.setHeaderEncoding("UTF-8");
        final List<FileItem> fileItems = upload.parseRequest(request);
        final FileUploadBean bean = new FileUploadBean();
        String uploadId = null;
        String currentPath = null;
        String filename = null;
        String returnPage = null;
        InputStream fileInputStream = null;
        for (FileItem item : fileItems) {
            if (LOGGER.isDebugEnabled()) {
                LOGGER.debug("item = " + item);
            }
            if (item.isFormField() && item.getFieldName().equals("upload-id")) {
                uploadId = item.getString();
                if (LOGGER.isDebugEnabled()) {
                    LOGGER.debug("uploadId is " + uploadId);
                }
            }
            if (item.isFormField() && item.getFieldName().equals("return-page")) {
                returnPage = item.getString();
                if (LOGGER.isDebugEnabled()) {
                    LOGGER.debug("returnPage is " + returnPage);
                }
            } else if (item.isFormField() && item.getFieldName().equals("currentPath")) {
                final String previewStorePath = AVMUtil
                        .getCorrespondingPathInPreviewStore(this.getCurrentAVMPath());
                currentPath = AVMUtil.buildPath(previewStorePath, item.getString(),
                        AVMUtil.PathRelation.WEBAPP_RELATIVE);
                if (LOGGER.isDebugEnabled()) {
                    LOGGER.debug("currentPath is " + currentPath);
                }
            } else {
                filename = FilenameUtils.getName(item.getName());
                fileInputStream = item.getInputStream();

                if (LOGGER.isDebugEnabled()) {
                    LOGGER.debug("uploading file " + filename);
                }
            }
        }

        if (LOGGER.isDebugEnabled()) {
            LOGGER.debug("saving file " + filename + " to " + currentPath);
        }

        try {
            FileCopyUtils.copy(fileInputStream, this.getAvmService().createFile(currentPath, filename));
            final Map<QName, PropertyValue> props = new HashMap<QName, PropertyValue>(1, 1.0f);
            props.put(ContentModel.PROP_TITLE, new PropertyValue(DataTypeDefinition.TEXT, filename));
            // props.put(ContentModel.PROP_DESCRIPTION,
            // new PropertyValue(DataTypeDefinition.TEXT,
            // "Uploaded for form " + this.xformsSession.getForm().getName()));
            this.getAvmService().setNodeProperties(currentPath + "/" + filename, props);
            this.getAvmService().addAspect(currentPath + "/" + filename, ContentModel.ASPECT_TITLED);

            this.uploads.add(AVMNodeConverter.ToNodeRef(-1, currentPath + "/" + filename));
            returnPage = returnPage.replace("${_FILE_TYPE_IMAGE}", org.alfresco.repo.web.scripts.FileTypeImageUtils
                    .getFileTypeImage(facesContext, filename, true));
        } catch (Exception e) {
            LOGGER.debug(e.getMessage(), e);
            returnPage = returnPage.replace("${_UPLOAD_ERROR}", e.getMessage());
        }

        LOGGER.debug("upload complete.  sending response: " + returnPage);
        final org.w3c.dom.Document result = XMLUtil.newDocument();
        final org.w3c.dom.Element htmlEl = result.createElement("html");
        result.appendChild(htmlEl);
        final org.w3c.dom.Element bodyEl = result.createElement("body");
        htmlEl.appendChild(bodyEl);

        final org.w3c.dom.Element scriptEl = result.createElement("script");
        bodyEl.appendChild(scriptEl);
        scriptEl.setAttribute("type", "text/javascript");
        final org.w3c.dom.Node scriptText = result.createTextNode(returnPage);
        scriptEl.appendChild(scriptText);

        final ResponseWriter out = facesContext.getResponseWriter();
        XMLUtil.print(result, out);
    }

    private String getCurrentAVMPath() {
        final AVMNode node = this.getAvmBrowseBean().getAvmActionNode();
        if (node == null) {
            return this.getAvmBrowseBean().getCurrentPath();
        }

        final String result = node.getPath();
        return node.isDirectory() ? result : AVMNodeConverter.SplitBase(result)[0];
    }

    private QName[] getSelectableTypes(final String[] selectableTypes) {
        final QName[] result = (selectableTypes == null
                ? new QName[] { WCMModel.TYPE_AVM_CONTENT, WCMModel.TYPE_AVM_FOLDER }
                : new QName[selectableTypes.length]);

        if (selectableTypes != null) {
            for (int i = 0; i < selectableTypes.length; i++) {
                result[i] = QName.resolveToQName(this.getNamespaceService(), selectableTypes[i]);
            }
        }
        return result;
    }

    private Pattern[] getFilterMimetypes(final String[] filterMimetypes) {
        final Pattern[] result = filterMimetypes == null ? new Pattern[0] : new Pattern[filterMimetypes.length];
        if (filterMimetypes != null) {
            for (int i = 0; i < filterMimetypes.length; i++) {
                result[i] = Pattern
                        .compile(filterMimetypes[i].replaceAll("\\*", "\\.*").replaceAll("\\/", "\\\\/"));
            }
        }
        return result;
    }

    /**
     * Add child nodes of supplied path to given parent element. Directory
     * listing is done on given path. Elements representing the child nodes
     * returned in the directory listing are added to the supplied parent
     * element.
     * 
     * @param doc
     *           XML document to which the parent node belongs
     * @param parent
     *           parent element to add given nodes to as child elements
     * @param path
     *           path from which to extract child nodes
     * @param selectableTypes
     *           array of types which are the only ones that should be selectable
     *           in the file picker
     * @param filterMimetypes
     *           array of MIME type patterns used to filter out child nodes
     *           extracted from the given path which don't match the given MIME
     *           type patterns
     * @param facesContext
     *           faces context used to set image attribute on each child element
     */
    private void addPathChildNodesToElement(org.w3c.dom.Document doc, org.w3c.dom.Element parent, String path,
            QName[] selectableTypes, Pattern[] filterMimetypes, FacesContext facesContext) {
        // append elements for the child AVM nodes of the current path
        // to parent element
        for (final Map.Entry<String, AVMNodeDescriptor> entry : this.getAvmService().getDirectoryListing(-1, path)
                .entrySet()) {
            // if AVM node is a content node and the filter MIME types parameter
            // has been set, then only add child element for AVM node if it matches
            // one of the specified MIME types in the parameter
            if (!entry.getValue().isDirectory() && filterMimetypes.length != 0) {
                final String contentMimetype = this.getAvmService().getContentDataForRead(entry.getValue())
                        .getMimetype();

                boolean matched = false;
                for (final Pattern p : filterMimetypes) {
                    matched = p.matcher(contentMimetype).matches();
                    if (LOGGER.isDebugEnabled()) {
                        LOGGER.debug(p + ".matches(" + contentMimetype + ") = " + matched);
                    }
                    if (matched) {
                        break;
                    }
                }

                // if AVM node MIME type doesn't match any of the types in the
                // filter MIME types parameter, then don't do any further processing
                // on it and jump back to the start of the AVM node traversal loop
                if (!matched) {
                    continue;
                }
            }

            // create child element representing AVM node and add to file picker
            // data element
            addAVMChildNodeToParentElement(doc, parent, entry.getValue(), selectableTypes, facesContext);
        }
    }

    /**
     * Add result content nodes from configured search as 
     * child node elements to the provided parent element
     * 
     * @param doc
     *           XML document to which the supplied parent node belongs
     * @param parent
     *           parent element to which to add search result content nodes 
     *           as child elements
     * @param configuredSearchNodeRef
     *           configured search node reference for which result content nodes 
     *           are added as child elements to the provided parent element
     * @param selectableTypes
     *           node types which must be marked as selectable in the file
     *           picker
     * @param facesContext
     *           faces context used to set image attribute on each child element
     */
    private void addSearchResultNodes(org.w3c.dom.Document doc, org.w3c.dom.Element parent,
            NodeRef configuredSearchNodeRef, QName[] selectableTypes, FacesContext facesContext) {
        // run configured search to get content nodes returned in search result
        List<AVMNodeDescriptor> searchResultNodes = runConfiguredSearch(configuredSearchNodeRef);

        // if there are no search results (i.e. null) throw exception
        if (searchResultNodes == null) {
            throw new SearcherException(
                    "No results returned by search query.\n" + "Search node reference: " + configuredSearchNodeRef);
        }

        for (AVMNodeDescriptor node : searchResultNodes) {
            // create child element representing AVM node and add to file picker
            // data element
            addAVMChildNodeToParentElement(doc, parent, node, selectableTypes, facesContext);
        }
    }

    /**
     * Run a configured search represented by the given node reference, against
     * the web project the XForm is currently within
     * 
     * @param configuredQueryNodRef
     *           NodeRef of the configured query with which to run the search
     * 
     * @return content nodes returned by the search
     */
    private List<AVMNodeDescriptor> runConfiguredSearch(NodeRef configSearchNodeRef) {
        // get the store id used to run the configured search query
        WebProject webProject = this.getAvmBrowseBean().getWebProject();
        String storeID = webProject.getStoreId();

        // extract the content of configured search node to XML document
        ContentReader contentReader = getContentService().getReader(configSearchNodeRef, ContentModel.PROP_CONTENT);
        InputStream queryInpStream = contentReader.getContentInputStream();
        SAXReader reader = new SAXReader();
        Document queryDoc = null;
        try {
            queryDoc = reader.read(queryInpStream);
        } catch (DocumentException de) {
            // ignore exception and return null
            return null;
        }

        // extract search query from configured search XML document
        String query = null;
        XPath queryXPath = DocumentHelper.createXPath(CONFIGURED_SEARCH_QUERY_XPATH);
        List xpathResult = queryXPath.selectNodes(queryDoc);
        if ((xpathResult != null) && (xpathResult.size() != 0)) {
            // get the text from the query element
            Element queryElement = (Element) xpathResult.get(0);
            String queryElemText = queryElement.getText();

            // now extract the actual search query string from the CDATA section
            // within that text

            int cdataStartDelimIndex = queryElemText.indexOf(CDATA_START_DELIM);
            int cdataEndDelimIndex = queryElemText.indexOf(CDATA_END_DELIM);

            // if the CDATA start delimiter is found in the query element text
            // && there is text between the CDATA start and end delimiters then
            // extract
            // the query string from the CDATA section
            if ((cdataStartDelimIndex > -1)
                    && ((cdataStartDelimIndex + CDATA_START_DELIM.length()) < cdataEndDelimIndex)) {
                query = queryElemText.substring(cdataStartDelimIndex + CDATA_START_DELIM.length(),
                        cdataEndDelimIndex);
            } else {
                // otherwise just use the text as is
                query = queryElemText;
            }
        }

        // perform the search against the repository
        // if query was extracted from the configured search successfully
        // (extracted query non-null)
        List<AVMNodeDescriptor> resultNodeDescriptors = null;
        if ((query != null) && (query.length() != 0)) {
            ResultSet results = null;
            try {
                results = this.getSearchService().query(new StoreRef(StoreRef.PROTOCOL_AVM, storeID),
                        SearchService.LANGUAGE_LUCENE, query);

                if (results.length() != 0) {
                    resultNodeDescriptors = new ArrayList<AVMNodeDescriptor>();
                    for (int i = 0; i < results.length(); i++) {
                        ResultSetRow row = results.getRow(i);
                        NodeRef resultNodeRef = row.getNodeRef();
                        Node resultNode = new Node(resultNodeRef);

                        // only add content type nodes to the search result
                        // as we don't want the user to navigate down into folders
                        // in the search results
                        if (getDictionaryService().isSubClass(resultNode.getType(), ContentModel.TYPE_CONTENT)) {
                            Pair<Integer, String> pair = AVMNodeConverter.ToAVMVersionPath(resultNodeRef);
                            Integer version = pair.getFirst();
                            String path = pair.getSecond();
                            resultNodeDescriptors.add(getAvmService().lookup(version, path));
                        }
                    }
                }
            } catch (Throwable err) {
                throw new AlfrescoRuntimeException("Failed to execute search: " + query, err);
            } finally {
                if (results != null) {
                    results.close();
                }
            }
        }

        return resultNodeDescriptors;
    }

    /**
     * Get the cached reference to the public saved searches folder. This method
     * will first get a reference to the public saved searches folder and assign
     * it to the cached reference if is it is null.
     * 
     * @return the cached reference to the public Saved Searches folder
     */
    private NodeRef getPublicSearchesRef() {
        // if the cached reference is null, then get a reference to the
        // public saved searches folder to assign to it
        if (publicSearchesRef == null) {
            // Use the search service get a reference to the
            // public saved searches folder.
            FacesContext fc = FacesContext.getCurrentInstance();
            String xpath = Application.getRootPath(fc) + "/" + Application.getGlossaryFolderName(fc) + "/"
                    + Application.getSavedSearchesFolderName(fc);

            List<NodeRef> results = null;
            try {
                results = getSearchService().selectNodes(getNodeService().getRootNode(Repository.getStoreRef()),
                        xpath, null, getNamespaceService(), false);
            } catch (AccessDeniedException err) {
                // ignore and return null
            }

            if (results != null && results.size() != 0) {
                publicSearchesRef = results.get(0);
            }
        }

        return publicSearchesRef;
    }

    /**
     * Get node for configured search by name.
     * 
     * @param configSearchName
     *           name of configured search for which to get node
     * @return node reference for configured search
     */
    public NodeRef getConfiguredSearches(String configSearchName) {
        NodeRef configSearchNodeRef = null;

        // get the folder reference from the
        // public searches location
        NodeRef publicSearchesFolderRef = getPublicSearchesRef();

        // read the content nodes under the folder
        List<ChildAssociationRef> childRefs = getNodeService().getChildAssocs(publicSearchesFolderRef,
                ContentModel.ASSOC_CONTAINS, RegexQNamePattern.MATCH_ALL);

        // return content node with name matching given configured search name
        if (childRefs.size() != 0) {
            for (ChildAssociationRef ref : childRefs) {
                NodeRef childNodeRef = ref.getChildRef();
                Node childNode = new Node(childNodeRef);
                if (getDictionaryService().isSubClass(childNode.getType(), ContentModel.TYPE_CONTENT)) {
                    String childNodeName = childNode.getName();
                    if (childNodeName.equals(configSearchName)) {
                        configSearchNodeRef = childNodeRef;
                        break;
                    }
                }
            }
        }

        return configSearchNodeRef;
    }

    /**
     * Create child element representing given AVM node and add to given parent
     * element
     * 
     * @param doc
     *           Document to which given parent element belongs
     * @param parent
     *           parent element to add AVM node to as child element
     * @param node
     *           AVM node to add as child node to given parent
     * @param selectableTypes
     *           AVM node types which must be marked as selectable
     * @param facesContent
     *           Faces context used to get file-type icon for given AVM node
     */
    private void addAVMChildNodeToParentElement(org.w3c.dom.Document doc, org.w3c.dom.Element parent,
            AVMNodeDescriptor node, QName[] selectableTypes, FacesContext facesContext) {
        // create child node element to add to file picker data
        org.w3c.dom.Element childNodeElement = doc.createElement("child-node");
        childNodeElement.setAttribute("avmPath", node.getPath());
        childNodeElement.setAttribute("webappRelativePath", AVMUtil.getWebappRelativePath(node.getPath()));
        childNodeElement.setAttribute("type", node.isDirectory() ? "directory" : "file");

        // Set image attribute on each child
        // TODO (Glen): IS this the right image to set?
        // originally from Ariel's code
        childNodeElement.setAttribute("image",
                (node.isDirectory() ? "/images/icons/space_small.gif"
                        : org.alfresco.repo.web.scripts.FileTypeImageUtils.getFileTypeImage(facesContext,
                                node.getName(), true)));

        boolean selectable = false;

        // set 'selectable' attribute on child node to mark whether node should
        // be selectable in file picker or not
        //
        // TODO Ariel: faking this for now since i can't figure out how to
        // efficiently get the type
        // qname from the avmservice
        for (final QName typeQName : selectableTypes) {
            selectable = selectable || (WCMModel.TYPE_AVM_FOLDER.equals(typeQName) && node.isDirectory());
            selectable = selectable || (WCMModel.TYPE_AVM_CONTENT.equals(typeQName) && !node.isDirectory());
        }
        childNodeElement.setAttribute("selectable", Boolean.toString(selectable));

        // append child node element to parent
        parent.appendChild(childNodeElement);
    }
}