package io.milton.mini.utils; import io.milton.resource.Resource; import java.math.BigDecimal; import java.math.RoundingMode; import java.text.DateFormat; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Calendar; import java.util.Date; import org.joda.time.DateTime; import org.joda.time.Duration; import org.joda.time.Interval; import org.joda.time.format.DateTimeFormat; import io.milton.cloud.common.CurrentDateService; import io.milton.common.FileUtils; import io.milton.common.Utils; import io.milton.http.HttpManager; import io.milton.http.Request; import io.milton.vfs.db.Profile; import java.io.ByteArrayOutputStream; import java.io.IOException; import io.milton.http.annotated.JsonWriter; import io.milton.http.annotated.ResourceList; /** * Handy functions exposes to rendering logic for formatting. * * @author brad */ public class Formatter { private static org.apache.log4j.Logger log = org.apache.log4j.Logger.getLogger(Formatter.class); public static final String CHECKBOX_SUFFIX = "_checkbox"; public static ThreadLocal<DateFormat> tlSdfUkShort = new ThreadLocal<DateFormat>() { @Override protected DateFormat initialValue() { return new SimpleDateFormat("dd/MM/yyyy"); } }; public static ThreadLocal<DateFormat> tlSdfUkLong = new ThreadLocal<DateFormat>() { @Override protected DateFormat initialValue() { return new SimpleDateFormat("dd MMMM yyyy"); } }; public static final ThreadLocal<DateFormat> sdfDateOnly = new ThreadLocal<DateFormat>() { @Override protected DateFormat initialValue() { return new SimpleDateFormat("dd/MM/yyyy"); } }; public static final ThreadLocal<DateFormat> sdfDateAndTime = new ThreadLocal<DateFormat>() { @Override protected DateFormat initialValue() { return new SimpleDateFormat("dd/MM/yyyy HH:mm"); } }; private final CurrentDateService currentDateService; public Formatter(CurrentDateService currentDateService) { this.currentDateService = currentDateService; } /** * Null safe method, returns empty string if the value is null * * @param o * @return */ public String toString(Object o) { if (o == null) { return ""; } else { return o.toString(); } } public Boolean toBool(Object o) { if (o == null) { return null; } else if (o instanceof Boolean) { return (Boolean) o; } else if (o instanceof Integer) { Integer i = (Integer) o; return i.intValue() == 0; } else if (o instanceof String) { String s = (String) o; s = s.toLowerCase(); s = s.trim(); if (s.length() > 0) { return s.equals("true") || s.equals("yes"); } else { return null; } } else { throw new RuntimeException("Unsupported boolean type: " + o.getClass()); } } public BigDecimal toDecimal(Object o, int places) { if (o == null) { return BigDecimal.ZERO; } else if (o instanceof BigDecimal) { BigDecimal bd = (BigDecimal) o; return bd.setScale(places, RoundingMode.HALF_UP); } else if (o instanceof Double) { Double d = (Double) o; return BigDecimal.valueOf(d).setScale(places, RoundingMode.HALF_UP); } else if (o instanceof Integer) { Integer i = (Integer) o; return BigDecimal.valueOf(i.longValue()).setScale(places, RoundingMode.HALF_UP); } else if (o instanceof Float) { Float f = (Float) o; return BigDecimal.valueOf(f.doubleValue()).setScale(places, RoundingMode.HALF_UP); } else if (o instanceof String) { String s = (String) o; s = s.trim(); if (s.length() == 0) { return BigDecimal.ZERO; } else { try { return new BigDecimal(s).setScale(places, RoundingMode.HALF_UP); } catch (NumberFormatException numberFormatException) { throw new RuntimeException("Non-numeric data: " + s); } } } else { throw new RuntimeException("Unsupported value type, should be numeric: " + o.getClass()); } } public Double toDouble(Object o) { if (o == null) { return 0d; } else if (o instanceof String) { String s = (String) o; s = s.trim(); if (s.length() == 0) { return 0d; } else { try { return Double.valueOf(s); } catch (NumberFormatException numberFormatException) { throw new RuntimeException("Non-numeric data: " + s); } } } else if (o instanceof Double) { return (Double) o; } else if (o instanceof Integer) { Integer i = (Integer) o; return (double) i; } else if (o instanceof Float) { Float f = (Float) o; return f.doubleValue(); } else if (o instanceof BigDecimal) { BigDecimal bd = (BigDecimal) o; return bd.doubleValue(); } else { throw new RuntimeException("Unsupported value type, should be numeric: " + o.getClass()); } } public Long toLong(Object oLimit) { return toLong(oLimit, false); } public Long toLong(Object oVal, boolean withNulls) { Long limit; if (oVal == null) { limit = withNulls ? null : 0l; } else if (oVal instanceof Long) { limit = (Long) oVal; } else if (oVal instanceof Integer) { int i = (Integer) oVal; limit = (long) i; } else if (oVal instanceof Double) { Double d = (Double) oVal; return d.longValue(); } else if (oVal instanceof Float) { Float d = (Float) oVal; return d.longValue(); } else if (oVal instanceof BigDecimal) { BigDecimal bd = (BigDecimal) oVal; return bd.longValue(); } else if (oVal instanceof Boolean) { Boolean bb = (Boolean) oVal; return bb ? 1l : 0l; } else if (oVal instanceof String) { String s = (String) oVal; if (s.length() == 0) { limit = withNulls ? null : 0l; } else { if (s.equals("true") || s.equals("false")) { Boolean b = Boolean.parseBoolean(s); return toLong(b); } else { if (s.contains(".")) { Double d = toDouble(s); limit = d.longValue(); } else { limit = Long.parseLong(s); } } } } else { throw new RuntimeException("unsupported class: " + oVal.getClass()); } return limit; } public int getYear(Object o) { if (o == null || !(o instanceof Date)) { return 0; } Date dt = (Date) o; Calendar cal = Calendar.getInstance(); cal.setTime(dt); return cal.get(Calendar.YEAR); } public int getMonth(Object o) { if (o == null || !(o instanceof Date)) { return 0; } Date dt = (Date) o; Calendar cal = Calendar.getInstance(); cal.setTime(dt); return cal.get(Calendar.MONTH) + 1; } public int getDayOfMonth(Object o) { if (o == null || !(o instanceof Date)) { return 0; } Date dt = (Date) o; Calendar cal = Calendar.getInstance(); cal.setTime(dt); return cal.get(Calendar.DAY_OF_MONTH) + 1; } public int getHour(Object o) { if (o == null || !(o instanceof Date)) { return 0; } Date dt = (Date) o; Calendar cal = Calendar.getInstance(); cal.setTime(dt); return cal.get(Calendar.HOUR_OF_DAY); } public int getMinute(Object o) { if (o == null || !(o instanceof Date)) { return 0; } Date dt = (Date) o; Calendar cal = Calendar.getInstance(); cal.setTime(dt); return cal.get(Calendar.MINUTE); } public String formatDate(Object o) { DateTime dt = getDateTime(o); if (dt == null) { return ""; } return pad2(dt.getDayOfMonth()) + "/" + pad2(dt.getMonthOfYear()) + "/" + pad(dt.getYear(), 4); } public String formatDateLong(Object o) { DateTime dt = getDateTime(o); if (dt == null) { return ""; } return DateTimeFormat.longDateTime().print(dt); } public String formatDateISO8601(Object o) { DateTime dt = getDateTime(o); if (dt == null) { return ""; } else { return dt.toString(); } } /** * Returns a user friendly description of the age of the date. Eg "4 minutes * ago" * * @param o * @return */ public String formatAge(Object o) { DateTime dt = getDateTime(o); DateTime now = new DateTime(); Interval i; if (dt.isBefore(now)) { i = new Interval(dt, now); } else { i = new Interval(now, dt); } Duration d = i.toDuration(); long secs = d.getStandardSeconds(); if (secs < 10) { return "Just now"; } else if (secs < 60) { return secs + " seconds ago"; } else if (secs < 60 * 60) { return secs / 60 + " minutes ago"; } else if (secs < 24 * 60 * 60) { return secs / (60 * 60) + " hours ago"; } else { long days = secs / (60 * 60 * 24); if (days < 2) { return "a day ago"; } else if (days < 30) { return days + " days ago"; } else if (days < 40) { return "a month ago"; } else if (days < 7 * 8) { return days / 7 + " weeks ago"; } else { return days / 30 + " months ago"; } } } public String formatMinsAsDuration(Object o) { return formatMinsAsDuration(o, true); } /** * Given a value which can be parsed to a Long, return it formatted as a * human readable duration such as 12:30 (12 mins, 30 seconds) or 12 mins, 3 * hrs 20 * * @param o * @param numeric * @return */ public String formatMinsAsDuration(Object o, boolean numeric) { Long l = toLong(o); if (l == null) { return ""; } else { if (l == 0) { return ""; } long hours = l / 60; long mins = l % 60; if (numeric) { return hours + ":" + pad(mins, 2); } else { if (hours == 0) { return mins + "mins"; } else if (hours == 1) { return hours + "hr " + mins; } else { return hours + "hrs " + mins; } } } } public String pad2(long l) { return pad(l, 2); } public String pad(long l, int length) { return padWith("0", l, length); } public String padWith(String padChar, long l, int length) { return _pad(padChar, l + "", length); } private String _pad(String padChar, String val, int length) { if (val.length() >= length) { return val; } return _pad(padChar, padChar + val, length); } public org.joda.time.DateTime getDateTime(Object o) { if (o == null) { return null; } else if (o instanceof Resource) { Resource res = (Resource) o; return getDateTime(res.getModifiedDate()); } else if (o instanceof String) { if (o.toString().length() == 0) { return null; } else { try { Date dt = tlSdfUkShort.get().parse(o.toString()); return new DateTime(dt.getTime()); } catch (ParseException ex) { throw new RuntimeException("Couldnt convert to date: " + o, ex); } } } return new DateTime(o); } /** * Format as a percentage, including a percentage symbol and where * blank/null values result in a blank output * * @param num - the numerator * @param div - the divisor * @return */ public String toPercent(Object num, Object div) { return toPercent(num, div, true, true); } /** * * @param num * @param div * @param appendSymbol - if true the percentage symbol is appended if a * non-blank value * @param withBlanks - if true, blank numerators or divisors result in a * blank value. Otherwise return zero. * @return */ public String toPercent(Object num, Object div, boolean appendSymbol, boolean withBlanks) { Long lNum = toLong(num, true); Long lDiv = toLong(div, true); if (lDiv == null || lDiv == 0 || lNum == null) { if (withBlanks) { return ""; } else { return "0" + (appendSymbol ? "%" : ""); } } else { long perc = lNum * 100 / lDiv; return perc + (appendSymbol ? "%" : ""); } } public String format(Object o) { if (o == null) { return ""; } else if (o instanceof Date) { return formatDate(o); } else { return o.toString().trim(); } } /** * Removes the file extension if present * * Eg file1.swf -> file1 * * file1 -> file1 * * @param s * @return */ public String stripExt(String s) { if (s == null || s.length() == 0) { return ""; } return FileUtils.stripExtension(s); } /** * True if val1 is greater then val2 * * will do string conversions * * @param val1 * @param val2 * @return */ public boolean gt(Object val1, Object val2) { if (val1 == null) { return false; } if (val2 == null) { return true; } Double d1 = toDouble(val1); Double d2 = toDouble(val2); return d1.doubleValue() > d2.doubleValue(); } public boolean lt(Object val1, Object val2) { if (val1 == null) { return false; } if (val2 == null) { return true; } Double d1 = toDouble(val1); Double d2 = toDouble(val2); return d1.doubleValue() < d2.doubleValue(); } public boolean eq(Object val1, Object val2) { if (val1 == null) { return (val2 == null); } if (val2 == null) { return false; } Double d1 = toDouble(val1); Double d2 = toDouble(val2); return d1.doubleValue() == d2.doubleValue(); } /** * Makes the given string suitable for rendering in HTML. Symbols like angle * brackets will be encoded so they can be displayed * * @param s * @return */ public String htmlEncode(String s) { return EncodeUtils.encodeHTML(s); } /** * Decode percentage encoded paths. Eg a%20b -> a b * * @param s * @return */ public String percentDecode(String s) { if (s == null) { return ""; } else if (s.length() == 0) { return ""; } return Utils.decodePath(s); } public String percentEncode(String s) { if (s == null) { return null; } return Utils.percentEncode(s); } /** * Returns true if the given value is between the start and finish dates, or * the respective values are null. Ie if start date is null and finish date * is given it will only check that the value is less then the finish date * * Values are converted using the joda time converters * * @param oVal * @param oStart * @param oFinish * @return */ public boolean between(Object oVal, Object oStart, Object oFinish) { DateTime val = getDateTime(oVal); if (val == null) { log.warn("null date value"); return false; } DateTime start = getDateTime(oStart); DateTime finish = getDateTime(oFinish); if (start != null) { if (val.isBefore(start)) { return false; } } if (finish != null) { if (val.isAfter(finish)) { return false; } } return true; } public Date toDate(Object oVal) { if (oVal == null) { return null; } else if (oVal instanceof Date) { return (Date) oVal; } else { if (oVal instanceof String) { String s = (String) oVal; return parseDate(s); } else { return null; } } } public java.sql.Date toSqlDate(Object oVal) { Date dt = toDate(oVal); if (dt == null) { return null; } else { return new java.sql.Date(dt.getTime()); } } public java.sql.Timestamp toSqlTimestamp(Object oVal) { Date dt = toDate(oVal); if (dt == null) { return null; } else { return new java.sql.Timestamp(dt.getTime()); } } public org.joda.time.DateTime toJodaDate(Object oVal) { Date dt = toDate(oVal); if (dt != null) { return new DateTime(dt.getTime()); } else { return null; } } public String toPlain(String html) { if (html == null) { return null; } html = replaceTag("br", html, "", "\n"); html = replaceTag("p", html, "", "\n"); html = replaceTag("b", html, "", ""); html = replaceTag("i", html, "", ""); html = replaceTag("h1", html, "", ""); html = replaceTag("h2", html, "", ""); html = replaceTag("h3", html, "", ""); return html; } private String replaceTag(String tag, String html, String replaceWithOpening, String replaceWithClosing) { html = html.replace("<" + tag + "/>", replaceWithClosing); // self closing html = html.replace("<" + tag + ">", replaceWithOpening); // opening tag html = html.replace("</" + tag + ">", replaceWithClosing); // closing tag return html; } public Date getNow() { return currentDateService.getNow(); } /** * Get the duration from the start to the finish date in seconds. * * @param start - any object which can be converted to a jodadate * @param finish - any object which can be converted to a jodadate * @return */ public long durationSecs(Object start, Object finish) { DateTime jodaSt = toJodaDate(start); DateTime jodaFn = toJodaDate(finish); Duration d = new Duration(jodaSt, jodaFn); return d.getStandardSeconds(); } /** * Get the duration from the start to the finish date in seconds. * * @param start - any object which can be converted to a jodadate * @param finish - any object which can be converted to a jodadate * @return */ public long durationHours(Object start, Object finish) { DateTime jodaSt = toJodaDate(start); DateTime jodaFn = toJodaDate(finish); Duration d = new Duration(jodaSt, jodaFn); return d.getStandardSeconds() / (60 * 60); } public String getMonthName(int i) { switch (i) { case 0: return "January"; case 1: return "February"; case 2: return "March"; case 3: return "April"; case 4: return "May"; case 5: return "June"; case 6: return "July"; case 7: return "August"; case 8: return "September"; case 9: return "October"; case 10: return "November"; case 11: return "December"; default: return "Unknown month " + i; } } public CurrentDateService getCurrentDateService() { return currentDateService; } /** * If o1 is equal to o2, then output the ifEqual parameter, otherwise the * ifNoteEqual parameter * * This is a nullsafe comparison * * @param ifEqual * @param ifNotEqual * @param o1 * @param o2 * @return */ public String ifEqual(String ifEqual, String ifNotEqual, Object o1, Object o2) { if (o1 == null) { return o2 == null ? ifEqual : ifNotEqual; } else { if ( o2 != null && o1.getClass() == o2.getClass()) { return o1.equals(o2) ? ifEqual : ifNotEqual; } else { String s1 = o1.toString(); String s2 = null; if (o2 != null) { s2 = o2.toString(); } return s1.equals(s2) ? ifEqual : ifNotEqual; } } } /** * This just permits simple templating syntax for basic conditional values * * Eg: <li><a class="$formatter.ifTrue($item.active, 'navActive', '')" * href="$item.href">$item.text</a></li> * * @param b * @param o1 * @param o2 * @return */ public Object ifTrue(Object bb, Object o1, Object o2) { Boolean b = toBool(bb); if (b == null) { b = Boolean.FALSE; } return b ? o1 : o2; } public ResourceList newList() { return new ResourceList(); } public ResourceList getList() { return new ResourceList(); } private Date parseDate(String s) { if (s == null || s.trim().length() == 0) { return null; } try { Date dt; if (s.contains(":")) { dt = sdf(true).parse(s); } else { dt = sdf(false).parse(s); } return dt; } catch (ParseException ex) { log.warn("couldnt parse date", ex); return null; // throw new RuntimeException(ex); } } public DateFormat sdf(boolean hasTime) { if (hasTime) { return sdfDateAndTime.get(); } else { return sdfDateOnly.get(); } } public BigDecimal toBigDecimal(Object o, int decimals) { if (o instanceof Integer) { Integer ii = (Integer) o; return new BigDecimal(ii.intValue()); } else if (o instanceof Double) { Double dd = (Double) o; return new BigDecimal(dd.doubleValue()).setScale(decimals, RoundingMode.HALF_UP); } else if (o instanceof Float) { Float ff = (Float) o; return new BigDecimal(ff); } else if (o instanceof String) { Double dd = toDouble(o); return toBigDecimal(dd, decimals); } else { log.warn("unhandled type: " + o.getClass()); return null; } } public String checkbox(String name, Object oChecked) { String s = checkbox(null, name, oChecked, "true"); return s; } public String checkbox(String id, String name, Object oChecked) { return checkbox(id, name, oChecked, "true"); } public String checkbox(String id, String name, Object oChecked, String value) { Boolean checked = toBool(oChecked); if (checked == null) { checked = Boolean.FALSE; } StringBuilder sb = new StringBuilder(); sb.append("<input type='hidden' value='' name='").append(name).append(CHECKBOX_SUFFIX).append("'/>"); sb.append("<input type=\"checkbox\""); sb.append(" name=\"").append(name).append("\" "); if (checked) { sb.append("checked=\"true\""); } appendValue(sb, value); if (id != null) { sb.append(" id=\"").append(id).append("\""); } sb.append(" />"); return sb.toString(); } public String radioEq(String id, String name, String currentValue, String value) { boolean isSet = (currentValue != null && currentValue.equals(value)); return radio(id, name, isSet, value); } public String radio(String id, String name, Object oChecked, String value) { Boolean checked = toBool(oChecked); if (checked == null) { checked = Boolean.FALSE; } StringBuilder sb = new StringBuilder("<input type=\"radio\""); sb.append(" name=\"").append(name).append("\""); if (checked) { sb.append(" checked=\"true\""); } appendValue(sb, value); if (id != null) { sb.append(" id=\"").append(id).append("\""); } sb.append(" />"); return sb.toString(); } /** * Generate an option element * * @return */ public String option(Object value, Object oText, Object currentValue) { String text = format(oText); StringBuilder sb = new StringBuilder("<option"); appendValue(sb, value); sb.append(ifEqual(" selected=\"true\"", "", value, currentValue)); sb.append(">"); sb.append(text).append("</option>"); return sb.toString(); } /** * Appends the html encoded value surrounded by quotes * * @param sb * @param value */ private void appendValue(StringBuilder sb, Object value) { sb.append(" value="); sb.append("\""); if (value != null) { sb.append(htmlEncode(value.toString())); } sb.append("\""); } /** * Attempts to find the port of the current request, defaults to 80 * * @return */ public int getPort() { int port = 80; Request req = HttpManager.request(); if (req != null) { String sHost = req.getHostHeader(); if (sHost != null) { String[] arr = sHost.split(":"); if (arr.length > 1) { String sPort = arr[1].trim(); if (sPort.length() > 0) { port = Integer.parseInt(sPort); } } } } return port; } /** * Returns empty string if the current request is on port 80, otherwise * returns the port number prefixed with a colon, eg :8080 * * @return */ public String getPortString() { int p = getPort(); if (p == 80) { return ""; } else { return ":" + p; } } public String profilePicHref(Profile p) { if (p.getPhotoHash() == null) { return "/templates/apps/user/profile.png"; } else { return "/_hashes/files/" + p.getPhotoHash(); } } public String toCsv(Iterable list) { StringBuilder sb = new StringBuilder(); if (list != null) { for (Object o : list) { if (sb.length() > 0) { sb.append(","); } sb.append(o.toString()); } } return sb.toString(); } public String toCsv(String[] list) { StringBuilder sb = new StringBuilder(); if (list != null) { for (Object o : list) { if (sb.length() > 0) { sb.append(","); } sb.append(o.toString()); } } return sb.toString(); } public String toJson(Object val) throws IOException { ByteArrayOutputStream bout = new ByteArrayOutputStream(); JsonWriter jsonWriter = new JsonWriter(); jsonWriter.write(val, bout); return bout.toString("UTF-8"); } /** * Return a date which has the given number of days added (or subtracted if * negative) to the given date * * @param now * @param i * @return */ public Date addDays(Date now, int days) { Calendar cal = Calendar.getInstance(); cal.setTime(now); cal.add(Calendar.DAY_OF_YEAR, days); return cal.getTime(); } public boolean isNotNull(Object o) { return o != null && !o.equals(""); } public boolean isNull(Object o) { return o == null || o.equals(""); } }