org.alfresco.web.ui.common.Utils.java Source code

Java tutorial

Introduction

Here is the source code for org.alfresco.web.ui.common.Utils.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.ui.common;

import java.io.IOException;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.StringTokenizer;

import javax.faces.application.FacesMessage;
import javax.faces.component.NamingContainer;
import javax.faces.component.UIComponent;
import javax.faces.component.UIForm;
import javax.faces.context.FacesContext;
import javax.faces.context.ResponseWriter;
import javax.faces.el.EvaluationException;
import javax.faces.el.MethodBinding;
import javax.faces.event.AbortProcessingException;
import javax.faces.event.ActionEvent;

import org.alfresco.error.AlfrescoRuntimeException;
import org.alfresco.filesys.repo.ContentContext;
import org.alfresco.jlan.server.config.ServerConfigurationAccessor;
import org.alfresco.jlan.server.core.SharedDevice;
import org.alfresco.jlan.server.core.SharedDeviceList;
import org.alfresco.jlan.server.filesys.DiskSharedDevice;
import org.alfresco.jlan.server.filesys.FilesystemsConfigSection;
import org.alfresco.model.ApplicationModel;
import org.alfresco.model.ContentModel;
import org.alfresco.repo.model.filefolder.FileFolderServiceImpl.InvalidTypeException;
import org.alfresco.repo.security.permissions.AccessDeniedException;
import org.alfresco.repo.webdav.WebDAVHelper;
import org.alfresco.repo.webdav.WebDAVServlet;
import org.alfresco.service.ServiceRegistry;
import org.alfresco.service.cmr.dictionary.DictionaryService;
import org.alfresco.service.cmr.model.FileFolderService;
import org.alfresco.service.cmr.model.FileInfo;
import org.alfresco.service.cmr.model.FileNotFoundException;
import org.alfresco.service.cmr.repository.InvalidNodeRefException;
import org.alfresco.service.cmr.repository.NoTransformerException;
import org.alfresco.service.cmr.repository.NodeRef;
import org.alfresco.service.cmr.repository.NodeService;
import org.alfresco.service.cmr.security.PersonService;
import org.alfresco.service.namespace.NamespaceService;
import org.alfresco.service.namespace.QName;
import org.alfresco.util.EqualsHelper;
import org.alfresco.util.Pair;
import org.alfresco.util.SearchLanguageConversion;
import org.alfresco.web.app.Application;
import org.alfresco.web.app.servlet.DownloadContentServlet;
import org.alfresco.web.app.servlet.ExternalAccessServlet;
import org.alfresco.web.bean.NavigationBean;
import org.alfresco.web.bean.repository.Node;
import org.alfresco.web.bean.repository.Repository;
import org.alfresco.web.data.IDataContainer;
import org.alfresco.web.ui.common.component.UIStatusMessage;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.myfaces.shared_impl.renderkit.html.HtmlFormRendererBase;
import org.springframework.extensions.config.ConfigElement;
import org.springframework.extensions.webscripts.ui.common.StringUtils;
import org.springframework.web.jsf.FacesContextUtils;

/**
 * Class containing misc helper methods used by the JSF components.
 * 
 * @author Kevin Roast
 */
public final class Utils extends StringUtils {
    public static final String USER_AGENT_FIREFOX = "Firefox";
    public static final String USER_AGENT_MSIE = "MSIE";
    private static final String MSG_TIME_PATTERN = "time_pattern";
    private static final String MSG_DATE_PATTERN = "date_pattern";
    private static final String MSG_DATE_TIME_PATTERN = "date_time_pattern";

    private static final Log logger = LogFactory.getLog(Utils.class);

    /** RegEx to split a String on the first space. */
    public static final String ON_FIRST_SPACE = " +";

    static {
        tagWhiteList.add("STRIKE");
    }

    /**
     * Private constructor
     */
    private Utils() {
    }

    /**
     * Helper to output an attribute to the output stream
     * 
     * @param out        ResponseWriter
     * @param attr       attribute value object (cannot be null)
     * @param mapping    mapping to output as e.g. style="..."
     * 
     * @throws IOException
     */
    public static void outputAttribute(ResponseWriter out, Object attr, String mapping) throws IOException {
        if (attr != null) {
            out.write(' ');
            out.write(mapping);
            out.write("=\"");
            out.write(attr.toString());
            out.write('"');
        }
    }

    /**
     * Get the hidden field name for any action component.
     * 
     * All components that wish to simply encode a form value with their client ID can reuse the same
     * hidden field within the parent form. NOTE: components which use this method must only encode
     * their client ID as the value and nothing else!
     * 
     * Build a shared field name from the parent form name and the string "act".
     * 
     * @return hidden field name shared by all action components within the Form.
     */
    public static String getActionHiddenFieldName(FacesContext context, UIComponent component) {
        return Utils.getParentForm(context, component).getClientId(context) + NamingContainer.SEPARATOR_CHAR
                + "act";
    }

