/** * OLAT - Online Learning and Training<br> * http://www.olat.org * <p> * Licensed under the Apache License, Version 2.0 (the "License"); <br> * you may not use this file except in compliance with the License.<br> * You may obtain a copy of the License at * <p> * http://www.apache.org/licenses/LICENSE-2.0 * <p> * Unless required by applicable law or agreed to in writing,<br> * software distributed under the License is distributed on an "AS IS" BASIS, <br> * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. <br> * See the License for the specific language governing permissions and <br> * limitations under the License. * <p> * Copyright (c) since 2004 at Multimedia- & E-Learning Services (MELS),<br> * University of Zurich, Switzerland. * <hr> * <a href="http://www.openolat.org"> * OpenOLAT - Online Learning and Training</a><br> * This file has been modified by the OpenOLAT community. Changes are licensed * under the Apache 2.0 license as the original file. * <p> */ package org.olat.core.util; import java.io.UnsupportedEncodingException; import java.math.BigDecimal; import java.net.URLEncoder; import java.text.DateFormat; import java.text.ParseException; import java.text.SimpleDateFormat; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.Period; import java.time.ZoneId; import java.util.Calendar; import java.util.Date; import java.util.GregorianCalendar; import java.util.HashMap; import java.util.Locale; import java.util.Map; import java.util.StringTokenizer; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.apache.commons.lang.time.DurationFormatUtils; import org.olat.core.dispatcher.impl.StaticMediaDispatcher; import org.olat.core.gui.render.StringOutput; import org.olat.core.helpers.Settings; /** * enclosing_type Description: <br> * A formatter to format locale-specific things (mainly dates and times) * * @author Felix Jost */ public class Formatter { private static final DateFormat formatterDatetimeFilesystem = new SimpleDateFormat("yyyy-MM-dd'T'HH-mm-ss_SSS"); private static final DateFormat formatterDatetimeWithMinutes = new SimpleDateFormat("yyyy-MM-dd'T'HH-mm"); private static final DateFormat formatDateTime = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss"); private static final DateFormat shortFormatDateFileSystem = new SimpleDateFormat("yyyyMMdd"); private static final Map<Locale,Formatter> localToFormatterMap = new HashMap<Locale,Formatter>(); private final Locale locale; private final DateFormat shortDateFormat; private final DateFormat longDateFormat; private final DateFormat shortDateTimeFormat; private final DateFormat longDateTimeFormat; private final DateFormat shortTimeFormat; private final DateFormat mediumTimeFormat; /** * Constructor for Formatter. */ private Formatter(Locale locale) { this.locale = locale; // Date only formats shortDateFormat = DateFormat.getDateInstance(DateFormat.SHORT, locale); shortDateFormat.setLenient(false); if (shortDateFormat instanceof SimpleDateFormat) { // by default year has only two digits, however most people prefer a four digits year, even in short format SimpleDateFormat sdf = (SimpleDateFormat) shortDateFormat; String pattern = sdf.toPattern().replaceAll("y+","yyyy"); sdf.applyPattern(pattern); } longDateFormat = DateFormat.getDateInstance(DateFormat.LONG, locale); longDateFormat.setLenient(false); // Time only formats shortTimeFormat = DateFormat.getTimeInstance(DateFormat.SHORT, locale); shortTimeFormat.setLenient(false); mediumTimeFormat = DateFormat.getTimeInstance(DateFormat.MEDIUM, locale); mediumTimeFormat.setLenient(false); // Date and time formats shortDateTimeFormat = DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT, locale); shortDateTimeFormat.setLenient(false); if (shortDateTimeFormat instanceof SimpleDateFormat) { // by default year has only two digits, however most people prefer a four digits year, even in short format SimpleDateFormat sdf = (SimpleDateFormat) shortDateTimeFormat; String pattern = sdf.toPattern().replaceAll("y+","yyyy"); sdf.applyPattern(pattern); } longDateTimeFormat = DateFormat.getDateTimeInstance(DateFormat.LONG, DateFormat.LONG, locale); longDateTimeFormat.setLenient(false); } /** * get an instance of the Formatter given the locale * * @param locale the locale which the formatter should use in its operations * @return the instance of the Formatter */ public static Formatter getInstance(Locale locale) { Formatter formatter; if(localToFormatterMap.containsKey(locale)) { formatter = localToFormatterMap.get(locale); } else { formatter = new Formatter(locale); localToFormatterMap.put(locale, formatter); } return formatter; } /** * Formats the given date in a short format, e.g. 05.12.2015 or 12/05/2015 * * @param date the date * @return a String with the formatted date */ public String formatDate(Date date) { if (date == null) return null; synchronized (shortDateFormat) { return shortDateFormat.format(date); } } /** * adds the given period in day/month/years to the baseLineDate and formats it in a short format, e.g. 05.12.2015 or 12/05/2015 * * @param baseLineDate the date * @return a String with the formatted date */ public String formatDateRelative(Date baseLineDate, int days, int months, int years) { if (baseLineDate == null) return null; LocalDate date = LocalDateTime.ofInstant(baseLineDate.toInstant(),ZoneId.systemDefault()).toLocalDate(); Period period = Period.of(years, months, days); LocalDate relativeDate = date.plus(period); Date result = Date.from(relativeDate.atStartOfDay().atZone(ZoneId.systemDefault()).toInstant()); return formatDate(result); } /** * Formats the given date in a medium sized format, e.g. 12. Dezember 2015 or December 12, 2015 * * @param date the date * @return a String with the formatted date */ public String formatDateLong(Date date) { if (date == null) return null; synchronized (longDateFormat) { return longDateFormat.format(date); } } public Date parseDate(String val) throws ParseException { synchronized (shortDateFormat) { return shortDateFormat.parse(val); } } /** * formats the given time period so it is friendly to read * * @param date the date * @return a String with the formatted time */ public String formatTime(Date date) { if (date == null) return null; synchronized (mediumTimeFormat) { return mediumTimeFormat.format(date); } } /** * Formats the given date in a short size with date and time, e.g. * 05.12.2015 14:35 * * @param date * the date * @return a String with the formatted date and time */ public String formatDateAndTime(Date date) { if (date == null) return null; synchronized (shortDateTimeFormat) { return shortDateTimeFormat.format(date); } } /** * Formats the given date in a long size with date and time, e.g. Tuesday, * 10. September 2015, 3:48 PM * * @param date * the date * @return a String with the formatted date and time */ public String formatDateAndTimeLong(Date date) { if (date == null) return null; synchronized (longDateTimeFormat) { return longDateTimeFormat.format(date); } } /** * Generate a simple date pattern that formats a date using the locale of the * formatter * * @return */ public String getSimpleDatePatternForDate() { Calendar cal = new GregorianCalendar(); cal.set( 1999, Calendar.MARCH, 1, 0, 0, 0 ); Date testDate = cal.getTime(); String formattedDate = formatDate(testDate); formattedDate = formattedDate.replace("1999", "%Y"); formattedDate = formattedDate.replace("99", "%Y"); formattedDate = formattedDate.replace("03", "%m"); formattedDate = formattedDate.replace("3", "%m"); formattedDate = formattedDate.replace("01", "%d"); formattedDate = formattedDate.replace("1", "%d"); return formattedDate; } /** * Generate a simple date pattern that formats a date with time using the * locale of the formatter * * @return */ public String getSimpleDatePatternForDateAndTime() { Calendar cal = new GregorianCalendar(); cal.set( 1999, Calendar.MARCH, 1, 4, 5, 0 ); Date testDate = cal.getTime(); String formattedDate = formatDateAndTime(testDate); formattedDate = formattedDate.replace("1999", "%Y"); formattedDate = formattedDate.replace("99", "%Y"); formattedDate = formattedDate.replace("03", "%m"); formattedDate = formattedDate.replace("3", "%m"); formattedDate = formattedDate.replace("01", "%d"); formattedDate = formattedDate.replace("1", "%d"); formattedDate = formattedDate.replace("04", "%H"); formattedDate = formattedDate.replace("4", "%H"); formattedDate = formattedDate.replace("05", "%M"); formattedDate = formattedDate.replace("5", "%M"); if(formattedDate.endsWith("AM")) { formattedDate = formattedDate.replace("%H","%I").replace("AM", "%p"); } return formattedDate; } /** * Formats the given date with the ISO 8601 standard also known as 'datetime' * See http://www.w3.org/TR/NOTE-datetime.html for more info. * * @param d the date to be formatted * @return a String with the formatted date and time */ public static String formatDatetime(Date d) { synchronized (formatDateTime) { return formatDateTime.format(d); } } /** * Parse the given date with the ISO 8601 standard also known as 'datetime' * See http://www.w3.org/TR/NOTE-datetime.html for more info. * * @param d the date as string to be parsed * @return The date */ public static Date parseDatetime(String d) throws ParseException { synchronized (formatDateTime) { return formatDateTime.parse(d); } } /** * Use this for naming files or directories with a timestamp. * As windows does not like ":" in filenames formatDateAndTime(d) does not work * * @param d the date to be formatted * @return a String with the formatted date and time */ public static String formatDatetimeFilesystemSave(Date d) { synchronized (formatterDatetimeFilesystem) { return formatterDatetimeFilesystem.format(d); } } /** * Use this for naming files or directories with a timestamp. No Seconds and millis! * As windows does not like ":" in filenames formatDateAndTime(d) does not work * * @param d the date to be formatted * @return a String with the formatted date and time */ public static String formatDatetimeWithMinutes(Date d) { synchronized (formatterDatetimeWithMinutes) { return formatterDatetimeWithMinutes.format(d); } } public static Date parseDatetimeFilesystemSave(String d) throws ParseException { synchronized (formatterDatetimeFilesystem) { return formatterDatetimeFilesystem.parse(d); } } public static String formatShortDateFilesystem(Date d) { synchronized (shortFormatDateFileSystem) { return shortFormatDateFileSystem.format(d); } } /** * formats the given time period so it is friendly to read * * @param d the date * @return a String with the formatted time */ public String formatTimeShort(Date d) { synchronized (shortTimeFormat) { return shortTimeFormat.format(d); } } /** * Formats a duration in millis to "XXh YYm ZZs" * @param millis * @return formatted string */ public static String formatDuration(long millis) { long h = millis / (1000*60*60); long hmins = h*1000*60*60; long m = (millis - hmins)/(1000*60); long s = (millis - hmins - m*1000*60)/1000; return h + "h " + m + "m " + s + "s"; } /** * Formats a duration in millis to "XX:YY:ZZ". Removes the hours if zero * * @param timecode in milliseconds * @return formatted timecode */ public static String formatTimecode(long timecode) { String result = DurationFormatUtils.formatDuration(timecode, "H:mm:ss", true); if (result.startsWith("0:")) { // remove empty hours result = result.substring(2); } if (result.startsWith("0")) { // remove zero from from 10x minutes (02:23 -> 2:23, 00:14 -> 0:14) result = result.substring(1); } return result; } /** * Format the given bytes to human readable format * @param bytes the byte count * @return human readable formatted bytes */ public static String formatBytes(long bytes) { int unit = 1000; if (bytes < unit) return bytes + " B"; int exp = (int) (Math.log(bytes) / Math.log(unit)); String pre = "kMGTPE".charAt(exp-1) + ""; return String.format("%.1f %sB", bytes / Math.pow(unit, exp), pre); } /** * Escape " with " in strings * @param source * @return escaped string */ public static StringBuilder escapeDoubleQuotes(String source) { StringBuilder sb = new StringBuilder(300); if (source != null) { int len = source.length(); char[] cs = source.toCharArray(); for (int i = 0; i < len; i++) { char c = cs[i]; switch (c) { case '"': sb.append("""); break; default: sb.append(c); } } } return sb; } /** * Escape " with \" and ' with \' in strings * @param source * @return escaped string * @deprecated use org.apache.commons.lang.StringEscapeUtils.escapeJavaScript() instead. */ public static StringBuilder escapeSingleAndDoubleQuotes(String source) { if (source == null) return null; StringBuilder sb = new StringBuilder(300); int len = source.length(); char[] cs = source.toCharArray(); for (int i = 0; i < len; i++) { char c = cs[i]; switch (c) { case '"': sb.append("\\\""); break; case '\'': sb.append("\\\'"); break; default: sb.append(c); } } return sb; } /** * replace a given String with a different String * * @param source the source * @param delim the String to replace * @param replacement the replacement String * @return StringBuilder */ public StringBuilder replace(String source, String delim, String replacement) { if (source == null) return null; StringBuilder sb = new StringBuilder(300); StringTokenizer st = new StringTokenizer(source, delim); while (st.hasMoreTokens()) { String tok = st.nextToken(); sb.append(tok); sb.append(replacement); } return sb; } /** * truncates the supplied string to len-3 and replaces the last three positions * with ... * * @param source * @param len * @return truncated string */ public static String truncate(String source, int len) { if (source == null) return null; if (source.length() > len && len > 3) return truncate(source, len - 3, "..."); else return source; } /** * This returns a substring of a len length from the input string, as opposite to the * <code> truncate </code> method. This should be used for processing strings before writing. * @param source * @param len * @return */ public static String truncateOnly(String source, int len) { if (source == null) return null; if (source.length() > len) return source.substring(0, len) ; else return source; } /** * replaces all non ASCII characters in a string and also the most common special characters like by urlencode it * "/" "\" ":" "*" "?" """ ' "<" ">" "|" * @param source * @return a string which is OS independant and save for using on any filesystem */ public static String makeStringFilesystemSave(String source) { try { source = removeUndesirableSubstrings(source); return URLEncoder.encode(source, "utf-8"); } catch (UnsupportedEncodingException e) { //utf-8 should be supported } return ""; } /** * * @param source * @return */ private static String removeUndesirableSubstrings(String source) { String returnString = source; //replace successive dots with only one, for now. while(returnString!=null && returnString.indexOf("..")>-1) { returnString = returnString.replaceAll("\\.\\.", "."); //replace recursive 2 dots with one } return returnString; } /** * truncates a String: useful to limit in GUI * * @param source * @param len length of the returned string; if negative, return n chars from * the end of the string, otherwise from the beginning of the string * @param delim * @return truncated string */ public static String truncate(String source, int len, String delim) { if (source == null) return null; int start, stop; int alen = source.length(); if (len > 0) { if (alen <= len) return source; start = 0; //TODO effizienter stop = (len > alen ? alen : len); StringBuilder sb = new StringBuilder(source.substring(start, stop)); if (alen > len) sb.append(delim); return sb.toString(); } start = (len < -alen ? 0 : alen + len); stop = alen; StringBuilder sb = new StringBuilder(source.substring(start, stop)); if (-alen <= len) sb.insert(0, delim); return sb.toString(); } /** * @return locale of this formatter */ public Locale getLocale() { return locale; } /** * @param source * @return escaped string */ public static StringBuilder escWithBR(String source) { if (source == null) return null; StringBuilder sb = new StringBuilder(300); int len = source.length(); char[] cs = source.toCharArray(); for (int i = 0; i < len; i++) { char c = cs[i]; switch (c) { case '"': sb.append("""); break; case '<': sb.append("<"); break; case '>': sb.append(">"); break; case '&': // check on &# first (entities -> don't escape) if (i < len - 1 && cs[i + 1] == '#') { // we have # as next char sb.append("&"); } else { sb.append("&"); } break; //case '\r':sb.append("<br />"); break; case '\n': sb.append("<br />"); break; default: sb.append(c); } } return sb; } /** * @param source * @return stripped string */ public static StringBuilder stripTabsAndReturns(String source) { if (source == null) return null; StringBuilder sb = new StringBuilder(source.length() + (source.length() / 10)); int len = source.length(); char[] cs = source.toCharArray(); for (int i = 0; i < len; i++) { char c = cs[i]; switch (c) { case '\t': sb.append(" "); break; case '\n': sb.append("<br />"); break; case '\r': sb.append("<br />"); break; case '\u2028': // unicode linebreak sb.append("<br />"); break; default: sb.append(c); } } return sb; } /** * Wrapp given html code with a wrapper an add code to transform latex * formulas to nice visual characters on the client side. The latex formulas * must be within an HTML element that has the class 'math' attached. * * @param htmlFragment A html element that might contain an element that has a * class 'math' with latex formulas * @return */ public static String formatLatexFormulas(String htmlFragment) { if (htmlFragment == null) return ""; // optimize, reduce jsmath calls on client if (htmlFragment.contains("<math") || htmlFragment.contains("class='math'") || htmlFragment.contains("class=\"math\"")) { // add math wrapper String domid = "mw_" + CodeHelper.getRAMUniqueID(); String elem = htmlFragment.contains("<div") ? "div" : "span"; StringBuilder sb = new StringBuilder(htmlFragment.length() + 200); sb.append("<").append(elem).append(" id=\"").append(domid).append("\">"); sb.append(htmlFragment); sb.append("</").append(elem).append(">"); sb.append("\n<script type='text/javascript'>\n/* <![CDATA[ */\n setTimeout(function() { BFormatter.formatLatexFormulas('").append(domid).append("');}, 100);\n/* ]]> */\n</script>"); return sb.toString(); } return htmlFragment; } // Pattern to find URL's in text private static final Pattern urlPattern = Pattern.compile("((mailto\\:|(news|(ht|f)tp(s?))\\://|www\\.)[-A-Za-z0-9+&@#/%?=~_|!:,\\.;]+[-A-Za-z0-9+&@#/%=~_|]*)"); /** * Search in given text fragment for URL's and surround them with clickable * HTML link objects. * * @param textFragment * @return text with clickable links */ public static String formatURLsAsLinks(String textFragment) { Matcher matcher = urlPattern.matcher(textFragment); StringBuilder sb = new StringBuilder(128); int pos = 0; while (matcher.find()) { // Add text since last match and set end of current patch as new end // of this match sb.append(textFragment.substring(pos, matcher.start())); pos = matcher.end(); // The URL is in group1, the other groups are ignored String url = matcher.group(1); // Fix URL's without protocol, assume http if (url.startsWith("www")) { url = "http://" + url; } // Fix URL's at end of a sentence if (url.endsWith(",") || url.endsWith(".") || url.endsWith(":")) { url = url.substring(0, url.length()-1); pos--; } sb.append("<a href=\""); sb.append(url); sb.append("\""); if (url.startsWith("mailto")) { sb.append(" target=\"_blank\""); } // OpenOLAT URL's are opened in same window, all other URL's in separate window else if (!url.startsWith(Settings.getServerContextPathURI())) { sb.append(" target=\"_blank\""); } sb.append(">"); if (url.startsWith("mailto")) { sb.append("<i class='o_icon o_icon_mail'> </i> "); } else if (!url.startsWith(Settings.getServerContextPathURI())) { sb.append("<i class='o_icon o_icon_link_extern'> </i> "); } else { sb.append("<i class='o_icon o_icon_star'> </i> "); } sb.append(url); sb.append("</a>"); } // Add rest of text sb.append(textFragment.substring(pos)); // return sb.toString(); } /* emoticon patterns */ private static final Pattern angelPattern = Pattern.compile("(O\\:-*(\\)|3))"); private static final Pattern angryPattern = Pattern.compile("(\\:-*(\\|\\||@))"); private static final Pattern confusedPattern = Pattern.compile("(%-*\\))"); private static final Pattern coolPattern = Pattern.compile("(8-*\\))"); private static final Pattern grinPattern = Pattern.compile("(;-*\\))"); private static final Pattern kissPattern = Pattern.compile("(\\:(\\^)*\\*)"); private static final Pattern ohohPattern = Pattern.compile("(\\:-*O)"); private static final Pattern sadPattern = Pattern.compile("(\\:-*\\()"); private static final Pattern smilePattern = Pattern.compile("(\\:-*\\))"); private static final Pattern tonguePattern = Pattern.compile("(\\:-*P)"); private static final Pattern upPattern = Pattern.compile("((^\\s*(\\+)\\s*$)|(\\(\\+\\)))"); private static final Pattern downPattern = Pattern.compile("((^\\s*(-)\\s*$)|(\\(\\-\\)))"); private static final StringOutput emptyGifUrl = new StringOutput(); static { StaticMediaDispatcher.renderStaticURI(emptyGifUrl, "images/transparent.gif"); } /** * Search in textFragment for emoticons such as :-) :-( etc and replace them * with image tags that render a nice icon. * * @param textFragment * @return replaced text */ public static String formatEmoticonsAsImages(String textFragment) { Matcher matcher; matcher = confusedPattern.matcher(textFragment); textFragment= matcher.replaceAll("<img src='" + emptyGifUrl + "' class='o_emoticons_confused' />"); matcher = coolPattern.matcher(textFragment); textFragment= matcher.replaceAll("<img src='" + emptyGifUrl + "' class='o_emoticons_cool' />"); matcher = angryPattern.matcher(textFragment); textFragment= matcher.replaceAll("<img src='" + emptyGifUrl + "' class='o_emoticons_angry' />"); matcher = grinPattern.matcher(textFragment); textFragment= matcher.replaceAll("<img src='" + emptyGifUrl + "' class='o_emoticons_grin' />"); matcher = kissPattern.matcher(textFragment); textFragment= matcher.replaceAll("<img src='" + emptyGifUrl + "' class='o_emoticons_kiss' />"); matcher = ohohPattern.matcher(textFragment); textFragment= matcher.replaceAll("<img src='" + emptyGifUrl + "' class='o_emoticons_ohoh' />"); matcher = angelPattern.matcher(textFragment); // must be before smile pattern textFragment= matcher.replaceAll("<img src='" + emptyGifUrl + "' class='o_emoticons_angel' />"); matcher = smilePattern.matcher(textFragment); textFragment= matcher.replaceAll("<img src='" + emptyGifUrl + "' class='o_emoticons_smile' />"); matcher = sadPattern.matcher(textFragment); textFragment= matcher.replaceAll("<img src='" + emptyGifUrl + "' class='o_emoticons_sad' />"); matcher = tonguePattern.matcher(textFragment); textFragment= matcher.replaceAll("<img src='" + emptyGifUrl + "' class='o_emoticons_tongue' />"); matcher = upPattern.matcher(textFragment); textFragment= matcher.replaceAll("<img src='" + emptyGifUrl + "' class='o_emoticons_up' />"); matcher = downPattern.matcher(textFragment); textFragment= matcher.replaceAll("<img src='" + emptyGifUrl + "' class='o_emoticons_down' />"); return textFragment; } /** * Round a double value to a double value with given number of * figures after comma * @param value * @param decimalPlace * @return rounded double value */ public static double round(double value, int decimalPlace) { BigDecimal bd = new BigDecimal(value); bd = bd.setScale(decimalPlace,BigDecimal.ROUND_HALF_UP); value = bd.doubleValue(); return value; } /** * Round a float value to a float value with given number of * figures after comma * @param value * @param decimalPlace * @return rounded float value */ public static float round(float value, int decimalPlace) { BigDecimal bd = new BigDecimal(value); bd = bd.setScale(decimalPlace,BigDecimal.ROUND_HALF_UP); value = bd.floatValue(); return value; } /** * Format a float as string with given number of figures after * comma * @param value * @param decimalPlace * @return formatted string */ public static String roundToString(float value, int decimalPlace) { BigDecimal bd = new BigDecimal(value); bd = bd.setScale(decimalPlace,BigDecimal.ROUND_HALF_UP); return bd.toString(); } }