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     * @deprecated since 5.9.1, use {@link #dateFormatter()} instead.
321     */
322    @Deprecated
323    public static String dateFormater(String formatLength) {
324        return dateFormatter(formatLength);
325    }
326
327    /**
328     * Return the date format to handle date taking the user's locale into account.
329     *
330     * @since 5.9.1
331     */
332    public static String dateFormatter(String formatLength) {
333        // A map to store temporary available date format
334        FacesContext context = FacesContext.getCurrentInstance();
335        Locale locale = context.getViewRoot().getLocale();
336
337        int style = DateFormat.SHORT;
338        String styleString = mapOfDateLength.get(formatLength.toLowerCase());
339        boolean addCentury = false;
340        if ("shortWithCentury".toLowerCase().equals(styleString)) {
341            addCentury = true;
342        } else {
343            style = Integer.parseInt(styleString);
344        }
345
346        DateFormat aDateFormat = DateFormat.getDateInstance(style, locale);
347
348        // Cast to SimpleDateFormat to make "toPattern" method available
349        SimpleDateFormat format = (SimpleDateFormat) aDateFormat;
350
351        // return the date pattern
352        String pattern = format.toPattern();
353
354        if (style == DateFormat.SHORT && addCentury) {
355            // hack to add century on generated pattern
356            pattern = YEAR_PATTERN.matcher(pattern).replaceAll("yyyy");
357        }
358        return pattern;
359    }
360
361    /**
362     * @deprecated since 5.9.1, use {@link #basicDateFormatter()} instead.
363     */
364    @Deprecated
365    public static String basicDateFormater() {
366        return basicDateFormatter();
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     * @deprecated since 5.9.1, use {@link #dateAndTimeFormatter(String)} instead.
381     */
382    @Deprecated
383    public static String dateAndTimeFormater(String formatLength) {
384        return dateAndTimeFormatter(formatLength);
385    }
386
387    /**
388     * Return the date format to handle date and time taking the user's locale into account.
389     *
390     * @since 5.9.1
391     */
392    public static String dateAndTimeFormatter(String formatLength) {
393
394        // A map to store temporary available date format
395
396        FacesContext context = FacesContext.getCurrentInstance();
397        Locale locale = context.getViewRoot().getLocale();
398
399        int style = DateFormat.SHORT;
400        String styleString = mapOfDateLength.get(formatLength.toLowerCase());
401        boolean addCentury = false;
402        if ("shortWithCentury".toLowerCase().equals(styleString)) {
403            addCentury = true;
404        } else {
405            style = Integer.parseInt(styleString);
406        }
407
408        DateFormat aDateFormat = DateFormat.getDateTimeInstance(style, style, locale);
409
410        // Cast to SimpleDateFormat to make "toPattern" method available
411        SimpleDateFormat format = (SimpleDateFormat) aDateFormat;
412
413        // return the date pattern
414        String pattern = format.toPattern();
415
416        if (style == DateFormat.SHORT && addCentury) {
417            // hack to add century on generated pattern
418            pattern = YEAR_PATTERN.matcher(pattern).replaceAll("yyyy");
419        }
420        return pattern;
421    }
422
423    /**
424     * @deprecated since 5.9.1, use {@link #basicDateAndTimeFormatter()} instead.
425     */
426    @Deprecated
427    public static String basicDateAndTimeFormater() {
428        return basicDateAndTimeFormatter();
429    }
430
431    /**
432     * Return the date format to handle date and time taking the user's locale into account. Uses the pseudo
433     * "shortWithCentury" format.
434     *
435     * @since 5.9.1
436     */
437    public static String basicDateAndTimeFormatter() {
438        return dateAndTimeFormatter("shortWithCentury");
439    }
440
441    /**
442     * Returns the default byte prefix.
443     *
444     * @since 7.4
445     */
446    public static BytePrefix getDefaultBytePrefix() {
447        ConfigurationService configurationService = Framework.getService(ConfigurationService.class);
448        return BytePrefix.valueOf(
449                configurationService.getProperty(BYTE_PREFIX_FORMAT_PROPERTY, DEFAULT_BYTE_PREFIX_FORMAT));
450    }
451
452    public static String printFileSize(String size) {
453        return printFormatedFileSize(size, getDefaultBytePrefix().name(), true);
454    }
455
456    public static String printFormatedFileSize(String sizeS, String format, Boolean isShort) {
457        long size = (sizeS == null || "".equals(sizeS)) ? 0 : Long.parseLong(sizeS);
458        BytePrefix prefix = Enum.valueOf(BytePrefix.class, format);
459        int base = prefix.getBase();
460        String[] suffix = isShort ? prefix.getShortSuffixes() : prefix.getLongSuffixes();
461        int ex = 0;
462        while (size > base - 1 || ex > suffix.length) {
463            ex++;
464            size /= base;
465        }
466
467        FacesContext context = FacesContext.getCurrentInstance();
468        String msg;
469        if (context != null) {
470            String bundleName = context.getApplication().getMessageBundle();
471            Locale locale = context.getViewRoot().getLocale();
472            msg = I18NUtils.getMessageString(bundleName, "label.bytes.suffix", null, locale);
473            if ("label.bytes.suffix".equals(msg)) {
474                // Set default value if no message entry found
475                msg = "B";
476            }
477        } else {
478            // No faces context, set default value
479            msg = "B";
480        }
481
482        return "" + size + " " + suffix[ex] + msg;
483    }
484
485    /**
486     * Format the duration of a media in a string of two consecutive units to best express the duration of a media,
487     * e.g.:
488     * <ul>
489     * <li>1 hr 42 min</li>
490     * <li>2 min 25 sec</li>
491     * <li>10 sec</li>
492     * <li>0 sec</li>
493     * </ul>
494     *
495     * @param durationObj a Float, Double, Integer, Long or String instance representing a duration in seconds
496     * @param i18nLabels a map to translate the days, hours, minutes and seconds labels
497     * @return the formatted string
498     */
499    public static String printFormattedDuration(Object durationObj, Map<String, String> i18nLabels) {
500
501        if (i18nLabels == null) {
502            i18nLabels = new HashMap<String, String>();
503        }
504        double duration = 0.0;
505        if (durationObj instanceof Float) {
506            duration = ((Float) durationObj).doubleValue();
507        } else if (durationObj instanceof Double) {
508            duration = ((Double) durationObj).doubleValue();
509        } else if (durationObj instanceof Integer) {
510            duration = ((Integer) durationObj).doubleValue();
511        } else if (durationObj instanceof Long) {
512            duration = ((Long) durationObj).doubleValue();
513        } else if (durationObj instanceof String) {
514            duration = Double.parseDouble((String) durationObj);
515        }
516
517        int days = (int) Math.floor(duration / (24 * 60 * 60));
518        int hours = (int) Math.floor(duration / (60 * 60)) - days * 24;
519        int minutes = (int) Math.floor(duration / 60) - days * 24 * 60 - hours * 60;
520        int seconds = (int) Math.floor(duration) - days * 24 * 3600 - hours * 3600 - minutes * 60;
521
522        int[] components = { days, hours, minutes, seconds };
523        String[] units = { "days", "hours", "minutes", "seconds" };
524        String[] defaultLabels = { "d", "hr", "min", "sec" };
525
526        String representation = null;
527        for (int i = 0; i < components.length; i++) {
528            if (components[i] != 0 || i == components.length - 1) {
529                String i18nLabel = i18nLabels.get(I18N_DURATION_PREFIX + units[i]);
530                if (i18nLabel == null) {
531                    i18nLabel = defaultLabels[i];
532                }
533                representation = String.format("%d %s", components[i], i18nLabel);
534                if (i < components.length - 1) {
535                    i18nLabel = i18nLabels.get(I18N_DURATION_PREFIX + units[i + 1]);
536                    if (i18nLabel == null) {
537                        i18nLabel = defaultLabels[i + 1];
538                    }
539                    representation += String.format(" %d %s", components[i + 1], i18nLabel);
540                }
541                break;
542            }
543        }
544        return representation;
545    }
546
547    public static String printFormattedDuration(Object durationObj) {
548        return printFormattedDuration(durationObj, null);
549    }
550
551    public static final String translate(String messageId, Object... params) {
552        return ComponentUtils.translate(FacesContext.getCurrentInstance(), messageId, params);
553    }
554
555    /**
556     * @return the big file size limit defined with the property org.nuxeo.big.file.size.limit
557     */
558    public static long getBigFileSizeLimit() {
559        return getFileSize(Framework.getProperty(BIG_FILE_SIZE_LIMIT_PROPERTY, ""));
560    }
561
562    public static long getFileSize(String value) {
563        Pattern pattern = Pattern.compile("([1-9][0-9]*)([kmgi]*)", Pattern.CASE_INSENSITIVE);
564        Matcher m = pattern.matcher(value.trim());
565        long number;
566        String multiplier;
567        if (!m.matches()) {
568            return DEFAULT_BIG_FILE_SIZE_LIMIT;
569        }
570        number = Long.valueOf(m.group(1));
571        multiplier = m.group(2);
572        return getValueFromMultiplier(multiplier) * number;
573    }
574
575    /**
576     * Transform the parameter in entry according to these unit systems:
577     * <ul>
578     * <li>SI prefixes: k/M/G for kilo, mega, giga</li>
579     * <li>IEC prefixes: Ki/Mi/Gi for kibi, mebi, gibi</li>
580     * </ul>
581     *
582     * @param m : binary prefix multiplier
583     * @return the value of the multiplier as a long
584     */
585    public static long getValueFromMultiplier(String m) {
586        if ("k".equalsIgnoreCase(m)) {
587            return 1L * 1000;
588        } else if ("Ki".equalsIgnoreCase(m)) {
589            return 1L << 10;
590        } else if ("M".equalsIgnoreCase(m)) {
591            return 1L * 1000 * 1000;
592        } else if ("Mi".equalsIgnoreCase(m)) {
593            return 1L << 20;
594        } else if ("G".equalsIgnoreCase(m)) {
595            return 1L * 1000 * 1000 * 1000;
596        } else if ("Gi".equalsIgnoreCase(m)) {
597            return 1L << 30;
598        } else {
599            return 1L;
600        }
601    }
602
603    /**
604     * Returns true if the faces context holds messages for given JSF component id, usually the form id.
605     * <p>
606     * Id given id is null, returns true if there is at least one client id with messages.
607     * <p>
608     * Since the form id might be prefixed with a container id in some cases, the method returns true if one of client
609     * ids with messages stats with given id, or if given id is contained in it.
610     *
611     * @since 5.4.2
612     */
613    public static boolean hasMessages(String clientId) {
614        Iterator<String> it = FacesContext.getCurrentInstance().getClientIdsWithMessages();
615        if (clientId == null) {
616            return it.hasNext();
617        } else {
618            while (it.hasNext()) {
619                String id = it.next();
620                if (id != null
621                        && (id.startsWith(clientId + ":") || id.contains(":" + clientId + ":") || id.equals(clientId) || id.endsWith(":"
622                                + clientId))) {
623                    return true;
624                }
625            }
626        }
627        return false;
628    }
629
630    public static String userUrl(String patternName, String username, String viewId, boolean newConversation) {
631        return userUrl(patternName, username, viewId, newConversation, null);
632    }
633
634    public static String userUrl(String patternName, String username, String viewId, boolean newConversation,
635            HttpServletRequest req) {
636        Map<String, String> parameters = new HashMap<String, String>();
637        parameters.put("username", username);
638        DocumentView docView = new DocumentViewImpl(null, viewId, parameters);
639
640        // generate url
641        URLPolicyService service = Framework.getService(URLPolicyService.class);
642        if (patternName == null || patternName.length() == 0) {
643            patternName = service.getDefaultPatternName();
644        }
645
646        String baseURL = null;
647        if (req == null) {
648            baseURL = BaseURL.getBaseURL();
649        } else {
650            baseURL = BaseURL.getBaseURL(req);
651        }
652
653        String url = service.getUrlFromDocumentView(patternName, docView, baseURL);
654
655        // pass conversation info if needed
656        if (!newConversation && url != null) {
657            url = RestHelper.addCurrentConversationParameters(url);
658        }
659
660        return url;
661    }
662
663    public static List<Object> combineLists(List<? extends Object>... lists) {
664        List<Object> combined = new ArrayList<Object>();
665        for (List<? extends Object> list : lists) {
666            combined.addAll(list);
667        }
668        return combined;
669    }
670
671    /**
672     * Helper that escapes a string used as a JSF tag id: this is useful to replace characters that are not handled
673     * correctly in JSF context.
674     * <p>
675     * This method currently removes ASCII characters from the given string, and replaces "-" characters by "_" because
676     * the dash is an issue for forms rendered in ajax (see NXP-10793).
677     * <p>
678     * Also starting digits are replaced by the "_" character because a tag id cannot start with a digit.
679     *
680     * @since 5.7
681     * @return the escaped string
682     */
683    public static String jsfTagIdEscape(String base) {
684        if (base == null) {
685            return null;
686        }
687        int n = base.length();
688        StringBuilder res = new StringBuilder();
689        for (int i = 0; i < n; i++) {
690            char c = base.charAt(i);
691            if (i == 0) {
692                if (!Character.isLetter(c) && (c != '_')) {
693                    res.append("_");
694                } else {
695                    res.append(c);
696                }
697            } else {
698                if (!Character.isLetter(c) && !Character.isDigit(c) && (c != '_')) {
699                    res.append("_");
700                } else {
701                    res.append(c);
702                }
703            }
704        }
705        return org.nuxeo.common.utils.StringUtils.toAscii(res.toString());
706    }
707
708    /**
709     * Returns the extension from the given {@code filename}.
710     * <p>
711     * See {@link FilenameUtils#getExtension(String)}.
712     *
713     * @since 5.7
714     */
715    public static String fileExtension(String filename) {
716        return FilenameUtils.getExtension(filename);
717    }
718
719    /**
720     * Returns the base name from the given {@code filename}.
721     * <p>
722     * See {@link FilenameUtils#getBaseName(String)}.
723     *
724     * @since 5.7
725     */
726    public static String fileBaseName(String filename) {
727        return FilenameUtils.getBaseName(filename);
728    }
729
730    /**
731     * Joins two strings to get a valid render attribute for ajax components.
732     *
733     * @since 6.0
734     */
735    public static String joinRender(String render1, String render2) {
736        if (StringUtils.isBlank(render1) && StringUtils.isBlank(render2)) {
737            return "";
738        }
739        String res;
740        if (StringUtils.isBlank(render1)) {
741            res = render2;
742        } else if (StringUtils.isBlank(render2)) {
743            res = render1;
744        } else {
745            res = StringUtils.join(new String[] { render1, render2 }, " ");
746            res = res.replaceAll("\\s+", " ");
747        }
748        res = res.trim();
749        return res;
750    }
751
752    /**
753     * Returns the target component absolute id given an anchor in the tree and a local id.
754     * <p>
755     * If given targetId parameter contains spaces, consider several ids should be resolved and split them.
756     *
757     * @since 6.0
758     * @param anchor the component anchor, used a localization for the target component in the tree.
759     * @param targetId the component to look for locally so as to return its absolute client id.
760     */
761    public static String componentAbsoluteId(UIComponent anchor, String targetId) {
762        // handle case where several target ids could be given as input
763        if (targetId == null) {
764            return null;
765        }
766        if (targetId.contains(" ")) {
767            String res = "";
768            for (String t : targetId.split(" ")) {
769                res = joinRender(res, ComponentRenderUtils.getComponentAbsoluteId(anchor, t.trim()));
770            }
771            return res;
772        } else {
773            return ComponentRenderUtils.getComponentAbsoluteId(anchor, targetId);
774        }
775    }
776}