org.nuxeo.ecm.platform.ui.web.util.ComponentUtils.java Source code

Java tutorial

Introduction

Here is the source code for org.nuxeo.ecm.platform.ui.web.util.ComponentUtils.java

Source

/*
 * (C) Copyright 2007 Nuxeo SA (http://nuxeo.com/) and others.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 * Contributors:
 *     Nuxeo - initial API and implementation
 *     Sean Radford
 *
 * $Id: ComponentUtils.java 28924 2008-01-10 14:04:05Z sfermigier $
 */

package org.nuxeo.ecm.platform.ui.web.util;

import java.io.File;
import java.io.IOException;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import java.util.Map;

import javax.el.ValueExpression;
import javax.faces.application.FacesMessage;
import javax.faces.component.UIComponent;
import javax.faces.component.UISelectItems;
import javax.faces.component.UISelectMany;
import javax.faces.context.ExternalContext;
import javax.faces.context.FacesContext;
import javax.faces.model.SelectItem;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.nuxeo.common.utils.i18n.I18NUtils;
import org.nuxeo.ecm.core.api.Blob;
import org.nuxeo.ecm.core.api.Blobs;
import org.nuxeo.ecm.core.api.DocumentModel;
import org.nuxeo.ecm.core.io.download.DownloadService;
import org.nuxeo.ecm.platform.ui.web.component.list.UIEditableList;
import org.nuxeo.runtime.api.Framework;

/**
 * Generic component helper methods.
 *
 * @author <a href="mailto:at@nuxeo.com">Anahide Tchertchian</a>
 */
public final class ComponentUtils {

    public static final String WHITE_SPACE_CHARACTER = "&#x0020;";

    private static final Log log = LogFactory.getLog(ComponentUtils.class);

    public static final String FORCE_NO_CACHE_ON_MSIE = "org.nuxeo.download.force.nocache.msie";

    // Utility class.
    private ComponentUtils() {
    }

    /**
     * Calls a component encodeBegin/encodeChildren/encodeEnd methods.
     */
    public static void encodeComponent(FacesContext context, UIComponent component) throws IOException {
        component.encodeBegin(context);
        component.encodeChildren(context);
        component.encodeEnd(context);
    }

    /**
     * Helper method meant to be called in the component constructor.
     * <p>
     * When adding sub components dynamically, the tree fetching could be a problem so all possible sub components must
     * be added.
     * <p>
     * Since 6.0, does not mark component as not rendered anymore, calls
     * {@link #hookSubComponent(FacesContext, UIComponent, UIComponent, String)} directly.
     *
     * @param parent
     * @param child
     * @param facetName facet name to put the child in.
     */
    public static void initiateSubComponent(UIComponent parent, String facetName, UIComponent child) {
        parent.getFacets().put(facetName, child);
        hookSubComponent(null, parent, child, facetName);
    }

    /**
     * Add a sub component to a UI component.
     * <p>
     * Since 6.0, does not the set the component as rendered anymore.
     *
     * @param context
     * @param parent
     * @param child
     * @param defaultChildId
     * @return child comp
     */
    public static UIComponent hookSubComponent(FacesContext context, UIComponent parent, UIComponent child,
            String defaultChildId) {
        // build a valid id using the parent id so that it's found everytime.
        String childId = child.getId();
        if (defaultChildId != null) {
            // override with default
            childId = defaultChildId;
        }
        // make sure it's set
        if (childId == null) {
            childId = context.getViewRoot().createUniqueId();
        }
        // reset client id
        child.setId(childId);
        child.setParent(parent);
        return child;
    }

    /**
     * Copies attributes and value expressions with given name from parent component to child component.
     */
    public static void copyValues(UIComponent parent, UIComponent child, String[] valueNames) {
        Map<String, Object> parentAttributes = parent.getAttributes();
        Map<String, Object> childAttributes = child.getAttributes();
        for (String name : valueNames) {
            // attributes
            if (parentAttributes.containsKey(name)) {
                childAttributes.put(name, parentAttributes.get(name));
            }
            // value expressions
            ValueExpression ve = parent.getValueExpression(name);
            if (ve != null) {
                child.setValueExpression(name, ve);
            }
        }
    }

