001/* 002 * (C) Copyright 2007 Nuxeo SAS (http://nuxeo.com/) and contributors. 003 * 004 * All rights reserved. This program and the accompanying materials 005 * are made available under the terms of the GNU Lesser General Public License 006 * (LGPL) version 2.1 which accompanies this distribution, and is available at 007 * http://www.gnu.org/licenses/lgpl.html 008 * 009 * This library is distributed in the hope that it will be useful, 010 * but WITHOUT ANY WARRANTY; without even the implied warranty of 011 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 012 * Lesser General Public License for more details. 013 * 014 * Contributors: 015 * Nuxeo - initial API and implementation 016 * Sean Radford 017 * 018 * $Id: ComponentUtils.java 28924 2008-01-10 14:04:05Z sfermigier $ 019 */ 020 021package org.nuxeo.ecm.platform.ui.web.util; 022 023import java.io.File; 024import java.io.IOException; 025import java.io.Serializable; 026import java.util.ArrayList; 027import java.util.Arrays; 028import java.util.List; 029import java.util.Locale; 030import java.util.Map; 031 032import javax.el.ValueExpression; 033import javax.faces.application.FacesMessage; 034import javax.faces.component.UIComponent; 035import javax.faces.component.UISelectItems; 036import javax.faces.component.UISelectMany; 037import javax.faces.context.ExternalContext; 038import javax.faces.context.FacesContext; 039import javax.faces.model.SelectItem; 040import javax.servlet.http.HttpServletRequest; 041import javax.servlet.http.HttpServletResponse; 042 043import org.apache.commons.logging.Log; 044import org.apache.commons.logging.LogFactory; 045import org.nuxeo.common.utils.i18n.I18NUtils; 046import org.nuxeo.ecm.core.api.Blob; 047import org.nuxeo.ecm.core.api.Blobs; 048import org.nuxeo.ecm.core.api.DocumentModel; 049import org.nuxeo.ecm.core.io.download.DownloadService; 050import org.nuxeo.ecm.platform.ui.web.component.list.UIEditableList; 051import org.nuxeo.ecm.platform.web.common.ServletHelper; 052import org.nuxeo.ecm.platform.web.common.exceptionhandling.ExceptionHelper; 053import org.nuxeo.runtime.api.Framework; 054 055/** 056 * Generic component helper methods. 057 * 058 * @author <a href="mailto:at@nuxeo.com">Anahide Tchertchian</a> 059 */ 060public final class ComponentUtils { 061 062 public static final String WHITE_SPACE_CHARACTER = " "; 063 064 private static final Log log = LogFactory.getLog(ComponentUtils.class); 065 066 private static final String VH_HEADER = "nuxeo-virtual-host"; 067 068 private static final String VH_PARAM = "nuxeo.virtual.host"; 069 070 public static final String FORCE_NO_CACHE_ON_MSIE = "org.nuxeo.download.force.nocache.msie"; 071 072 // Utility class. 073 private ComponentUtils() { 074 } 075 076 /** 077 * Calls a component encodeBegin/encodeChildren/encodeEnd methods. 078 */ 079 public static void encodeComponent(FacesContext context, UIComponent component) throws IOException { 080 component.encodeBegin(context); 081 component.encodeChildren(context); 082 component.encodeEnd(context); 083 } 084 085 /** 086 * Helper method meant to be called in the component constructor. 087 * <p> 088 * When adding sub components dynamically, the tree fetching could be a problem so all possible sub components must 089 * be added. 090 * <p> 091 * Since 6.0, does not mark component as not rendered anymore, calls 092 * {@link #hookSubComponent(FacesContext, UIComponent, UIComponent, String)} directly. 093 * 094 * @param parent 095 * @param child 096 * @param facetName facet name to put the child in. 097 */ 098 public static void initiateSubComponent(UIComponent parent, String facetName, UIComponent child) { 099 parent.getFacets().put(facetName, child); 100 hookSubComponent(null, parent, child, facetName); 101 } 102 103 /** 104 * Add a sub component to a UI component. 105 * <p> 106 * Since 6.0, does not the set the component as rendered anymore. 107 * 108 * @param context 109 * @param parent 110 * @param child 111 * @param defaultChildId 112 * @return child comp 113 */ 114 public static UIComponent hookSubComponent(FacesContext context, UIComponent parent, UIComponent child, 115 String defaultChildId) { 116 // build a valid id using the parent id so that it's found everytime. 117 String childId = child.getId(); 118 if (defaultChildId != null) { 119 // override with default 120 childId = defaultChildId; 121 } 122 // make sure it's set 123 if (childId == null) { 124 childId = context.getViewRoot().createUniqueId(); 125 } 126 // reset client id 127 child.setId(childId); 128 child.setParent(parent); 129 return child; 130 } 131 132 /** 133 * Copies attributes and value expressions with given name from parent component to child component. 134 */ 135 public static void copyValues(UIComponent parent, UIComponent child, String[] valueNames) { 136 Map<String, Object> parentAttributes = parent.getAttributes(); 137 Map<String, Object> childAttributes = child.getAttributes(); 138 for (String name : valueNames) { 139 // attributes 140 if (parentAttributes.containsKey(name)) { 141 childAttributes.put(name, parentAttributes.get(name)); 142 } 143 // value expressions 144 ValueExpression ve = parent.getValueExpression(name); 145 if (ve != null) { 146 child.setValueExpression(name, ve); 147 } 148 } 149 } 150 151 public static void copyLinkValues(UIComponent parent, UIComponent child) { 152 String[] valueNames = { "accesskey", "charset", "coords", "dir", "disabled", "hreflang", "lang", "onblur", 153 "onclick", "ondblclick", "onfocus", "onkeydown", "onkeypress", "onkeyup", "onmousedown", "onmousemove", 154 "onmouseout", "onmouseover", "onmouseup", "rel", "rev", "shape", "style", "styleClass", "tabindex", 155 "target", "title", "type" }; 156 copyValues(parent, child, valueNames); 157 } 158 159 public static Object getAttributeValue(UIComponent component, String attributeName, Object defaultValue) { 160 Object value = component.getAttributes().get(attributeName); 161 if (value == null) { 162 value = defaultValue; 163 } 164 return value; 165 } 166 167 public static Object getAttributeOrExpressionValue(FacesContext context, UIComponent component, 168 String attributeName, Object defaultValue) { 169 Object value = component.getAttributes().get(attributeName); 170 if (value == null) { 171 ValueExpression schemaExpr = component.getValueExpression(attributeName); 172 value = schemaExpr.getValue(context.getELContext()); 173 } 174 if (value == null) { 175 value = defaultValue; 176 } 177 return value; 178 } 179 180 /** 181 * Downloads a blob and sends it to the requesting user, in the JSF current context. 182 * 183 * @param doc the document, if available 184 * @param xpath the blob's xpath or blobholder index, if available 185 * @param blob the blob, if already fetched 186 * @param filename the filename to use 187 * @param reason the download reason 188 * 189 * @since 7.3 190 */ 191 public static void download(DocumentModel doc, String xpath, Blob blob, String filename, String reason) { 192 download(doc, xpath, blob, filename, reason, null); 193 } 194 195 /** 196 * Downloads a blob and sends it to the requesting user, in the JSF current context. 197 * 198 * @param doc the document, if available 199 * @param xpath the blob's xpath or blobholder index, if available 200 * @param blob the blob, if already fetched 201 * @param filename the filename to use 202 * @param reason the download reason 203 * @param extendedInfos an optional map of extended informations to log 204 * 205 * @since 7.3 206 */ 207 public static void download(DocumentModel doc, String xpath, Blob blob, String filename, String reason, 208 Map<String, Serializable> extendedInfos) { 209 FacesContext facesContext = FacesContext.getCurrentInstance(); 210 if (facesContext.getResponseComplete()) { 211 // nothing can be written, an error was probably already sent. don't bother 212 log.debug("Cannot send " + filename + ", response already complete"); 213 return; 214 } 215 if (facesContext.getPartialViewContext().isAjaxRequest()) { 216 // do not perform download in an ajax request 217 return; 218 } 219 ExternalContext externalContext = facesContext.getExternalContext(); 220 HttpServletRequest request = (HttpServletRequest) externalContext.getRequest(); 221 HttpServletResponse response = (HttpServletResponse) externalContext.getResponse(); 222 try { 223 DownloadService downloadService = Framework.getService(DownloadService.class); 224 downloadService.downloadBlob(request, response, doc, xpath, blob, filename, reason, extendedInfos); 225 } catch (IOException e) { 226 log.error("Error while downloading the file: " + filename, e); 227 } finally { 228 facesContext.responseComplete(); 229 } 230 } 231 232 public static String downloadFile(File file, String filename, String reason) throws IOException { 233 Blob blob = Blobs.createBlob(file); 234 download(null, null, blob, filename, reason); 235 return null; 236 } 237 238 /** 239 * @deprecated since 7.3, use {@link #downloadFile(Blob, String)} instead 240 */ 241 @Deprecated 242 public static String download(FacesContext faces, Blob blob, String filename) { 243 download(null, null, blob, filename, "download"); 244 return null; 245 } 246 247 /** 248 * @deprecated since 7.3, use {@link #downloadFile(File, String)} instead 249 */ 250 @Deprecated 251 public static String downloadFile(FacesContext faces, String filename, File file) throws IOException { 252 return downloadFile(file, filename, null); 253 } 254 255 protected static boolean forceNoCacheOnMSIE() { 256 // see NXP-7759 257 return Framework.isBooleanPropertyTrue(FORCE_NO_CACHE_ON_MSIE); 258 } 259 260 /** 261 * Internet Explorer file downloads over SSL do not work with certain HTTP cache control headers 262 * <p> 263 * See http://support.microsoft.com/kb/323308/ 264 * <p> 265 * What is not mentioned in the above Knowledge Base is that "Pragma: no-cache" also breaks download in MSIE over 266 * SSL 267 */ 268 private static void addCacheControlHeaders(HttpServletRequest request, HttpServletResponse response) { 269 String userAgent = request.getHeader("User-Agent"); 270 boolean secure = request.isSecure(); 271 if (!secure) { 272 String nvh = request.getHeader(VH_HEADER); 273 if (nvh == null) { 274 nvh = Framework.getProperty(VH_PARAM); 275 } 276 if (nvh != null) { 277 secure = nvh.startsWith("https"); 278 } 279 } 280 log.debug("User-Agent: " + userAgent); 281 log.debug("secure: " + secure); 282 if (userAgent.contains("MSIE") && (secure || forceNoCacheOnMSIE())) { 283 log.debug("Setting \"Cache-Control: max-age=15, must-revalidate\""); 284 response.setHeader("Cache-Control", "max-age=15, must-revalidate"); 285 } else { 286 log.debug("Setting \"Cache-Control: private\" and \"Pragma: no-cache\""); 287 response.setHeader("Cache-Control", "private, must-revalidate"); 288 response.setHeader("Pragma", "no-cache"); 289 response.setDateHeader("Expires", 0); 290 } 291 } 292 293 // hook translation passing faces context 294 295 public static String translate(FacesContext context, String messageId) { 296 return translate(context, messageId, (Object[]) null); 297 } 298 299 public static String translate(FacesContext context, String messageId, Object... params) { 300 String bundleName = context.getApplication().getMessageBundle(); 301 Locale locale = context.getViewRoot().getLocale(); 302 return I18NUtils.getMessageString(bundleName, messageId, evaluateParams(context, params), locale); 303 } 304 305 public static void addErrorMessage(FacesContext context, UIComponent component, String message) { 306 addErrorMessage(context, component, message, null); 307 } 308 309 public static void addErrorMessage(FacesContext context, UIComponent component, String message, Object[] params) { 310 String bundleName = context.getApplication().getMessageBundle(); 311 Locale locale = context.getViewRoot().getLocale(); 312 message = I18NUtils.getMessageString(bundleName, message, evaluateParams(context, params), locale); 313 FacesMessage msg = new FacesMessage(message); 314 msg.setSeverity(FacesMessage.SEVERITY_ERROR); 315 context.addMessage(component.getClientId(context), msg); 316 } 317 318 /** 319 * Evaluates parameters to pass to translation methods if they are value expressions. 320 * 321 * @since 5.7 322 */ 323 protected static Object[] evaluateParams(FacesContext context, Object[] params) { 324 if (params == null) { 325 return null; 326 } 327 Object[] res = new Object[params.length]; 328 for (int i = 0; i < params.length; i++) { 329 Object val = params[i]; 330 if (val instanceof String && ComponentTagUtils.isValueReference((String) val)) { 331 ValueExpression ve = context.getApplication().getExpressionFactory().createValueExpression( 332 context.getELContext(), (String) val, Object.class); 333 res[i] = ve.getValue(context.getELContext()); 334 } else { 335 res[i] = val; 336 } 337 } 338 return res; 339 } 340 341 /** 342 * Gets the base naming container from anchor. 343 * <p> 344 * Gets out of suggestion box as it's a naming container and we can't get components out of it with a relative path 345 * => take above first found container. 346 * 347 * @since 5.3.1 348 */ 349 public static UIComponent getBase(UIComponent anchor) { 350 UIComponent base = anchor; 351 UIComponent container = anchor.getNamingContainer(); 352 if (container != null) { 353 UIComponent supContainer = container.getNamingContainer(); 354 if (supContainer != null) { 355 container = supContainer; 356 } 357 } 358 if (log.isDebugEnabled()) { 359 log.debug(String.format("Resolved base '%s' for anchor '%s'", base.getId(), anchor.getId())); 360 } 361 return base; 362 } 363 364 /** 365 * Returns the component specified by the {@code componentId} parameter from the {@code base} component. 366 * <p> 367 * Does not throw any exception if the component is not found, returns {@code null} instead. 368 * 369 * @since 5.4 370 */ 371 @SuppressWarnings("unchecked") 372 public static <T> T getComponent(UIComponent base, String componentId, Class<T> expectedComponentClass) { 373 if (componentId == null) { 374 log.error("Cannot retrieve component with a null id"); 375 return null; 376 } 377 UIComponent component = ComponentRenderUtils.getComponent(base, componentId); 378 if (component == null) { 379 log.error("Could not find component with id: " + componentId); 380 } else { 381 try { 382 return (T) component; 383 } catch (ClassCastException e) { 384 log.error(String.format( 385 "Invalid component with id %s: %s, expected a " + "component with interface %s", componentId, 386 component, expectedComponentClass)); 387 } 388 } 389 return null; 390 } 391 392 static void clearTargetList(UIEditableList targetList) { 393 int rc = targetList.getRowCount(); 394 for (int i = 0; i < rc; i++) { 395 targetList.removeValue(0); 396 } 397 } 398 399 static void addToTargetList(UIEditableList targetList, SelectItem[] items) { 400 for (int i = 0; i < items.length; i++) { 401 targetList.addValue(items[i].getValue()); 402 } 403 } 404 405 /** 406 * Move items up inside the target select 407 */ 408 public static void shiftItemsUp(UISelectMany targetSelect, UISelectItems targetItems, 409 UIEditableList hiddenTargetList) { 410 String[] selected = (String[]) targetSelect.getSelectedValues(); 411 SelectItem[] all = (SelectItem[]) targetItems.getValue(); 412 if (selected == null) { 413 // nothing to do 414 return; 415 } 416 shiftUp(selected, all); 417 targetItems.setValue(all); 418 clearTargetList(hiddenTargetList); 419 addToTargetList(hiddenTargetList, all); 420 } 421 422 public static void shiftItemsDown(UISelectMany targetSelect, UISelectItems targetItems, 423 UIEditableList hiddenTargetList) { 424 String[] selected = (String[]) targetSelect.getSelectedValues(); 425 SelectItem[] all = (SelectItem[]) targetItems.getValue(); 426 if (selected == null) { 427 // nothing to do 428 return; 429 } 430 shiftDown(selected, all); 431 targetItems.setValue(all); 432 clearTargetList(hiddenTargetList); 433 addToTargetList(hiddenTargetList, all); 434 } 435 436 public static void shiftItemsFirst(UISelectMany targetSelect, UISelectItems targetItems, 437 UIEditableList hiddenTargetList) { 438 String[] selected = (String[]) targetSelect.getSelectedValues(); 439 SelectItem[] all = (SelectItem[]) targetItems.getValue(); 440 if (selected == null) { 441 // nothing to do 442 return; 443 } 444 all = shiftFirst(selected, all); 445 targetItems.setValue(all); 446 clearTargetList(hiddenTargetList); 447 addToTargetList(hiddenTargetList, all); 448 } 449 450 public static void shiftItemsLast(UISelectMany targetSelect, UISelectItems targetItems, 451 UIEditableList hiddenTargetList) { 452 String[] selected = (String[]) targetSelect.getSelectedValues(); 453 SelectItem[] all = (SelectItem[]) targetItems.getValue(); 454 if (selected == null) { 455 // nothing to do 456 return; 457 } 458 all = shiftLast(selected, all); 459 targetItems.setValue(all); 460 clearTargetList(hiddenTargetList); 461 addToTargetList(hiddenTargetList, all); 462 } 463 464 /** 465 * Make a new SelectItem[] with items whose ids belong to selected first, preserving inner ordering of selected and 466 * its complement in all. 467 * <p> 468 * Again this assumes that selected is an ordered sub-list of all 469 * </p> 470 * 471 * @param selected ids of selected items 472 * @param all 473 * @return 474 */ 475 static SelectItem[] shiftFirst(String[] selected, SelectItem[] all) { 476 SelectItem[] res = new SelectItem[all.length]; 477 int sl = selected.length; 478 int i = 0; 479 int j = sl; 480 for (SelectItem item : all) { 481 if (i < sl && item.getValue().toString().equals(selected[i])) { 482 res[i++] = item; 483 } else { 484 res[j++] = item; 485 } 486 } 487 return res; 488 } 489 490 /** 491 * Make a new SelectItem[] with items whose ids belong to selected last, preserving inner ordering of selected and 492 * its complement in all. 493 * <p> 494 * Again this assumes that selected is an ordered sub-list of all 495 * </p> 496 * 497 * @param selected ids of selected items 498 * @param all 499 * @return 500 */ 501 static SelectItem[] shiftLast(String[] selected, SelectItem[] all) { 502 SelectItem[] res = new SelectItem[all.length]; 503 int sl = selected.length; 504 int cut = all.length - sl; 505 int i = 0; 506 int j = 0; 507 for (SelectItem item : all) { 508 if (i < sl && item.getValue().toString().equals(selected[i])) { 509 res[cut + i++] = item; 510 } else { 511 res[j++] = item; 512 } 513 } 514 return res; 515 } 516 517 static void swap(Object[] ar, int i, int j) { 518 Object t = ar[i]; 519 ar[i] = ar[j]; 520 ar[j] = t; 521 } 522 523 static void shiftUp(String[] selected, SelectItem[] all) { 524 int pos = -1; 525 for (int i = 0; i < selected.length; i++) { 526 String s = selected[i]; 527 // "pos" is the index of previous "s" 528 int previous = pos; 529 while (!all[++pos].getValue().equals(s)) { 530 } 531 // now current "s" is at "pos" index 532 if (pos > previous + 1) { 533 swap(all, pos, --pos); 534 } 535 } 536 } 537 538 static void shiftDown(String[] selected, SelectItem[] all) { 539 int pos = all.length; 540 for (int i = selected.length - 1; i >= 0; i--) { 541 String s = selected[i]; 542 // "pos" is the index of previous "s" 543 int previous = pos; 544 while (!all[--pos].getValue().equals(s)) { 545 } 546 // now current "s" is at "pos" index 547 if (pos < previous - 1) { 548 swap(all, pos, ++pos); 549 } 550 } 551 } 552 553 /** 554 * Move items from components to others. 555 */ 556 public static void moveItems(UISelectMany sourceSelect, UISelectItems sourceItems, UISelectItems targetItems, 557 UIEditableList hiddenTargetList, boolean setTargetIds) { 558 String[] selected = (String[]) sourceSelect.getSelectedValues(); 559 if (selected == null) { 560 // nothing to do 561 return; 562 } 563 List<String> selectedList = Arrays.asList(selected); 564 565 SelectItem[] all = (SelectItem[]) sourceItems.getValue(); 566 List<SelectItem> toMove = new ArrayList<SelectItem>(); 567 List<SelectItem> toKeep = new ArrayList<SelectItem>(); 568 List<String> hiddenIds = new ArrayList<String>(); 569 if (all != null) { 570 for (SelectItem item : all) { 571 String itemId = item.getValue().toString(); 572 if (selectedList.contains(itemId)) { 573 toMove.add(item); 574 } else { 575 toKeep.add(item); 576 if (!setTargetIds) { 577 hiddenIds.add(itemId); 578 } 579 } 580 } 581 } 582 // reset left values 583 sourceItems.setValue(toKeep.toArray(new SelectItem[] {})); 584 sourceSelect.setSelectedValues(new Object[0]); 585 586 // change right values 587 List<SelectItem> newSelectItems = new ArrayList<SelectItem>(); 588 SelectItem[] oldSelectItems = (SelectItem[]) targetItems.getValue(); 589 if (oldSelectItems == null) { 590 newSelectItems.addAll(toMove); 591 } else { 592 newSelectItems.addAll(Arrays.asList(oldSelectItems)); 593 List<String> oldIds = new ArrayList<String>(); 594 for (SelectItem oldItem : oldSelectItems) { 595 String id = oldItem.getValue().toString(); 596 oldIds.add(id); 597 } 598 if (setTargetIds) { 599 hiddenIds.addAll(0, oldIds); 600 } 601 for (SelectItem toMoveItem : toMove) { 602 String id = toMoveItem.getValue().toString(); 603 if (!oldIds.contains(id)) { 604 newSelectItems.add(toMoveItem); 605 if (setTargetIds) { 606 hiddenIds.add(id); 607 } 608 } 609 } 610 } 611 targetItems.setValue(newSelectItems.toArray(new SelectItem[] {})); 612 613 // update hidden values 614 int numValues = hiddenTargetList.getRowCount(); 615 if (numValues > 0) { 616 for (int i = numValues - 1; i > -1; i--) { 617 hiddenTargetList.removeValue(i); 618 } 619 } 620 for (String newHiddenValue : hiddenIds) { 621 hiddenTargetList.addValue(newHiddenValue); 622 } 623 } 624 625 /** 626 * Move items from components to others. 627 */ 628 public static void moveAllItems(UISelectItems sourceItems, UISelectItems targetItems, 629 UIEditableList hiddenTargetList, boolean setTargetIds) { 630 SelectItem[] all = (SelectItem[]) sourceItems.getValue(); 631 List<SelectItem> toMove = new ArrayList<SelectItem>(); 632 List<SelectItem> toKeep = new ArrayList<SelectItem>(); 633 List<String> hiddenIds = new ArrayList<String>(); 634 if (all != null) { 635 for (SelectItem item : all) { 636 if (!item.isDisabled()) { 637 toMove.add(item); 638 } else { 639 toKeep.add(item); 640 } 641 } 642 } 643 // reset left values 644 sourceItems.setValue(toKeep.toArray(new SelectItem[] {})); 645 646 // change right values 647 List<SelectItem> newSelectItems = new ArrayList<SelectItem>(); 648 SelectItem[] oldSelectItems = (SelectItem[]) targetItems.getValue(); 649 if (oldSelectItems == null) { 650 newSelectItems.addAll(toMove); 651 } else { 652 newSelectItems.addAll(Arrays.asList(oldSelectItems)); 653 List<String> oldIds = new ArrayList<String>(); 654 for (SelectItem oldItem : oldSelectItems) { 655 String id = oldItem.getValue().toString(); 656 oldIds.add(id); 657 } 658 if (setTargetIds) { 659 hiddenIds.addAll(0, oldIds); 660 } 661 for (SelectItem toMoveItem : toMove) { 662 String id = toMoveItem.getValue().toString(); 663 if (!oldIds.contains(id)) { 664 newSelectItems.add(toMoveItem); 665 if (setTargetIds) { 666 hiddenIds.add(id); 667 } 668 } 669 } 670 } 671 targetItems.setValue(newSelectItems.toArray(new SelectItem[] {})); 672 673 // update hidden values 674 int numValues = hiddenTargetList.getRowCount(); 675 if (numValues > 0) { 676 for (int i = numValues - 1; i > -1; i--) { 677 hiddenTargetList.removeValue(i); 678 } 679 } 680 for (String newHiddenValue : hiddenIds) { 681 hiddenTargetList.addValue(newHiddenValue); 682 } 683 } 684 685}