org.sakaiproject.profile2.util.ProfileUtils Maven / Gradle / Ivy
/**
* 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.text.StringEscapeUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.time.DateUtils;
import org.apache.commons.text.WordUtils;
import org.imgscalr.Scalr;
import org.sakaiproject.util.FormattedText;
import org.sakaiproject.util.ResourceLoader;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class ProfileUtils {
/**
* 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 allowedTypes = new ArrayList();
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.
*
* 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 formats = new HashMap();
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();
//log.info("currentDate:" + currentTimeMillis);
//log.info("postingDate:" + postingTimeMillis);
//log.info("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();
}
/**
* Gets the users preferred orientation, either from the user's session or Sakai preferences and returns it
* This depends on Sakai's ResourceLoader.
*
* @return
*/
public static String getUserPreferredOrientation() {
ResourceLoader rl = new ResourceLoader();
return rl.getOrientation(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 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.
*
* 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.escapeHtml4(stripped);
}
/**
* Strips html/xml tags from a string and returns the cleaned version.
*
* @param text any text (if this is null or empty then the input text is returned unchanged)
* @param smartSpacing if true then try to make the text represent the intent of the html,
* trims out duplicate spaces, converts block type html into a space, etc.,
* else just removes html tags and leaves all other parts of the string intact,
* NOTE: false is also slightly faster
* @param stripEscapeSequences if true, strips out any escape sequences such as ' '
* @return the cleaned string
* @see #convertFormattedTextToPlaintext(String) for alternative mechanism
*/
public static String stripHtmlFromText(String text, boolean smartSpacing, boolean stripEscapeSequences) {
return FormattedText.stripHtmlFromText(text, smartSpacing, stripEscapeSequences);
}
/**
* 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 void removeDuplicates(List list){
Set set = new HashSet();
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 void removeDuplicatesWithOrder(List list) {
Set set = new HashSet ();
List newList = new ArrayList();
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;
}
}