/*
* Geotoolkit.org - An Open Source Java GIS Toolkit
* http://www.geotoolkit.org
*
* (C) 2003-2012, Open Source Geospatial Foundation (OSGeo)
* (C) 2009-2012, Geomatys
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation;
* version 2.1 of the License.
*
* This library 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 GNU
* Lesser General Public License for more details.
*/
package org.geotoolkit.test;
import java.io.*;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLDecoder;
import java.nio.channels.Channels;
import java.nio.channels.ReadableByteChannel;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.Properties;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
/**
* Provides access to "{@code test-data}" directories associated with JUnit tests. The expected
* directory name is "{@code test-data}" to follow the javadoc "{@code doc-files}" convention of
* ensuring that data directories don't look anything like normal java packages.
* <p>
* Example:
*
* {@preformat java
* class MyClass {
* public void example() {
* Image testImage = new ImageIcon(TestData.url(this, "test.png")).getImage();
* Reader reader = TestData.openReader(this, "script.xml");
* // ... do some process
* reader.close();
* }
* }
* }
*
* Where the directory structure goes as bellow:
* <p>
* <ul>
* <li>{@code MyClass.java}<li>
* <li>{@code test-data/test.png}</li>
* <li>{@code test-data/script.xml}</li>
* </ul>
* <p>
* By convention developers should locate "{@code test-data}" near the JUnit test
* cases that uses it.
*
* @author James McGill (Leeds)
* @author Martin Desruisseaux (IRD, Geomatys)
* @author Simone Giannecchini (Geosolutions)
* @version 3.19
*
* @since 2.4
*/
public final strictfp class TestData implements Runnable {
/**
* The test data directory.
*/
private static final String DIRECTORY = "test-data";
/**
* Encoding of files and URL path.
*/
private static final String ENCODING = "UTF-8";
/**
* The files to delete at shutdown time. {@link File#deleteOnExit} alone doesn't seem
* sufficient since it will preserve any overwritten files.
*/
private static final LinkedList<Deletable> toDelete = new LinkedList<>();
/**
* Registers the thread to be automatically executed at shutdown time.
* This thread will delete all temporary files registered in {@link #toDelete}.
*/
static {
Runtime.getRuntime().addShutdownHook(new Thread(new TestData(), "Test data cleaner"));
}
/**
* Do not allow instantiation of this class.
*/
private TestData() {
}
/**
* Locates named test-data resource for caller. <strong>Note:</strong> Consider using the
* <code>{@link #url url}(caller, name)</code> method instead if the resource should always
* exists.
*
* @param caller Calling class or object used to locate {@code test-data}.
* @param name Name of the resource to find in the {@code test-data} directory,
* or (@code null} for the {@code test-data} directory itself.
* @return URL or {@code null} if the named test-data could not be found.
*
* @see #url
*/
public static URL getResource(final Object caller, String name) {
if (name == null || (name=name.trim()).isEmpty()) {
name = DIRECTORY;
} else {
name = DIRECTORY + '/' + name;
}
if (caller != null) {
final Class<?> c = (caller instanceof Class<?>) ? (Class<?>) caller : caller.getClass();
return c.getResource(name);
} else {
return Thread.currentThread().getContextClassLoader().getResource(name);
}
}
/**
* Access to <code>{@linkplain #getResource getResource}(caller, path)</code> as a non-null
* {@link URL}. At the difference of {@code getResource}, this method throws an exception if
* the resource is not found. This provides a more explicit explanation about the failure
* reason than the infamous {@link NullPointerException}.
*
* @param caller Calling class or object used to locate {@code test-data}.
* @param path Path to the resource to find in the {@code test-data} directory,
* or (@code null} for the {@code test-data} directory itself.
* @return The URL to the {@code test-data} resource.
* @throws FileNotFoundException if the resource is not found.
*
* @since 2.2
*/
public static URL url(final Object caller, final String path) throws FileNotFoundException {
final URL url = getResource(caller, path);
if (url == null) {
throw new FileNotFoundException("Can not locate test-data for \"" + path + '"');
}
return url;
}
/**
* Access to <code>{@linkplain #getResource getResource}(caller, path)</code> as a non-null
* {@link File}. It allows access the {@code test-data} directory with:
*
* {@preformat java
* TestData.file(MyClass.class, null);
* }
*
* @param caller Calling class or object used to locate {@code test-data}.
* @param path Path to the resource to find in the {@code test-data} directory,
* or (@code null} for the {@code test-data} directory itself.
* @return The file to the {@code test-data} resource.
* @throws FileNotFoundException if the file is not found.
* @throws IOException if the resource can't be fetched for an other reason.
*/
public static File file(final Object caller, final String path)
throws FileNotFoundException, IOException
{
final URL url = url(caller, path);
final File file = new File(URLDecoder.decode(url.getPath(), ENCODING));
if (!file.exists()) {
throw new FileNotFoundException("Can not locate test-data for \"" + file.getAbsolutePath() + '"');
}
return file;
}
/**
* Creates a temporary file with the given name. The file will be created in the
* {@code test-data} directory and will be deleted on exit.
*
* @param caller Calling class or object used to locate {@code test-data}.
* @param name A base name for the temporary file.
* @return The temporary file in the {@code test-data} directory.
* @throws IOException if the file can't be created.
*/
public static File temp(final Object caller, final String name) throws IOException {
final File testData = file(caller, null);
final int split = name.lastIndexOf('.');
final String prefix = (split < 0) ? name : name.substring(0,split);
final String suffix = (split < 0) ? "tmp" : name.substring(split+1);
final File tmp = File.createTempFile(prefix, '.' + suffix, testData);
deleteOnExit(tmp, true);
return tmp;
}
/**
* Provides a non-null {@link InputStream} for named test data.
* It is the caller responsibility to close this stream after usage.
*
* @param caller Calling class or object used to locate {@code test-data}.
* @param name Filename of test data to load.
* @return The input stream.
* @throws FileNotFoundException if the resource is not found.
* @throws IOException if an error occurs during an input operation.
*
* @since 2.2
*/
public static InputStream openStream(final Object caller, final String name)
throws FileNotFoundException, IOException
{
return new BufferedInputStream(url(caller, name).openStream());
}
/**
* Provides a {@link BufferedReader} for named test data in UTF-8 encoding. The buffered
* reader is provided as a {@link LineNumberReader} instance, which is useful for displaying
* line numbers where error occur. It is the caller responsibility to close this reader after
* usage.
*
* @param caller The class of the object associated with named data.
* @param name Filename of test data to load.
* @return The buffered reader.
* @throws FileNotFoundException if the resource is not found.
* @throws IOException if an error occurs during an input operation.
*
* @since 2.2
*/
public static LineNumberReader openReader(final Object caller, final String name)
throws FileNotFoundException, IOException
{
return new LineNumberReader(new InputStreamReader(url(caller, name).openStream(), ENCODING));
}
/**
* Provides a channel for named test data. It is the caller responsibility to close this
* chanel after usage.
*
* @param caller The class of the object associated with named data.
* @param name Filename of test data to load.
* @return The chanel.
* @throws FileNotFoundException if the resource is not found.
* @throws IOException if an error occurs during an input operation.
*
* @since 2.2
*/
public static ReadableByteChannel openChannel(final Object caller, final String name)
throws FileNotFoundException, IOException
{
final URL url = url(caller, name);
final File file = new File(URLDecoder.decode(url.getPath(), ENCODING));
if (file.exists()) {
return new RandomAccessFile(file, "r").getChannel();
}
return Channels.newChannel(url.openStream());
}
/**
* Reads the given resource as a text file, assuming a UTF-8 encoding.
* The returned text uses always the Unix style of EOL.
*
* @param caller The class of the object associated with named data.
* @param name Filename of test data to load.
* @return The loaded test data as a text.
* @throws IOException if an error occurs during an input operation.
*
* @since 3.04
*/
public static String readText(final Object caller, final String name) throws IOException {
return read(openReader(caller, name));
}
/**
* Reads the given file as a text file, assuming a UTF-8 encoding.
* The returned text uses always the Unix style of EOL.
*
* @param file The file of test data to load.
* @return The loaded test data as a text.
* @throws IOException if an error occurs during an input operation.
*
* @since 3.07
*/
public static String readText(final File file) throws IOException {
return readText(file, ENCODING);
}
/**
* Reads the given file as a text file, assuming a ISO-LATIN-1 encoding.
* The returned text uses always the Unix style of EOL.
*
* @param file The file of test data to load.
* @return The loaded test data as a text.
* @throws IOException if an error occurs during an input operation.
*
* @since 3.07
*/
public static String readLatinText(final File file) throws IOException {
return readText(file, "ISO-8859-1");
}
/**
* Reads the given file as a text file, assuming the given encoding.
* The returned text uses always the Unix style of EOL.
*/
private static String readText(final File file, final String encoding) throws IOException {
return read(new BufferedReader(new InputStreamReader(new FileInputStream(file), encoding)));
}
/**
* Reads the given stream as a text. The returned text uses always the Unix style of EOL.
* The given stream is closed by this method.
*/
private static String read(final BufferedReader in) throws IOException {
final StringBuilder buffer = new StringBuilder();
String line;
while ((line = in.readLine()) != null) {
buffer.append(line).append('\n');
}
in.close();
return buffer.toString();
}
/**
* Reads the given file as a properties file.
*
* @param file The file of test properties to load.
* @return The loaded test data as a properties file.
* @throws IOException if an error occurs during an input operation.
*
* @since 3.10
*/
public static Properties readProperties(final File file) throws IOException {
final Properties properties;
try (InputStream in = new FileInputStream(file)) {
properties = new Properties();
properties.load(in);
}
return properties;
}
/**
* Unzip a file in the {@code test-data} directory. The zip file content is inflated in place,
* i.e. inflated files are written in the same {@code test-data} directory. If a file to be
* inflated already exists in the {@code test-data} directory, then the existing file is left
* untouched and the corresponding ZIP entry is silently skipped. This approach avoid the
* overhead of inflating the same files many time if this {@code unzipFile} method is invoked
* before every tests.
* <p>
* Inflated files will be automatically {@linkplain File#deleteOnExit deleted on exit}
* if and only if they have been modified. Callers don't need to worry about cleanup,
* because the files are inflated in the {@code target/.../test-data} directory, which
* is not versionned by SVN and is cleaned by Maven on {@code mvn clean} execution.
*
* @param caller The class of the object associated with named data.
* @param name The file name to unzip in place.
* @throws FileNotFoundException if the specified zip file is not found.
* @throws IOException if an error occurs during an input or output operation.
*
* @since 2.2
*/
public static void unzipFile(final Object caller, final String name)
throws FileNotFoundException, IOException
{
final File file = file(caller, name);
final File parent = file.getParentFile().getAbsoluteFile();
try (ZipFile zipFile = new ZipFile(file)) {
final byte[] buffer = new byte[4096];
final Enumeration<? extends ZipEntry> entries = zipFile.entries();
while (entries.hasMoreElements()) {
final ZipEntry entry = entries.nextElement();
if (entry.isDirectory()) {
continue;
}
final File path = new File(parent, entry.getName());
if (path.exists()) {
continue;
}
final File directory = path.getParentFile();
if (directory != null && !directory.exists()) {
directory.mkdirs();
}
// Copy the file. Note: no need for a BufferedOutputStream,
// since we are already using a buffer of type byte[4096].
try (InputStream in = zipFile.getInputStream(entry);
OutputStream out = new FileOutputStream(path))
{
int len;
while ((len = in.read(buffer)) >= 0) {
out.write(buffer, 0, len);
}
}
// Call 'deleteOnExit' only after after we closed the file,
// because this method will save the modification time.
deleteOnExit(path, false);
}
}
}
/**
* Deletes the given file or directory. If the given argument denotes a directory,
* then the directory content is deleted as well.
*
* @param file The file or directory to delete.
* @return {@code true} on success.
*
* @since 3.03
*/
public static boolean deleteRecursively(final File file) {
if (file.isDirectory()) {
for (final File child : file.listFiles()) {
if (!deleteRecursively(child)) {
return false;
}
}
}
return file.delete();
}
/**
* Requests that the file or directory denoted by the specified pathname be deleted
* when the virtual machine terminates. This method can optionally delete the file
* only if it has been modified, thus giving a chance for test suites to copy their
* resources only once.
*
* @param file The file to delete.
* @param force If {@code true}, delete the file in all cases. If {@code false},
* delete the file if and only if it has been modified.
*
* @since 2.4
*/
public static void deleteOnExit(final File file, final boolean force) {
if (force) {
file.deleteOnExit();
}
final Deletable entry = new Deletable(file, force);
synchronized (toDelete) {
if (file.isFile()) {
toDelete.addFirst(entry);
} else {
toDelete.addLast(entry);
}
}
}
/**
* A file that may be deleted on JVM shutdown.
*/
private static final class Deletable {
/**
* The file to delete.
*/
private final File file;
/**
* The initial timestamp. Used in order to determine if the file has been modified.
*/
private final long timestamp;
/**
* Constructs an entry for a file to be deleted.
*/
public Deletable(final File file, final boolean force) {
this.file = file;
timestamp = force ? Long.MIN_VALUE : file.lastModified();
}
/**
* Returns {@code true} if failure to delete this file can be ignored.
*/
public boolean canIgnore() {
return timestamp != Long.MIN_VALUE && file.isDirectory();
}
/**
* Deletes this file, if modified. Returns {@code false} only
* if the file should be deleted but the operation failed.
*/
public boolean delete() {
if (!file.exists() || file.lastModified() <= timestamp) {
return true;
}
return file.delete();
}
/**
* Returns the filepath.
*/
@Override
public String toString() {
return String.valueOf(file);
}
}
/**
* Deletes all temporary files. This method is invoked automatically at shutdown time and
* should not be invoked directly. It is public only as an implementation side effect.
*/
@Override
public void run() {
int iteration = 5; // Maximum number of iterations
synchronized (toDelete) {
while (!toDelete.isEmpty()) {
if (--iteration < 0) {
break;
}
/*
* Before to try to delete the files, invokes the finalizers in a hope to close
* any input streams that the user didn't explicitly closed. Leaving streams open
* seems to occurs way too often in our test suite...
*/
System.gc();
System.runFinalization();
for (final Iterator<Deletable> it=toDelete.iterator(); it.hasNext();) {
final Deletable f = it.next();
try {
if (f.delete()) {
it.remove();
continue;
}
} catch (SecurityException e) {
if (iteration == 0) {
System.err.print(e.getClass().getCanonicalName());
System.err.print(": ");
}
}
// Can't use logging, since logger are not available anymore at shutdown time.
if (iteration == 0 && !f.canIgnore()) {
System.err.print("Can't delete ");
System.err.println(f);
}
}
}
}
}
}