001: /*
002: * GeoTools - OpenSource mapping toolkit
003: * http://geotools.org
004: * (C) 2003-2006, Geotools Project Managment Committee (PMC)
005: *
006: * This library is free software; you can redistribute it and/or
007: * modify it under the terms of the GNU Lesser General Public
008: * License as published by the Free Software Foundation; either
009: * version 2.1 of the License, or (at your option) any later version.
010: *
011: * This library is distributed in the hope that it will be useful,
012: * but WITHOUT ANY WARRANTY; without even the implied warranty of
013: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
014: * Lesser General Public License for more details.
015: */
016: package org.geotools.test;
017:
018: // J2SE dependencies
019: import java.io.BufferedInputStream;
020: import java.io.BufferedReader;
021: import java.io.File;
022: import java.io.FileNotFoundException;
023: import java.io.FileOutputStream;
024: import java.io.IOException;
025: import java.io.InputStream;
026: import java.io.InputStreamReader;
027: import java.io.LineNumberReader;
028: import java.io.OutputStream;
029: import java.io.RandomAccessFile;
030: import java.net.URL;
031: import java.net.URLDecoder;
032: import java.nio.channels.Channels;
033: import java.nio.channels.ReadableByteChannel;
034: import java.util.Enumeration;
035: import java.util.Iterator;
036: import java.util.LinkedList;
037: import java.util.logging.Logger;
038: import java.util.zip.ZipEntry;
039: import java.util.zip.ZipFile;
040:
041: /**
042: * Provides access to {@code test-data} directories associated with JUnit tests.
043: * <p>
044: * We have chosen "{@code test-data}" to follow the javadoc "{@code doc-files}" convention
045: * of ensuring that data directories don't look anything like normal java packages.
046: * <p>
047: * Example:
048: * <pre>
049: * class MyClass {
050: * public void example() {
051: * Image testImage = new ImageIcon(TestData.url(this, "test.png")).getImage();
052: * Reader reader = TestData.openReader(this, "script.xml");
053: * // ... do some process
054: * reader.close();
055: * }
056: * }
057: * </pre>
058: * Where the directory structure goes as bellow:
059: * <ul>
060: * <li>{@code MyClass.java}<li>
061: * <li>{@code test-data/test.png}</li>
062: * <li>{@code test-data/script.xml}</li>
063: * </ul>
064: * <p>
065: * By convention you should try and locate {@code test-data} near the JUnit test
066: * cases that uses it. If you need an access to shared test data, import the
067: * {@link org.geotools.TestData} class from the {@code sample-module} instead
068: * of this one.
069: *
070: * @since 2.4
071: * @source $URL: http://svn.geotools.org/geotools/tags/2.4.1/modules/library/sample-data/src/main/java/org/geotools/test/TestData.java $
072: * @version $Id: TestData.java 27862 2007-11-12 19:51:19Z desruisseaux $
073: * @author James McGill
074: * @author Simone Giannecchiin
075: * @author Martin Desruisseaux
076: *
077: * @tutorial http://www.geotools.org/display/GEOT/5.8+Test+Data
078: */
079: public class TestData implements Runnable {
080: /**
081: * The test data directory.
082: */
083: private static final String DIRECTORY = "test-data";
084:
085: /**
086: * Encoding of URL path.
087: */
088: private static final String ENCODING = "UTF-8";
089:
090: /**
091: * The {@linkplain System#getProperty(String) system property} key for more extensive test
092: * suite. The value for this key is returned by the {@link #isExtensiveTest} method. Some
093: * test suites will perform more extensive test coverage if this property is set to
094: * {@code true}. The value for this property is typically defined on the command line as a
095: * <code>-D{@value}=true</code> option at Java or Maven starting time.
096: */
097: public static final String EXTENSIVE_TEST_KEY = "org.geotools.test.extensive";
098:
099: /**
100: * The {@linkplain System#getProperty(String) system property} key for interactive tests.
101: * The value for this key is returned by the {@link #isInteractiveTest} method. Some
102: * test suites will show windows with maps and other artifacts related to testing
103: * if this property is set to {@code true}.
104: * The value for this property is typically defined on the command line as a
105: * <code>-D{@value}=true</code> option at Java or Maven starting time.
106: */
107: public static final String INTERACTIVE_TEST_KEY = "org.geotools.test.interactive";
108:
109: /**
110: * The files to delete at shutdown time. {@link File#deleteOnExit} alone doesn't seem
111: * suffisient since it will preserve any overwritten files.
112: */
113: private static final LinkedList toDelete = new LinkedList();
114:
115: /**
116: * {@code true} if JAI media lib is available.
117: */
118: private static final boolean mediaLibAvailable;
119: static {
120: Class mediaLibImage = null;
121: try {
122: mediaLibImage = Class
123: .forName("com.sun.medialib.mlib.Image");
124: } catch (ClassNotFoundException e) {
125: }
126: mediaLibAvailable = (mediaLibImage != null);
127: }
128:
129: /**
130: * Register the thread to be automatically executed at shutdown time.
131: * This thread will delete all temporary files registered in {@link #toDelete}.
132: */
133: static {
134: Runtime.getRuntime().addShutdownHook(
135: new Thread(new TestData(), "Test data cleaner"));
136: }
137:
138: /**
139: * Do not allow instantiation of this class, except for extending it.
140: */
141: protected TestData() {
142: }
143:
144: /**
145: * Get a property as a boolean value. If the property can't be
146: * fetch for security reason, then default to {@code false}.
147: */
148: private static boolean getBoolean(final String name) {
149: try {
150: return Boolean.getBoolean(name);
151: } catch (SecurityException exception) {
152: // Note: we use Java Logger instead of Geotools Logging because this module
153: // do not depends on the module that defines Logging. This class is used for
154: // test purpose only anyway, so it should not be an issue.
155: Logger.getLogger("org.geotools").warning(
156: exception.getLocalizedMessage());
157: return false;
158: }
159: }
160:
161: /**
162: * Returns {@code true} if the running Java virtual machine is 1.4. This is the lowest
163: * Java version currently supported by Geotools. This version will increase in future
164: * Geotools version.
165: * <p>
166: * This method is used for some broken JUnit test that are know to run on JSE 1.4 but
167: * not on JSE 1.6 for example.
168: */
169: public static boolean isBaseJavaPlatform() {
170: return System.getProperty("java.version").startsWith("1.4");
171: }
172:
173: /**
174: * Returns {@code true} if JAI MediaLib acceleration is available.
175: * <p>
176: * This method is used to disable some checks in unit tests that fail when JAI is
177: * run in pure java mode.
178: */
179: public static boolean isMediaLibAvailable() {
180: return mediaLibAvailable;
181: }
182:
183: /**
184: * Returns {@code true} if {@value #EXTENSIVE_TEST_KEY} system property is set to
185: * {@code true}. Test suites should check this value before to perform lengthly tests.
186: */
187: public static boolean isExtensiveTest() {
188: return getBoolean(EXTENSIVE_TEST_KEY);
189: }
190:
191: /**
192: * Returns {@code true} if {@value #INTERACTIVE_TEST_KEY} system property is set to {@code true}.
193: * Test suites should check this value before showing any kind of graphical window to the user.
194: */
195: public static boolean isInteractiveTest() {
196: return getBoolean(INTERACTIVE_TEST_KEY);
197: }
198:
199: /**
200: * Locates named test-data resource for caller. <strong>Note:</strong> Consider using the
201: * <code>{@link #url url}(caller, name)</code> method instead if the resource should always
202: * exists.
203: *
204: * @param caller Calling class or object used to locate {@code test-data}.
205: * @param name resource name in {@code test-data} directory.
206: * @return URL or {@code null} if the named test-data could not be found.
207: *
208: * @see #url
209: */
210: public static URL getResource(final Object caller, String name) {
211: if (name == null || (name = name.trim()).length() == 0) {
212: name = DIRECTORY;
213: } else {
214: name = DIRECTORY + '/' + name;
215: }
216: if (caller != null) {
217: final Class c = (caller instanceof Class) ? (Class) caller
218: : caller.getClass();
219: return c.getResource(name);
220: } else {
221: return Thread.currentThread().getContextClassLoader()
222: .getResource(name);
223: }
224: }
225:
226: /**
227: * Access to <code>{@linkplain #getResource getResource}(caller, path)</code> as a non-null
228: * {@link URL}. At the difference of {@code getResource}, this method throws an exception if
229: * the resource is not found. This provides a more explicit explanation about the failure
230: * reason than the infamous {@link NullPointerException}.
231: *
232: * @param caller Calling class or object used to locate {@code test-data}.
233: * @param path Path to file in {@code test-data}.
234: * @return The URL to the {@code test-data} resource.
235: * @throws FileNotFoundException if the resource is not found.
236: *
237: * @since 2.2
238: */
239: public static URL url(final Object caller, final String path)
240: throws FileNotFoundException {
241: final URL url = getResource(caller, path);
242: if (url == null) {
243: throw new FileNotFoundException(
244: "Could not locate test-data: " + path);
245: }
246: return url;
247: }
248:
249: /**
250: * Access to <code>{@linkplain #getResource getResource}(caller, path)</code> as a non-null
251: * {@link File}. You can access the {@code test-data} directory with:
252: *
253: * <blockquote><pre>
254: * TestData.file(MyClass.class, null);
255: * </pre></blockquote>
256: *
257: * @param caller Calling class or object used to locate {@code test-data}.
258: * @param path Path to file in {@code test-data}.
259: * @return The file to the {@code test-data} resource.
260: * @throws FileNotFoundException if the file is not found.
261: * @throws IOException if the resource can't be fetched for an other reason.
262: */
263: public static File file(final Object caller, final String path)
264: throws IOException {
265: final URL url = url(caller, path);
266: final File file = new File(URLDecoder.decode(url.getPath(),
267: ENCODING));
268: if (!file.exists()) {
269: throw new FileNotFoundException(
270: "Could not locate test-data: " + path);
271: }
272: return file;
273: }
274:
275: /**
276: * Creates a temporary file with the given name. The file will be created in the
277: * {@code test-data} directory and will be deleted on exit.
278: *
279: * @param caller Calling class or object used to locate {@code test-data}.
280: * @param A base name for the temporary file.
281: * @return The temporary file in the {@code test-data} directory.
282: * @throws IOException if the file can't be created.
283: */
284: public static File temp(final Object caller, final String name)
285: throws IOException {
286: final File testData = file(caller, null);
287: final int split = name.lastIndexOf('.');
288: final String prefix = (split < 0) ? name : name.substring(0,
289: split);
290: final String suffix = (split < 0) ? "tmp" : name
291: .substring(split + 1);
292: final File tmp = File.createTempFile(prefix, '.' + suffix,
293: testData);
294: deleteOnExit(tmp);
295: return tmp;
296: }
297:
298: /**
299: * Provides a non-null {@link InputStream} for named test data.
300: * It is the caller responsability to close this stream after usage.
301: *
302: * @param caller Calling class or object used to locate {@code test-data}.
303: * @param name of test data to load.
304: * @return The input stream.
305: * @throws FileNotFoundException if the resource is not found.
306: * @throws IOException if an error occurs during an input operation.
307: *
308: * @since 2.2
309: */
310: public static InputStream openStream(final Object caller,
311: final String name) throws IOException {
312: return new BufferedInputStream(url(caller, name).openStream());
313: }
314:
315: /**
316: * Provides a {@link BufferedReader} for named test data. The buffered reader is provided as
317: * an {@link LineNumberReader} instance, which is useful for displaying line numbers where
318: * error occur. It is the caller responsability to close this reader after usage.
319: *
320: * @param caller The class of the object associated with named data.
321: * @param name of test data to load.
322: * @return The buffered reader.
323: * @throws FileNotFoundException if the resource is not found.
324: * @throws IOException if an error occurs during an input operation.
325: *
326: * @since 2.2
327: */
328: public static LineNumberReader openReader(final Object caller,
329: final String name) throws IOException {
330: return new LineNumberReader(new InputStreamReader(url(caller,
331: name).openStream()));
332: }
333:
334: /**
335: * Provides a {@link java.io.BufferedReader} for named test data.
336: * It is the caller responsability to close this reader after usage.
337: *
338: * @param caller The class of the object associated with named data.
339: * @param name of test data to load.
340: * @return The reader, or {@code null} if the named test data are not found.
341: * @throws IOException if an error occurs during an input operation.
342: *
343: * @deprecated Use {@link #openReader} instead. The {@code openReader} method throws an
344: * exception if the resource is not found, instead of returning null. This make debugging
345: * easier, since it replaces infamous {@link NullPointerException} by a more explicit error
346: * message during tests. Furthermore, the {@code openReader} name make it more obvious that
347: * the stream is not closed automatically and is also consistent with other method names in
348: * this class.
349: */
350: public static BufferedReader getReader(final Object caller,
351: final String name) throws IOException {
352: final URL url = getResource(caller, name);
353: if (url == null) {
354: return null; // echo handling of getResource( ... )
355: }
356: return new BufferedReader(new InputStreamReader(url
357: .openStream()));
358: }
359:
360: /**
361: * Provides a channel for named test data. It is the caller responsability to close this
362: * chanel after usage.
363: *
364: * @param caller The class of the object associated with named data.
365: * @param name of test data to load.
366: * @return The chanel.
367: * @throws FileNotFoundException if the resource is not found.
368: * @throws IOException if an error occurs during an input operation.
369: *
370: * @since 2.2
371: */
372: public static ReadableByteChannel openChannel(final Object caller,
373: final String name) throws IOException {
374: final URL url = url(caller, name);
375: final File file = new File(URLDecoder.decode(url.getPath(),
376: ENCODING));
377: if (file.exists()) {
378: return new RandomAccessFile(file, "r").getChannel();
379: }
380: return Channels.newChannel(url.openStream());
381: }
382:
383: /**
384: * Unzip a file in the {@code test-data} directory. The zip file content is inflated in place,
385: * i.e. inflated files are written in the same {@code test-data} directory. If a file to be
386: * inflated already exists in the {@code test-data} directory, then the existing file is left
387: * untouched and the corresponding ZIP entry is silently skipped. This approach avoid the
388: * overhead of inflating the same files many time if this {@code unzipFile} method is invoked
389: * before every tests.
390: * <p>
391: * Inflated files will be automatically {@linkplain File#deleteOnExit deleted on exit}
392: * if and only if they have been modified. Callers don't need to worry about cleanup,
393: * because the files are inflated in the {@code target/.../test-data} directory, which
394: * is not versionned by SVN and is cleaned by Maven on {@code mvn clean} execution.
395: *
396: * @param caller The class of the object associated with named data.
397: * @param name The file name to unzip in place.
398: * @throws FileNotFoundException if the specified zip file is not found.
399: * @throws IOException if an error occurs during an input or output operation.
400: *
401: * @since 2.2
402: */
403: public static void unzipFile(final Object caller, final String name)
404: throws IOException {
405: final File file = file(caller, name);
406: final File parent = file.getParentFile().getAbsoluteFile();
407: final ZipFile zipFile = new ZipFile(file);
408: final Enumeration entries = zipFile.entries();
409: final byte[] buffer = new byte[4096];
410: while (entries.hasMoreElements()) {
411: final ZipEntry entry = (ZipEntry) entries.nextElement();
412: if (entry.isDirectory()) {
413: continue;
414: }
415: final File path = new File(parent, entry.getName());
416: if (path.exists()) {
417: continue;
418: }
419: final File directory = path.getParentFile();
420: if (directory != null && !directory.exists()) {
421: directory.mkdirs();
422: }
423: // Copy the file. Note: no need for a BufferedOutputStream,
424: // since we are already using a buffer of type byte[4096].
425: final InputStream in = zipFile.getInputStream(entry);
426: final OutputStream out = new FileOutputStream(path);
427: int len;
428: while ((len = in.read(buffer)) >= 0) {
429: out.write(buffer, 0, len);
430: }
431: out.close();
432: in.close();
433: // Call 'deleteOnExit' only after after we closed the file,
434: // because this method will save the modification time.
435: deleteOnExit(path, false);
436: }
437: zipFile.close();
438: }
439:
440: /**
441: * Requests that the file or directory denoted by the specified
442: * pathname be deleted when the virtual machine terminates.
443: */
444: protected static void deleteOnExit(final File file) {
445: deleteOnExit(file, true);
446: }
447:
448: /**
449: * Requests that the file or directory denoted by the specified pathname be deleted
450: * when the virtual machine terminates. This method can optionnaly delete the file
451: * only if it has been modified, thus giving a chance for test suites to copy their
452: * resources only once.
453: *
454: * @param file The file to delete.
455: * @param force If {@code true}, delete the file in all cases. If {@code false},
456: * delete the file if and only if it has been modified. The default value
457: * if {@code true}.
458: *
459: * @since 2.4
460: */
461: protected static void deleteOnExit(final File file,
462: final boolean force) {
463: if (force) {
464: file.deleteOnExit();
465: }
466: final Deletable entry = new Deletable(file, force);
467: synchronized (toDelete) {
468: if (file.isFile()) {
469: toDelete.addFirst(entry);
470: } else {
471: toDelete.addLast(entry);
472: }
473: }
474: }
475:
476: /**
477: * A file that may be deleted on JVM shutdown.
478: */
479: private static final class Deletable {
480: /**
481: * The file to delete.
482: */
483: private final File file;
484:
485: /**
486: * The initial timestamp. Used in order to determine if the file has been modified.
487: */
488: private final long timestamp;
489:
490: /**
491: * Constructs an entry for a file to be deleted.
492: */
493: public Deletable(final File file, final boolean force) {
494: this .file = file;
495: timestamp = force ? Long.MIN_VALUE : file.lastModified();
496: }
497:
498: /**
499: * Returns {@code true} if failure to delete this file can be ignored.
500: */
501: public boolean canIgnore() {
502: return timestamp != Long.MIN_VALUE && file.isDirectory();
503: }
504:
505: /**
506: * Deletes this file, if modified. Returns {@code false} only
507: * if the file should be deleted but the operation failed.
508: */
509: public boolean delete() {
510: if (!file.exists() || file.lastModified() <= timestamp) {
511: return true;
512: }
513: return file.delete();
514: }
515:
516: /**
517: * Returns the filepath.
518: */
519: public String toString() {
520: return String.valueOf(file);
521: }
522: }
523:
524: /**
525: * Deletes all temporary files. This method is invoked automatically at shutdown time and
526: * should not be invoked directly. It is public only as an implementation side effect.
527: */
528: public void run() {
529: int iteration = 5; // Maximum number of iterations
530: synchronized (toDelete) {
531: while (!toDelete.isEmpty()) {
532: if (--iteration < 0) {
533: break;
534: }
535: /*
536: * Before to try to delete the files, invokes the finalizers in a hope to close
537: * any input streams that the user didn't explicitly closed. Leaving streams open
538: * seems to occurs way too often in our test suite...
539: */
540: System.gc();
541: System.runFinalization();
542: for (final Iterator it = toDelete.iterator(); it
543: .hasNext();) {
544: final Deletable f = (Deletable) it.next();
545: try {
546: if (f.delete()) {
547: it.remove();
548: continue;
549: }
550: } catch (SecurityException e) {
551: if (iteration == 0) {
552: System.err.print(e.getClass().getName());
553: System.err.print(": ");
554: }
555: }
556: // Can't use logging, since logger are not available anymore at shutdown time.
557: if (iteration == 0 && !f.canIgnore()) {
558: System.err.print("Can't delete ");
559: System.err.println(f);
560: }
561: }
562: }
563: }
564: }
565: }
|