com.aragost.javahg.internals.Utils Maven / Gradle / Ivy
/*
* #%L
* JavaHg
* %%
* Copyright (C) 2011 aragost Trifork ag
* %%
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
* #L%
*/
package com.aragost.javahg.internals;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.lang.reflect.Array;
import java.lang.reflect.InvocationTargetException;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.CharacterCodingException;
import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;
import java.util.Collection;
import java.util.Map;
import java.util.Random;
import java.util.concurrent.ExecutionException;
import com.aragost.javahg.log.Logger;
import com.aragost.javahg.log.LoggerFactory;
import com.google.common.io.ByteStreams;
import com.google.common.io.CharStreams;
import com.google.common.io.Files;
/**
* Miscellaneous utils methods for JavaHg.
*/
public class Utils {
private static final Logger LOG = LoggerFactory.getLogger(Utils.class);
/**
* Flag indicating if we are on a Windows box
*/
private static final boolean IS_WINDOWS;
/**
* Temp dir where resources are copied from the jar (classpath) to
* an actual file
*/
private static final File RESOURCES_TEMP_DIR;
static {
String osName = System.getProperty("os.name");
IS_WINDOWS = osName.startsWith("Windows");
RESOURCES_TEMP_DIR = Files.createTempDir();
Runtime.getRuntime().addShutdownHook(new Thread() {
@Override
public void run() {
try {
Utils.deleteTempDir(RESOURCES_TEMP_DIR);
LOG.info("Deleted temp resource dir " + RESOURCES_TEMP_DIR);
} catch (IOException e) {
System.err.println("Could not delete temp resource dir " + RESOURCES_TEMP_DIR);
e.printStackTrace();
}
}
});
}
/**
*
* @return true if the OS is Windows, otherwise false
*/
public static boolean isWindows() {
return IS_WINDOWS;
}
public static Charset getUtf8Charset() {
return Charset.forName("UTF-8");
}
/**
* Read four bytes from the stream, and return the integer
* represented by these bytes in big endian. If EOF is reached
* then -1 is returned.
*
* @param stream
* the input stream
* @return the decoded integer.
* @throws IOException
*/
public static int readBigEndian(InputStream stream) throws IOException {
byte b0 = (byte) stream.read();
byte b1 = (byte) stream.read();
byte b2 = (byte) stream.read();
int b3 = stream.read();
if (b3 == -1) {
return -1;
}
return (b0 << 24) + ((b1 & 0xFF) << 16) + ((b2 & 0xFF) << 8) + ((byte) b3 & 0xFF);
}
/**
* Write 4 bytes representing the specified integer in big-endian.
*
* @param n
* the integer
* @param stream
* the output stream
* @throws IOException
*/
public static void writeBigEndian(int n, OutputStream stream) throws IOException {
byte[] bytes = new byte[] { (byte) (n >>> 24), (byte) (n >>> 16), (byte) (n >>> 8), (byte) n };
stream.write(bytes);
}
/**
* Read and discard everything from the specified InputStream
*
* @param stream
* @throws IOException
*/
public static void consumeAll(InputStream stream) throws IOException {
while (stream.read(BUF) != -1)
;
}
private static final byte[] BUF = new byte[256];
/**
* Read everything from the stream and return it as a String
*
* @param in
* the input stream
* @param decoder
* the {@link CharsetDecoder} encapsulating the policy
* of handling errors while decoding.
* @return the decoded String.
*/
public static String readStream(InputStream in, CharsetDecoder decoder) {
try {
return CharStreams.toString(new InputStreamReader(in, decoder));
} catch (IOException e) {
throw new RuntimeIOException(e);
}
}
/**
* Decode the bytes with the decoder
*
* @param bytes
* @param decoder
* @return the decoded bytes
*/
public static String decodeBytes(byte[] bytes, CharsetDecoder decoder) {
ByteBuffer wrap = ByteBuffer.wrap(bytes);
CharBuffer chars;
try {
chars = decoder.decode(wrap);
} catch (CharacterCodingException e) {
throw asRuntime(e);
}
return new String(chars.array(), chars.arrayOffset(), chars.limit());
}
/**
* Insert an element as the first to an array. The input array is
* not changed, instead a copy is returned.
*
* @param
* @param first
* element to insert at position 0 in the result.
* @param rest
* elements to insert at position 1 to
* {@code rest.length}.
* @return the unshifted array of length {@code 1 + rest.length}.
*/
public static T[] arrayUnshift(T first, T[] rest) {
@SuppressWarnings("unchecked")
T[] result = (T[]) Array.newInstance(first.getClass(), 1 + rest.length);
result[0] = first;
System.arraycopy(rest, 0, result, 1, rest.length);
return result;
}
/**
* Concatenate two arrays. The input arrays are copied into the
* output array.
*
* @param
* @param a
* @param b
* @return the concatenated array of length
* {@code a.length + b.length}.
*/
public static T[] arrayConcat(T[] a, T[] b) {
@SuppressWarnings("unchecked")
T[] result = (T[]) Array.newInstance(a.getClass().getComponentType(), a.length + b.length);
System.arraycopy(a, 0, result, 0, a.length);
System.arraycopy(b, 0, result, a.length, b.length);
return result;
}
/**
* Convert an array of Files to an array of Strings.
*
* Each file is converted with the getPath() method.
*
* @param files
* @return array of file names as Strings
*/
public static String[] fileArray2StringArray(File[] files) {
String[] result = new String[files.length];
for (int i = 0; i < files.length; i++) {
File file = files[i];
result[i] = file.getPath();
}
return result;
}
/**
*
* @param baseDir
* @param strings
* @return array of File objects
*/
public static File[] stringArray2FileArray(File baseDir, String[] strings) {
File[] result = new File[strings.length];
for (int i = 0; i < strings.length; i++) {
String s = strings[i];
File file = new File(s);
if (!file.isAbsolute()) {
file = new File(baseDir, s);
}
result[i] = Utils.resolveSymlinks(file);
}
return result;
}
/**
* Return a new File object where symlinks are resolved.
*
* @param file
* @return the canonical file name
*/
public static File resolveSymlinks(File file) {
// On windows calling File.getCanonicalFile will result in
// abc~ parts will be expanded. We don't want that.
if (IS_WINDOWS) {
return file;
} else {
try {
return file.getCanonicalFile();
} catch (IOException e) {
throw new RuntimeIOException(e);
}
}
}
/**
* Return the single element in the specified Collection.
*
* If the collection is empty null is return
*
* @param coll
* @return null or the singleton element in the collection
* @throws IllegalArgumentException
* if the collection has more than one element
*/
public static T single(Collection coll) {
switch (coll.size()) {
case 0:
return null;
case 1:
return coll.iterator().next();
default:
throw new IllegalArgumentException("Collection has more than one element");
}
}
/**
* Create a temporary file.
*
* This method is identical to
* {@link java.io.File#createTempFile(String, String)} except it
* throws a {@link RuntimeIOException}.
*
* @param suffix
* @return the temporary file
*/
public static File createTempFile(String suffix) {
try {
return File.createTempFile("javahg-", suffix);
} catch (IOException e) {
throw new RuntimeIOException(e);
}
}
/**
* Determines whether the specified file is a Symbolic Link rather
* than an actual file.
*
* Will not return true if there is a Symbolic Link anywhere in
* the path, only if the specific file is.
*
* Note: The implementation is adapted from the similar method in
* Apache Commons IO
*
* @param file
* the file to check
* @return true if the file is a Symbolic Link
*/
public static boolean isSymlink(File file) {
if (IS_WINDOWS) {
return false;
}
try {
File fileInCanonicalDir = null;
if (file.getParent() == null) {
fileInCanonicalDir = file;
} else {
File canonicalDir = file.getParentFile().getCanonicalFile();
fileInCanonicalDir = new File(canonicalDir, file.getName());
}
return !fileInCanonicalDir.getCanonicalFile().equals(fileInCanonicalDir.getAbsoluteFile());
} catch (IOException e) {
throw new RuntimeIOException(e);
}
}
/**
* Convert the specified exception into a RuntimeException
*
* @param e
* @return the RuntimeException
*/
public static RuntimeException asRuntime(Throwable e) {
if (e instanceof RuntimeException) {
return (RuntimeException) e;
}
if (e instanceof ExecutionException) {
return asRuntime(e.getCause());
}
if (e instanceof InvocationTargetException) {
return asRuntime(e.getCause());
}
if (e instanceof IOException) {
return new RuntimeIOException((IOException) e);
}
return new RuntimeException(e);
}
/**
* Delete a directory in the system temporary directory
* (java.io.tmpdir).
*
* @throws IOException
*/
public static void deleteTempDir(File file) throws IOException {
file = file.getCanonicalFile();
String javaTmpDir = new File(System.getProperty("java.io.tmpdir")).getCanonicalPath();
if (file.getPath().startsWith(javaTmpDir)) {
deleteRecursive(file);
} else {
throw new IllegalArgumentException("Can only delete files in " + javaTmpDir);
}
}
private static void deleteRecursive(File file) throws IOException {
if (file.isDirectory()) {
File[] files = file.listFiles();
for (File f : files) {
deleteRecursive(f);
}
}
// Windows often fails to delete on first attempt, but after a
// short
// break the file can be deleted. Wait up to 5 sec.
// I believe that it is because the cmdserver process (which
// has been
// terminated before calling this method)
// still has a lock on the dir. So we wait for it to actually
// finish and
// release lock
for (int i = 20; !file.delete(); i--) {
if (i == 0) {
throw new IOException("Failed to delete: " + file);
}
try {
System.err.println("Sleep a bit and try again to delete " + file + "[" + i + "/20]");
Thread.sleep(250);
} catch (InterruptedException e) {
}
}
}
/**
* Return a File that is created from a resource name found on the
* classpath. The resource is written into the File the first time
* it's needed.
*
* @param resourceName
* @return the File
* @throws IOException
*/
public static File resourceAsFile(String resourceName) {
return resourceAsFile(resourceName, null);
}
/**
* Return a File that is created from a resource name found on the
* classpath. The resource is written into the File the first time
* it's needed.
*
* Patterns of the form %{name} will be replace with the
* corresponding value from the replacement map.
*
* @param resourceName
* @parem replacements if null no replacements will be performed
* @return the File
* @throws IOException
*/
public static File resourceAsFile(String resourceName, Map replacements) {
File file = new File(RESOURCES_TEMP_DIR, resourceName);
if (!file.exists()) {
file.getParentFile().mkdirs();
InputStream stream = Utils.class.getResourceAsStream(resourceName);
try {
OutputStream fileOutput = new BufferedOutputStream(new FileOutputStream(file));
OutputStream output = fileOutput;
if (replacements != null) {
output = new PatternReplacingOutputStream(fileOutput, replacements);
}
ByteStreams.copy(stream, output);
output.close();
} catch (IOException e) {
throw new RuntimeIOException(e);
}
LOG.info("Copy resource {} to {}", resourceName, file.getAbsolutePath());
}
return file;
}
/**
* Generate some random bytes that is printable Ascii characters.
*
* They will be used in a Mercurial style file. To make it easy to
* use the bytes in the style file the characters '"' and '\' will
* not be part of the byte array.
*
* @return
*/
public static byte[] randomBytes() {
byte[] bytes = new byte[20];
new Random().nextBytes(bytes);
// Make the bytes printable by modulo 64 and add to '"' and
// use 126 for '\\'
assert '"' + 64 < 126;
for (int i = 0; i < bytes.length; i++) {
int b = '"' + 1 + (bytes[i] & 63);
if (b == '\\') {
b = 126;
}
bytes[i] = (byte) b;
}
LOG.info("Random bytes generated: {}", new String(bytes));
return bytes;
}
}