    /**
     * Helper to recursively render a component and it's child components
     * 
     * @param context    FacesContext
     * @param component  UIComponent
     * 
     * @throws IOException
     */
    public static void encodeRecursive(FacesContext context, UIComponent component) throws IOException {
        if (component.isRendered() == true) {
            component.encodeBegin(context);

            // follow the spec for components that render their children
            if (component.getRendersChildren() == true) {
                component.encodeChildren(context);
            } else {
                if (component.getChildCount() != 0) {
                    for (Iterator i = component.getChildren().iterator(); i.hasNext(); /**/) {
                        encodeRecursive(context, (UIComponent) i.next());
                    }
                }
            }

            component.encodeEnd(context);
        }
    }

    /**
     * Generate the JavaScript to submit set the specified hidden Form field to the
     * supplied value and submit the parent Form.
     * 
     * NOTE: the supplied hidden field name is added to the Form Renderer map for output.
     * 
     * @param context       FacesContext
     * @param component     UIComponent to generate JavaScript for
     * @param fieldId       Hidden field id to set value for
     * @param fieldValue    Hidden field value to set hidden field too on submit
     * 
     * @return JavaScript event code
     */
    public static String generateFormSubmit(FacesContext context, UIComponent component, String fieldId,
            String fieldValue) {
        return generateFormSubmit(context, component, fieldId, fieldValue, false, null);
    }

    /**
     * Generate the JavaScript to submit set the specified hidden Form field to the
     * supplied value and submit the parent Form.
     * 
     * NOTE: the supplied hidden field name is added to the Form Renderer map for output.
     * 
     * @param context       FacesContext
     * @param component     UIComponent to generate JavaScript for
     * @param fieldId       Hidden field id to set value for
     * @param fieldValue    Hidden field value to set hidden field too on submit
     * @param params        Optional map of param name/values to output
     * 
     * @return JavaScript event code
     */
    public static String generateFormSubmit(FacesContext context, UIComponent component, String fieldId,
            String fieldValue, Map<String, String> params) {
        return generateFormSubmit(context, component, fieldId, fieldValue, false, params);
    }

    /**
     * Generate the JavaScript to submit set the specified hidden Form field to the
     * supplied value and submit the parent Form.
     * 
     * NOTE: the supplied hidden field name is added to the Form Renderer map for output.
     * 
     * @param context       FacesContext
     * @param component     UIComponent to generate JavaScript for
     * @param fieldId       Hidden field id to set value for
     * @param fieldValue    Hidden field value to set hidden field too on submit
     * @param valueIsParam  Determines whether the fieldValue parameter should be treated
     *                      as a parameter in the generated JavaScript, false will treat
     *                      the value i.e. surround it with single quotes
     * @param params        Optional map of param name/values to output
     * 
     * @return JavaScript event code
     */
    public static String generateFormSubmit(FacesContext context, UIComponent component, String fieldId,
            String fieldValue, boolean valueIsParam, Map<String, String> params) {
        UIForm form = Utils.getParentForm(context, component);
        if (form == null) {
            throw new IllegalStateException("Must nest components inside UIForm to generate form submit!");
        }

        String formClientId = form.getClientId(context);

        StringBuilder buf = new StringBuilder(200);
        buf.append("document.forms['");
        buf.append(formClientId);
        buf.append("']['");
        buf.append(fieldId);
        buf.append("'].value=");
        if (valueIsParam == false) {
            buf.append("'");
        }
        buf.append(Utils.encode(fieldValue));
        if (valueIsParam == false) {
            buf.append("'");
        }
        buf.append(";");

        if (params != null) {
            for (String name : params.keySet()) {
                buf.append("document.forms['");
                buf.append(formClientId);
                buf.append("']['");
                buf.append(name);
                buf.append("'].value='");
                String val = params.get(name);
                if (val != null) {
                    val = Utils.encode(val);
                }
                val = replace(val, "\\", "\\\\"); // encode escape character
                val = replace(val, "'", "\\'"); // encode single quote as we wrap string with that
                buf.append(val);
                buf.append("';");

                // weak, but this seems to be the way Sun RI do it...
                //FormRenderer.addNeededHiddenField(context, name);
                HtmlFormRendererBase.addHiddenCommandParameter(context, form, name);
            }
        }

        buf.append("document.forms['");
        buf.append(formClientId);
        buf.append("'].submit();");

        if (valueIsParam == false) {
            buf.append("return false;");
        }

        // weak, but this seems to be the way Sun RI do it...
        //FormRenderer.addNeededHiddenField(context, fieldId);
        HtmlFormRendererBase.addHiddenCommandParameter(context, form, fieldId);

        return buf.toString();
    }