    public static void copyLinkValues(UIComponent parent, UIComponent child) {
        String[] valueNames = { "accesskey", "charset", "coords", "dir", "disabled", "hreflang", "lang", "onblur",
                "onclick", "ondblclick", "onfocus", "onkeydown", "onkeypress", "onkeyup", "onmousedown",
                "onmousemove", "onmouseout", "onmouseover", "onmouseup", "rel", "rev", "shape", "style",
                "styleClass", "tabindex", "target", "title", "type" };
        copyValues(parent, child, valueNames);
    }

    public static Object getAttributeValue(UIComponent component, String attributeName, Object defaultValue) {
        return getAttributeValue(component, attributeName, Object.class, defaultValue, false);
    }

    /**
     * @since 8.2
     */
    public static <T> T getAttributeValue(UIComponent component, String name, Class<T> klass, T defaultValue,
            boolean required) {
        Object value = component.getAttributes().get(name);
        if (value == null) {
            value = defaultValue;
        }
        if (required && value == null) {
            throw new IllegalArgumentException(
                    "Component attribute with name '" + name + "' cannot be null: " + value);
        }
        if (value == null || value.getClass().isAssignableFrom(klass)) {
            return (T) value;
        }
        throw new IllegalArgumentException(
                "Component attribute with name '" + name + "' is not a " + klass + ": " + value);
    }

    public static Object getAttributeOrExpressionValue(FacesContext context, UIComponent component,
            String attributeName, Object defaultValue) {
        Object value = component.getAttributes().get(attributeName);
        if (value == null) {
            ValueExpression schemaExpr = component.getValueExpression(attributeName);
            value = schemaExpr.getValue(context.getELContext());
        }
        if (value == null) {
            value = defaultValue;
        }
        return value;
    }

    /**
     * Downloads a blob and sends it to the requesting user, in the JSF current context.
     *
     * @param doc the document, if available
     * @param xpath the blob's xpath or blobholder index, if available
     * @param blob the blob, if already fetched
     * @param filename the filename to use
     * @param reason the download reason
     * @since 7.3
     */
    public static void download(DocumentModel doc, String xpath, Blob blob, String filename, String reason) {
        download(doc, xpath, blob, filename, reason, null);
    }

    /**
     * Downloads a blob and sends it to the requesting user, in the JSF current context.
     *
     * @param doc the document, if available
     * @param xpath the blob's xpath or blobholder index, if available
     * @param blob the blob, if already fetched
     * @param filename the filename to use
     * @param reason the download reason
     * @param extendedInfos an optional map of extended informations to log
     * @since 7.3
     */
    public static void download(DocumentModel doc, String xpath, Blob blob, String filename, String reason,
            Map<String, Serializable> extendedInfos) {
        FacesContext facesContext = FacesContext.getCurrentInstance();
        if (facesContext.getResponseComplete()) {
            // nothing can be written, an error was probably already sent. don't bother
            log.debug("Cannot send " + filename + ", response already complete");
            return;
        }
        if (facesContext.getPartialViewContext().isAjaxRequest()) {
            // do not perform download in an ajax request
            return;
        }
        ExternalContext externalContext = facesContext.getExternalContext();
        HttpServletRequest request = (HttpServletRequest) externalContext.getRequest();
        HttpServletResponse response = (HttpServletResponse) externalContext.getResponse();
        try {
            DownloadService downloadService = Framework.getService(DownloadService.class);
            downloadService.downloadBlob(request, response, doc, xpath, blob, filename, reason, extendedInfos);
        } catch (IOException e) {
            log.error("Error while downloading the file: " + filename, e);
        } finally {
            facesContext.responseComplete();
        }
    }

    public static String downloadFile(File file, String filename, String reason) throws IOException {
        Blob blob = Blobs.createBlob(file);
        download(null, null, blob, filename, reason);
        return null;
    }

