package org.limewire.util;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.URL;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.CharacterCodingException;
import java.nio.charset.Charset;
import java.nio.charset.CharsetEncoder;
import java.nio.charset.CoderResult;
/**
* Provides convenience functionality ranging from getting user information,
* copying files to getting the stack traces of all current threads.
* <DL>
* <DT>User Information
* <DD>Get a username, a user home directory, etc.
*
* <DT>File Operation
* <DD>Copy resource files, get the current directory, set, get and validate the
* directory to store user settings. Also, you can use convertFileName to replace
* operating system specific illegal characters.
*
* <DT>Threads
* <DD>Get the stack traces of all current threads.
*
* <DT>Time
* <DD>Convert an integer value representing the seconds into an appropriate days,
* hour, minutes and seconds format (d:hh:mm:ss).
*
* <DT>Decode
* <DD>Decode a URL encoded from a string.
*
* <DT>Resources
* <DD>Retrieve a resource file and a stream.
* </DL>
*/
public class CommonUtils {
/**
* Several arrays of illegal characters on various operating systems. Used
* by convertFileName
*/
private static final char[] ILLEGAL_CHARS_ANY_OS = { '/', '\n', '\r', '\t', '\0', '\f' };
private static final char[] ILLEGAL_CHARS_UNIX = { '`' };
private static final char[] ILLEGAL_CHARS_WINDOWS = { '?', '*', '\\', '<', '>', '|', '\"', ':' };
private static final char[] ILLEGAL_CHARS_MACOS = { ':' };
/** The location where settings are stored. */
private static volatile File settingsDirectory = null;
/**
* Returns the user home directory.
*
* @return the <tt>File</tt> instance denoting the abstract pathname of the
* user's home directory, or <tt>null</tt> if the home directory
* does not exist
*/
public static File getUserHomeDir() {
return new File(System.getProperty("user.home"));
}
/**
* Return the user's name.
*
* @return the <tt>String</tt> denoting the user's name.
*/
public static String getUserName() {
return System.getProperty("user.name");
}
/**
* Gets an InputStream from a resource file.
*
* @param location the location of the resource in the resource file
* @return an <tt>InputStream</tt> for the resource
* @throws IOException if the resource could not be located or there was
* another IO error accessing the resource
*/
public static InputStream getResourceStream(String location) throws IOException {
ClassLoader cl = CommonUtils.class.getClassLoader();
URL resource = null;
if (cl == null) {
resource = ClassLoader.getSystemResource(location);
} else {
resource = cl.getResource(location);
}
if (resource == null)
throw new IOException("null resource: " + location);
else
return resource.openStream();
}
/**
* Copied from URLDecoder.java.
*/
public static String decode(String s) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
switch (c) {
case '+':
sb.append(' ');
break;
case '%':
try {
sb.append((char) Integer.parseInt(s.substring(i + 1, i + 3), 16));
} catch (NumberFormatException e) {
throw new IllegalArgumentException(s);
}
i += 2;
break;
default:
sb.append(c);
break;
}
}
// Undo conversion to external encoding
String result = sb.toString();
try {
byte[] inputBytes = result.getBytes("8859_1");
result = new String(inputBytes);
} catch (UnsupportedEncodingException e) {
// The system should always have 8859_1
}
return result;
}
/**
* Copies the specified resource file into the current directory from the
* jar file. If the file already exists, no copy is performed.
*
* @param fileName the name of the file to copy, relative to the jar file --
* such as "org/limewire/gui/images/image.gif"
* @param newFile the new <tt>File</tt> instance where the resource file
* will be copied to -- if this argument is null, the file will be
* copied to the current directory
* @param forceOverwrite specifies whether or not to overwrite the file if
* it already exists
*/
public static void copyResourceFile(String fileName, File newFile, boolean forceOverwrite)
throws IOException {
if (newFile == null)
newFile = new File(".", fileName);
// return quickly if the file is already there, no copy necessary
if (!forceOverwrite && newFile.exists())
return;
String parentString = newFile.getParent();
if (parentString == null)
return;
File parentFile = new File(parentString);
if (!parentFile.isDirectory())
parentFile.mkdirs();
ClassLoader cl = CommonUtils.class.getClassLoader();
// load resource using my class loader or system class loader
// Can happen if Launcher loaded by system class loader
URL resource = cl != null ? cl.getResource(fileName) : ClassLoader
.getSystemResource(fileName);
if (resource == null)
throw new IOException("resource: " + fileName + " doesn't exist.");
saveStream(resource.openStream(), newFile);
}
/**
* Copies the src file to the destination file. This will always overwrite
* the destination.
*/
public static void copyFile(File src, File dst) throws IOException {
saveStream(new FileInputStream(src), dst);
}
/**
* Saves all data from the stream into the destination file. This will
* always overwrite the file.
*/
public static void saveStream(InputStream inStream, File newFile) throws IOException {
BufferedInputStream bis = null;
BufferedOutputStream bos = null;
try {
// buffer the streams to improve I/O performance
final int bufferSize = 2048;
bis = new BufferedInputStream(inStream, bufferSize);
bos = new BufferedOutputStream(new FileOutputStream(newFile), bufferSize);
byte[] buffer = new byte[bufferSize];
int c = 0;
do { // read and write in chunks of buffer size until EOF reached
c = bis.read(buffer, 0, bufferSize);
if (c > 0)
bos.write(buffer, 0, c);
} while (c == bufferSize); // (# of bytes read)c will = bufferSize
// until EOF
bos.flush();
} catch (IOException e) {
// if there is any error, delete any portion of file that did write
newFile.delete();
} finally {
if (bis != null) {
try {
bis.close();
} catch (IOException ignored) {
}
}
if (bos != null) {
try {
bos.close();
} catch (IOException ignored) {
}
}
}
}
/**
* Converts a value in seconds to: "d:hh:mm:ss" where d=days, hh=hours,
* mm=minutes, ss=seconds, or "h:mm:ss" where h=hours<24, mm=minutes,
* ss=seconds, or "m:ss" where m=minutes<60, ss=seconds.
*/
public static String seconds2time(long seconds) {
long minutes = seconds / 60;
seconds = seconds - minutes * 60;
long hours = minutes / 60;
minutes = minutes - hours * 60;
long days = hours / 24;
hours = hours - days * 24;
// build the numbers into a string
StringBuilder time = new StringBuilder();
if (days != 0) {
time.append(Long.toString(days));
time.append(":");
if (hours < 10)
time.append("0");
}
if (days != 0 || hours != 0) {
time.append(Long.toString(hours));
time.append(":");
if (minutes < 10)
time.append("0");
}
time.append(Long.toString(minutes));
time.append(":");
if (seconds < 10)
time.append("0");
time.append(Long.toString(seconds));
return time.toString();
}
/**
* Returns a normalized and shortened valid file name taking the length of
* the path of the parent directory into account.
* <p>
* The name is cleared from illegal file system characters and it is ensured
* that the maximum path system on the system is not exceeded unless the
* parent directory path has already the maximum path length.
*
* @throws IOException if the parent directory's path takes up
* {@link OSUtils#getMaxPathLength()}.
*/
public static String convertFileName(File parentDir, String name) throws IOException {
int parentLength = parentDir.getAbsolutePath().getBytes(Charset.defaultCharset().name()).length;
if (parentLength >= OSUtils.getMaxPathLength() - 1 /*
* for the separator
* char
*/) {
throw new IOException("Path too long");
}
return convertFileName(name, Math.min(OSUtils.getMaxPathLength() - parentLength - 1, 180));
}
/**
* Cleans up the filename and truncates it to length of 180 bytes by calling
* {@link #convertFileName(String, int) convertFileName(String, 180)}.
*/
public static String convertFileName(String name) {
return convertFileName(name, 180);
}
/**
* Cleans up the filename from illegal characters and truncates it to the
* length of bytes specified.
*
* @param name the filename to clean up
* @param maxBytes the maximum number of bytes the cleaned up file name can
* take up
* @return the cleaned up file name
*/
public static String convertFileName(String name, int maxBytes) {
// use default encoding which is also used for files judging from the
// property name "file.encoding"
try {
return convertFileName(name, maxBytes, Charset.defaultCharset());
} catch (CharacterCodingException cce) {
try {
// UTF-8 should always be available
return convertFileName(name, maxBytes, Charset.forName("UTF-8"));
} catch (CharacterCodingException e) {
// should not happen, UTF-8 can encode unicode and gives us a
// good length estimate
throw new RuntimeException("UTF-8 should have encoded: " + name, e);
}
}
}
/**
* Replaces OS specific illegal characters from any filename with '_',
* including ( / \n \r \t ) on all operating systems, ( ? * \ < > | " ) on
* Windows, ( ` ) on Unix.
*
* @param name the filename to check for illegal characters
* @param maxBytes the maximum number of bytes for the resulting file name,
* must be > 0
* @return String containing the cleaned filename
*
* @throws CharacterCodingException if the charset could not encode the
* characters in <code>name</code>
* @throws IllegalArgumentException if maxBytes <= 0
*/
public static String convertFileName(String name, int maxBytes, Charset charSet)
throws CharacterCodingException {
if (maxBytes <= 0) {
throw new IllegalArgumentException("maxBytes must be > 0");
}
// ensure that block-characters aren't in the filename.
name = I18NConvert.instance().compose(name);
// if the name is too long, reduce it. We don't go all the way
// up to 255 because we don't know how long the directory name is
// We want to keep the extension, though.
if (name.length() > maxBytes || name.getBytes().length > maxBytes) {
int extStart = name.lastIndexOf('.');
if (extStart == -1) { // no extension, weird, but possible
name = getPrefixWithMaxBytes(name, maxBytes, charSet);
} else {
// if extension is greater than 11, we truncate it.
// ( 11 = '.' + 10 extension bytes )
int extLength = name.length() - extStart;
int extEnd = extLength > 11 ? extStart + 11 : name.length();
byte[] extension = getMaxBytes(name.substring(extStart, extEnd), 16, charSet);
try {
// disregard extension if we lose too much of the name
// since the name is also used for searching
if (extension.length >= maxBytes - 10) {
name = getPrefixWithMaxBytes(name, maxBytes, charSet);
} else {
name = getPrefixWithMaxBytes(name, maxBytes - extension.length, charSet)
+ new String(extension, charSet.name());
}
} catch (UnsupportedEncodingException uee) {
throw new RuntimeException("Could not handle string", uee);
}
}
}
for (char aILLEGAL_CHARS_ANY_OS : ILLEGAL_CHARS_ANY_OS) {
name = name.replace(aILLEGAL_CHARS_ANY_OS, '_');
}
if (OSUtils.isWindows() || OSUtils.isOS2()) {
for (char aILLEGAL_CHARS_WINDOWS : ILLEGAL_CHARS_WINDOWS) {
name = name.replace(aILLEGAL_CHARS_WINDOWS, '_');
}
} else if (OSUtils.isLinux() || OSUtils.isSolaris()) {
for (char aILLEGAL_CHARS_UNIX : ILLEGAL_CHARS_UNIX) {
name = name.replace(aILLEGAL_CHARS_UNIX, '_');
}
} else if (OSUtils.isMacOSX()) {
for (char aILLEGAL_CHARS_MACOS : ILLEGAL_CHARS_MACOS) {
name = name.replace(aILLEGAL_CHARS_MACOS, '_');
}
}
return name;
}
/**
* Sanitizes a String for use in a directory and file name and removes any
* illegal characters from it.
*
* @param name String to check
* @return sanitized String
*/
public static String santizeString(String name) {
for (char aILLEGAL_CHARS_ANY_OS : ILLEGAL_CHARS_ANY_OS) {
name = name.replace(aILLEGAL_CHARS_ANY_OS, '_');
}
if (OSUtils.isWindows() || OSUtils.isOS2()) {
for (char aILLEGAL_CHARS_WINDOWS : ILLEGAL_CHARS_WINDOWS) {
name = name.replace(aILLEGAL_CHARS_WINDOWS, '_');
}
} else if (OSUtils.isLinux() || OSUtils.isSolaris()) {
for (char aILLEGAL_CHARS_UNIX : ILLEGAL_CHARS_UNIX) {
name = name.replace(aILLEGAL_CHARS_UNIX, '_');
}
} else if (OSUtils.isMacOSX()) {
for (char aILLEGAL_CHARS_MACOS : ILLEGAL_CHARS_MACOS) {
name = name.replace(aILLEGAL_CHARS_MACOS, '_');
}
}
return name;
}
/**
* Returns the prefix of <code>string</code> which takes up a maximum of
* <code>maxBytes</code>.
*
* @throws CharacterCodingException
*/
static String getPrefixWithMaxBytes(String string, int maxBytes, Charset charSet)
throws CharacterCodingException {
try {
return new String(getMaxBytes(string, maxBytes, charSet), charSet.name());
} catch (UnsupportedEncodingException uee) {
throw new RuntimeException("Could not recreate string", uee);
}
}
/**
* Returns the first <code>maxBytes</code> of <code>string</code> encoded
* using the encoder of <code>charSet</code>
*
* @param string whose prefix bytes to return
* @param maxBytes the maximum number of bytes to return
* @param charSet the char set used for encoding the characters into bytes
* @return the array of bytes of length <= maxBytes
* @throws CharacterCodingException if the char set's encoder could not
* handle the characters in the string
*/
static byte[] getMaxBytes(String string, int maxBytes, Charset charSet)
throws CharacterCodingException {
byte[] bytes = new byte[maxBytes];
ByteBuffer out = ByteBuffer.wrap(bytes);
CharBuffer in = CharBuffer.wrap(string.toCharArray());
CharsetEncoder encoder = charSet.newEncoder();
CoderResult cr = encoder.encode(in, out, true);
encoder.flush(out);
if (cr.isError()) {
cr.throwException();
}
byte[] result = new byte[out.position()];
System.arraycopy(bytes, 0, result, 0, result.length);
return result;
}
/**
* Returns the user's current working directory as a <tt>File</tt> instance,
* or <tt>null</tt> if the property is not set.
*
* @return the user's current working directory as a <tt>File</tt> instance,
* or <tt>null</tt> if the property is not set
*/
public static File getCurrentDirectory() {
return new File(System.getProperty("user.dir"));
}
/**
* Validates a potential settings directory. This returns the validated
* directory, or throws an IOException if it can't be validated.
*/
public static File validateSettingsDirectory(File dir) throws IOException {
dir = dir.getAbsoluteFile();
if (!dir.isDirectory()) {
dir.delete(); // delete whatever it may have been
if (!dir.mkdirs())
throw new IOException("could not create preferences directory: " + dir);
}
if (!FileUtils.canWrite(dir))
throw new IOException("settings dir not writable: " + dir);
if (!dir.canRead())
throw new IOException("settings dir not readable: " + dir);
// Validate that you can write a file into settings directory.
// catches vista problem where if settings directory is
// locked canRead and canWrite still return true
File file = File.createTempFile("test", "test", dir);
if (!file.exists())
throw new IOException("can't write test file in directory: " + dir);
file.delete();
return dir;
}
/**
* Sets the new settings directory. The settings directory cannot be set
* more than once.
* <p>
* If the directory can't be set (because it isn't a folder, can't be made
* into a folder, or isn't readable and writable), an IOException is thrown.
*
* @throws IOException
*/
public static void setUserSettingsDir(File settingsDir) throws IOException {
if (settingsDirectory != null)
throw new IllegalStateException("settings directory already set!");
settingsDirectory = validateSettingsDirectory(settingsDir);
}
/**
* Returns the directory where all user settings should be stored. This is
* where all application data should be stored. If the directory is not set,
* this returns the user's home directory.
*/
public synchronized static File getUserSettingsDir() {
if (settingsDirectory != null)
return settingsDirectory;
else
return getUserHomeDir();
}
/**
* Parses a long from the given string swallowing any exceptions. null is
* returned if there is an error parsing the string.
*/
public static Long parseLongNoException(String str) {
Long num = null;
if (str != null) {
try {
num = Long.valueOf(str);
} catch (NumberFormatException e) {
// continue; null is returned
}
}
return num;
}
}