    /**
     * Generate the JavaScript to submit the parent Form.
     * 
     * @param context       FacesContext
     * @param component     UIComponent to generate JavaScript for
     * 
     * @return JavaScript event code
     */
    public static String generateFormSubmit(FacesContext context, UIComponent component) {
        UIForm form = Utils.getParentForm(context, component);
        if (form == null) {
            throw new IllegalStateException("Must nest components inside UIForm to generate form submit!");
        }

        String formClientId = form.getClientId(context);

        StringBuilder buf = new StringBuilder(48);

        buf.append("document.forms['");
        buf.append(formClientId);
        buf.append("'].submit()");

        buf.append(";return false;");

        return buf.toString();
    }

    /**
     * Enum representing the client URL type to generate
     */
    public enum URLMode {
        HTTP_DOWNLOAD, HTTP_INLINE, WEBDAV, CIFS, SHOW_DETAILS, BROWSE, FTP
    }

    /**
     * Generates a URL for the given usage for the given node. If the URL cannot be generated
     * then null is returned.
     * 
     * The supported values for the usage parameter are of URLMode enum type
     * @see URLMode
     * 
     * @param context    Faces context
     * @param node       The node to generate the URL for
     * @param name       Name to use for the download file part of the link if any
     * @param usage      What the URL is going to be used for
     * 
     * @return The URL for the requested usage without the context path
     */
    public static String generateURL(FacesContext context, Node node, String name, URLMode usage) {
        String url = null;

        switch (usage) {
        case WEBDAV: {
            // calculate a WebDAV URL for the given node
            FileFolderService fileFolderService = Repository.getServiceRegistry(context).getFileFolderService();
            try {
                NodeRef rootNode = WebDAVServlet.getWebdavRootNode();
                if (rootNode != null) {
                    // build up the webdav url
                    StringBuilder path = new StringBuilder("/").append(WebDAVServlet.WEBDAV_PREFIX);

                    if (!rootNode.equals(node.getNodeRef())) {

                        List<FileInfo> paths = fileFolderService.getNamePath(rootNode, node.getNodeRef());

                        // build up the path skipping the first path as it is the root folder
                        for (int x = 0; x < paths.size(); x++) {
                            path.append("/")
                                    .append(WebDAVHelper.encodeURL(paths.get(x).getName(), getUserAgent(context)));
                        }
                    }
                    url = path.toString();
                }
            } catch (AccessDeniedException e) {
                // cannot build path if user don't have access all the way up
            } catch (FileNotFoundException nodeErr) {
                // cannot build path if file no longer exists
            } catch (InvalidTypeException e) {
                // primary path does not translate to a file/folder path.
            }
            break;
        }

        case CIFS: {
            // calculate a CIFS path for the given node

            // get hold of the node service, cifsServer and navigation bean
            ServiceRegistry serviceRegistry = Repository.getServiceRegistry(context);
            NodeService nodeService = serviceRegistry.getNodeService();
            FileFolderService fileFolderService = serviceRegistry.getFileFolderService();
            NavigationBean navBean = (NavigationBean) context.getExternalContext().getSessionMap()
                    .get(NavigationBean.BEAN_NAME);
            ServerConfigurationAccessor serverConfiguration = (ServerConfigurationAccessor) FacesContextUtils
                    .getRequiredWebApplicationContext(context).getBean("fileServerConfiguration");

            if (nodeService != null && fileFolderService != null && navBean != null
                    && serverConfiguration != null) {
                // Resolve CIFS network folder location for this node
                FilesystemsConfigSection filesysConfig = (FilesystemsConfigSection) serverConfiguration
                        .getConfigSection(FilesystemsConfigSection.SectionName);
                DiskSharedDevice diskShare = null;

                SharedDeviceList shares = filesysConfig.getShares();
                Enumeration<SharedDevice> shareEnum = shares.enumerateShares();

                while (shareEnum.hasMoreElements() && diskShare == null) {
                    SharedDevice curShare = shareEnum.nextElement();
                    if (curShare.getContext() instanceof ContentContext) {
                        // ALF-6863: Check if the node has a path beneath contentContext.getRootNode() 
                        ContentContext contentContext = (ContentContext) curShare.getContext();
                        NodeRef rootNode = contentContext.getRootNode();
                        if (node.getNodeRef().equals(rootNode)) {
                            diskShare = (DiskSharedDevice) curShare;
                            break;
                        }
                        try {
                            fileFolderService.getNamePath(rootNode, node.getNodeRef());
                            if (logger.isDebugEnabled()) {
                                logger.debug(" Node " + node.getName() + " HAS been found on "
                                        + contentContext.getDeviceName());
                            }
                            diskShare = (DiskSharedDevice) curShare;
                            break;
                        } catch (FileNotFoundException ex) {
                            if (logger.isDebugEnabled()) {
                                logger.debug(" Node " + node.getName() + " HAS NOT been found on "
                                        + contentContext.getDeviceName());
                            }
                            // There is no such node on this SharedDevice, continue 
                            continue;
                        } catch (InvalidTypeException e) {
                            if (logger.isDebugEnabled()) {
                                logger.debug(" Node " + node.getName() + " HAS NOT been found on "
                                        + contentContext.getDeviceName());
                            }
                            // primary path does not translate to a file/folder path.
                        }
                    }
                }

                if (diskShare != null) {
                    ContentContext contentCtx = (ContentContext) diskShare.getContext();
                    NodeRef rootNode = contentCtx.getRootNode();
                    try {
                        if (!EqualsHelper.nullSafeEquals(rootNode, node.getNodeRef())) {
                            String userAgent = Utils.getUserAgent(context);
                            final boolean isIE = Utils.USER_AGENT_MSIE.equals(userAgent);

                            if (isIE) {
                                url = Repository.getNamePathEx(context, node.getNodePath(), rootNode, "\\",
                                        "file:///" + navBean.getCIFSServerPath(diskShare));
                            } else {
                                // build up the CIFS url
                                StringBuilder path = new StringBuilder("file:///")
                                        .append(navBean.getCIFSServerPath(diskShare));
                                List<FileInfo> paths = fileFolderService.getNamePath(rootNode, node.getNodeRef());

                                // build up the path skipping the first path as it is the root folder
                                for (int x = 0; x < paths.size(); x++) {
                                    path.append("\\")
                                            .append(WebDAVHelper.encodeURL(paths.get(x).getName(), userAgent));
                                }
                                url = path.toString();
                            }
                        }
                    } catch (AccessDeniedException e) {
                        // cannot build path if user don't have access all the way up
                    } catch (FileNotFoundException nodeErr) {
                        // cannot build path if file no longer exists
                    } catch (InvalidNodeRefException nodeErr) {
                        // cannot build path if node no longer exists
                    }
                }
            }
            break;
        }

        case HTTP_DOWNLOAD: {
            url = DownloadContentServlet.generateDownloadURL(node.getNodeRef(), name);
            break;
        }

        case HTTP_INLINE: {
            url = DownloadContentServlet.generateBrowserURL(node.getNodeRef(), name);
            break;
        }

        case SHOW_DETAILS: {
            DictionaryService dd = Repository.getServiceRegistry(context).getDictionaryService();

            // default to showing details of content
            String outcome = ExternalAccessServlet.OUTCOME_DOCDETAILS;

            // if the node is a type of folder then make the outcome to show space details
            if ((dd.isSubClass(node.getType(), ContentModel.TYPE_FOLDER))
                    || (dd.isSubClass(node.getType(), ApplicationModel.TYPE_FOLDERLINK))) {
                outcome = ExternalAccessServlet.OUTCOME_SPACEDETAILS;
            }

            // build the url
            url = ExternalAccessServlet.generateExternalURL(outcome, Repository.getStoreRef().getProtocol() + "/"
                    + Repository.getStoreRef().getIdentifier() + "/" + node.getId());
            break;
        }

        case BROWSE: {
            url = ExternalAccessServlet.generateExternalURL(ExternalAccessServlet.OUTCOME_BROWSE,
                    Repository.getStoreRef().getProtocol() + "/" + Repository.getStoreRef().getIdentifier() + "/"
                            + node.getId());
        }

        case FTP: {
            // not implemented yet!
            break;
        }
        }

        return url;
    }