    /**
     * @deprecated since 7.3, use {@link #downloadFile(Blob, String)} instead
     */
    @Deprecated
    public static String download(FacesContext faces, Blob blob, String filename) {
        download(null, null, blob, filename, "download");
        return null;
    }

    /**
     * @deprecated since 7.3, use {@link #downloadFile(File, String)} instead
     */
    @Deprecated
    public static String downloadFile(FacesContext faces, String filename, File file) throws IOException {
        return downloadFile(file, filename, null);
    }

    protected static boolean forceNoCacheOnMSIE() {
        // see NXP-7759
        return Framework.isBooleanPropertyTrue(FORCE_NO_CACHE_ON_MSIE);
    }

    // hook translation passing faces context

    public static String translate(FacesContext context, String messageId) {
        return translate(context, messageId, (Object[]) null);
    }

    public static String translate(FacesContext context, String messageId, Object... params) {
        String bundleName = context.getApplication().getMessageBundle();
        Locale locale = context.getViewRoot().getLocale();
        return I18NUtils.getMessageString(bundleName, messageId, evaluateParams(context, params), locale);
    }

    public static void addErrorMessage(FacesContext context, UIComponent component, String message) {
        addErrorMessage(context, component, message, null);
    }

    public static void addErrorMessage(FacesContext context, UIComponent component, String message,
            Object[] params) {
        String bundleName = context.getApplication().getMessageBundle();
        Locale locale = context.getViewRoot().getLocale();
        message = I18NUtils.getMessageString(bundleName, message, evaluateParams(context, params), locale);
        FacesMessage msg = new FacesMessage(message);
        msg.setSeverity(FacesMessage.SEVERITY_ERROR);
        context.addMessage(component.getClientId(context), msg);
    }

    /**
     * Evaluates parameters to pass to translation methods if they are value expressions.
     *
     * @since 5.7
     */
    protected static Object[] evaluateParams(FacesContext context, Object[] params) {
        if (params == null) {
            return null;
        }
        Object[] res = new Object[params.length];
        for (int i = 0; i < params.length; i++) {
            Object val = params[i];
            if (val instanceof String && ComponentTagUtils.isValueReference((String) val)) {
                ValueExpression ve = context.getApplication().getExpressionFactory()
                        .createValueExpression(context.getELContext(), (String) val, Object.class);
                res[i] = ve.getValue(context.getELContext());
            } else {
                res[i] = val;
            }
        }
        return res;
    }

    /**
     * Gets the base naming container from anchor.
     * <p>
     * Gets out of suggestion box as it's a naming container and we can't get components out of it with a relative path
     * => take above first found container.
     *
     * @since 5.3.1
     */
    public static UIComponent getBase(UIComponent anchor) {
        // init base to given component in case there's no naming container for it
        UIComponent base = anchor;
        UIComponent container = anchor.getNamingContainer();
        if (container != null) {
            UIComponent supContainer = container.getNamingContainer();
            if (supContainer != null) {
                container = supContainer;
            }
        }
        if (container != null) {
            base = container;
        }
        if (log.isDebugEnabled()) {
            log.debug(String.format("Resolved base '%s' for anchor '%s'", base.getId(), anchor.getId()));
        }
        return base;
    }

    /**
     * Returns the component specified by the {@code componentId} parameter from the {@code base} component.
     * <p>
     * Does not throw any exception if the component is not found, returns {@code null} instead.
     *
     * @since 5.4
     */
    @SuppressWarnings("unchecked")
    public static <T> T getComponent(UIComponent base, String componentId, Class<T> expectedComponentClass) {
        if (componentId == null) {
            log.error("Cannot retrieve component with a null id");
            return null;
        }
        UIComponent component = ComponentRenderUtils.getComponent(base, componentId);
        if (component == null) {
            log.error("Could not find component with id: " + componentId);
        } else {
            try {
                return (T) component;
            } catch (ClassCastException e) {
                log.error("Invalid component with id '" + componentId + "': " + component
                        + ", expected a component with interface " + expectedComponentClass);
            }
        }
        return null;
    }

