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