    /**
     * Generates a URL for the given usage for the given node.
     * 
     * The supported values for the usage parameter are of URLMode enum type
     * @see URLMode
     * 
     * @param context    Faces context
     * @param node       The node to generate the URL for
     * @param usage      What the URL is going to be used for
     * 
     * @return The URL for the requested usage without the context path
     */
    public static String generateURL(FacesContext context, Node node, URLMode usage) {
        return generateURL(context, node, node.getName(), usage);
    }

    /**
     * Build a context path safe image tag for the supplied image path.
     * Image path should be supplied with a leading slash '/'.
     * 
     * @param context       FacesContext
     * @param image         The local image path from the web folder with leading slash '/'
     * @param width         Width in pixels
     * @param height        Height in pixels
     * @param alt           Optional alt/title text
     * @param onclick       JavaScript onclick event handler code
     * 
     * @return Populated <code>img</code> tag
     */
    public static String buildImageTag(FacesContext context, String image, int width, int height, String alt,
            String onclick) {
        return buildImageTag(context, image, width, height, alt, onclick, null);
    }

    /**
     * Build a context path safe image tag for the supplied image path.
     * Image path should be supplied with a leading slash '/'.
     * 
     * @param context       FacesContext
     * @param image         The local image path from the web folder with leading slash '/'
     * @param width         Width in pixels
     * @param height        Height in pixels
     * @param alt           Optional alt/title text
     * @param onclick       JavaScript onclick event handler code
     * @param verticalAlign Optional HTML alignment value
     * 
     * @return Populated <code>img</code> tag
     */
    public static String buildImageTag(FacesContext context, String image, int width, int height, String alt,
            String onclick, String verticalAlign) {
        return buildImageTag(context, image, width, height, alt, onclick, verticalAlign, null);
    }

