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}