    static void clearTargetList(UIEditableList targetList) {
        int rc = targetList.getRowCount();
        for (int i = 0; i < rc; i++) {
            targetList.removeValue(0);
        }
    }

    static void addToTargetList(UIEditableList targetList, SelectItem[] items) {
        for (int i = 0; i < items.length; i++) {
            targetList.addValue(items[i].getValue());
        }
    }

    /**
     * Move items up inside the target select
     */
    public static void shiftItemsUp(UISelectMany targetSelect, UISelectItems targetItems,
            UIEditableList hiddenTargetList) {
        String[] selected = (String[]) targetSelect.getSelectedValues();
        SelectItem[] all = (SelectItem[]) targetItems.getValue();
        if (selected == null) {
            // nothing to do
            return;
        }
        shiftUp(selected, all);
        targetItems.setValue(all);
        clearTargetList(hiddenTargetList);
        addToTargetList(hiddenTargetList, all);
    }

    public static void shiftItemsDown(UISelectMany targetSelect, UISelectItems targetItems,
            UIEditableList hiddenTargetList) {
        String[] selected = (String[]) targetSelect.getSelectedValues();
        SelectItem[] all = (SelectItem[]) targetItems.getValue();
        if (selected == null) {
            // nothing to do
            return;
        }
        shiftDown(selected, all);
        targetItems.setValue(all);
        clearTargetList(hiddenTargetList);
        addToTargetList(hiddenTargetList, all);
    }

    public static void shiftItemsFirst(UISelectMany targetSelect, UISelectItems targetItems,
            UIEditableList hiddenTargetList) {
        String[] selected = (String[]) targetSelect.getSelectedValues();
        SelectItem[] all = (SelectItem[]) targetItems.getValue();
        if (selected == null) {
            // nothing to do
            return;
        }
        all = shiftFirst(selected, all);
        targetItems.setValue(all);
        clearTargetList(hiddenTargetList);
        addToTargetList(hiddenTargetList, all);
    }

    public static void shiftItemsLast(UISelectMany targetSelect, UISelectItems targetItems,
            UIEditableList hiddenTargetList) {
        String[] selected = (String[]) targetSelect.getSelectedValues();
        SelectItem[] all = (SelectItem[]) targetItems.getValue();
        if (selected == null) {
            // nothing to do
            return;
        }
        all = shiftLast(selected, all);
        targetItems.setValue(all);
        clearTargetList(hiddenTargetList);
        addToTargetList(hiddenTargetList, all);
    }

    /**
     * Make a new SelectItem[] with items whose ids belong to selected first, preserving inner ordering of selected and
     * its complement in all.
     * <p>
     * Again this assumes that selected is an ordered sub-list of all
     * </p>
     *
     * @param selected ids of selected items
     * @param all
     * @return
     */
    static SelectItem[] shiftFirst(String[] selected, SelectItem[] all) {
        SelectItem[] res = new SelectItem[all.length];
        int sl = selected.length;
        int i = 0;
        int j = sl;
        for (SelectItem item : all) {
            if (i < sl && item.getValue().toString().equals(selected[i])) {
                res[i++] = item;
            } else {
                res[j++] = item;
            }
        }
        return res;
    }

    /**
     * Make a new SelectItem[] with items whose ids belong to selected last, preserving inner ordering of selected and
     * its complement in all.
     * <p>
     * Again this assumes that selected is an ordered sub-list of all
     * </p>
     *
     * @param selected ids of selected items
     * @param all
     * @return
     */
    static SelectItem[] shiftLast(String[] selected, SelectItem[] all) {
        SelectItem[] res = new SelectItem[all.length];
        int sl = selected.length;
        int cut = all.length - sl;
        int i = 0;
        int j = 0;
        for (SelectItem item : all) {
            if (i < sl && item.getValue().toString().equals(selected[i])) {
                res[cut + i++] = item;
            } else {
                res[j++] = item;
            }
        }
        return res;
    }

    static void swap(Object[] ar, int i, int j) {
        Object t = ar[i];
        ar[i] = ar[j];
        ar[j] = t;
    }