    /**
     * Build a context path safe image tag for the supplied image path.
     * Image path should be supplied with a leading slash '/'.
     * 
     * @param context       FacesContext
     * @param image         The local image path from the web folder with leading slash '/'
     * @param width         Width in pixels
     * @param height        Height in pixels
     * @param alt           Optional alt/title text
     * @param onclick       JavaScript onclick event handler code
     * @param verticalAlign Optional HTML alignment value
     * @param style         Optional inline CSS styling
     * 
     * @return Populated <code>img</code> tag
     */
    public static String buildImageTag(FacesContext context, String image, int width, int height, String alt,
            String onclick, String verticalAlign, String style) {
        StringBuilder buf = new StringBuilder(200);

        style = style != null ? "border-width:0px; " + style : "border-width:0px;";
        buf.append("<img src='").append(context.getExternalContext().getRequestContextPath()).append(image)
                .append("' width='").append(width).append("' height='").append(height).append("'");

        if (alt != null) {
            alt = Utils.encode(alt);
            buf.append(" alt=\"").append(alt).append("\" title=\"").append(alt).append("\"");
        } else {
            buf.append(" alt=''");
        }

        if (verticalAlign != null) {
            StringBuilder styleBuf = new StringBuilder(40);
            styleBuf.append(style).append("vertical-align:").append(verticalAlign).append(";");
            style = styleBuf.toString();
        }

        if (onclick != null) {
            buf.append(" onclick=\"").append(onclick).append('"');
            StringBuilder styleBuf = new StringBuilder(style.length() + 16);
            styleBuf.append(style).append("cursor:pointer;");
            style = styleBuf.toString();
        }
        buf.append(" style='").append(style).append("'/>");

        return buf.toString();
    }

    /**
     * Build a context path safe image tag for the supplied image path.
     * Image path should be supplied with a leading slash '/'.
     * 
     * @param context       FacesContext
     * @param image         The local image path from the web folder with leading slash '/'
     * @param width         Width in pixels
     * @param height        Height in pixels
     * @param alt           Optional alt/title text
     * 
     * @return Populated <code>img</code> tag
     */
    public static String buildImageTag(FacesContext context, String image, int width, int height, String alt) {
        return buildImageTag(context, image, width, height, alt, null);
    }

    /**
     * Build a context path safe image tag for the supplied image path.
     * Image path should be supplied with a leading slash '/'.
     * 
     * @param context       FacesContext
     * @param image         The local image path from the web folder with leading slash '/'
     * @param alt           Optional alt/title text
     * 
     * @return Populated <code>img</code> tag
     */
    public static String buildImageTag(FacesContext context, String image, String alt) {
        return buildImageTag(context, image, alt, null);
    }

    /**
     * Build a context path safe image tag for the supplied image path.
     * Image path should be supplied with a leading slash '/'.
     * 
     * @param context       FacesContext
     * @param image         The local image path from the web folder with leading slash '/'
     * @param alt           Optional alt/title text
     * @param verticalAlign         Optional HTML alignment value
     * 
     * @return Populated <code>img</code> tag
     */
    public static String buildImageTag(FacesContext context, String image, String alt, String verticalAlign) {
        StringBuilder buf = new StringBuilder(128);
        buf.append("<img src='").append(context.getExternalContext().getRequestContextPath()).append(image)
                .append("' ");

        String style = "border-width:0px;";
        if (alt != null) {
            alt = Utils.encode(alt);
            buf.append(" alt=\"").append(alt).append("\" title=\"").append(alt).append('"');
        } else {
            buf.append(" alt=''");
        }

        if (verticalAlign != null) {
            StringBuilder styleBuf = new StringBuilder(40);
            styleBuf.append(style).append("vertical-align:").append(verticalAlign).append(";");
            style = styleBuf.toString();
        }

        buf.append(" style='").append(style).append("'/>");

        return buf.toString();
    }

