/*
* (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.Collections;
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 = " ";
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, Collections.emptyMap());
}
/**
* 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;
}
}