    static void shiftUp(String[] selected, SelectItem[] all) {
        int pos = -1;
        for (int i = 0; i < selected.length; i++) {
            String s = selected[i];
            // "pos" is the index of previous "s"
            int previous = pos;
            while (!all[++pos].getValue().equals(s)) {
            }
            // now current "s" is at "pos" index
            if (pos > previous + 1) {
                swap(all, pos, --pos);
            }
        }
    }

    static void shiftDown(String[] selected, SelectItem[] all) {
        int pos = all.length;
        for (int i = selected.length - 1; i >= 0; i--) {
            String s = selected[i];
            // "pos" is the index of previous "s"
            int previous = pos;
            while (!all[--pos].getValue().equals(s)) {
            }
            // now current "s" is at "pos" index
            if (pos < previous - 1) {
                swap(all, pos, ++pos);
            }
        }
    }

    /**
     * Move items from components to others.
     */
    public static void moveItems(UISelectMany sourceSelect, UISelectItems sourceItems, UISelectItems targetItems,
            UIEditableList hiddenTargetList, boolean setTargetIds) {
        String[] selected = (String[]) sourceSelect.getSelectedValues();
        if (selected == null) {
            // nothing to do
            return;
        }
        List<String> selectedList = Arrays.asList(selected);

        SelectItem[] all = (SelectItem[]) sourceItems.getValue();
        List<SelectItem> toMove = new ArrayList<SelectItem>();
        List<SelectItem> toKeep = new ArrayList<SelectItem>();
        List<String> hiddenIds = new ArrayList<String>();
        if (all != null) {
            for (SelectItem item : all) {
                String itemId = item.getValue().toString();
                if (selectedList.contains(itemId)) {
                    toMove.add(item);
                } else {
                    toKeep.add(item);
                    if (!setTargetIds) {
                        hiddenIds.add(itemId);
                    }
                }
            }
        }
        // reset left values
        sourceItems.setValue(toKeep.toArray(new SelectItem[] {}));
        sourceSelect.setSelectedValues(new Object[0]);

        // change right values
        List<SelectItem> newSelectItems = new ArrayList<SelectItem>();
        SelectItem[] oldSelectItems = (SelectItem[]) targetItems.getValue();
        if (oldSelectItems == null) {
            newSelectItems.addAll(toMove);
        } else {
            newSelectItems.addAll(Arrays.asList(oldSelectItems));
            List<String> oldIds = new ArrayList<String>();
            for (SelectItem oldItem : oldSelectItems) {
                String id = oldItem.getValue().toString();
                oldIds.add(id);
            }
            if (setTargetIds) {
                hiddenIds.addAll(0, oldIds);
            }
            for (SelectItem toMoveItem : toMove) {
                String id = toMoveItem.getValue().toString();
                if (!oldIds.contains(id)) {
                    newSelectItems.add(toMoveItem);
                    if (setTargetIds) {
                        hiddenIds.add(id);
                    }
                }
            }
        }
        targetItems.setValue(newSelectItems.toArray(new SelectItem[] {}));

        // update hidden values
        int numValues = hiddenTargetList.getRowCount();
        if (numValues > 0) {
            for (int i = numValues - 1; i > -1; i--) {
                hiddenTargetList.removeValue(i);
            }
        }
        for (String newHiddenValue : hiddenIds) {
            hiddenTargetList.addValue(newHiddenValue);
        }
    }