    /**
     * Return the parent UIForm component for the specified UIComponent
     * 
     * @param context       FaceContext
     * @param component     The UIComponent to find parent Form for
     * 
     * @return UIForm parent or null if none found in hiearachy
     */
    public static UIForm getParentForm(FacesContext context, UIComponent component) {
        UIComponent parent = component.getParent();
        while (parent != null) {
            if (parent instanceof UIForm) {
                break;
            }
            parent = parent.getParent();
        }
        return (UIForm) parent;
    }

    /**
     * Return the parent UIComponent implementing the NamingContainer interface for
     * the specified UIComponent.
     * 
     * @param context       FaceContext
     * @param component     The UIComponent to find parent Form for
     * 
     * @return NamingContainer parent or null if none found in hiearachy
     */
    public static UIComponent getParentNamingContainer(FacesContext context, UIComponent component) {
        UIComponent parent = component.getParent();
        while (parent != null) {
            if (parent instanceof NamingContainer) {
                break;
            }
            parent = parent.getParent();
        }
        return (UIComponent) parent;
    }

    /**
     * Return the parent UIComponent implementing the IDataContainer interface for
     * the specified UIComponent.
     * 
     * @param context       FaceContext
     * @param component     The UIComponent to find parent IDataContainer for
     * 
     * @return IDataContainer parent or null if none found in hiearachy
     */
    public static IDataContainer getParentDataContainer(FacesContext context, UIComponent component) {
        UIComponent parent = component.getParent();
        while (parent != null) {
            if (parent instanceof IDataContainer) {
                break;
            }
            parent = parent.getParent();
        }
        return (IDataContainer) parent;
    }

    /**
     * Determines whether the given component is disabled or readonly
     * 
     * @param component The component to test
     * @return true if the component is either disabled or set to readonly
     */
    public static boolean isComponentDisabledOrReadOnly(UIComponent component) {
        boolean disabled = false;
        boolean readOnly = false;

        Object disabledAttr = component.getAttributes().get("disabled");
        if (disabledAttr != null) {
            disabled = disabledAttr.equals(Boolean.TRUE);
        }

        if (disabled == false) {
            Object readOnlyAttr = component.getAttributes().get("readonly");
            if (readOnlyAttr != null) {
                readOnly = readOnlyAttr.equals(Boolean.TRUE);
            }
        }

        return disabled || readOnly;
    }

    /**
     * Invoke the method encapsulated by the supplied MethodBinding
     * 
     * @param context    FacesContext
     * @param method     MethodBinding to invoke
     * @param event      ActionEvent to pass to the method of signature:
     *                   public void myMethodName(ActionEvent event)
     */
    public static void processActionMethod(FacesContext context, MethodBinding method, ActionEvent event) {
        try {
            method.invoke(context, new Object[] { event });
        } catch (EvaluationException e) {
            Throwable cause = e.getCause();
            if (cause instanceof AbortProcessingException) {
                throw (AbortProcessingException) cause;
            } else {
                throw e;
            }
        }
    }

    /**
     * Adds a global error message
     * 
     * @param msg        The error message
     */
    public static void addErrorMessage(String msg) {
        addErrorMessage(msg, null);
    }

    /**
     * Adds a global error message and logs exception details
     * 
     * @param msg        The error message
     * @param err        The exception to log
     */
    public static void addErrorMessage(String msg, Throwable err) {
        FacesContext context = FacesContext.getCurrentInstance();
        FacesMessage facesMsg = new FacesMessage(FacesMessage.SEVERITY_ERROR, msg, msg);
        context.addMessage(null, facesMsg);
        if (err != null) {
            if ((err instanceof InvalidNodeRefException == false && err instanceof AccessDeniedException == false
                    && err instanceof NoTransformerException == false) || logger.isDebugEnabled()) {
                logger.error(msg, err);
            }
        }
    }

    /**
     * Adds a global status message that will be displayed by a Status Message UI component
     * 
     * @param severity   Severity of the message
     * @param msg        Text of the message
     */
    public static void addStatusMessage(FacesMessage.Severity severity, String msg) {
        FacesContext fc = FacesContext.getCurrentInstance();
        String time = getTimeFormat(fc).format(new Date(System.currentTimeMillis()));
        FacesMessage fm = new FacesMessage(severity, time, msg);
        fc.addMessage(UIStatusMessage.STATUS_MESSAGE, fm);
    }

    /**
     * @return the formatter for locale sensitive Time formatting
     */
    public static DateFormat getTimeFormat(FacesContext fc) {
        return getDateFormatFromPattern(fc, Application.getMessage(fc, MSG_TIME_PATTERN));
    }

    /**
     * @return the formatter for locale sensitive Date formatting
     */
    public static DateFormat getDateFormat(FacesContext fc) {
        return getDateFormatFromPattern(fc, Application.getMessage(fc, MSG_DATE_PATTERN));
    }

