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