    /**
     * Move items from components to others.
     */
    public static void moveAllItems(UISelectItems sourceItems, UISelectItems targetItems,
            UIEditableList hiddenTargetList, boolean setTargetIds) {
        SelectItem[] all = (SelectItem[]) sourceItems.getValue();
        List<SelectItem> toMove = new ArrayList<SelectItem>();
        List<SelectItem> toKeep = new ArrayList<SelectItem>();
        List<String> hiddenIds = new ArrayList<String>();
        if (all != null) {
            for (SelectItem item : all) {
                if (!item.isDisabled()) {
                    toMove.add(item);
                } else {
                    toKeep.add(item);
                }
            }
        }
        // reset left values
        sourceItems.setValue(toKeep.toArray(new SelectItem[] {}));

        // change right values
        List<SelectItem> newSelectItems = new ArrayList<SelectItem>();
        SelectItem[] oldSelectItems = (SelectItem[]) targetItems.getValue();
        if (oldSelectItems == null) {
            newSelectItems.addAll(toMove);
        } else {
            newSelectItems.addAll(Arrays.asList(oldSelectItems));
            List<String> oldIds = new ArrayList<String>();
            for (SelectItem oldItem : oldSelectItems) {
                String id = oldItem.getValue().toString();
                oldIds.add(id);
            }
            if (setTargetIds) {
                hiddenIds.addAll(0, oldIds);
            }
            for (SelectItem toMoveItem : toMove) {
                String id = toMoveItem.getValue().toString();
                if (!oldIds.contains(id)) {
                    newSelectItems.add(toMoveItem);
                    if (setTargetIds) {
                        hiddenIds.add(id);
                    }
                }
            }
        }
        targetItems.setValue(newSelectItems.toArray(new SelectItem[] {}));

        // update hidden values
        int numValues = hiddenTargetList.getRowCount();
        if (numValues > 0) {
            for (int i = numValues - 1; i > -1; i--) {
                hiddenTargetList.removeValue(i);
            }
        }
        for (String newHiddenValue : hiddenIds) {
            hiddenTargetList.addValue(newHiddenValue);
        }
    }

    public static String verifyTarget(String toVerify, String defaultTarget) {
        if (StringUtils.isBlank(toVerify)) {
            return null;
        }
        FacesContext context = FacesContext.getCurrentInstance();
        boolean ajaxRequest = context.getPartialViewContext().isAjaxRequest();
        if (ajaxRequest) {
            // ease up ajax re-rendering in case of js scripts parsing defer
            return null;
        }
        return defaultTarget;
    }

    public static String NUXEO_RESOURCE_RELOCATED = "NUXEO_RESOURCE_RELOCATED_MARKER";

    /**
     * Marks given component as relocated, so that subsequent calls to {@link #isRelocated(UIComponent)} returns true.
     *
     * @since 8.1
     */
    public static void setRelocated(UIComponent component) {
        component.getAttributes().put(NUXEO_RESOURCE_RELOCATED, "true");
    }

    /**
     * Returns true if given component is marked as relocated.
     *
     * @see #setRelocated(UIComponent)
     * @see #relocate(UIComponent, String, String)
     * @since 8.1
     */
    public static boolean isRelocated(UIComponent component) {
        return component.getAttributes().containsKey(NUXEO_RESOURCE_RELOCATED);
    }

    /**
     * Relocates given component, adding it to the view root resources for given target.
     * <p>
     * If given composite key is not null, current composite component client id is saved using this key on the
     * component attributes, for later reuse.
     * <p>
     * Component is also marked as relocated so that subsequent calls to {@link #isRelocated(UIComponent)} returns true.
     *
     * @since 8.1
     */
    public static void relocate(UIComponent component, String target, String compositeKey) {
        FacesContext context = FacesContext.getCurrentInstance();
        if (compositeKey != null) {
            // We're checking for a composite component here as if the resource
            // is relocated, it may still require it's composite component context
            // in order to properly render. Store it for later use by
            // encodeBegin() and encodeEnd().
            UIComponent cc = UIComponent.getCurrentCompositeComponent(context);
            if (cc != null) {
                component.getAttributes().put(compositeKey, cc.getClientId(context));
            }
        }
        // avoid relocating resources that are not actually rendered
        if (isRendered(component)) {
            setRelocated(component);
            context.getViewRoot().addComponentResource(context, component, target);
        }
    }

    protected static boolean isRendered(UIComponent component) {
        UIComponent comp = component;
        while (comp.isRendered()) {
            UIComponent parent = comp.getParent();
            if (parent == null) {
                // reached root
                return true;
            } else {
                comp = parent;
            }
        }
        return false;
    }

}