/* Util.java created 2007-09-18
*
*/
package org.signalml.util;
import java.io.BufferedInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStream;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.Map;
import java.util.Properties;
import java.util.Random;
import java.util.Set;
import java.util.regex.Pattern;
import java.util.zip.Checksum;
import java.util.zip.DataFormatException;
import java.util.zip.Deflater;
import java.util.zip.Inflater;
import org.apache.log4j.Logger;
import org.signalml.app.document.signal.SignalChecksumProgressMonitor;
import org.signalml.domain.signal.SignalChecksum;
import org.signalml.exception.SanityCheckException;
import org.signalml.plugin.export.SignalMLException;
/**
* Util provides various String to Date conversions, signal checksums, file and String MD5 sums.
* It also allows to get and change file extension, invert map, save system information, split String to separate lines, etc.
*
* @author Michal Dobaczewski © 2007-2008 CC Otwarte Systemy Komputerowe Sp. z o.o.
* parts based on code Copyright (C) 2003 Dobieslaw Ircha <dircha@eranet.pl> Artur Biesiadowski <abies@adres.pl> Piotr J. Durka <Piotr-J.Durka@fuw.edu.pl>
*/
public abstract class Util {
protected static final Logger logger = Logger.getLogger(Util.class);
/**
* line separator
*/
public static final String LINE_SEP = System.getProperty("line.separator");
/**
* file separator
*/
public static final String FILE_SEP = System.getProperty("file.separator");
/**
* numbers in base sixteen
*/
public static final char[] HEX_CHARS = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' };
public static final Pattern WINDOWS_OS_PATTERN = Pattern.compile(".*[Ww]indows.*");
public static final Pattern LINUX_OS_PATTERN = Pattern.compile(".*[Ll]inux.*");
public static final Pattern MAC_OS_PATTERN = Pattern.compile(".*[Mm]ac.*");
private static Pattern fqClassNamePattern = null;
/**
* Checks if two objects are equal (if both of them are nulls, it returns true even if they are not of the same class).
* @param o1 first object to compare
* @param o2 second object to compare
* @return true if specified objects are equal
*/
public static boolean equalsWithNulls(Object o1, Object o2) {
if (o1 == null) {
return (o2 == null) ? true : false;
}
return o1.equals(o2);
}
/**
* Computes specified checksum for given file and monitor length of processed data.
* @param file file to read signal from
* @param checksumTypes array of checksums to count
* @param monitor monitors legth of processed data once every 1000 iterations
* @return array of computed checksum for given file
* @throws SignalMlException when in array of checksums exists unsupported checksum type or when occur any problem while reading data from file
*/
public static SignalChecksum[] getSignalChecksums(File file, String[] checksumTypes, SignalChecksumProgressMonitor monitor) throws SignalMLException {
return getSignalChecksums(file, checksumTypes, 0, (int) file.length(), monitor);
}
/**
* Computes specified checksum for given file and monitor length of processed data. It also can skip given number of starting data and process only specified number of data.
* @param file file to read signal from
* @param checksumTypes array of checksums to count
* @param offset number of starting data to skip (if it is negative, no bytes are skipped)
* @param length length of data to process
* @param monitor monitors legth of processed data once every 1000 iterations
* @return array of computed checksum for given file
* @throws SignalMlException when in array of checksums exists unsupported checksum type or when occur any problem while reading data from file
*/
public static SignalChecksum[] getSignalChecksums(File file, String[] checksumTypes, int offset, int length, SignalChecksumProgressMonitor monitor) throws SignalMLException {
if (checksumTypes.length == 0) {
return new SignalChecksum[0];
}
Checksum[] checksums = new Checksum[checksumTypes.length];
int i;
for (i=0; i<checksumTypes.length; i++) {
if (checksumTypes[i].equalsIgnoreCase("crc32")) {
checksums[i] = new java.util.zip.CRC32();
} else if (checksumTypes[i].equalsIgnoreCase("adler32")) {
checksums[i] = new java.util.zip.Adler32();
} else {
throw new SignalMLException("error.noSuchChecksumType");
}
}
InputStream is = null;
try {
is = new BufferedInputStream(new FileInputStream(file));
byte[] buf = new byte[8*1024];
is.skip(offset);
int iteration = 0;
long cnt, lengthProceeded = 0;
while (((cnt = is.read(buf)) > 0) && (lengthProceeded < length)) {
if (monitor != null) {
if (monitor.isCancelled()) {
return null;
}
iteration = (iteration + 1) % 1000;
if (iteration == 0) {
monitor.setBytesProcessed(lengthProceeded);
}
}
if (lengthProceeded + cnt <= length) {
for (i=0; i<checksumTypes.length; i++) {
checksums[i].update(buf, 0, (int) cnt);
}
lengthProceeded += cnt;
} else {
for (i=0; i<checksumTypes.length; i++) {
checksums[i].update(buf, 0, (int)(length - lengthProceeded));
}
lengthProceeded = length;
}
}
} catch (IOException e) {
throw new SignalMLException(e);
} finally {
if (is != null) {
try {
is.close();
} catch (IOException ex) {
}
}
}
SignalChecksum[] results = new SignalChecksum[checksums.length];
long checksum;
byte[] bytes;
for (i=0; i<checksumTypes.length; i++) {
checksum = checksums[i].getValue();
bytes = new byte[4];
bytes[0] = (byte)((checksum & 0xFF000000) >> 24);
bytes[1] = (byte)((checksum & 0xFF0000) >> 16);
bytes[2] = (byte)((checksum & 0xFF00) >> 8);
bytes[3] = (byte)(checksum & 0xFF);
results[i] = new SignalChecksum(checksumTypes[i], offset, length, toHexString(bytes));
}
return results;
}
/**
* Returs MD5 checksum of specified file.
* @param file file to count checksum
* @return String with MD5 checksum
* @throws IOException if an I/O error occurs
*/
public static String getFileSignature(File file) throws IOException {
long len = file.length();
FileReader fr = new FileReader(file);
char[] cbuf = new char[(int) len];
fr.read(cbuf);
fr.close();
return toMD5String(new String(cbuf));
}
/**
* Returns hexadecimal representation of specified array of bytes.
* @param bytes data to convert
* @return String with hexadecimal representation of given data
*/
public static String toHexString(byte[] bytes) {
StringBuilder hexString = new StringBuilder();
for (int i = 0; i < bytes.length; i++) {
hexString.append(HEX_CHARS[(bytes[i] & 0xF0) >> 4]).append(HEX_CHARS[bytes[i] & 0x0F]);
}
return hexString.toString();
}
/**
* Resturns MD5 checksum of specified String.
* @param s String to count checksum
* @return String with MD5 checksum
* @throws NullPointerException if given String is null
*/
public static String toMD5String(CharSequence s) {
if (s == null)
throw new NullPointerException();
byte[] bytes;
try {
MessageDigest md5 = MessageDigest.getInstance("MD5");
md5.update(s.toString().getBytes());
bytes = md5.digest();
} catch (NoSuchAlgorithmException ex) {
logger.error("Failed to create a digest", ex);
return "";
}
return toHexString(bytes);
}
/**
* Returns random String in hexadecimal representation of specified length.
* @param byteCount length of target String
* @return random String
*/
public static String getRandomHexString(int byteCount) {
byte[] bytes = new byte[byteCount];
(new Random()).nextBytes(bytes);
return toHexString(bytes);
}
/**
* Returns extension of specified file. The extension can be returned with or without preceding dot.
* @param file File to extract extension from
* @param withDot If is true then returned extension is proceded by a dot.
* @return extension of specified file
*/
public static String getFileExtension(File file, boolean withDot) {
if (file == null) {
return null;
}
String path = file.getAbsolutePath();
int dotAt = path.lastIndexOf('.');
if (dotAt < 0) {
return null;
}
if (!withDot) {
dotAt++;
}
return path.substring(dotAt);
}
/**
* Returns name of specified file without its extension.
* @param file File to extract name from
* @return name of File
*/
public static String getFileNameWithoutExtension(File file) {
if (file == null) {
return null;
}
String path = file.getName();
int dotAt = path.lastIndexOf('.');
if (dotAt < 0) {
return null;
}
return path.substring(0,dotAt);
}
/**
* Adds specified extension to file. If any extension exists before, it is changed.
* @param file File to change extension
* @param extension new extension
* @return file with new extension
*/
public static File changeOrAddFileExtension(File file, String extension) {
String name = file.getName();
File parent = file.getParentFile();
int dotAt = name.lastIndexOf('.');
if (dotAt < 0) {
name = name + "." + extension;
} else {
name = name.substring(0, dotAt+1) + extension;
}
return new File(parent, name);
}
/**
* Inverts Map of Strings (values become keys, keys become values).
* @param input Map to invert
* @return inverted Map
*/
public static HashMap<String,String> invertStringMap(Map<String,String> input) {
HashMap<String,String> output = new HashMap<String, String>(input.size());
Set<Map.Entry<String,String>> entries = input.entrySet();
for (Map.Entry<String, String> entry : entries) {
output.put(entry.getValue(), entry.getKey());
}
return output;
}
/**
* Finds and replaces every key from specified Map in specified String by value from this map in brackets '${...}'.
* Map can be inverted before this operation.
* @param input String to process
* @param tokenMap map to take keys and values from for replacing process
* @param invertMap if is true then Map is inverted before replacing
* @return String with every occurence of key from Map in input String replaced by its value
*/
public static String substituteForTokens(String input, Map<String,String> tokenMap, boolean invertMap) {
if (invertMap) {
tokenMap = invertStringMap(tokenMap);
}
input = input.replaceAll("\\$", "\\$\\$");
Set<String> keySet = tokenMap.keySet();
boolean somethingDone = false;
int index;
do {
somethingDone = false;
for (String key : keySet) {
index = input.indexOf(key);
if (index >= 0) {
input = input.substring(0,index) + "${" + tokenMap.get(key) + "}" + input.substring(index+key.length());
somethingDone = true;
break;
}
}
} while (somethingDone);
return input;
}
/**
* Finds and replaces every key from specified Map in brackets '${...}' in specified String by value from this map in brackets.
* @param input String to process
* @param tokenMap map to take keys and values from for replacing process
* @return String with every occurence of key from Map in input String replaced by its value
*/
public static String expandTokens(String input, Map<String,String> tokenMap) {
Set<String> keySet = tokenMap.keySet();
boolean somethingDone = false;
int index;
do {
somethingDone = false;
for (String key : keySet) {
String skey = "${" + key + "}";
index = input.indexOf(skey);
if (index >= 0) {
input = input.substring(0,index) + tokenMap.get(key) + input.substring(index+skey.length());
somethingDone = true;
break;
}
}
} while (somethingDone);
return input.replaceAll("\\$\\$", "\\$");
}
/**
* Checks whether s looks like a valid unicode string and if
* any of the characters of s are control characters.
* This is a general sanity check, not enough for security
* purposes, mainly useful to avoid display problems.
* @param s String to validate
* @return false if any of code points in this string are of
* Unicode class Cc (control characters) or s contains
* truncated codepoints or the first code point is combining.
*/
public static boolean hasSpecialChars(String s) {
final int length = s.length();
for (int offset = 0; offset < length;) {
final int codepoint = s.codePointAt(offset);
final int type = Character.getType(codepoint);
switch (type) {
case Character.CONTROL:
case Character.UNASSIGNED:
case Character.PRIVATE_USE:
logger.warn(String.format("string '%s' failed validation at offset %d",
s, offset));
return true;
case Character.SURROGATE:
logger.warn(String.format("truncated string '%s' failed validation", s));
return true;
default:
if (offset == 0 && isCombining(codepoint))
return true;
}
offset += Character.charCount(codepoint);
}
return false;
}
/**
* @return true if the codepoint represents a combining character
* @seealso http://en.wikipedia.org/wiki/Combining_character#Unicode_ranges
*/
public static boolean isCombining(int codepoint) {
return (codepoint >= 0x0300 && codepoint <= 0x036f) ||
(codepoint >= 0x1dc0 && codepoint <= 0x1dff) ||
(codepoint >= 0x20d0 && codepoint <= 0x20ff) ||
(codepoint >= 0xfe20 && codepoint <= 0xfe2f);
}
/**
* Returns String with those elements from specified String which are valid Unicode characters.
* @param s String to trim
* @return valid String
*/
public static String trimString(String s) {
StringBuilder sb = new StringBuilder();
int cnt = s.length();
int firstGood = -1;
boolean good = false;
int code;
for (int i=0; i<cnt; i++) {
code = s.codePointAt(i);
if ((code == 0x9 || code == 0xA || code == 0xD || (code >= 0x20 && code <= 0xD7FF) || (code >= 0xE000 && code <= 0xFFFD))) {
if (!good) {
good = true;
firstGood = i;
}
} else {
if (good) {
good = false;
sb.append(s, firstGood, i);
}
}
}
if (good) {
sb.append(s, firstGood, cnt);
}
return sb.toString();
}
/**
* Returns array of String which contains every separate line from specified String with limited number of characters in one line
* NOTE: Single word will not be splitted even if it is longer then limit.
* @param string String to split
* @param limit maximal number of characters in one line
* @return splitted String
*/
public static String[] splitTextIntoLines(String string, int limit) {
LinkedList<String> list = new LinkedList<String>();
int firstLineChar = 0;
int lastSpaceChar = -1;
int len = string.length();
int i;
String line;
char ch;
for (i=0; i<len; i++) {
ch = string.charAt(i);
if (ch == '\n') {
line = string.substring(firstLineChar, ((i != 0 && string.charAt(i-1) == '\r') ? i-1 : i));
list.add(line);
firstLineChar = i+1;
lastSpaceChar = -1;
continue;
}
if ((i - firstLineChar) > limit) {
if (lastSpaceChar < 0) {
if (ch == ' ' || ch == '\t') {
line = string.substring(firstLineChar, i);
list.add(line);
firstLineChar = i+1;
continue;
}
} else {
line = string.substring(firstLineChar, lastSpaceChar);
list.add(line);
firstLineChar = lastSpaceChar+1;
lastSpaceChar = -1;
continue;
}
}
}
if (firstLineChar < len) {
list.add(string.substring(firstLineChar));
}
String[] result = new String[list.size()];
list.toArray(result);
return result;
}
/**
* Writes system information to "logger" variable.
*/
public static void dumpDebuggingInfo() {
Runtime runtime = Runtime.getRuntime();
logger.info("Java vendor: " + System.getProperty("java.vendor"));
logger.info("Java version: " + System.getProperty("java.version"));
logger.info("Java home: " + System.getProperty("java.home"));
logger.info("Operating system: " + System.getProperty("os.name") + " " + System.getProperty("os.arch") + " " + System.getProperty("os.version"));
logger.info("Memory max: " + runtime.maxMemory() + " current: " + runtime.totalMemory() + " free: " + runtime.freeMemory());
logger.info("Class path: " + System.getProperty("java.class.path"));
Properties properties = System.getProperties();
Set<String> names = properties.stringPropertyNames();
for (String s : names) {
logger.debug("Property [" + s + "] -> [" + properties.getProperty(s) + "]");
}
}
/**
* Compresses specified data and encodes it using Base64.
* @param data Data to process
* @return String representation of compressed and encoded data
*/
public static String compressAndBase64Encode(byte[] data) {
if (data == null) {
return null;
}
if (data.length == 0) {
return "";
}
ByteArrayOutputStream baos = new ByteArrayOutputStream(data.length);
Deflater deflater = new Deflater(9);
deflater.setInput(data);
deflater.finish();
byte[] temp = new byte[8192];
int cnt;
do {
cnt = deflater.deflate(temp);
if (cnt > 0) {
baos.write(temp,0,cnt);
}
} while (cnt > 0);
deflater.end();
byte[] compressed = baos.toByteArray();
// caution: request processing is considerably slower when Base64 output is wrapped
// (probably because of multiple text nodes being added to the dom tree)
return Base64.encodeToString(compressed, false);
}
/**
* Decompresses specified data and decodes it using Base64.
* @param encoded data to process
* @return String representation of decompressed and decoded data
* @throws DataFormatException when data is not correctly compressed
*/
public static byte[] base64DecodeAndDecompress(String encoded) throws DataFormatException {
if (encoded == null) {
return null;
}
if (encoded.isEmpty()) {
return new byte[0];
}
byte[] compressed = Base64.decodeToBytes(encoded);
ByteArrayOutputStream baos = new ByteArrayOutputStream(compressed.length);
Inflater inflater = new Inflater();
inflater.setInput(compressed);
byte[] temp = new byte[8192];
int cnt;
do {
cnt = inflater.inflate(temp);
if (cnt > 0) {
baos.write(temp,0,cnt);
}
} while (cnt > 0);
if (!inflater.finished()) {
throw new DataFormatException("Bad compressed data");
}
inflater.end();
return baos.toByteArray();
}
/**
* Returns MD5 hash of given data.
* @param userName name of the user
* @param loginTime time of logging in
* @param sharedSecret some shared message
* @return MD5 hash of given data
*/
public static String createSharedSecretToken(String userName, Date loginTime, String sharedSecret) {
MessageDigest digest;
try {
digest = MessageDigest.getInstance("md5");
} catch (NoSuchAlgorithmException ex) {
throw new SanityCheckException("Md5 not supported", ex);
}
String input = userName + "!" + org.signalml.util.FormatUtils.formatTime(loginTime) + "!" + sharedSecret;
byte[] digestBytes = digest.digest(input.getBytes());
return toHexString(digestBytes);
}
/**
* Returns square of specified number.
* @param x number to count square
* @return square of given number
*/
public static double sqr(double x) {
return (x*x);
}
/**
* Converts representation of color in RGB model in triplet (r, g, b) to single number in hexadecimal system.
* @param red amount of red included (between 0 and 255 inclusive)
* @param green amount of green included (between 0 and 255 inclusive)
* @param blue amount of blue included (between 0 and 255 inclusive)
* @return hexadecimal representation of color in RGB model
*/
public static int RGBToInteger(int red,int green,int blue) {
return 0x00000000|(red<<16)|(green<<8)|blue;
}
/**
* Check if class name matches pattern ^[a-zA-Z0-9_][a-zA-Z0-9$._]*$
* @param fqClassName class name to check
* @return true if class name matches pattern above
*/
public static boolean validateFqClassName(String fqClassName) {
if (fqClassNamePattern == null) {
fqClassNamePattern = Pattern.compile("^[a-zA-Z0-9_][a-zA-Z0-9$._]*$");
}
return fqClassNamePattern.matcher(fqClassName).matches();
}
/**
* Returns actual time and/or date in specified format.
* @param dateFormat format of date to count
* @return actual time and/or date
*/
public static String now(String dateFormat) {
Calendar cal = Calendar.getInstance();
SimpleDateFormat sdf = new SimpleDateFormat(dateFormat);
return sdf.format(cal.getTime());
}
/**
* Return a new string with the first letter capitalized.
* @param string the string to capitalize
* @return string with the first letter possibly changed
*/
public static String capitalize(String string) {
return string.substring(0, 1).toUpperCase() + string.substring(1);
}
}