    /**
     * @return the formatter for locale sensitive Date & Time formatting
     */
    public static DateFormat getDateTimeFormat(FacesContext fc) {
        return getDateFormatFromPattern(fc, Application.getMessage(fc, MSG_DATE_TIME_PATTERN));
    }

    /**
     * @return DataFormat object for the specified pattern
     */
    private static DateFormat getDateFormatFromPattern(FacesContext fc, String pattern) {
        if (pattern == null) {
            throw new IllegalArgumentException("DateTime pattern is mandatory.");
        }
        try {
            return new SimpleDateFormat(pattern, Application.getLanguage(fc));
        } catch (IllegalArgumentException err) {
            throw new AlfrescoRuntimeException("Invalid DateTime pattern", err);
        }
    }

    /**
     * Parse XML format date YYYY-MM-DDTHH:MM:SS
     * @param isoDate String
     * @return Date or null if failed to parse
     */
    public static Date parseXMLDateFormat(String isoDate) {
        Date parsed = null;

        try {
            int offset = 0;

            // extract year
            int year = Integer.parseInt(isoDate.substring(offset, offset += 4));
            if (isoDate.charAt(offset) != '-') {
                throw new IndexOutOfBoundsException("Expected - character but found " + isoDate.charAt(offset));
            }

            // extract month
            int month = Integer.parseInt(isoDate.substring(offset += 1, offset += 2));
            if (isoDate.charAt(offset) != '-') {
                throw new IndexOutOfBoundsException("Expected - character but found " + isoDate.charAt(offset));
            }

            // extract day
            int day = Integer.parseInt(isoDate.substring(offset += 1, offset += 2));
            if (isoDate.charAt(offset) != 'T') {
                throw new IndexOutOfBoundsException("Expected T character but found " + isoDate.charAt(offset));
            }

            // extract hours, minutes, seconds and milliseconds
            int hour = Integer.parseInt(isoDate.substring(offset += 1, offset += 2));
            if (isoDate.charAt(offset) != ':') {
                throw new IndexOutOfBoundsException("Expected : character but found " + isoDate.charAt(offset));
            }
            int minutes = Integer.parseInt(isoDate.substring(offset += 1, offset += 2));
            if (isoDate.charAt(offset) != ':') {
                throw new IndexOutOfBoundsException("Expected : character but found " + isoDate.charAt(offset));
            }
            int seconds = Integer.parseInt(isoDate.substring(offset += 1, offset += 2));

            // initialize Calendar object
            Calendar calendar = Calendar.getInstance();
            calendar.setLenient(false);
            calendar.set(Calendar.YEAR, year);
            calendar.set(Calendar.MONTH, month - 1);
            calendar.set(Calendar.DAY_OF_MONTH, day);
            calendar.set(Calendar.HOUR_OF_DAY, hour);
            calendar.set(Calendar.MINUTE, minutes);
            calendar.set(Calendar.SECOND, seconds);

            // extract the date
            parsed = calendar.getTime();
        } catch (IndexOutOfBoundsException e) {
        } catch (NumberFormatException e) {
        } catch (IllegalArgumentException e) {
        }

        return parsed;
    }

    /**
     * Given a ConfigElement instance retrieve the display label, this could be
     * dervied from a message bundle key or a literal string
     * 
     * @param context FacesContext
     * @param configElement The ConfigElement to test
     * @return The resolved display label
     */
    public static String getDisplayLabel(FacesContext context, ConfigElement configElement) {
        String label = null;

        // look for a localized string
        String msgId = configElement.getAttribute("display-label-id");
        if (msgId != null) {
            label = Application.getMessage(context, msgId);
        }

        // if there wasn't an externalized string look for a literal string
        if (label == null) {
            label = configElement.getAttribute("display-label");
        }

        return label;
    }

    /**
     * Given a ConfigElement instance retrieve the description, this could be
     * dervied from a message bundle key or a literal string
     * 
     * @param context FacesContext
     * @param configElement The ConfigElement to test
     * @return The resolved description
     */
    public static String getDescription(FacesContext context, ConfigElement configElement) {
        String description = null;

        // look for a localized string
        String msgId = configElement.getAttribute("description-id");
        if (msgId != null) {
            description = Application.getMessage(context, msgId);
        }

        // if there wasn't an externalized string look for a literal string
        if (description == null) {
            description = configElement.getAttribute("description");
        }

        return description;
    }

    /**
     * @return the browser User-Agent header value trimmed to either "Firefox" or "MSIE" as appropriate.
     */
    public static String getUserAgent(FacesContext context) {
        Object userAgent = context.getExternalContext().getRequestHeaderMap().get("User-Agent");
        if (userAgent != null) {
            if (userAgent.toString().indexOf("Firefox/") != -1) {
                return USER_AGENT_FIREFOX;
            } else if (userAgent.toString().indexOf("MSIE") != -1) {
                return USER_AGENT_MSIE;
            } else {
                return userAgent.toString();
            }
        }
        return "";
    }

