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.text.StringEscapeUtils;
043import org.apache.commons.lang3.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.escapeHtml4(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.escapeEcmaScript(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    /**
201     * Formats a date using the pattern "EEEE, MMMM d, yyyy h:mm:ss a z". This pattern is the US one and is
202     * being used for consistency since it changes through the various locales
203     *
204     * @since 10.1
205     */
206    public static String formatDateUsingFullDateAndTimeFormatter(Date date) {
207        // We're using the US one for consistency.
208        String dateFormat = "EEEE, MMMM d, yyyy h:mm:ss a z";
209        return formatDate(date, dateFormat);
210    }
211
212    public static String formatDate(Date date, String format) {
213        FacesContext context = FacesContext.getCurrentInstance();
214        Locale locale = context.getViewRoot().getLocale();
215
216        return new SimpleDateFormat(format, locale)
217                .format(date);
218    }
219
220    public static String concat(String s1, String s2) {
221        return s1 + s2;
222    }
223
224    public static String indentString(Integer level, String text) {
225        StringBuilder label = new StringBuilder("");
226        for (int i = 0; i < level; i++) {
227            label.append(text);
228        }
229        return label.toString();
230    }
231
232    public static boolean userIsMemberOf(String groupName) {
233        FacesContext context = FacesContext.getCurrentInstance();
234        NuxeoPrincipal principal = (NuxeoPrincipal) context.getExternalContext().getUserPrincipal();
235        return principal.isMemberOf(groupName);
236    }
237
238    private static UserManager getUserManager() {
239        if (userManager == null) {
240            userManager = Framework.getService(UserManager.class);
241        }
242        return userManager;
243    }
244
245    /**
246     * Returns the full name of a user, or its username if user if not found.
247     * <p>
248     * Since 5.5, returns null if given username is null (instead of returning the current user full name).
249     */
250    public static String userFullName(String username) {
251        if (SecurityConstants.SYSTEM_USERNAME.equals(username)) {
252            // avoid costly and useless calls to the user directory
253            return username;
254        }
255
256        // empty user name is current user
257        if (StringUtils.isBlank(username)) {
258            return null;
259        }
260        return userNameResolver.getUserFullName(username);
261    }
262
263    /**
264     * Returns the full name of a group from his id
265     *
266     * @see #groupDisplayName(String, String)
267     * @param groupId the group id
268     * @return the group full name
269     * @since 5.5
270     */
271    public static String groupFullName(String groupId) {
272        NuxeoGroup group = getUserManager().getGroup(groupId);
273        String groupLabel = group.getLabel();
274        String groupName = group.getName();
275        return groupDisplayName(groupName, groupLabel);
276    }
277
278    // this should be a method of the principal itself
279    public static String principalFullName(NuxeoPrincipal principal) {
280        String first = principal.getFirstName();
281        String last = principal.getLastName();
282        return userDisplayName(principal.getName(), first, last);
283    }
284
285    public static String userDisplayName(String id, String first, String last) {
286        if (first == null || first.length() == 0) {
287            if (last == null || last.length() == 0) {
288                return id;
289            } else {
290                return last;
291            }
292        } else {
293            if (last == null || last.length() == 0) {
294                return first;
295            } else {
296                return first + ' ' + last;
297            }
298        }
299    }
300
301    /**
302     * Return, from the id, the id its-self if neither last name nor name are found or the full name plus the email if
303     * this one exists
304     *
305     * @param id id of the user
306     * @param first first name of the user
307     * @param last last name of the user
308     * @param email email of the user
309     * @return id or full name with email if exists
310     * @since 5.5
311     */
312    public static String userDisplayNameAndEmail(String id, String first, String last, String email) {
313        String userDisplayedName = userDisplayName(id, first, last);
314        if (userDisplayedName.equals(id)) {
315            return userDisplayedName;
316        }
317        if (email == null || email.length() == 0) {
318            return userDisplayedName;
319        }
320        return userDisplayedName + " " + email;
321    }
322
323    /**
324     * Choose between label or name the best string to display a group
325     *
326     * @param name the group name
327     * @param label the group name
328     * @return label if not empty or null, otherwise group name
329     * @since 5.5
330     */
331    public static String groupDisplayName(String name, String label) {
332        return StringUtils.isBlank(label) ? name : label;
333    }
334
335    /**
336     * Return the date format to handle date taking the user's locale into account.
337     *
338     * @since 5.9.1
339     */
340    public static String dateFormatter(String formatLength) {
341        // A map to store temporary available date format
342        FacesContext context = FacesContext.getCurrentInstance();
343        Locale locale = context.getViewRoot().getLocale();
344
345        int style = DateFormat.SHORT;
346        String styleString = mapOfDateLength.get(formatLength.toLowerCase());
347        boolean addCentury = false;
348        if ("shortWithCentury".toLowerCase().equals(styleString)) {
349            addCentury = true;
350        } else {
351            style = Integer.parseInt(styleString);
352        }
353
354        DateFormat aDateFormat = DateFormat.getDateInstance(style, locale);
355
356        // Cast to SimpleDateFormat to make "toPattern" method available
357        SimpleDateFormat format = (SimpleDateFormat) aDateFormat;
358
359        // return the date pattern
360        String pattern = format.toPattern();
361
362        if (style == DateFormat.SHORT && addCentury) {
363            // hack to add century on generated pattern
364            pattern = YEAR_PATTERN.matcher(pattern).replaceAll("yyyy");
365        }
366        return pattern;
367    }
368
369    /**
370     * Return the date format to handle date taking the user's locale into account. Uses the pseudo "shortWithCentury"
371     * format.
372     *
373     * @since 5.9.1
374     */
375    public static String basicDateFormatter() {
376        return dateFormatter("shortWithCentury");
377    }
378
379    /**
380     * Return the date format to handle date and time taking the user's locale into account.
381     *
382     * @since 5.9.1
383     */
384    public static String dateAndTimeFormatter(String formatLength) {
385
386        // A map to store temporary available date format
387
388        FacesContext context = FacesContext.getCurrentInstance();
389        Locale locale = context.getViewRoot().getLocale();
390
391        int style = DateFormat.SHORT;
392        String styleString = mapOfDateLength.get(formatLength.toLowerCase());
393        boolean addCentury = false;
394        if ("shortWithCentury".toLowerCase().equals(styleString)) {
395            addCentury = true;
396        } else {
397            style = Integer.parseInt(styleString);
398        }
399
400        DateFormat aDateFormat = DateFormat.getDateTimeInstance(style, style, locale);
401
402        // Cast to SimpleDateFormat to make "toPattern" method available
403        SimpleDateFormat format = (SimpleDateFormat) aDateFormat;
404
405        // return the date pattern
406        String pattern = format.toPattern();
407
408        if (style == DateFormat.SHORT && addCentury) {
409            // hack to add century on generated pattern
410            pattern = YEAR_PATTERN.matcher(pattern).replaceAll("yyyy");
411        }
412        return pattern;
413    }
414
415    /**
416     * Return the date format to handle date and time taking the user's locale into account. Uses the pseudo
417     * "shortWithCentury" format.
418     *
419     * @since 5.9.1
420     */
421    public static String basicDateAndTimeFormatter() {
422        return dateAndTimeFormatter("shortWithCentury");
423    }
424
425    /**
426     * Returns the default byte prefix.
427     *
428     * @since 7.4
429     */
430    public static BytePrefix getDefaultBytePrefix() {
431        ConfigurationService configurationService = Framework.getService(ConfigurationService.class);
432        return BytePrefix.valueOf(
433                configurationService.getProperty(BYTE_PREFIX_FORMAT_PROPERTY, DEFAULT_BYTE_PREFIX_FORMAT));
434    }
435
436    public static String printFileSize(String size) {
437        return printFormatedFileSize(size, getDefaultBytePrefix().name(), true);
438    }
439
440    public static String printFormatedFileSize(String sizeS, String format, Boolean isShort) {
441        long size = (sizeS == null || "".equals(sizeS)) ? 0 : Long.parseLong(sizeS);
442        BytePrefix prefix = Enum.valueOf(BytePrefix.class, format);
443        int base = prefix.getBase();
444        String[] suffix = isShort ? prefix.getShortSuffixes() : prefix.getLongSuffixes();
445        int ex = 0;
446        while (size > base - 1 || ex > suffix.length) {
447            ex++;
448            size /= base;
449        }
450
451        FacesContext context = FacesContext.getCurrentInstance();
452        String msg;
453        if (context != null) {
454            String bundleName = context.getApplication().getMessageBundle();
455            Locale locale = context.getViewRoot().getLocale();
456            msg = I18NUtils.getMessageString(bundleName, "label.bytes.suffix", null, locale);
457            if ("label.bytes.suffix".equals(msg)) {
458                // Set default value if no message entry found
459                msg = "B";
460            }
461        } else {
462            // No faces context, set default value
463            msg = "B";
464        }
465
466        return "" + size + " " + suffix[ex] + msg;
467    }
468
469    /**
470     * Format the duration of a media in a string of two consecutive units to best express the duration of a media,
471     * e.g.:
472     * <ul>
473     * <li>1 hr 42 min</li>
474     * <li>2 min 25 sec</li>
475     * <li>10 sec</li>
476     * <li>0 sec</li>
477     * </ul>
478     *
479     * @param durationObj a Float, Double, Integer, Long or String instance representing a duration in seconds
480     * @param i18nLabels a map to translate the days, hours, minutes and seconds labels
481     * @return the formatted string
482     */
483    public static String printFormattedDuration(Object durationObj, Map<String, String> i18nLabels) {
484
485        if (i18nLabels == null) {
486            i18nLabels = new HashMap<String, String>();
487        }
488        double duration = 0.0;
489        if (durationObj instanceof Float) {
490            duration = ((Float) durationObj).doubleValue();
491        } else if (durationObj instanceof Double) {
492            duration = ((Double) durationObj).doubleValue();
493        } else if (durationObj instanceof Integer) {
494            duration = ((Integer) durationObj).doubleValue();
495        } else if (durationObj instanceof Long) {
496            duration = ((Long) durationObj).doubleValue();
497        } else if (durationObj instanceof String) {
498            duration = Double.parseDouble((String) durationObj);
499        }
500
501        int days = (int) Math.floor(duration / (24 * 60 * 60));
502        int hours = (int) Math.floor(duration / (60 * 60)) - days * 24;
503        int minutes = (int) Math.floor(duration / 60) - days * 24 * 60 - hours * 60;
504        int seconds = (int) Math.floor(duration) - days * 24 * 3600 - hours * 3600 - minutes * 60;
505
506        int[] components = { days, hours, minutes, seconds };
507        String[] units = { "days", "hours", "minutes", "seconds" };
508        String[] defaultLabels = { "d", "hr", "min", "sec" };
509
510        String representation = null;
511        for (int i = 0; i < components.length; i++) {
512            if (components[i] != 0 || i == components.length - 1) {
513                String i18nLabel = i18nLabels.get(I18N_DURATION_PREFIX + units[i]);
514                if (i18nLabel == null) {
515                    i18nLabel = defaultLabels[i];
516                }
517                representation = String.format("%d %s", components[i], i18nLabel);
518                if (i < components.length - 1) {
519                    i18nLabel = i18nLabels.get(I18N_DURATION_PREFIX + units[i + 1]);
520                    if (i18nLabel == null) {
521                        i18nLabel = defaultLabels[i + 1];
522                    }
523                    representation += String.format(" %d %s", components[i + 1], i18nLabel);
524                }
525                break;
526            }
527        }
528        return representation;
529    }
530
531    public static String printFormattedDuration(Object durationObj) {
532        return printFormattedDuration(durationObj, null);
533    }
534
535    public static final String translate(String messageId, Object... params) {
536        return ComponentUtils.translate(FacesContext.getCurrentInstance(), messageId, params);
537    }
538
539    /**
540     * @return the big file size limit defined with the property org.nuxeo.big.file.size.limit
541     */
542    public static long getBigFileSizeLimit() {
543        return getFileSize(Framework.getProperty(BIG_FILE_SIZE_LIMIT_PROPERTY, ""));
544    }
545
546    public static long getFileSize(String value) {
547        Pattern pattern = Pattern.compile("([1-9][0-9]*)([kmgi]*)", Pattern.CASE_INSENSITIVE);
548        Matcher m = pattern.matcher(value.trim());
549        long number;
550        String multiplier;
551        if (!m.matches()) {
552            return DEFAULT_BIG_FILE_SIZE_LIMIT;
553        }
554        number = Long.valueOf(m.group(1));
555        multiplier = m.group(2);
556        return getValueFromMultiplier(multiplier) * number;
557    }
558
559    /**
560     * Transform the parameter in entry according to these unit systems:
561     * <ul>
562     * <li>SI prefixes: k/M/G for kilo, mega, giga</li>
563     * <li>IEC prefixes: Ki/Mi/Gi for kibi, mebi, gibi</li>
564     * </ul>
565     *
566     * @param m : binary prefix multiplier
567     * @return the value of the multiplier as a long
568     */
569    public static long getValueFromMultiplier(String m) {
570        if ("k".equalsIgnoreCase(m)) {
571            return 1L * 1000;
572        } else if ("Ki".equalsIgnoreCase(m)) {
573            return 1L << 10;
574        } else if ("M".equalsIgnoreCase(m)) {
575            return 1L * 1000 * 1000;
576        } else if ("Mi".equalsIgnoreCase(m)) {
577            return 1L << 20;
578        } else if ("G".equalsIgnoreCase(m)) {
579            return 1L * 1000 * 1000 * 1000;
580        } else if ("Gi".equalsIgnoreCase(m)) {
581            return 1L << 30;
582        } else {
583            return 1L;
584        }
585    }
586
587    /**
588     * Returns true if the faces context holds messages for given JSF component id, usually the form id.
589     * <p>
590     * Id given id is null, returns true if there is at least one client id with messages.
591     * <p>
592     * Since the form id might be prefixed with a container id in some cases, the method returns true if one of client
593     * ids with messages stats with given id, or if given id is contained in it.
594     *
595     * @since 5.4.2
596     */
597    public static boolean hasMessages(String clientId) {
598        Iterator<String> it = FacesContext.getCurrentInstance().getClientIdsWithMessages();
599        if (clientId == null) {
600            return it.hasNext();
601        } else {
602            while (it.hasNext()) {
603                String id = it.next();
604                if (id != null
605                        && (id.startsWith(clientId + ":") || id.contains(":" + clientId + ":") || id.equals(clientId) || id.endsWith(":"
606                                + clientId))) {
607                    return true;
608                }
609            }
610        }
611        return false;
612    }
613
614    public static String userUrl(String patternName, String username, String viewId, boolean newConversation) {
615        return userUrl(patternName, username, viewId, newConversation, null);
616    }
617
618    public static String userUrl(String patternName, String username, String viewId, boolean newConversation,
619            HttpServletRequest req) {
620        Map<String, String> parameters = new HashMap<String, String>();
621        parameters.put("username", username);
622        DocumentView docView = new DocumentViewImpl(null, viewId, parameters);
623
624        // generate url
625        URLPolicyService service = Framework.getService(URLPolicyService.class);
626        if (patternName == null || patternName.length() == 0) {
627            patternName = service.getDefaultPatternName();
628        }
629
630        String baseURL = null;
631        if (req == null) {
632            baseURL = BaseURL.getBaseURL();
633        } else {
634            baseURL = BaseURL.getBaseURL(req);
635        }
636
637        String url = service.getUrlFromDocumentView(patternName, docView, baseURL);
638
639        // pass conversation info if needed
640        if (!newConversation && url != null) {
641            url = RestHelper.addCurrentConversationParameters(url);
642        }
643
644        return url;
645    }
646
647    public static List<Object> combineLists(List<? extends Object>... lists) {
648        List<Object> combined = new ArrayList<Object>();
649        for (List<? extends Object> list : lists) {
650            combined.addAll(list);
651        }
652        return combined;
653    }
654
655    /**
656     * Helper that escapes a string used as a JSF tag id: this is useful to replace characters that are not handled
657     * correctly in JSF context.
658     * <p>
659     * This method currently removes ASCII characters from the given string, and replaces "-" characters by "_" because
660     * the dash is an issue for forms rendered in ajax (see NXP-10793).
661     * <p>
662     * Also starting digits are replaced by the "_" character because a tag id cannot start with a digit.
663     *
664     * @since 5.7
665     * @return the escaped string
666     */
667    public static String jsfTagIdEscape(String base) {
668        if (base == null) {
669            return null;
670        }
671        int n = base.length();
672        StringBuilder res = new StringBuilder();
673        for (int i = 0; i < n; i++) {
674            char c = base.charAt(i);
675            if (i == 0) {
676                if (!Character.isLetter(c) && (c != '_')) {
677                    res.append("_");
678                } else {
679                    res.append(c);
680                }
681            } else {
682                if (!Character.isLetter(c) && !Character.isDigit(c) && (c != '_')) {
683                    res.append("_");
684                } else {
685                    res.append(c);
686                }
687            }
688        }
689        return org.nuxeo.common.utils.StringUtils.toAscii(res.toString());
690    }
691
692    /**
693     * Returns the extension from the given {@code filename}.
694     * <p>
695     * See {@link FilenameUtils#getExtension(String)}.
696     *
697     * @since 5.7
698     */
699    public static String fileExtension(String filename) {
700        return FilenameUtils.getExtension(filename);
701    }
702
703    /**
704     * Returns the base name from the given {@code filename}.
705     * <p>
706     * See {@link FilenameUtils#getBaseName(String)}.
707     *
708     * @since 5.7
709     */
710    public static String fileBaseName(String filename) {
711        return FilenameUtils.getBaseName(filename);
712    }
713
714    /**
715     * Joins two strings to get a valid render attribute for ajax components.
716     *
717     * @since 6.0
718     */
719    public static String joinRender(String render1, String render2) {
720        if (StringUtils.isBlank(render1) && StringUtils.isBlank(render2)) {
721            return "";
722        }
723        String res;
724        if (StringUtils.isBlank(render1)) {
725            res = render2;
726        } else if (StringUtils.isBlank(render2)) {
727            res = render1;
728        } else {
729            res = StringUtils.join(new String[] { render1, render2 }, " ");
730            res = res.replaceAll("\\s+", " ");
731        }
732        res = res.trim();
733        return res;
734    }
735
736    /**
737     * Returns the target component absolute id given an anchor in the tree and a local id.
738     * <p>
739     * If given targetId parameter contains spaces, consider several ids should be resolved and split them.
740     *
741     * @since 6.0
742     * @param anchor the component anchor, used a localization for the target component in the tree.
743     * @param targetId the component to look for locally so as to return its absolute client id.
744     */
745    public static String componentAbsoluteId(UIComponent anchor, String targetId) {
746        // handle case where several target ids could be given as input
747        if (targetId == null) {
748            return null;
749        }
750        if (targetId.contains(" ")) {
751            String res = "";
752            for (String t : targetId.split(" ")) {
753                res = joinRender(res, ComponentRenderUtils.getComponentAbsoluteId(anchor, t.trim()));
754            }
755            return res;
756        } else {
757            return ComponentRenderUtils.getComponentAbsoluteId(anchor, targetId);
758        }
759    }
760}