/* * (C) Copyright 2007 Nuxeo SAS (http://nuxeo.com/) and contributors. * * All rights reserved. This program and the accompanying materials * are made available under the terms of the GNU Lesser General Public License * (LGPL) version 2.1 which accompanies this distribution, and is available at * http://www.gnu.org/licenses/lgpl.html * * This library 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. * * 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.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.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.impl.blob.FileBlob; import org.nuxeo.ecm.core.storage.StorageBlob; import org.nuxeo.ecm.platform.ui.web.component.list.UIEditableList; import org.nuxeo.ecm.platform.web.common.ServletHelper; 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); private static final String VH_HEADER = "nuxeo-virtual-host"; 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) { Object value = component.getAttributes().get(attributeName); if (value == null) { value = defaultValue; } return 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; } public static String download(FacesContext faces, Blob blob, String filename) { if (!faces.getResponseComplete()) { // do not perform download in an ajax request boolean ajaxRequest = faces.getPartialViewContext().isAjaxRequest(); if (ajaxRequest) { return null; } if (blob == null) { log.error("No bytes available for the file: " + filename); } else { ExternalContext econtext = faces.getExternalContext(); HttpServletResponse response = (HttpServletResponse) econtext.getResponse(); if (filename == null || filename.length() == 0) { filename = "file"; } HttpServletRequest request = (HttpServletRequest) econtext.getRequest(); String digest = null; if (blob instanceof StorageBlob) { digest = ((StorageBlob) blob).getBinary().getDigest(); } try { String previousToken = request.getHeader("If-None-Match"); if (previousToken != null && previousToken.equals(digest)) { response.sendError(HttpServletResponse.SC_NOT_MODIFIED); } else { response.setHeader("ETag", digest); response.setHeader("Content-Disposition", ServletHelper.getRFC2231ContentDisposition( request, filename)); addCacheControlHeaders(request, response); log.debug("Downloading with mime/type : " + blob.getMimeType()); response.setContentType(blob.getMimeType()); long fileSize = blob.getLength(); if (fileSize > 0) { response.setContentLength((int) fileSize); } blob.transferTo(response.getOutputStream()); response.flushBuffer(); } } catch (IOException e) { log.error("Error while downloading the file: " + filename, e); } faces.responseComplete(); } } return null; } public static String downloadFile(FacesContext faces, String filename, File file) { FileBlob fileBlob = new FileBlob(file); return download(faces, fileBlob, filename); } protected static boolean forceNoCacheOnMSIE() { // see NXP-7759 return Framework.isBooleanPropertyTrue(FORCE_NO_CACHE_ON_MSIE); } /** * Internet Explorer file downloads over SSL do not work with certain HTTP * cache control headers * <p> * See http://support.microsoft.com/kb/323308/ * <p> * What is not mentioned in the above Knowledge Base is that * "Pragma: no-cache" also breaks download in MSIE over SSL */ private static void addCacheControlHeaders(HttpServletRequest request, HttpServletResponse response) { String userAgent = request.getHeader("User-Agent"); boolean secure = request.isSecure(); if (!secure) { String nvh = request.getHeader(VH_HEADER); if (nvh != null) { secure = nvh.startsWith("https"); } } log.debug("User-Agent: " + userAgent); log.debug("secure: " + secure); if (userAgent.contains("MSIE") && (secure || forceNoCacheOnMSIE())) { log.debug("Setting \"Cache-Control: max-age=15, must-revalidate\""); response.setHeader("Cache-Control", "max-age=15, must-revalidate"); } else { log.debug("Setting \"Cache-Control: private\" and \"Pragma: no-cache\""); response.setHeader("Cache-Control", "private, must-revalidate"); response.setHeader("Pragma", "no-cache"); response.setDateHeader("Expires", 0); } } // 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) { UIComponent base = anchor; UIComponent container = anchor.getNamingContainer(); if (container != null) { UIComponent supContainer = container.getNamingContainer(); if (supContainer != null) { container = supContainer; } } 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; } try { 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(String.format( "Invalid component with id %s: %s, expected a " + "component with interface %s", componentId, component, expectedComponentClass)); } } } catch (Exception e) { log.error("Error when trying to retrieve component with id " + componentId, e); } 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); } } }