/** * Copyright (c) 2008-2012 The Sakai Foundation * * Licensed under the Educational Community License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.osedu.org/licenses/ECL-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.sakaiproject.profile2.util; import java.awt.image.BufferedImage; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.text.DateFormat; import java.text.DateFormatSymbols; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Arrays; import java.util.Calendar; import java.util.Date; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Set; import java.util.UUID; import javax.imageio.ImageIO; import org.apache.commons.codec.digest.DigestUtils; import org.apache.commons.lang.StringEscapeUtils; import org.apache.commons.lang.StringUtils; import org.apache.commons.lang.WordUtils; import org.apache.commons.lang.time.DateUtils; import org.apache.log4j.Logger; import org.imgscalr.Scalr; import org.sakaiproject.util.FormattedText; import org.sakaiproject.util.ResourceLoader; public class ProfileUtils { private static final Logger log = Logger.getLogger(ProfileUtils.class); /** * Check content type against allowed types. only JPEG,GIF and PNG are support at the moment * * @param contentType string of the content type determined by some image parser */ public static boolean checkContentTypeForProfileImage(String contentType) { ArrayList<String> allowedTypes = new ArrayList<String>(); allowedTypes.add("image/jpeg"); allowedTypes.add("image/gif"); allowedTypes.add("image/png"); //Adding MIME types that Internet Explorer returns PRFL-98 allowedTypes.add("image/x-png"); allowedTypes.add("image/pjpeg"); allowedTypes.add("image/jpg"); //add more here as required, BUT also add them below. //You will need to check ImageIO for the informal names. if(allowedTypes.contains(contentType)) { return true; } return false; } /** * Helper to get the informal format name that is used by ImageIO. * We have access to the mimetype so we can map them. * * <p>If no valid mapping is found, it will default to "jpg". * * @param mimeType the mimetype of the original image, eg image/jpeg */ public static String getInformalFormatForMimeType(String mimeType){ Map<String,String> formats = new HashMap<String,String>(); formats.put("image/jpeg", "jpg"); formats.put("image/gif", "gif"); formats.put("image/png", "png"); formats.put("image/x-png", "png"); formats.put("image/pjpeg", "jpg"); formats.put("image/jpg", "jpg"); String format = formats.get(mimeType); if(format != null) { return format; } return "jpg"; } public static byte[] scaleImage(byte[] imageData, int maxSize, String mimeType) { InputStream in = null; try { in = new ByteArrayInputStream(imageData); return scaleImage(in, maxSize, mimeType); } finally { if (in != null) { try { in.close(); log.debug("Image stream closed."); } catch (IOException e) { log.error("Error closing image stream: ", e); } } } } /** * Scale an image so it is fit within a give width and height, whilst maintaining its original proportions * * @param imageData bytes of the original image * @param maxSize maximum dimension in px */ public static byte[] scaleImage(InputStream in, int maxSize, String mimeType) { byte[] scaledImageBytes = null; try { //convert original image to inputstream //original buffered image BufferedImage originalImage = ImageIO.read(in); //scale the image using the imgscalr library BufferedImage scaledImage = Scalr.resize(originalImage, maxSize); //convert BufferedImage to byte array ByteArrayOutputStream baos = new ByteArrayOutputStream(); ImageIO.write(scaledImage, getInformalFormatForMimeType(mimeType), baos); baos.flush(); scaledImageBytes = baos.toByteArray(); baos.close(); } catch (Exception e) { log.error("Scaling image failed.", e); } return scaledImageBytes; } /** * Convert a Date into a String according to format, or, if format * is set to null, do a current locale based conversion. * * @param date date to convert * @param format format in SimpleDateFormat syntax. Set to null to force as locale based conversion. */ public static String convertDateToString(Date date, String format) { if(date == null || "".equals(format)) { throw new IllegalArgumentException("Null Argument in Profile.convertDateToString()"); } String dateStr = null; if(format != null) { SimpleDateFormat dateFormat = new SimpleDateFormat(format); dateStr = dateFormat.format(date); } else { // Since no specific format has been specced, we use the user's locale. Locale userLocale = (new ResourceLoader()).getLocale(); DateFormat formatter = DateFormat.getDateInstance(DateFormat.MEDIUM, userLocale); dateStr = formatter.format(date); } if(log.isDebugEnabled()) { log.debug("Profile.convertDateToString(): Input date: " + date.toString()); log.debug("Profile.convertDateToString(): Converted date string: " + dateStr); } return dateStr; } /** * Convert a string into a Date object (reverse of above * * @param dateStr date string to convert * @param format format of the input date in SimpleDateFormat syntax */ public static Date convertStringToDate(String dateStr, String format) { if("".equals(dateStr) || "".equals(format)) { throw new IllegalArgumentException("Null Argument in Profile.convertStringToDate()"); } SimpleDateFormat dateFormat = new SimpleDateFormat(format); try { Date date = dateFormat.parse(dateStr); log.debug("Profile.convertStringToDate(): Input date string: " + dateStr); log.debug("Profile.convertStringToDate(): Converted date: " + date.toString()); return date; } catch (ParseException e) { log.error("Profile.convertStringToDate() failed. " + e.getClass() + ": " + e.getMessage()); return null; } } /** * Strip the year from a given date (actually just sets it to 1) * * @param date original date * @return */ public static Date stripYear(Date date){ return DateUtils.setYears(date, 1); } /** * Get the localised name of the day (ie Monday for en, Maandag for nl) * @param day int according to Calendar.DAY_OF_WEEK * @param locale locale to render dayname in * @return */ public static String getDayName(int day, Locale locale) { //localised daynames String dayNames[] = new DateFormatSymbols(locale).getWeekdays(); String dayName = null; try { dayName = dayNames[day]; } catch (Exception e) { log.error("Profile.getDayName() failed. " + e.getClass() + ": " + e.getMessage()); } return dayName; } /** * Convert a string to propercase. ie This Is Proper Text * @param input string to be formatted * @return */ public static String toProperCase(String input) { return WordUtils.capitalizeFully(input); } /** * Convert a date into a field like "just then, 2 minutes ago, 4 hours ago, yesterday, on sunday, etc" * * @param date date to convert */ public static String convertDateForStatus(Date date) { //current time Calendar currentCal = Calendar.getInstance(); long currentTimeMillis = currentCal.getTimeInMillis(); //posting time long postingTimeMillis = date.getTime(); //difference int diff = (int)(currentTimeMillis - postingTimeMillis); Locale locale = getUserPreferredLocale(); //System.out.println("currentDate:" + currentTimeMillis); //System.out.println("postingDate:" + postingTimeMillis); //System.out.println("diff:" + diff); int MILLIS_IN_SECOND = 1000; int MILLIS_IN_MINUTE = 1000 * 60; int MILLIS_IN_HOUR = 1000 * 60 * 60; int MILLIS_IN_DAY = 1000 * 60 * 60 * 24; int MILLIS_IN_WEEK = 1000 * 60 * 60 * 24 * 7; if(diff < MILLIS_IN_SECOND) { //less than a second return Messages.getString("Label.just_then"); } else if (diff < MILLIS_IN_MINUTE) { //less than a minute, calc seconds int numSeconds = diff/MILLIS_IN_SECOND; if(numSeconds == 1) { //one sec return Messages.getString("Label.second_ago", new Object[] {numSeconds}); } else { //more than one sec return Messages.getString("Label.seconds_ago", new Object[] {numSeconds}); } } else if (diff < MILLIS_IN_HOUR) { //less than an hour, calc minutes int numMinutes = diff/MILLIS_IN_MINUTE; if(numMinutes == 1) { //one minute return Messages.getString("Label.minute_ago", new Object[] {numMinutes}); } else { //more than one minute return Messages.getString("Label.minutes_ago", new Object[] {numMinutes}); } } else if (diff < MILLIS_IN_DAY) { //less than a day, calc hours int numHours = diff/MILLIS_IN_HOUR; if(numHours == 1) { //one hour return Messages.getString("Label.hour_ago", new Object[] {numHours}); } else { //more than one hour return Messages.getString("Label.hours_ago", new Object[] {numHours}); } } else if (diff < MILLIS_IN_WEEK) { //less than a week, calculate days int numDays = diff/MILLIS_IN_DAY; //now calculate which day it was if(numDays == 1) { return Messages.getString("Label.yesterday"); } else { //set calendar and get day of week Calendar postingCal = Calendar.getInstance(); postingCal.setTimeInMillis(postingTimeMillis); int postingDay = postingCal.get(Calendar.DAY_OF_WEEK); //set to localised value: 'on Wednesday' for example String dayName = getDayName(postingDay,locale); if(dayName != null) { return Messages.getString("Label.on", new Object[] {toProperCase(dayName)}); } } } else { //over a week ago, we want it blank though. } return null; } /** * Gets the users preferred locale, either from the user's session or Sakai preferences and returns it * This depends on Sakai's ResourceLoader. * * @return */ public static Locale getUserPreferredLocale() { ResourceLoader rl = new ResourceLoader(); return rl.getLocale(); } /** * Creates a full profile event reference for a given reference * @param ref * @return */ public static String createEventRef(String ref) { return "/profile/"+ref; } /** * Method for getting a value from a map based on the given key, but if it does not exist, use the given default * @param map * @param key * @param defaultValue * @return */ public static Object getValueFromMapOrDefault(Map<?,?> map, Object key, Object defaultValue) { return (map.containsKey(key) ? map.get(key) : defaultValue); } /** * Method to chop a String into it's parts based on the separator and return as a List. Useful for multi valued Sakai properties * @param str the String to split * @param separator separator character * @return */ public static List<String> getListFromString(String str, char separator) { String[] items = StringUtils.split(str, separator); return Arrays.asList(items); } /** * Processes HTML and escapes evils tags like <script>, also converts newlines to proper HTML breaks. * @param s * @return */ public static String processHtml(String s){ return FormattedText.processFormattedText(s, new StringBuilder(), true, false); } /** * Strips string of HTML and returns plain text. * * @param s * @return */ public static String stripHtml(String s) { return FormattedText.convertFormattedTextToPlaintext(s); } /** * Strips string of HTML, escaping anything that is left to return plain text. * * <p>Deals better with poorly formed HTML than just stripHtml and is best for XSS protection, not for storing actual data. * * @param s The string to process * @return */ public static String stripAndCleanHtml(String s) { //Attempt to strip HTML. This doesn't work on poorly formatted HTML though String stripped = FormattedText.convertFormattedTextToPlaintext(s); //so we escape anything that is left return StringEscapeUtils.escapeHtml(stripped); } /** * Trims text to the given maximum number of displayed characters. * Supports HTML and preserves formatting. * * @param s the string * @param maxNumOfChars num chars to keep. If HTML, it's the number of content chars, ignoring tags. * @param isHtml is the string HTML? * @return */ public static String truncate(String s, int maxNumOfChars, boolean isHtml) { if (StringUtils.isBlank(s)) { return ""; } //html if(isHtml) { StringBuilder trimmedHtml = new StringBuilder(); FormattedText.trimFormattedText(s, maxNumOfChars, trimmedHtml); return trimmedHtml.toString(); } //plain text return StringUtils.substring(s, 0, maxNumOfChars); } /** * Trims and abbreviates text to the given maximum number of displayed * characters (less 3 characters, in case "..." must be appended). * Supports HTML and preserves formatting. * * @param s the string * @param maxNumOfChars num chars to keep. If HTML, it's the number of content chars, ignoring tags. * @param isHtml is the string HTML? * @return */ public static String truncateAndAbbreviate(String s, int maxNumOfChars, boolean isHtml) { if (StringUtils.isBlank(s)) { return ""; } //html if(isHtml) { StringBuilder trimmedHtml = new StringBuilder(); boolean trimmed = FormattedText.trimFormattedText(s, maxNumOfChars - 3, trimmedHtml); if (trimmed) { int index = trimmedHtml.lastIndexOf("</"); if (-1 != index) { trimmedHtml.insert(index, "..."); } else { trimmedHtml.append("..."); } } return trimmedHtml.toString(); } //plain text return StringUtils.abbreviate(s, maxNumOfChars); } /** * Generate a UUID * @return */ public static String generateUuid() { UUID uuid = UUID.randomUUID(); return uuid.toString(); } /** * Returns the SkypeMe URL for the specified Skype username. * * @param skypeUsername * @return the SkypeMe URL for the specified Skype username. */ public static String getSkypeMeURL(String skypeUsername) { return "skype:" + skypeUsername + "?call"; } /** * Remove duplicates from a list, order is not retained. * * @param list list of objects to clean */ public static <T> void removeDuplicates(List<T> list){ Set<T> set = new HashSet<T>(); set.addAll(list); list.clear(); list.addAll(set); } /** * Remove duplicates from a list, order is retained. * * @param list list of objects to clean */ public static <T> void removeDuplicatesWithOrder(List<T> list) { Set<T> set = new HashSet<T> (); List<T> newList = new ArrayList<T>(); for(T e: list) { if (set.add(e)) { newList.add(e); } } list.clear(); list.addAll(newList); } /** * Calculate an MD5 hash of a string * @param s String to hash * @return MD5 hash as a String */ public static String calculateMD5(String s){ return DigestUtils.md5Hex(s); } /** * Creates a square avatar image by taking a segment out of the centre of the original image and resizing to the appropriate dimensions * * @param imageData original bytes of the image * @param mimeType mimetype of image * @return */ public static byte[] createAvatar(byte[] imageData, String mimeType) { InputStream in = null; byte[] outputBytes = null; try { //convert original image to inputstream in = new ByteArrayInputStream(imageData); //original buffered image BufferedImage originalImage = ImageIO.read(in); //OPTION 1 //determine the smaller side of the image and use that as the size of the cropped square //to be taken out of the centre //then resize to the avatar size =80 square. int smallestSide = originalImage.getWidth(); if(originalImage.getHeight() < originalImage.getWidth()) { smallestSide = originalImage.getHeight(); } if(log.isDebugEnabled()){ log.debug("smallestSide:" + smallestSide); } int startX = (originalImage.getWidth() / 2) - (smallestSide/2); int startY = (originalImage.getHeight() / 2) - (smallestSide/2); //OPTION 2 (unused) //determine a percentage of the original image which we want to keep, say 90%. //then figure out the dimensions of the box and crop to that. //then resize to the avatar size =80 square. //int percentWidth = (originalImage.getWidth() / 100) * 90; //int startX = (originalImage.getWidth() / 2) - (percentWidth/2); //int percentHeight = (originalImage.getHeight() / 100) * 90; //int startY = (originalImage.getHeight() / 2) - (percentHeight/2); //log.debug("percentWidth:" + percentWidth); //log.debug("percentHeight:" + percentHeight); //so it is square, we can only use one dimension for both side, so choose the smaller one //int croppedSize = percentWidth; //if(percentHeight < percentWidth) { // croppedSize = percentHeight; //} //log.debug("croppedSize:" + croppedSize); if(log.isDebugEnabled()){ log.debug("originalImage.getWidth():" + originalImage.getWidth()); log.debug("originalImage.getHeight():" + originalImage.getHeight()); log.debug("startX:" + startX); log.debug("startY:" + startY); } //crop to these bounds and starting positions BufferedImage croppedImage = Scalr.crop(originalImage, startX, startY, smallestSide, smallestSide); //now resize it to the desired avatar size BufferedImage scaledImage = Scalr.resize(croppedImage, 80); //convert BufferedImage to byte array ByteArrayOutputStream baos = new ByteArrayOutputStream(); ImageIO.write(scaledImage, getInformalFormatForMimeType(mimeType), baos); baos.flush(); outputBytes = baos.toByteArray(); baos.close(); } catch (Exception e) { log.error("Cropping and scaling image failed.", e); } finally { if (in != null) { try { in.close(); log.debug("Image stream closed."); } catch (IOException e) { log.error("Error closing image stream: ", e); } } } return outputBytes; } }