    /**
     * Generate the QName sort for a standard Person lookup. The filter is
     * standardised across multiple JSF components and beans, and used with
     * {@link PersonService#getPeople(List, boolean, List, org.alfresco.query.PagingRequest)}
     */
    public static List<Pair<QName, Boolean>> generatePersonSort() {
        List<Pair<QName, Boolean>> sort = new ArrayList<Pair<QName, Boolean>>();
        sort.add(new Pair<QName, Boolean>(ContentModel.PROP_FIRSTNAME, true));
        sort.add(new Pair<QName, Boolean>(ContentModel.PROP_LASTNAME, true));
        sort.add(new Pair<QName, Boolean>(ContentModel.PROP_USERNAME, true));
        return sort;
    }

    /**
     * Generate the QName filter for a standard Person lookup. The filter is
     * standardised across multiple JSF components and beans, and used with
     * {@link PersonService#getPeople(List, boolean, List, org.alfresco.query.PagingRequest)}
     *  
     * @param term    Search term
     */
    public static List<Pair<QName, String>> generatePersonFilter(String term) {
        List<Pair<QName, String>> filter = new ArrayList<Pair<QName, String>>();
        filter.add(new Pair<QName, String>(ContentModel.PROP_FIRSTNAME, term));
        filter.add(new Pair<QName, String>(ContentModel.PROP_LASTNAME, term));
        filter.add(new Pair<QName, String>(ContentModel.PROP_USERNAME, term));

        // In order to support queries for "Alan Smithee", we'll parse these tokens
        // and add them in to the query.
        Pair<String, String> tokenisedName = tokeniseName(term);
        if (tokenisedName != null) {
            filter.add(new Pair<QName, String>(ContentModel.PROP_FIRSTNAME, tokenisedName.getFirst()));
            filter.add(new Pair<QName, String>(ContentModel.PROP_LASTNAME, tokenisedName.getSecond()));
        }

        return filter;
    }

    /**
     * This method will tokenise a name string in order to extract first name, last name - if possible.
     * The split is simple - it's made on the first whitespace within the trimmed nameFilter String. So
     * <p/>
     * "Luke Skywalker" becomes ["Luke", "Skywalker"].
     * <p/>
     * "Jar Jar Binks" becomes ["Jar", "Jar Binks"].
     * <p/>
     * "C-3PO" becomes null.
     * 
     * @param nameFilter String
     * @return A Pair<firstName, lastName> if the String is valid, else <tt>null</tt>.
     */
    private static Pair<String, String> tokeniseName(String nameFilter) {
        Pair<String, String> result = null;

        if (nameFilter != null) {
            final String trimmedNameFilter = nameFilter.trim();

            // We can only have a first name and a last name if we have at least 3 characters e.g. "A B".
            if (trimmedNameFilter.length() > 3) {
                final String[] tokens = trimmedNameFilter.split(ON_FIRST_SPACE, 2);
                if (tokens.length == 2) {
                    result = new Pair<String, String>(tokens[0], tokens[1]);
                }
            }
        }

        return result;
    }

    /**
     * How many results should a person search return up to? This is needed
     * because the JSF components do paging differently.
     * For now, hard coded at 1000, may be configurable later
     */
    public static int getPersonMaxResults() {
        return 1000;
    }

    /**
     * Generate the Lucene query for a standard Person search. The query used is standardised
     * across multiple JSF components and beans.
     * 
     * @param query   Buffer for the query
     * @param term    Search term
     * @deprecated Use {@link #generatePersonFilter(String)} and {@link PersonService#getPeople(List, boolean, List, org.alfresco.query.PagingRequest)} instead
     */
    public static void generatePersonSearch(StringBuilder query, String term) {
        // define the query to find people by their first or last name
        for (StringTokenizer t = new StringTokenizer(term.trim(), " "); t.hasMoreTokens(); /**/) {
            String token = SearchLanguageConversion.escapeLuceneQuery(t.nextToken());
            query.append("+TYPE:\"").append(ContentModel.TYPE_PERSON).append("\" ");
            query.append("+(@").append(NamespaceService.CONTENT_MODEL_PREFIX).append("\\:firstName:\"*");
            query.append(token);
            query.append("*\" @").append(NamespaceService.CONTENT_MODEL_PREFIX).append("\\:lastName:\"*");
            query.append(token);
            query.append("*\" @").append(NamespaceService.CONTENT_MODEL_PREFIX).append("\\:userName:");
            query.append(token);
            query.append("*) ");
        }
    }
}