001/* 002 * (C) Copyright 2007-2008 Nuxeo SA (http://nuxeo.com/) and others. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 * 016 * Contributors: 017 * Nuxeo - initial API and implementation 018 * 019 * $Id: Functions.java 28572 2008-01-08 14:40:44Z fguillaume $ 020 */ 021 022package org.nuxeo.ecm.platform.ui.web.tag.fn; 023 024import java.text.DateFormat; 025import java.text.SimpleDateFormat; 026import java.util.ArrayList; 027import java.util.Collection; 028import java.util.Date; 029import java.util.HashMap; 030import java.util.Iterator; 031import java.util.List; 032import java.util.Locale; 033import java.util.Map; 034import java.util.regex.Matcher; 035import java.util.regex.Pattern; 036 037import javax.faces.component.UIComponent; 038import javax.faces.context.FacesContext; 039import javax.servlet.http.HttpServletRequest; 040 041import org.apache.commons.io.FilenameUtils; 042import org.apache.commons.lang.StringEscapeUtils; 043import org.apache.commons.lang.StringUtils; 044import org.nuxeo.common.utils.i18n.I18NUtils; 045import org.nuxeo.ecm.core.api.NuxeoGroup; 046import org.nuxeo.ecm.core.api.NuxeoPrincipal; 047import org.nuxeo.ecm.core.api.security.SecurityConstants; 048import org.nuxeo.ecm.platform.ui.web.rest.RestHelper; 049import org.nuxeo.ecm.platform.ui.web.rest.api.URLPolicyService; 050import org.nuxeo.ecm.platform.ui.web.util.BaseURL; 051import org.nuxeo.ecm.platform.ui.web.util.ComponentRenderUtils; 052import org.nuxeo.ecm.platform.ui.web.util.ComponentUtils; 053import org.nuxeo.ecm.platform.url.DocumentViewImpl; 054import org.nuxeo.ecm.platform.url.api.DocumentView; 055import org.nuxeo.ecm.platform.usermanager.UserManager; 056import org.nuxeo.runtime.api.Framework; 057import org.nuxeo.runtime.services.config.ConfigurationService; 058 059/** 060 * Util functions. 061 * 062 * @author <a href="mailto:at@nuxeo.com">Anahide Tchertchian</a> 063 * @author <a href="mailto:tm@nuxeo.com">Thierry Martins</a> 064 */ 065public final class Functions { 066 067 public static final String I18N_DURATION_PREFIX = "label.duration.unit."; 068 069 public static final String BIG_FILE_SIZE_LIMIT_PROPERTY = "org.nuxeo.big.file.size.limit"; 070 071 public static final long DEFAULT_BIG_FILE_SIZE_LIMIT = 5 * 1024 * 1024; 072 073 /** 074 * @since 7.4 075 */ 076 public static final String BYTE_PREFIX_FORMAT_PROPERTY = "nuxeo.jsf.defaultBytePrefixFormat"; 077 078 /** 079 * @since 7.4 080 */ 081 public static final String DEFAULT_BYTE_PREFIX_FORMAT = "SI"; 082 083 public static final Pattern YEAR_PATTERN = Pattern.compile("y+"); 084 085 public enum BytePrefix { 086 087 SI(1000, new String[] { "", "k", "M", "G", "T", "P", "E", "Z", "Y" }, new String[] { "", "kilo", "mega", 088 "giga", "tera", "peta", "exa", "zetta", "yotta" }), IEC(1024, new String[] { "", "Ki", "Mi", "Gi", 089 "Ti", "Pi", "Ei", "Zi", "Yi" }, new String[] { "", "kibi", "mebi", "gibi", "tebi", "pebi", "exbi", 090 "zebi", "yobi" }), JEDEC(1024, new String[] { "", "K", "M", "G" }, new String[] { "", "kilo", "mega", 091 "giga" }); 092 093 private final int base; 094 095 private final String[] shortSuffixes; 096 097 private final String[] longSuffixes; 098 099 BytePrefix(int base, String[] shortSuffixes, String[] longSuffixes) { 100 this.base = base; 101 this.shortSuffixes = shortSuffixes; 102 this.longSuffixes = longSuffixes; 103 } 104 105 public int getBase() { 106 return base; 107 } 108 109 public String[] getShortSuffixes() { 110 return shortSuffixes; 111 } 112 113 public String[] getLongSuffixes() { 114 return longSuffixes; 115 } 116 117 } 118 119 // XXX we should not use a static variable for this cache, but use a cache 120 // at a higher level in the Framework or in a facade. 121 private static UserManager userManager; 122 123 /** 124 * @since 7.2 125 */ 126 private static UserNameResolverHelper userNameResolver = new UserNameResolverHelper(); 127 128 static final Map<String, String> mapOfDateLength = new HashMap<String, String>() { 129 { 130 put("short", String.valueOf(DateFormat.SHORT)); 131 put("shortWithCentury".toLowerCase(), "shortWithCentury".toLowerCase()); 132 put("medium", String.valueOf(DateFormat.MEDIUM)); 133 put("long", String.valueOf(DateFormat.LONG)); 134 put("full", String.valueOf(DateFormat.FULL)); 135 } 136 137 private static final long serialVersionUID = 8465772256977862352L; 138 }; 139 140 // Utility class. 141 private Functions() { 142 } 143 144 public static Object test(Boolean test, Object onSuccess, Object onFailure) { 145 return test ? onSuccess : onFailure; 146 } 147 148 public static String join(String[] list, String separator) { 149 return StringUtils.join(list, separator); 150 } 151 152 public static String joinCollection(Collection<Object> collection, String separator) { 153 if (collection == null) { 154 return null; 155 } 156 return StringUtils.join(collection.iterator(), separator); 157 } 158 159 public static String htmlEscape(String data) { 160 return StringEscapeUtils.escapeHtml(data); 161 } 162 163 /** 164 * Escapes a given string to be used in a JavaScript function (escaping single quote characters for instance). 165 * 166 * @since 5.4.2 167 */ 168 public static String javaScriptEscape(String data) { 169 if (data != null) { 170 data = StringEscapeUtils.escapeJavaScript(data); 171 } 172 return data; 173 } 174 175 /** 176 * Can be used in order to produce something like that "Julien, Alain , Thierry et Marc-Aurele" where ' , ' and ' et 177 * ' is the final one. 178 */ 179 public static String joinCollectionWithFinalDelimiter(Collection<Object> collection, String separator, 180 String finalSeparator) { 181 return joinArrayWithFinalDelimiter(collection.toArray(), separator, finalSeparator); 182 } 183 184 public static String joinArrayWithFinalDelimiter(Object[] collection, String separator, String finalSeparator) { 185 if (collection == null) { 186 return null; 187 } 188 StringBuffer result = new StringBuffer(); 189 result.append(StringUtils.join(collection, separator)); 190 if (collection != null && collection.length > 0 && !StringUtils.isBlank(finalSeparator)) { 191 result.append(finalSeparator); 192 } 193 return result.toString(); 194 } 195 196 public static String formatDateUsingBasicFormatter(Date date) { 197 return formatDate(date, basicDateFormatter()); 198 } 199 200 public static String formatDate(Date date, String format) { 201 return new SimpleDateFormat(format).format(date); 202 } 203 204 public static String concat(String s1, String s2) { 205 return s1 + s2; 206 } 207 208 public static String indentString(Integer level, String text) { 209 StringBuilder label = new StringBuilder(""); 210 for (int i = 0; i < level; i++) { 211 label.append(text); 212 } 213 return label.toString(); 214 } 215 216 public static boolean userIsMemberOf(String groupName) { 217 FacesContext context = FacesContext.getCurrentInstance(); 218 NuxeoPrincipal principal = (NuxeoPrincipal) context.getExternalContext().getUserPrincipal(); 219 return principal.isMemberOf(groupName); 220 } 221 222 private static UserManager getUserManager() { 223 if (userManager == null) { 224 userManager = Framework.getService(UserManager.class); 225 } 226 return userManager; 227 } 228 229 /** 230 * Returns the full name of a user, or its username if user if not found. 231 * <p> 232 * Since 5.5, returns null if given username is null (instead of returning the current user full name). 233 */ 234 public static String userFullName(String username) { 235 if (SecurityConstants.SYSTEM_USERNAME.equals(username)) { 236 // avoid costly and useless calls to the user directory 237 return username; 238 } 239 240 // empty user name is current user 241 if (StringUtils.isBlank(username)) { 242 return null; 243 } 244 return userNameResolver.getUserFullName(username); 245 } 246 247 /** 248 * Returns the full name of a group from his id 249 * 250 * @see #groupDisplayName(String, String) 251 * @param groupId the group id 252 * @return the group full name 253 * @since 5.5 254 */ 255 public static String groupFullName(String groupId) { 256 NuxeoGroup group = getUserManager().getGroup(groupId); 257 String groupLabel = group.getLabel(); 258 String groupName = group.getName(); 259 return groupDisplayName(groupName, groupLabel); 260 } 261 262 // this should be a method of the principal itself 263 public static String principalFullName(NuxeoPrincipal principal) { 264 String first = principal.getFirstName(); 265 String last = principal.getLastName(); 266 return userDisplayName(principal.getName(), first, last); 267 } 268 269 public static String userDisplayName(String id, String first, String last) { 270 if (first == null || first.length() == 0) { 271 if (last == null || last.length() == 0) { 272 return id; 273 } else { 274 return last; 275 } 276 } else { 277 if (last == null || last.length() == 0) { 278 return first; 279 } else { 280 return first + ' ' + last; 281 } 282 } 283 } 284 285 /** 286 * Return, from the id, the id its-self if neither last name nor name are found or the full name plus the email if 287 * this one exists 288 * 289 * @param id id of the user 290 * @param first first name of the user 291 * @param last last name of the user 292 * @param email email of the user 293 * @return id or full name with email if exists 294 * @since 5.5 295 */ 296 public static String userDisplayNameAndEmail(String id, String first, String last, String email) { 297 String userDisplayedName = userDisplayName(id, first, last); 298 if (userDisplayedName.equals(id)) { 299 return userDisplayedName; 300 } 301 if (email == null || email.length() == 0) { 302 return userDisplayedName; 303 } 304 return userDisplayedName + " " + email; 305 } 306 307 /** 308 * Choose between label or name the best string to display a group 309 * 310 * @param name the group name 311 * @param label the group name 312 * @return label if not empty or null, otherwise group name 313 * @since 5.5 314 */ 315 public static String groupDisplayName(String name, String label) { 316 return StringUtils.isBlank(label) ? name : label; 317 } 318 319 /** 320 * Return the date format to handle date taking the user's locale into account. 321 * 322 * @since 5.9.1 323 */ 324 public static String dateFormatter(String formatLength) { 325 // A map to store temporary available date format 326 FacesContext context = FacesContext.getCurrentInstance(); 327 Locale locale = context.getViewRoot().getLocale(); 328 329 int style = DateFormat.SHORT; 330 String styleString = mapOfDateLength.get(formatLength.toLowerCase()); 331 boolean addCentury = false; 332 if ("shortWithCentury".toLowerCase().equals(styleString)) { 333 addCentury = true; 334 } else { 335 style = Integer.parseInt(styleString); 336 } 337 338 DateFormat aDateFormat = DateFormat.getDateInstance(style, locale); 339 340 // Cast to SimpleDateFormat to make "toPattern" method available 341 SimpleDateFormat format = (SimpleDateFormat) aDateFormat; 342 343 // return the date pattern 344 String pattern = format.toPattern(); 345 346 if (style == DateFormat.SHORT && addCentury) { 347 // hack to add century on generated pattern 348 pattern = YEAR_PATTERN.matcher(pattern).replaceAll("yyyy"); 349 } 350 return pattern; 351 } 352 353 /** 354 * Return the date format to handle date taking the user's locale into account. Uses the pseudo "shortWithCentury" 355 * format. 356 * 357 * @since 5.9.1 358 */ 359 public static String basicDateFormatter() { 360 return dateFormatter("shortWithCentury"); 361 } 362 363 /** 364 * Return the date format to handle date and time taking the user's locale into account. 365 * 366 * @since 5.9.1 367 */ 368 public static String dateAndTimeFormatter(String formatLength) { 369 370 // A map to store temporary available date format 371 372 FacesContext context = FacesContext.getCurrentInstance(); 373 Locale locale = context.getViewRoot().getLocale(); 374 375 int style = DateFormat.SHORT; 376 String styleString = mapOfDateLength.get(formatLength.toLowerCase()); 377 boolean addCentury = false; 378 if ("shortWithCentury".toLowerCase().equals(styleString)) { 379 addCentury = true; 380 } else { 381 style = Integer.parseInt(styleString); 382 } 383 384 DateFormat aDateFormat = DateFormat.getDateTimeInstance(style, style, locale); 385 386 // Cast to SimpleDateFormat to make "toPattern" method available 387 SimpleDateFormat format = (SimpleDateFormat) aDateFormat; 388 389 // return the date pattern 390 String pattern = format.toPattern(); 391 392 if (style == DateFormat.SHORT && addCentury) { 393 // hack to add century on generated pattern 394 pattern = YEAR_PATTERN.matcher(pattern).replaceAll("yyyy"); 395 } 396 return pattern; 397 } 398 399 /** 400 * Return the date format to handle date and time taking the user's locale into account. Uses the pseudo 401 * "shortWithCentury" format. 402 * 403 * @since 5.9.1 404 */ 405 public static String basicDateAndTimeFormatter() { 406 return dateAndTimeFormatter("shortWithCentury"); 407 } 408 409 /** 410 * Returns the default byte prefix. 411 * 412 * @since 7.4 413 */ 414 public static BytePrefix getDefaultBytePrefix() { 415 ConfigurationService configurationService = Framework.getService(ConfigurationService.class); 416 return BytePrefix.valueOf( 417 configurationService.getProperty(BYTE_PREFIX_FORMAT_PROPERTY, DEFAULT_BYTE_PREFIX_FORMAT)); 418 } 419 420 public static String printFileSize(String size) { 421 return printFormatedFileSize(size, getDefaultBytePrefix().name(), true); 422 } 423 424 public static String printFormatedFileSize(String sizeS, String format, Boolean isShort) { 425 long size = (sizeS == null || "".equals(sizeS)) ? 0 : Long.parseLong(sizeS); 426 BytePrefix prefix = Enum.valueOf(BytePrefix.class, format); 427 int base = prefix.getBase(); 428 String[] suffix = isShort ? prefix.getShortSuffixes() : prefix.getLongSuffixes(); 429 int ex = 0; 430 while (size > base - 1 || ex > suffix.length) { 431 ex++; 432 size /= base; 433 } 434 435 FacesContext context = FacesContext.getCurrentInstance(); 436 String msg; 437 if (context != null) { 438 String bundleName = context.getApplication().getMessageBundle(); 439 Locale locale = context.getViewRoot().getLocale(); 440 msg = I18NUtils.getMessageString(bundleName, "label.bytes.suffix", null, locale); 441 if ("label.bytes.suffix".equals(msg)) { 442 // Set default value if no message entry found 443 msg = "B"; 444 } 445 } else { 446 // No faces context, set default value 447 msg = "B"; 448 } 449 450 return "" + size + " " + suffix[ex] + msg; 451 } 452 453 /** 454 * Format the duration of a media in a string of two consecutive units to best express the duration of a media, 455 * e.g.: 456 * <ul> 457 * <li>1 hr 42 min</li> 458 * <li>2 min 25 sec</li> 459 * <li>10 sec</li> 460 * <li>0 sec</li> 461 * </ul> 462 * 463 * @param durationObj a Float, Double, Integer, Long or String instance representing a duration in seconds 464 * @param i18nLabels a map to translate the days, hours, minutes and seconds labels 465 * @return the formatted string 466 */ 467 public static String printFormattedDuration(Object durationObj, Map<String, String> i18nLabels) { 468 469 if (i18nLabels == null) { 470 i18nLabels = new HashMap<String, String>(); 471 } 472 double duration = 0.0; 473 if (durationObj instanceof Float) { 474 duration = ((Float) durationObj).doubleValue(); 475 } else if (durationObj instanceof Double) { 476 duration = ((Double) durationObj).doubleValue(); 477 } else if (durationObj instanceof Integer) { 478 duration = ((Integer) durationObj).doubleValue(); 479 } else if (durationObj instanceof Long) { 480 duration = ((Long) durationObj).doubleValue(); 481 } else if (durationObj instanceof String) { 482 duration = Double.parseDouble((String) durationObj); 483 } 484 485 int days = (int) Math.floor(duration / (24 * 60 * 60)); 486 int hours = (int) Math.floor(duration / (60 * 60)) - days * 24; 487 int minutes = (int) Math.floor(duration / 60) - days * 24 * 60 - hours * 60; 488 int seconds = (int) Math.floor(duration) - days * 24 * 3600 - hours * 3600 - minutes * 60; 489 490 int[] components = { days, hours, minutes, seconds }; 491 String[] units = { "days", "hours", "minutes", "seconds" }; 492 String[] defaultLabels = { "d", "hr", "min", "sec" }; 493 494 String representation = null; 495 for (int i = 0; i < components.length; i++) { 496 if (components[i] != 0 || i == components.length - 1) { 497 String i18nLabel = i18nLabels.get(I18N_DURATION_PREFIX + units[i]); 498 if (i18nLabel == null) { 499 i18nLabel = defaultLabels[i]; 500 } 501 representation = String.format("%d %s", components[i], i18nLabel); 502 if (i < components.length - 1) { 503 i18nLabel = i18nLabels.get(I18N_DURATION_PREFIX + units[i + 1]); 504 if (i18nLabel == null) { 505 i18nLabel = defaultLabels[i + 1]; 506 } 507 representation += String.format(" %d %s", components[i + 1], i18nLabel); 508 } 509 break; 510 } 511 } 512 return representation; 513 } 514 515 public static String printFormattedDuration(Object durationObj) { 516 return printFormattedDuration(durationObj, null); 517 } 518 519 public static final String translate(String messageId, Object... params) { 520 return ComponentUtils.translate(FacesContext.getCurrentInstance(), messageId, params); 521 } 522 523 /** 524 * @return the big file size limit defined with the property org.nuxeo.big.file.size.limit 525 */ 526 public static long getBigFileSizeLimit() { 527 return getFileSize(Framework.getProperty(BIG_FILE_SIZE_LIMIT_PROPERTY, "")); 528 } 529 530 public static long getFileSize(String value) { 531 Pattern pattern = Pattern.compile("([1-9][0-9]*)([kmgi]*)", Pattern.CASE_INSENSITIVE); 532 Matcher m = pattern.matcher(value.trim()); 533 long number; 534 String multiplier; 535 if (!m.matches()) { 536 return DEFAULT_BIG_FILE_SIZE_LIMIT; 537 } 538 number = Long.valueOf(m.group(1)); 539 multiplier = m.group(2); 540 return getValueFromMultiplier(multiplier) * number; 541 } 542 543 /** 544 * Transform the parameter in entry according to these unit systems: 545 * <ul> 546 * <li>SI prefixes: k/M/G for kilo, mega, giga</li> 547 * <li>IEC prefixes: Ki/Mi/Gi for kibi, mebi, gibi</li> 548 * </ul> 549 * 550 * @param m : binary prefix multiplier 551 * @return the value of the multiplier as a long 552 */ 553 public static long getValueFromMultiplier(String m) { 554 if ("k".equalsIgnoreCase(m)) { 555 return 1L * 1000; 556 } else if ("Ki".equalsIgnoreCase(m)) { 557 return 1L << 10; 558 } else if ("M".equalsIgnoreCase(m)) { 559 return 1L * 1000 * 1000; 560 } else if ("Mi".equalsIgnoreCase(m)) { 561 return 1L << 20; 562 } else if ("G".equalsIgnoreCase(m)) { 563 return 1L * 1000 * 1000 * 1000; 564 } else if ("Gi".equalsIgnoreCase(m)) { 565 return 1L << 30; 566 } else { 567 return 1L; 568 } 569 } 570 571 /** 572 * Returns true if the faces context holds messages for given JSF component id, usually the form id. 573 * <p> 574 * Id given id is null, returns true if there is at least one client id with messages. 575 * <p> 576 * Since the form id might be prefixed with a container id in some cases, the method returns true if one of client 577 * ids with messages stats with given id, or if given id is contained in it. 578 * 579 * @since 5.4.2 580 */ 581 public static boolean hasMessages(String clientId) { 582 Iterator<String> it = FacesContext.getCurrentInstance().getClientIdsWithMessages(); 583 if (clientId == null) { 584 return it.hasNext(); 585 } else { 586 while (it.hasNext()) { 587 String id = it.next(); 588 if (id != null 589 && (id.startsWith(clientId + ":") || id.contains(":" + clientId + ":") || id.equals(clientId) || id.endsWith(":" 590 + clientId))) { 591 return true; 592 } 593 } 594 } 595 return false; 596 } 597 598 public static String userUrl(String patternName, String username, String viewId, boolean newConversation) { 599 return userUrl(patternName, username, viewId, newConversation, null); 600 } 601 602 public static String userUrl(String patternName, String username, String viewId, boolean newConversation, 603 HttpServletRequest req) { 604 Map<String, String> parameters = new HashMap<String, String>(); 605 parameters.put("username", username); 606 DocumentView docView = new DocumentViewImpl(null, viewId, parameters); 607 608 // generate url 609 URLPolicyService service = Framework.getService(URLPolicyService.class); 610 if (patternName == null || patternName.length() == 0) { 611 patternName = service.getDefaultPatternName(); 612 } 613 614 String baseURL = null; 615 if (req == null) { 616 baseURL = BaseURL.getBaseURL(); 617 } else { 618 baseURL = BaseURL.getBaseURL(req); 619 } 620 621 String url = service.getUrlFromDocumentView(patternName, docView, baseURL); 622 623 // pass conversation info if needed 624 if (!newConversation && url != null) { 625 url = RestHelper.addCurrentConversationParameters(url); 626 } 627 628 return url; 629 } 630 631 public static List<Object> combineLists(List<? extends Object>... lists) { 632 List<Object> combined = new ArrayList<Object>(); 633 for (List<? extends Object> list : lists) { 634 combined.addAll(list); 635 } 636 return combined; 637 } 638 639 /** 640 * Helper that escapes a string used as a JSF tag id: this is useful to replace characters that are not handled 641 * correctly in JSF context. 642 * <p> 643 * This method currently removes ASCII characters from the given string, and replaces "-" characters by "_" because 644 * the dash is an issue for forms rendered in ajax (see NXP-10793). 645 * <p> 646 * Also starting digits are replaced by the "_" character because a tag id cannot start with a digit. 647 * 648 * @since 5.7 649 * @return the escaped string 650 */ 651 public static String jsfTagIdEscape(String base) { 652 if (base == null) { 653 return null; 654 } 655 int n = base.length(); 656 StringBuilder res = new StringBuilder(); 657 for (int i = 0; i < n; i++) { 658 char c = base.charAt(i); 659 if (i == 0) { 660 if (!Character.isLetter(c) && (c != '_')) { 661 res.append("_"); 662 } else { 663 res.append(c); 664 } 665 } else { 666 if (!Character.isLetter(c) && !Character.isDigit(c) && (c != '_')) { 667 res.append("_"); 668 } else { 669 res.append(c); 670 } 671 } 672 } 673 return org.nuxeo.common.utils.StringUtils.toAscii(res.toString()); 674 } 675 676 /** 677 * Returns the extension from the given {@code filename}. 678 * <p> 679 * See {@link FilenameUtils#getExtension(String)}. 680 * 681 * @since 5.7 682 */ 683 public static String fileExtension(String filename) { 684 return FilenameUtils.getExtension(filename); 685 } 686 687 /** 688 * Returns the base name from the given {@code filename}. 689 * <p> 690 * See {@link FilenameUtils#getBaseName(String)}. 691 * 692 * @since 5.7 693 */ 694 public static String fileBaseName(String filename) { 695 return FilenameUtils.getBaseName(filename); 696 } 697 698 /** 699 * Joins two strings to get a valid render attribute for ajax components. 700 * 701 * @since 6.0 702 */ 703 public static String joinRender(String render1, String render2) { 704 if (StringUtils.isBlank(render1) && StringUtils.isBlank(render2)) { 705 return ""; 706 } 707 String res; 708 if (StringUtils.isBlank(render1)) { 709 res = render2; 710 } else if (StringUtils.isBlank(render2)) { 711 res = render1; 712 } else { 713 res = StringUtils.join(new String[] { render1, render2 }, " "); 714 res = res.replaceAll("\\s+", " "); 715 } 716 res = res.trim(); 717 return res; 718 } 719 720 /** 721 * Returns the target component absolute id given an anchor in the tree and a local id. 722 * <p> 723 * If given targetId parameter contains spaces, consider several ids should be resolved and split them. 724 * 725 * @since 6.0 726 * @param anchor the component anchor, used a localization for the target component in the tree. 727 * @param targetId the component to look for locally so as to return its absolute client id. 728 */ 729 public static String componentAbsoluteId(UIComponent anchor, String targetId) { 730 // handle case where several target ids could be given as input 731 if (targetId == null) { 732 return null; 733 } 734 if (targetId.contains(" ")) { 735 String res = ""; 736 for (String t : targetId.split(" ")) { 737 res = joinRender(res, ComponentRenderUtils.getComponentAbsoluteId(anchor, t.trim())); 738 } 739 return res; 740 } else { 741 return ComponentRenderUtils.getComponentAbsoluteId(anchor, targetId); 742 } 743 } 744}