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 // init base to given component in case there's no naming container for it 351 UIComponent base = anchor; 352 UIComponent container = anchor.getNamingContainer(); 353 if (container != null) { 354 UIComponent supContainer = container.getNamingContainer(); 355 if (supContainer != null) { 356 container = supContainer; 357 } 358 } 359 if (container != null) { 360 base = container; 361 } 362 if (log.isDebugEnabled()) { 363 log.debug(String.format("Resolved base '%s' for anchor '%s'", base.getId(), anchor.getId())); 364 } 365 return base; 366 } 367 368 /** 369 * Returns the component specified by the {@code componentId} parameter from the {@code base} component. 370 * <p> 371 * Does not throw any exception if the component is not found, returns {@code null} instead. 372 * 373 * @since 5.4 374 */ 375 @SuppressWarnings("unchecked") 376 public static <T> T getComponent(UIComponent base, String componentId, Class<T> expectedComponentClass) { 377 if (componentId == null) { 378 log.error("Cannot retrieve component with a null id"); 379 return null; 380 } 381 UIComponent component = ComponentRenderUtils.getComponent(base, componentId); 382 if (component == null) { 383 log.error("Could not find component with id: " + componentId); 384 } else { 385 try { 386 return (T) component; 387 } catch (ClassCastException e) { 388 log.error(String.format( 389 "Invalid component with id %s: %s, expected a " + "component with interface %s", componentId, 390 component, expectedComponentClass)); 391 } 392 } 393 return null; 394 } 395 396 static void clearTargetList(UIEditableList targetList) { 397 int rc = targetList.getRowCount(); 398 for (int i = 0; i < rc; i++) { 399 targetList.removeValue(0); 400 } 401 } 402 403 static void addToTargetList(UIEditableList targetList, SelectItem[] items) { 404 for (int i = 0; i < items.length; i++) { 405 targetList.addValue(items[i].getValue()); 406 } 407 } 408 409 /** 410 * Move items up inside the target select 411 */ 412 public static void shiftItemsUp(UISelectMany targetSelect, UISelectItems targetItems, 413 UIEditableList hiddenTargetList) { 414 String[] selected = (String[]) targetSelect.getSelectedValues(); 415 SelectItem[] all = (SelectItem[]) targetItems.getValue(); 416 if (selected == null) { 417 // nothing to do 418 return; 419 } 420 shiftUp(selected, all); 421 targetItems.setValue(all); 422 clearTargetList(hiddenTargetList); 423 addToTargetList(hiddenTargetList, all); 424 } 425 426 public static void shiftItemsDown(UISelectMany targetSelect, UISelectItems targetItems, 427 UIEditableList hiddenTargetList) { 428 String[] selected = (String[]) targetSelect.getSelectedValues(); 429 SelectItem[] all = (SelectItem[]) targetItems.getValue(); 430 if (selected == null) { 431 // nothing to do 432 return; 433 } 434 shiftDown(selected, all); 435 targetItems.setValue(all); 436 clearTargetList(hiddenTargetList); 437 addToTargetList(hiddenTargetList, all); 438 } 439 440 public static void shiftItemsFirst(UISelectMany targetSelect, UISelectItems targetItems, 441 UIEditableList hiddenTargetList) { 442 String[] selected = (String[]) targetSelect.getSelectedValues(); 443 SelectItem[] all = (SelectItem[]) targetItems.getValue(); 444 if (selected == null) { 445 // nothing to do 446 return; 447 } 448 all = shiftFirst(selected, all); 449 targetItems.setValue(all); 450 clearTargetList(hiddenTargetList); 451 addToTargetList(hiddenTargetList, all); 452 } 453 454 public static void shiftItemsLast(UISelectMany targetSelect, UISelectItems targetItems, 455 UIEditableList hiddenTargetList) { 456 String[] selected = (String[]) targetSelect.getSelectedValues(); 457 SelectItem[] all = (SelectItem[]) targetItems.getValue(); 458 if (selected == null) { 459 // nothing to do 460 return; 461 } 462 all = shiftLast(selected, all); 463 targetItems.setValue(all); 464 clearTargetList(hiddenTargetList); 465 addToTargetList(hiddenTargetList, all); 466 } 467 468 /** 469 * Make a new SelectItem[] with items whose ids belong to selected first, preserving inner ordering of selected and 470 * its complement in all. 471 * <p> 472 * Again this assumes that selected is an ordered sub-list of all 473 * </p> 474 * 475 * @param selected ids of selected items 476 * @param all 477 * @return 478 */ 479 static SelectItem[] shiftFirst(String[] selected, SelectItem[] all) { 480 SelectItem[] res = new SelectItem[all.length]; 481 int sl = selected.length; 482 int i = 0; 483 int j = sl; 484 for (SelectItem item : all) { 485 if (i < sl && item.getValue().toString().equals(selected[i])) { 486 res[i++] = item; 487 } else { 488 res[j++] = item; 489 } 490 } 491 return res; 492 } 493 494 /** 495 * Make a new SelectItem[] with items whose ids belong to selected last, preserving inner ordering of selected and 496 * its complement in all. 497 * <p> 498 * Again this assumes that selected is an ordered sub-list of all 499 * </p> 500 * 501 * @param selected ids of selected items 502 * @param all 503 * @return 504 */ 505 static SelectItem[] shiftLast(String[] selected, SelectItem[] all) { 506 SelectItem[] res = new SelectItem[all.length]; 507 int sl = selected.length; 508 int cut = all.length - sl; 509 int i = 0; 510 int j = 0; 511 for (SelectItem item : all) { 512 if (i < sl && item.getValue().toString().equals(selected[i])) { 513 res[cut + i++] = item; 514 } else { 515 res[j++] = item; 516 } 517 } 518 return res; 519 } 520 521 static void swap(Object[] ar, int i, int j) { 522 Object t = ar[i]; 523 ar[i] = ar[j]; 524 ar[j] = t; 525 } 526 527 static void shiftUp(String[] selected, SelectItem[] all) { 528 int pos = -1; 529 for (int i = 0; i < selected.length; i++) { 530 String s = selected[i]; 531 // "pos" is the index of previous "s" 532 int previous = pos; 533 while (!all[++pos].getValue().equals(s)) { 534 } 535 // now current "s" is at "pos" index 536 if (pos > previous + 1) { 537 swap(all, pos, --pos); 538 } 539 } 540 } 541 542 static void shiftDown(String[] selected, SelectItem[] all) { 543 int pos = all.length; 544 for (int i = selected.length - 1; i >= 0; i--) { 545 String s = selected[i]; 546 // "pos" is the index of previous "s" 547 int previous = pos; 548 while (!all[--pos].getValue().equals(s)) { 549 } 550 // now current "s" is at "pos" index 551 if (pos < previous - 1) { 552 swap(all, pos, ++pos); 553 } 554 } 555 } 556 557 /** 558 * Move items from components to others. 559 */ 560 public static void moveItems(UISelectMany sourceSelect, UISelectItems sourceItems, UISelectItems targetItems, 561 UIEditableList hiddenTargetList, boolean setTargetIds) { 562 String[] selected = (String[]) sourceSelect.getSelectedValues(); 563 if (selected == null) { 564 // nothing to do 565 return; 566 } 567 List<String> selectedList = Arrays.asList(selected); 568 569 SelectItem[] all = (SelectItem[]) sourceItems.getValue(); 570 List<SelectItem> toMove = new ArrayList<SelectItem>(); 571 List<SelectItem> toKeep = new ArrayList<SelectItem>(); 572 List<String> hiddenIds = new ArrayList<String>(); 573 if (all != null) { 574 for (SelectItem item : all) { 575 String itemId = item.getValue().toString(); 576 if (selectedList.contains(itemId)) { 577 toMove.add(item); 578 } else { 579 toKeep.add(item); 580 if (!setTargetIds) { 581 hiddenIds.add(itemId); 582 } 583 } 584 } 585 } 586 // reset left values 587 sourceItems.setValue(toKeep.toArray(new SelectItem[] {})); 588 sourceSelect.setSelectedValues(new Object[0]); 589 590 // change right values 591 List<SelectItem> newSelectItems = new ArrayList<SelectItem>(); 592 SelectItem[] oldSelectItems = (SelectItem[]) targetItems.getValue(); 593 if (oldSelectItems == null) { 594 newSelectItems.addAll(toMove); 595 } else { 596 newSelectItems.addAll(Arrays.asList(oldSelectItems)); 597 List<String> oldIds = new ArrayList<String>(); 598 for (SelectItem oldItem : oldSelectItems) { 599 String id = oldItem.getValue().toString(); 600 oldIds.add(id); 601 } 602 if (setTargetIds) { 603 hiddenIds.addAll(0, oldIds); 604 } 605 for (SelectItem toMoveItem : toMove) { 606 String id = toMoveItem.getValue().toString(); 607 if (!oldIds.contains(id)) { 608 newSelectItems.add(toMoveItem); 609 if (setTargetIds) { 610 hiddenIds.add(id); 611 } 612 } 613 } 614 } 615 targetItems.setValue(newSelectItems.toArray(new SelectItem[] {})); 616 617 // update hidden values 618 int numValues = hiddenTargetList.getRowCount(); 619 if (numValues > 0) { 620 for (int i = numValues - 1; i > -1; i--) { 621 hiddenTargetList.removeValue(i); 622 } 623 } 624 for (String newHiddenValue : hiddenIds) { 625 hiddenTargetList.addValue(newHiddenValue); 626 } 627 } 628 629 /** 630 * Move items from components to others. 631 */ 632 public static void moveAllItems(UISelectItems sourceItems, UISelectItems targetItems, 633 UIEditableList hiddenTargetList, boolean setTargetIds) { 634 SelectItem[] all = (SelectItem[]) sourceItems.getValue(); 635 List<SelectItem> toMove = new ArrayList<SelectItem>(); 636 List<SelectItem> toKeep = new ArrayList<SelectItem>(); 637 List<String> hiddenIds = new ArrayList<String>(); 638 if (all != null) { 639 for (SelectItem item : all) { 640 if (!item.isDisabled()) { 641 toMove.add(item); 642 } else { 643 toKeep.add(item); 644 } 645 } 646 } 647 // reset left values 648 sourceItems.setValue(toKeep.toArray(new SelectItem[] {})); 649 650 // change right values 651 List<SelectItem> newSelectItems = new ArrayList<SelectItem>(); 652 SelectItem[] oldSelectItems = (SelectItem[]) targetItems.getValue(); 653 if (oldSelectItems == null) { 654 newSelectItems.addAll(toMove); 655 } else { 656 newSelectItems.addAll(Arrays.asList(oldSelectItems)); 657 List<String> oldIds = new ArrayList<String>(); 658 for (SelectItem oldItem : oldSelectItems) { 659 String id = oldItem.getValue().toString(); 660 oldIds.add(id); 661 } 662 if (setTargetIds) { 663 hiddenIds.addAll(0, oldIds); 664 } 665 for (SelectItem toMoveItem : toMove) { 666 String id = toMoveItem.getValue().toString(); 667 if (!oldIds.contains(id)) { 668 newSelectItems.add(toMoveItem); 669 if (setTargetIds) { 670 hiddenIds.add(id); 671 } 672 } 673 } 674 } 675 targetItems.setValue(newSelectItems.toArray(new SelectItem[] {})); 676 677 // update hidden values 678 int numValues = hiddenTargetList.getRowCount(); 679 if (numValues > 0) { 680 for (int i = numValues - 1; i > -1; i--) { 681 hiddenTargetList.removeValue(i); 682 } 683 } 684 for (String newHiddenValue : hiddenIds) { 685 hiddenTargetList.addValue(newHiddenValue); 686 } 687 } 688 689}