/* Copyright (C) 2009 Mobile Sorcery AB This program is free software; you can redistribute it and/or modify it under the terms of the Eclipse Public License v1.0. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the Eclipse Public License v1.0 for more details. You should have received a copy of the Eclipse Public License v1.0 along with this program. It is also available at http://www.eclipse.org/legal/epl-v10.html */ package com.mobilesorcery.sdk.core; import java.io.ByteArrayOutputStream; import java.io.Closeable; import java.io.EOFException; import java.io.File; import java.io.FileFilter; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.FileWriter; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.Reader; import java.io.Writer; import java.net.URLEncoder; import java.text.FieldPosition; import java.text.MessageFormat; import java.util.ArrayList; import java.util.Arrays; import java.util.Comparator; import java.util.HashMap; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.Map; import java.util.jar.JarEntry; import java.util.jar.JarInputStream; import java.util.jar.JarOutputStream; import java.util.jar.Manifest; import java.util.zip.ZipEntry; import java.util.zip.ZipInputStream; import java.util.zip.ZipOutputStream; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.core.runtime.NullProgressMonitor; import org.eclipse.core.runtime.Path; import com.mobilesorcery.sdk.internal.ReverseComparator; public class Util { private static final class ExtensionFileFilter implements FileFilter { private final String[] exts; private ExtensionFileFilter(String... ext) { this.exts = ext; } @Override public boolean accept(File pathname) { for (String ext : exts) { if (ext.equals(Util.getExtension(pathname))) { return true; } } return false; } } public static final char[] BASE16_CHARS = "0123456789ABCDEF".toCharArray(); private static final int _1KB = 1024; private static final int _1MB = 1024 * 1024; private static final int _5KB = 5 * 1024; private static final int _5MB = 5 * 1024 * 1024; private static final int _1GB = 1024 * 1024 * 1024; private static final MessageFormat DATASIZE_FORMAT = new MessageFormat( "{0,number,#.0} {1}"); private static final int MAX_DEPTH = 8; public static final int INFINITE_DEPTH = Short.MAX_VALUE; public static String join(String[] s, String delim) { StringBuffer result = new StringBuffer(); for (int i = 0; i < s.length; i++) { if (i > 0) { result.append(delim); } result.append(s[i]); } return result.toString(); } public static String join(Object[] o, String delim) { if (o == null) { return ""; } String[] toString = new String[o.length]; for (int i = 0; i < toString.length; i++) { toString[i] = "" + o[i]; } return join(toString, delim); } public static String join(String[] components, String delim, int start, int end) { String[] subarray = new String[end - start + 1]; System.arraycopy(components, start, subarray, 0, subarray.length); return join(subarray, delim); } public static String[] ensureQuoted(Object[] obj) { String[] result = new String[obj.length]; for (int i = 0; i < result.length; i++) { result[i] = ensureQuoted(obj[i]); } return result; } public static String ensureQuoted(Object obj) { String str = obj == null ? "" : obj.toString(); if (str.indexOf(' ') != -1 || str.indexOf('\t') != -1) { return '\"' + str + '\"'; } return str; } public static String trimQuotes(Object str) { return trim(str, '\"'); } static String trim(Object str, char ch) { if (str == null) { return null; } String result = str.toString(); if (result.length() > 0 && result.charAt(0) == ch) { result = result.substring(1); } if (result.length() > 0 && result.charAt(result.length() - 1) == ch) { result = result.substring(0, result.length() - 1); } return result; } public static String fill(char c, int length) { char[] result = new char[length]; Arrays.fill(result, c); return new String(result); } public static void unjar(File jar, File targetDir) throws IOException { unzip(jar, targetDir, true); } public static void unzip(File zip, File targetDir) throws IOException { unzip(zip, targetDir, false); } private static void unzip(File zip, File targetDir, boolean isJar) throws IOException { ZipInputStream input = createZipInputStream(new FileInputStream(zip), isJar); OutputStream currentOutput = null; targetDir.mkdirs(); try { for (ZipEntry entry = input.getNextEntry(); entry != null; entry = input .getNextEntry()) { File currentFile = new File(targetDir, entry.getName()); if (!entry.isDirectory()) { int readBytes = 0; byte[] buffer = new byte[512]; currentFile.getParentFile().mkdirs(); currentOutput = new FileOutputStream(currentFile); for (int read = input.read(buffer, 0, buffer.length); read != -1; read = input .read(buffer, 0, buffer.length)) { currentOutput.write(buffer, 0, read); readBytes += read; } currentOutput.close(); } else { currentFile.mkdirs(); } currentFile.setLastModified(entry.getTime()); } } finally { Util.safeClose(input); Util.safeClose(currentOutput); } } private static void zip(File sourceDir, File targetZip, boolean isJar) throws IOException { ZipOutputStream output = createZipOutputStream(new FileOutputStream(targetZip), isJar); try { for (File file : listFiles(sourceDir, true)) { if (!file.isDirectory()) { String relativePath = file.getAbsolutePath().substring(sourceDir.getAbsolutePath().length()); ZipEntry entry = createZipEntry(relativePath, isJar); output.putNextEntry(entry); FileInputStream fileInput = new FileInputStream(file); try { transfer(fileInput, output); } finally { Util.safeClose(fileInput); output.closeEntry(); } } } } finally { Util.safeClose(output); } } private static ZipOutputStream createZipOutputStream(OutputStream output, boolean isJar) throws IOException { return isJar ? new JarOutputStream(output) : new ZipOutputStream(output); } private static ZipInputStream createZipInputStream(InputStream input, boolean isJar) throws IOException { //return isJar ? new JarInputStream(input) : new ZipInputStream(input); // We usually want to unjar the manifest as well return new ZipInputStream(input); } private static ZipEntry createZipEntry(String relativePath, boolean isJar) { relativePath = relativePath.replace('\\', '/'); if (relativePath.length() > 0 && relativePath.charAt(0) == '/') { relativePath = relativePath.substring(1); } return isJar ? new JarEntry(relativePath) : new ZipEntry(relativePath); } public static void jar(File sourceDir, File targetZip) throws IOException { zip(sourceDir, targetZip, true); } public static void zip(File sourceDir, File targetZip) throws IOException { zip(sourceDir, targetZip, false); } public static File[] listFiles(File directory, boolean recursive) { ArrayList<File> result = new ArrayList<File>(); innerListFiles(directory, result, recursive); return result.toArray(new File[result.size()]); } private static void innerListFiles(File directory, ArrayList<File> result, boolean recursive) { File[] files = directory.listFiles(); for (File file : files) { result.add(file); if (recursive && file.isDirectory()) { innerListFiles(file, result, true); } } } public static String[] parseCommandLine(String command) { ArrayList<String> result = new ArrayList<String>(); StringBuffer current = new StringBuffer(); char[] chars = command.toCharArray(); boolean inQuote = false; for (int i = 0; i < chars.length; i++) { if (chars[i] == '\"') { inQuote = !inQuote; } else if (chars[i] == ' ') { if (!inQuote) { addIfNotEmpty(result, current); current = new StringBuffer(); } else { current.append(chars[i]); } } else { current.append(chars[i]); } } addIfNotEmpty(result, current); return result.toArray(new String[result.size()]); } private static void addIfNotEmpty(ArrayList<String> result, StringBuffer current) { if (current.toString().trim().length() > 0) { result.add(current.toString()); } } public static void mergeFiles(IProgressMonitor monitor, File[] src, File dest) throws IOException { dest.getParentFile().mkdirs(); FileOutputStream output = new FileOutputStream(dest); try { mergeFiles(monitor, src, output); } finally { output.close(); } } private static void mergeFiles(IProgressMonitor monitor, File[] src, OutputStream output) throws IOException { for (int i = 0; i < src.length; i++) { if (!src[i].exists()) { throw new FileNotFoundException(src[i].getAbsolutePath()); } if (src[i].isDirectory()) { throw new IOException(src[i] + " is a directory, cannot be copied"); } } if (monitor == null) { monitor = new NullProgressMonitor(); } monitor.beginTask("Copying...", src.length); for (int i = 0; i < src.length; i++) { monitor.setTaskName(MessageFormat.format("Copying {0}", src[i] .getName())); FileInputStream input = new FileInputStream(src[i]); try { transfer(input, output); } finally { input.close(); } monitor.worked(1); } } public static void transfer(InputStream src, OutputStream dest) throws IOException { byte[] buffer = new byte[65536]; for (int read = src.read(buffer); read != -1; read = src .read(buffer)) { dest.write(buffer, 0, read); } } public static void copy(IProgressMonitor monitor, File src, File dest, FileFilter filter) throws IOException { if (src.isDirectory()) { copyDir(monitor, src, dest, filter); } else { copyFile(monitor, src, dest); } } public static void copyFile(IProgressMonitor monitor, File src, File dest) throws IOException { if (dest.isDirectory()) { dest = new File(dest, src.getName()); } mergeFiles(monitor, new File[] { src }, dest); } public static void copyDir(IProgressMonitor monitor, File srcDir, File destDir, FileFilter filter) throws IOException { copyDir(monitor, srcDir, destDir, filter, 0); } public static void copyDir(IProgressMonitor monitor, File srcDir, File destDir, FileFilter filter, int depth) throws IOException { if (depth > MAX_DEPTH) { return; } destDir.mkdirs(); File[] files = srcDir.listFiles(); monitor.beginTask("Recursive copy", IProgressMonitor.UNKNOWN); for (int i = 0; i < files.length; i++) { File src = new File(srcDir, files[i].getName()); if (filter == null || filter.accept(src)) { File dest = new File(destDir, files[i].getName()); if (files[i].isDirectory()) { copyDir(new NullProgressMonitor(), src, dest, filter, depth + 1); } else { copyFile(new NullProgressMonitor(), src, dest); } } } } public static byte[] fromBase16(String data) { boolean extraByte = data.length() % 2 == 1; byte[] result = new byte[data.length() / 2 + (extraByte ? 1 : 0)]; for (int i = 0; i < result.length; i++) { int value = Integer.parseInt(data.substring(2 * i, Math.min( 2 * i + 2, data.length())), 16); result[i] = (byte) (value & 0xff); } return result; } public static String toBase16(byte[] data) { return toBase16(data, 0, data.length); } public static String toBase16(byte[] data, int offset, int length) { char[] result = new char[length * 2]; for (int i = 0; i < length; i++) { result[2 * i] = BASE16_CHARS[(data[offset + i] >> 4) & 0xf]; result[2 * i + 1] = BASE16_CHARS[data[offset + i] & 0xf]; } return new String(result); } /** * Returns a simple string representation of data size, eg "23 bytes", * "2 KB", "11 MB", etc * * @param size * @return */ public static String dataSize(long size) { if (size < _5KB) { return size + " bytes"; } String unit = ""; float value = size; if (size > _1GB) { unit = "GB"; value = value / _1GB; } else if (size > _5MB) { unit = "MB"; value = value / _1MB; } else { unit = "KB"; value = value / _1KB; } return DATASIZE_FORMAT.format(new Object[] { value, unit }, new StringBuffer(30), new FieldPosition(0)).toString(); } public static String elapsedTime(long ms) { String unit = "ms"; long value = ms; long rem = 0; if (ms >= 3600000) { unit = "h"; value = ms / 3600000; rem = ms % 3600000; } else if (ms >= 60000) { unit = "m"; value = ms / 60000; rem = ms % 60000; } else if (ms >= 1000) { unit = "s"; value = ms / 1000; rem = ms % 1000; } String minor = rem == 0 ? "" : " " + elapsedTime(rem); return value + unit + minor; } public static String getExtension(File file) { return getExtension(file.getName()); } public static String getExtension(String filename) { if (filename == null) { return ""; } int index = filename.lastIndexOf('.'); if (index != -1) { return filename.substring(index + 1); } else { return ""; } } public static String getNameWithoutExtension(File file) { return getNameWithoutExtension(file.getName()); } public static String getNameWithoutExtension(String name) { if (name == null) { return ""; } int index = name.lastIndexOf('.'); if (index == -1) { return name; } else { return name.substring(0, index); } } /** * Truncates a {@link String}, and if necessary adds an ellipsis to * the end of the string. * @param original * @param ellipsis The string to use as an ellipsis, or {@code null} to use the default (...). * @param maxLength If the length of the {@code original} {@link String} is greater than * this value, an ellipsis will be added. * @return A {@link String} that is guaranteed to be no longer than {@code maxLength}. * @throws StringIndexOutOfBoundsException If the length of the ellipsis is greater * than {@code maxLength} */ public static String truncate(String original, String ellipsis, int maxLength) { if (original.length() > maxLength) { if (ellipsis == null) { ellipsis = "..."; } return original.substring(0, maxLength - ellipsis.length()) + ellipsis; } return original; } public static void writeToFile(File file, String text) throws IOException { FileWriter output = new FileWriter(file); try { output.write(text); } finally { if (output != null) { output.close(); } } } public static boolean deleteFiles(File file, FileFilter filter, int maxDepth, IProgressMonitor monitor) { if (monitor.isCanceled()) { return false; } if (maxDepth < 0) { return true; } monitor.setTaskName(MessageFormat.format("Deleting {0}", file)); if (file.isDirectory()) { boolean result = true; File[] filesToDelete = file.listFiles(); for (int i = 0; i < filesToDelete.length; i++) { result &= deleteFiles(filesToDelete[i], filter, maxDepth - 1, monitor); } result &= file.delete(); return result; } else { if (filter == null || filter.accept(file)) { return file.delete(); } else { return true; } } } public static FileFilter getExtensionFilter(final String... ext) { FileFilter filter = new ExtensionFileFilter(ext); return filter; } public static String replaceExtension(String filename, String newExtension) { int where = filename.lastIndexOf('.'); if (where != -1) { filename = filename.substring(0, where); } return filename + (isEmpty(newExtension) ? "" : ".") + newExtension; } public static void safeClose(Closeable closeable) { if (closeable != null) { try { closeable.close(); } catch (IOException e) { // Ignore. } } } public static String readFile(String filename) throws IOException { return readFile(filename, null); } public static String readFile(String filename, String enc) throws IOException { if (filename == null) { return null; } File file = new File(filename); if (!file.exists()) { throw new IOException(MessageFormat.format( "File ''{0}'' does not exist", filename)); } ByteArrayOutputStream result = new ByteArrayOutputStream(); mergeFiles(new NullProgressMonitor(), new File[] { file }, result); return enc == null ? new String(result.toByteArray()) : new String( result.toByteArray(), enc); } public static int readInt(InputStream input) throws IOException { // LE or BE? byte[] intBuf = new byte[4]; int totalRead = 0; for (int read = input.read(intBuf, totalRead, 4 - totalRead); read != 4 && totalRead < 4; read = input.read(intBuf, totalRead, 4 - totalRead)) { if (read < 1) { throw new EOFException(); } totalRead += read; } int result = (intBuf[3] & 0xff) << 24; result |= (intBuf[2] & 0xff) << 16; result |= (intBuf[1] & 0xff) << 8; result |= (intBuf[0] & 0xff); return result; } public static short readShort(InputStream input) throws IOException { byte[] shortBuf = new byte[2]; int totalRead = 0; for (int read = input.read(shortBuf, totalRead, 2 - totalRead); read != 2 && totalRead < 2; read = input.read(shortBuf, totalRead, 2 - totalRead)) { if (read < 1) { throw new EOFException(); } totalRead += read; } short result = (short) ((shortBuf[1] & 0xff) << 8); result |= (shortBuf[0] & 0xff); return result; } public static File relativeTo(File peer, String filename) { File filenameFile = new File(filename); if (filenameFile.isAbsolute()) { return filenameFile; } File dir = peer.isDirectory() ? peer : peer.getParentFile(); return new File(dir, filename).getAbsoluteFile(); } public static boolean isParent(File potentialParent, File potentialChild) { if (potentialParent == null || potentialChild == null) { return false; } return potentialChild.getAbsolutePath().startsWith(potentialParent.getAbsolutePath()); } public static boolean isEmpty(String text) { return text == null || text.isEmpty(); } public static boolean isEmptyDirectory(File file) { return !file.isDirectory() || file.list().length == 0; } /** * A utility method for handling <code>equals</code> of * <code>null</code> objects. * @param o1 * @param o2 */ public static boolean equals(Object o1, Object o2) { if (o1 == null) { return o2 == null; } return o1.equals(o2); } /** * A utility method for handling <code>compareTo</code> of * <code>null</code> objects. <code>null</code> is always considered * "less than" and will return <code>-1</code> * @param o1 * @param o2 */ public static int compare(Comparable c1, Comparable c2) { if (c1 == null) { return c2 == null ? 0 : -1; } if (c2 == null) { return +1; } return c1.compareTo(c2); } /** * Replaces parameters tagged with <code>%</code>s and returns * the resolved string. * @param input * @param map * @return */ public static String replace(String input, Map<String, String> map) { try { return innerReplace(input, new DefaultParameterResolver(map), 0).toString(); } catch (ParameterResolverException e) { // The default should never throw this exception throw new RuntimeException(e); } } /** * Replaces parameters tagged with <code>%</code>s and returns * the resolved string. * @param input * @param map * @return * @throws ParameterResolverException */ public static String replace(String input, ParameterResolver resolver) throws ParameterResolverException { return innerReplace(input, resolver, 0).toString(); } public static String[] replace(String[] input, ParameterResolver resolver) throws ParameterResolverException { String[] output = new String[input.length]; for (int i = 0; i < input.length; i++) { output[i] = replace(input[i], resolver); } return output; } private static StringBuffer innerReplace(String input, ParameterResolver map, int depth) throws ParameterResolverException { if (depth > 12) { throw new IllegalArgumentException("Cyclic parameters"); //$NON-NLS-1$ } StringBuffer result = new StringBuffer(); char[] chars = input.toCharArray(); boolean inParam = false; int paramStart = 0; for (int i = 0; i < chars.length; i++) { if ('%' == chars[i]) { if (!inParam) { paramStart = i; } else { String paramName = input.substring(paramStart + 1, i); if (paramName.length() > 0 && paramName.charAt(0) == '#') { // TODO. } else { if (paramName.length() == 0) { // Escape pattern %% => % result.append("%"); //$NON-NLS-1$ } else { String paramValue = map == null ? null : map.get(paramName); if (paramValue != null) { result.append(innerReplace(paramValue, map, depth + 1)); } else { // Just leave as-is result.append('%'); result.append(paramName); result.append('%'); } } } } inParam = !inParam; } else if (!inParam) { result.append(chars[i]); } } return result; } /** * Creates a GET URL, given a baseURL and a map of parameters to send in the GET * @param service * @param params * @return A URL of the format <code>baseURL?param1=value1¶m2=value2...</code> */ public static String toGetUrl(String baseURL, Map<String, String> params) { StringBuffer paramsStr = new StringBuffer(); if (baseURL == null) { baseURL = ""; } if (params != null && !params.isEmpty()) { if (!Util.isEmpty(baseURL)) { paramsStr.append("?"); //$NON-NLS-1$ } int paramCnt = 0; for (Map.Entry<String, String> param : params.entrySet()) { String key = param.getKey(); String value = param.getValue(); if (key != null && value != null) { paramCnt++; if (paramCnt > 1) { paramsStr.append("&"); //$NON-NLS-1$ } paramsStr.append(URLEncoder.encode(key) + "=" + URLEncoder.encode(value)); //$NON-NLS-1$ } } } return baseURL + paramsStr; } public static <T> Comparator<T> reverseComparator(Comparator<T> original) { return new ReverseComparator<T>(original); } /** * Returns a 'parent' key of a key using path separators. So, if the input key is A/B/C, this * method returns A/B * @param key * @return <code>null</code> if <code>key</code> has no path separator */ public static String getParentKey(String key) { Path permissionPath = new Path(key); return permissionPath.segmentCount() > 1 ? permissionPath.removeLastSegments(1).toPortableString() : null; } public static String convertSlashes(String str) { if (str == null) { return str; } return str.replace('\\', File.separatorChar).replace('/', File.separatorChar); } /** * Given a string, returns a proper C identifier. * @param name * @return */ public static String toIdentifier(String str) { // C = Java identifiers :) StringBuffer buf = new StringBuffer(); if (!str.isEmpty()) { char first = str.charAt(0); if (Character.isJavaIdentifierPart(first) && !Character.isJavaIdentifierStart(first)) { buf.append("_"); } for (int i = 0; i < str.length(); i++) { char ch = str.charAt(i); if (Character.isJavaIdentifierPart(ch)) { buf.append(Character.toUpperCase(ch)); } else { buf.append("_"); } } } return buf.toString(); } /** * Reverses a map into a {@link HashMap}. * @param original The original map to reverse. * @return A reverse map. If the original map contains several * keys with the same value an {@link IllegalArgumentException} * is thrown. */ public static <K, V> HashMap<V, K> reverseMap(Map<K, V> map) { HashMap<V, K> result = new HashMap<V, K>(); for (Map.Entry<K, V> entry : map.entrySet()) { V key = entry.getValue(); if (result.containsKey(key)) { throw new IllegalArgumentException("Key already present."); } result.put(key, entry.getKey()); } return result; } }