/* * (C) Copyright 2007-2008 Nuxeo SAS (http://nuxeo.com/) and contributors. * * All rights reserved. This program and the accompanying materials * are made available under the terms of the GNU Lesser General Public License * (LGPL) version 2.1 which accompanies this distribution, and is available at * http://www.gnu.org/licenses/lgpl.html * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * Contributors: * Nuxeo - initial API and implementation * * $Id: Functions.java 28572 2008-01-08 14:40:44Z fguillaume $ */ package org.nuxeo.ecm.platform.ui.web.tag.fn; import java.text.DateFormat; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Collection; import java.util.Date; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.faces.component.UIComponent; import javax.faces.context.ExternalContext; import javax.faces.context.FacesContext; import javax.servlet.http.HttpServletRequest; import org.apache.commons.io.FilenameUtils; import org.apache.commons.lang.StringEscapeUtils; import org.apache.commons.lang.StringUtils; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.nuxeo.common.utils.i18n.I18NUtils; import org.nuxeo.ecm.core.api.ClientException; import org.nuxeo.ecm.core.api.NuxeoGroup; import org.nuxeo.ecm.core.api.NuxeoPrincipal; import org.nuxeo.ecm.core.api.security.SecurityConstants; import org.nuxeo.ecm.platform.ui.web.rest.RestHelper; import org.nuxeo.ecm.platform.ui.web.rest.api.URLPolicyService; import org.nuxeo.ecm.platform.ui.web.util.BaseURL; import org.nuxeo.ecm.platform.ui.web.util.ComponentRenderUtils; import org.nuxeo.ecm.platform.ui.web.util.ComponentUtils; import org.nuxeo.ecm.platform.url.DocumentViewImpl; import org.nuxeo.ecm.platform.url.api.DocumentView; import org.nuxeo.ecm.platform.usermanager.UserManager; import org.nuxeo.runtime.api.Framework; /** * Util functions. * * @author <a href="mailto:at@nuxeo.com">Anahide Tchertchian</a> * @author <a href="mailto:tm@nuxeo.com">Thierry Martins</a> */ public final class Functions { private static final Log log = LogFactory.getLog(Functions.class); public static final String I18N_DURATION_PREFIX = "label.duration.unit."; public static final String BIG_FILE_SIZE_LIMIT_PROPERTY = "org.nuxeo.big.file.size.limit"; public static final long DEFAULT_BIG_FILE_SIZE_LIMIT = 5 * 1024 * 1024; public static final Pattern YEAR_PATTERN = Pattern.compile("y+"); public enum BytePrefix { SI(1000, new String[] { "", "k", "M", "G", "T", "P", "E", "Z", "Y" }, new String[] { "", "kilo", "mega", "giga", "tera", "peta", "exa", "zetta", "yotta" }), IEC(1024, new String[] { "", "Ki", "Mi", "Gi", "Ti", "Pi", "Ei", "Zi", "Yi" }, new String[] { "", "kibi", "mebi", "gibi", "tebi", "pebi", "exbi", "zebi", "yobi" }), JEDEC(1024, new String[] { "", "K", "M", "G" }, new String[] { "", "kilo", "mega", "giga" }); private final int base; private final String[] shortSuffixes; private final String[] longSuffixes; BytePrefix(int base, String[] shortSuffixes, String[] longSuffixes) { this.base = base; this.shortSuffixes = shortSuffixes; this.longSuffixes = longSuffixes; } public int getBase() { return base; } public String[] getShortSuffixes() { return shortSuffixes; } public String[] getLongSuffixes() { return longSuffixes; } } // XXX we should not use a static variable for this cache, but use a cache // at a higher level in the Framework or in a facade. private static UserManager userManager; /** * Key in the session holding a map caching user full names. */ private static final String FULLNAMES_MAP_KEY = Functions.class.getName() + ".FULLNAMES_MAP"; static final Map<String, String> mapOfDateLength = new HashMap<String, String>() { { put("short", String.valueOf(DateFormat.SHORT)); put("shortWithCentury".toLowerCase(), "shortWithCentury".toLowerCase()); put("medium", String.valueOf(DateFormat.MEDIUM)); put("long", String.valueOf(DateFormat.LONG)); put("full", String.valueOf(DateFormat.FULL)); } private static final long serialVersionUID = 8465772256977862352L; }; // Utility class. private Functions() { } public static Object test(Boolean test, Object onSuccess, Object onFailure) { return test ? onSuccess : onFailure; } public static String join(String[] list, String separator) { return StringUtils.join(list, separator); } public static String joinCollection(Collection<Object> collection, String separator) { if (collection == null) { return null; } return StringUtils.join(collection.iterator(), separator); } public static String htmlEscape(String data) { return StringEscapeUtils.escapeHtml(data); } /** * Escapes a given string to be used in a JavaScript function (escaping * single quote characters for instance). * * @since 5.4.2 */ public static String javaScriptEscape(String data) { if (data != null) { data = StringEscapeUtils.escapeJavaScript(data); } return data; } /** * Can be used in order to produce something like that "Julien, Alain , * Thierry et Marc-Aurele" where ' , ' and ' et ' is the final one. */ public static String joinCollectionWithFinalDelimiter( Collection<Object> collection, String separator, String finalSeparator) { return joinArrayWithFinalDelimiter(collection.toArray(), separator, finalSeparator); } public static String joinArrayWithFinalDelimiter(Object[] collection, String separator, String finalSeparator) { if (collection == null) { return null; } StringBuffer result = new StringBuffer(); int i = 0; for (Object object : collection) { result.append(object); if (++i == collection.length - 1) { separator = finalSeparator; } if (i != collection.length) { result.append(separator); } } return result.toString(); } public static String formatDateUsingBasicFormatter(Date date) { return formatDate(date, basicDateFormatter()); } public static String formatDate(Date date, String format) { return new SimpleDateFormat(format).format(date); } public static String concat(String s1, String s2) { return s1 + s2; } public static String indentString(Integer level, String text) { StringBuilder label = new StringBuilder(""); for (int i = 0; i < level; i++) { label.append(text); } return label.toString(); } public static boolean userIsMemberOf(String groupName) { FacesContext context = FacesContext.getCurrentInstance(); NuxeoPrincipal principal = (NuxeoPrincipal) context.getExternalContext().getUserPrincipal(); return principal.isMemberOf(groupName); } private static UserManager getUserManager() throws ClientException { if (userManager == null) { try { // XXX this should not use a static variable to do the caching userManager = Framework.getService(UserManager.class); } catch (Exception e) { throw new ClientException(e); } } return userManager; } /** * Returns the full name of a user, or its username if user if not found. * <p> * Since 5.5, returns null if given username is null (instead of returning * the current user full name). */ @SuppressWarnings("unchecked") public static String userFullName(String username) { if (SecurityConstants.SYSTEM_USERNAME.equals(username)) { // avoid costly and useless calls to the user directory return username; } ExternalContext externalContext = FacesContext.getCurrentInstance().getExternalContext(); // empty user name is current user if (StringUtils.isBlank(username)) { return null; } // check cache Map<String, Object> session = externalContext.getSessionMap(); Map<String, String> fullNames = (Map<String, String>) session.get(FULLNAMES_MAP_KEY); if (fullNames != null && fullNames.containsKey(username)) { return fullNames.get(username); } // compute full name String fullName; try { NuxeoPrincipal principal = getUserManager().getPrincipal(username); if (principal != null) { fullName = principalFullName(principal); } else { fullName = username; } } catch (ClientException e) { fullName = username; } // put in cache if (fullNames == null) { fullNames = new HashMap<String, String>(); session.put(FULLNAMES_MAP_KEY, fullNames); } fullNames.put(username, fullName); return fullName; } /** * Returns the full name of a group from his id * * @see #groupDisplayName(String, String) * @param groupId the group id * @return the group full name * @since 5.5 */ public static String groupFullName(String groupId) { try { NuxeoGroup group = getUserManager().getGroup(groupId); String groupLabel = group.getLabel(); String groupName = group.getName(); return groupDisplayName(groupName, groupLabel); } catch (Exception e) { return groupId; } } // this should be a method of the principal itself public static String principalFullName(NuxeoPrincipal principal) { String first = principal.getFirstName(); String last = principal.getLastName(); return userDisplayName(principal.getName(), first, last); } public static String userDisplayName(String id, String first, String last) { if (first == null || first.length() == 0) { if (last == null || last.length() == 0) { return id; } else { return last; } } else { if (last == null || last.length() == 0) { return first; } else { return first + ' ' + last; } } } /** * Return, from the id, the id its-self if neither last name nor name are * found or the full name plus the email if this one exists * * @param id id of the user * @param first first name of the user * @param last last name of the user * @param email email of the user * @return id or full name with email if exists * @since 5.5 */ public static String userDisplayNameAndEmail(String id, String first, String last, String email) { String userDisplayedName = userDisplayName(id, first, last); if (userDisplayedName.equals(id)) { return userDisplayedName; } if (email == null || email.length() == 0) { return userDisplayedName; } return userDisplayedName + " " + email; } /** * Choose between label or name the best string to display a group * * @param name the group name * @param label the group name * @return label if not empty or null, otherwise group name * @since 5.5 */ public static String groupDisplayName(String name, String label) { return StringUtils.isBlank(label) ? name : label; } /** * @deprecated since 5.9.1, use {@link #dateFormatter()} instead. */ @Deprecated public static String dateFormater(String formatLength) { return dateFormatter(formatLength); } /** * Return the date format to handle date taking the user's locale into * account. * * @since 5.9.1 */ public static String dateFormatter(String formatLength) { // A map to store temporary available date format FacesContext context = FacesContext.getCurrentInstance(); Locale locale = context.getViewRoot().getLocale(); int style = DateFormat.SHORT; String styleString = mapOfDateLength.get(formatLength.toLowerCase()); boolean addCentury = false; if ("shortWithCentury".toLowerCase().equals(styleString)) { addCentury = true; } else { style = Integer.parseInt(styleString); } DateFormat aDateFormat = DateFormat.getDateInstance(style, locale); // Cast to SimpleDateFormat to make "toPattern" method available SimpleDateFormat format = (SimpleDateFormat) aDateFormat; // return the date pattern String pattern = format.toPattern(); if (style == DateFormat.SHORT && addCentury) { // hack to add century on generated pattern pattern = YEAR_PATTERN.matcher(pattern).replaceAll("yyyy"); } return pattern; } /** * @deprecated since 5.9.1, use {@link #basicDateFormatter()} instead. */ @Deprecated public static String basicDateFormater() { return basicDateFormatter(); } /** * Return the date format to handle date taking the user's locale into * account. Uses the pseudo "shortWithCentury" format. * * @since 5.9.1 */ public static String basicDateFormatter() { return dateFormatter("shortWithCentury"); } /** * @deprecated since 5.9.1, use {@link #dateAndTimeFormatter(String)} * instead. */ @Deprecated public static String dateAndTimeFormater(String formatLength) { return dateAndTimeFormatter(formatLength); } /** * Return the date format to handle date and time taking the user's locale * into account. * * @since 5.9.1 */ public static String dateAndTimeFormatter(String formatLength) { // A map to store temporary available date format FacesContext context = FacesContext.getCurrentInstance(); Locale locale = context.getViewRoot().getLocale(); int style = DateFormat.SHORT; String styleString = mapOfDateLength.get(formatLength.toLowerCase()); boolean addCentury = false; if ("shortWithCentury".toLowerCase().equals(styleString)) { addCentury = true; } else { style = Integer.parseInt(styleString); } DateFormat aDateFormat = DateFormat.getDateTimeInstance(style, style, locale); // Cast to SimpleDateFormat to make "toPattern" method available SimpleDateFormat format = (SimpleDateFormat) aDateFormat; // return the date pattern String pattern = format.toPattern(); if (style == DateFormat.SHORT && addCentury) { // hack to add century on generated pattern pattern = YEAR_PATTERN.matcher(pattern).replaceAll("yyyy"); } return pattern; } /** * @deprecated since 5.9.1, use {@link #basicDateAndTimeFormatter()} * instead. */ @Deprecated public static String basicDateAndTimeFormater() { return basicDateAndTimeFormatter(); } /** * Return the date format to handle date and time taking the user's locale * into account. Uses the pseudo "shortWithCentury" format. * * @since 5.9.1 */ public static String basicDateAndTimeFormatter() { return dateAndTimeFormatter("shortWithCentury"); } public static String printFileSize(String size) { return printFormatedFileSize(size, "SI", true); } public static String printFormatedFileSize(String sizeS, String format, Boolean isShort) { long size = (sizeS == null || "".equals(sizeS)) ? 0 : Long.parseLong(sizeS); BytePrefix prefix = Enum.valueOf(BytePrefix.class, format); int base = prefix.getBase(); String[] suffix = isShort ? prefix.getShortSuffixes() : prefix.getLongSuffixes(); int ex = 0; while (size > base - 1 || ex > suffix.length) { ex++; size /= base; } FacesContext context = FacesContext.getCurrentInstance(); String msg; if (context != null) { String bundleName = context.getApplication().getMessageBundle(); Locale locale = context.getViewRoot().getLocale(); msg = I18NUtils.getMessageString(bundleName, "label.bytes.suffix", null, locale); if ("label.bytes.suffix".equals(msg)) { // Set default value if no message entry found msg = "B"; } } else { // No faces context, set default value msg = "B"; } return "" + size + " " + suffix[ex] + msg; } public static Integer integerDivision(Integer x, Integer y) { return x / y; } /** * Format the duration of a media in a string of two consecutive units to * best express the duration of a media, e.g.: * <ul> * <li>1 hr 42 min</li> * <li>2 min 25 sec</li> * <li>10 sec</li> * <li>0 sec</li> * </ul> * * @param durationObj a Float, Double, Integer, Long or String instance * representing a duration in seconds * @param i18nLabels a map to translate the days, hours, minutes and * seconds labels * @return the formatted string */ public static String printFormattedDuration(Object durationObj, Map<String, String> i18nLabels) { if (i18nLabels == null) { i18nLabels = new HashMap<String, String>(); } double duration = 0.0; if (durationObj instanceof Float) { duration = ((Float) durationObj).doubleValue(); } else if (durationObj instanceof Double) { duration = ((Double) durationObj).doubleValue(); } else if (durationObj instanceof Integer) { duration = ((Integer) durationObj).doubleValue(); } else if (durationObj instanceof Long) { duration = ((Long) durationObj).doubleValue(); } else if (durationObj instanceof String) { duration = Double.parseDouble((String) durationObj); } int days = (int) Math.floor(duration / (24 * 60 * 60)); int hours = (int) Math.floor(duration / (60 * 60)) - days * 24; int minutes = (int) Math.floor(duration / 60) - days * 24 * 60 - hours * 60; int seconds = (int) Math.floor(duration) - days * 24 * 3600 - hours * 3600 - minutes * 60; int[] components = { days, hours, minutes, seconds }; String[] units = { "days", "hours", "minutes", "seconds" }; String[] defaultLabels = { "d", "hr", "min", "sec" }; String representation = null; for (int i = 0; i < components.length; i++) { if (components[i] != 0 || i == components.length - 1) { String i18nLabel = i18nLabels.get(I18N_DURATION_PREFIX + units[i]); if (i18nLabel == null) { i18nLabel = defaultLabels[i]; } representation = String.format("%d %s", components[i], i18nLabel); if (i < components.length - 1) { i18nLabel = i18nLabels.get(I18N_DURATION_PREFIX + units[i + 1]); if (i18nLabel == null) { i18nLabel = defaultLabels[i + 1]; } representation += String.format(" %d %s", components[i + 1], i18nLabel); } break; } } return representation; } public static String printFormattedDuration(Object durationObj) { return printFormattedDuration(durationObj, null); } public static final String translate(String messageId, Object... params) { return ComponentUtils.translate(FacesContext.getCurrentInstance(), messageId, params); } /** * @return the big file size limit defined with the property * org.nuxeo.big.file.size.limit */ public static long getBigFileSizeLimit() { return getFileSize(Framework.getProperty(BIG_FILE_SIZE_LIMIT_PROPERTY, "")); } public static long getFileSize(String value) { Pattern pattern = Pattern.compile("([1-9][0-9]*)([kmgi]*)", Pattern.CASE_INSENSITIVE); Matcher m = pattern.matcher(value.trim()); long number; String multiplier; if (!m.matches()) { return DEFAULT_BIG_FILE_SIZE_LIMIT; } number = Long.valueOf(m.group(1)); multiplier = m.group(2); return getValueFromMultiplier(multiplier) * number; } /** * Transform the parameter in entry according to these unit systems: * <ul> * <li>SI prefixes: k/M/G for kilo, mega, giga</li> * <li>IEC prefixes: Ki/Mi/Gi for kibi, mebi, gibi</li> * </ul> * * @param m : binary prefix multiplier * @return the value of the multiplier as a long */ public static long getValueFromMultiplier(String m) { if ("k".equalsIgnoreCase(m)) { return 1L * 1000; } else if ("Ki".equalsIgnoreCase(m)) { return 1L << 10; } else if ("M".equalsIgnoreCase(m)) { return 1L * 1000 * 1000; } else if ("Mi".equalsIgnoreCase(m)) { return 1L << 20; } else if ("G".equalsIgnoreCase(m)) { return 1L * 1000 * 1000 * 1000; } else if ("Gi".equalsIgnoreCase(m)) { return 1L << 30; } else { return 1L; } } /** * Returns true if the faces context holds messages for given JSF component * id, usually the form id. * <p> * Id given id is null, returns true if there is at least one client id * with messages. * <p> * Since the form id might be prefixed with a container id in some cases, * the method returns true if one of client ids with messages stats with * given id, or if given id is contained in it. * * @since 5.4.2 */ public static boolean hasMessages(String clientId) { Iterator<String> it = FacesContext.getCurrentInstance().getClientIdsWithMessages(); if (clientId == null) { return it.hasNext(); } else { while (it.hasNext()) { String id = it.next(); if (id != null && (id.startsWith(clientId + ":") || id.contains(":" + clientId + ":") || id.equals(clientId) || id.endsWith(":" + clientId))) { return true; } } } return false; } public static String userUrl(String patternName, String username, String viewId, boolean newConversation) { return userUrl(patternName, username, viewId, newConversation, null); } public static String userUrl(String patternName, String username, String viewId, boolean newConversation, HttpServletRequest req) { try { Map<String, String> parameters = new HashMap<String, String>(); parameters.put("username", username); DocumentView docView = new DocumentViewImpl(null, viewId, parameters); // generate url URLPolicyService service = Framework.getService(URLPolicyService.class); if (patternName == null || patternName.length() == 0) { patternName = service.getDefaultPatternName(); } String baseURL = null; if (req == null) { baseURL = BaseURL.getBaseURL(); } else { baseURL = BaseURL.getBaseURL(req); } String url = service.getUrlFromDocumentView(patternName, docView, baseURL); // pass conversation info if needed if (!newConversation && url != null) { url = RestHelper.addCurrentConversationParameters(url); } return url; } catch (Exception e) { log.error("Could not generate user url", e); } return null; } public static List<Object> combineLists(List<? extends Object>... lists) { List<Object> combined = new ArrayList<Object>(); for (List<? extends Object> list : lists) { combined.addAll(list); } return combined; } /** * Helper that escapes a string used as a JSF tag id: this is useful to * replace characters that are not handled correctly in JSF context. * <p> * This method currently removes ASCII characters from the given string, * and replaces "-" characters by "_" because the dash is an issue for * forms rendered in ajax (see NXP-10793). * <p> * Also starting digits are replaced by the "_" character because a tag id * cannot start with a digit. * * @since 5.7 * @return the escaped string */ public static String jsfTagIdEscape(String base) { if (base == null) { return null; } int n = base.length(); StringBuilder res = new StringBuilder(); for (int i = 0; i < n; i++) { char c = base.charAt(i); if (i == 0) { if (!Character.isLetter(c) && (c != '_')) { res.append("_"); } else { res.append(c); } } else { if (!Character.isLetter(c) && !Character.isDigit(c) && (c != '_')) { res.append("_"); } else { res.append(c); } } } return org.nuxeo.common.utils.StringUtils.toAscii(res.toString()); } /** * Returns the extension from the given {@code filename}. * <p> * See {@link FilenameUtils#getExtension(String)}. * * @since 5.7 */ public static String fileExtension(String filename) { return FilenameUtils.getExtension(filename); } /** * Returns the base name from the given {@code filename}. * <p> * See {@link FilenameUtils#getBaseName(String)}. * * @since 5.7 */ public static String fileBaseName(String filename) { return FilenameUtils.getBaseName(filename); } /** * Joins two strings to get a valid render attribute for ajax components. * * @since 6.0 */ public static String joinRender(String render1, String render2) { if (StringUtils.isBlank(render1) && StringUtils.isBlank(render2)) { return ""; } String res; if (StringUtils.isBlank(render1)) { res = render2; } else if (StringUtils.isBlank(render2)) { res = render1; } else { res = StringUtils.join(new String[] { render1, render2 }, " "); res = res.replaceAll("\\s+", " "); } res = res.trim(); return res; } /** * Returns the target component absolute id given an anchor in the tree and * a local id. * <p> * If given targetId parameter contains spaces, consider several ids should * be resolved and split them. * * @since 6.0 * @param anchor the component anchor, used a localization for the target * component in the tree. * @param targetId the component to look for locally so as to return its * absolute client id. */ public static String componentAbsoluteId(UIComponent anchor, String targetId) { // handle case where several target ids could be given as input if (targetId == null) { return null; } if (targetId.contains(" ")) { String res = ""; for (String t : targetId.split(" ")) { res = joinRender( res, ComponentRenderUtils.getComponentAbsoluteId(anchor, t.trim())); } return res; } else { return ComponentRenderUtils.getComponentAbsoluteId(anchor, targetId); } } }