001package org.junit.rules;
002
003import static org.junit.Assert.fail;
004
005import java.io.File;
006import java.io.IOException;
007import java.lang.reflect.Array;
008import java.lang.reflect.InvocationTargetException;
009import java.lang.reflect.Method;
010
011import org.junit.Rule;
012
013/**
014 * The TemporaryFolder Rule allows creation of files and folders that should
015 * be deleted when the test method finishes (whether it passes or
016 * fails).
017 * By default no exception will be thrown in case the deletion fails.
018 *
019 * <p>Example of usage:
020 * <pre>
021 * public static class HasTempFolder {
022 *  &#064;Rule
023 *  public TemporaryFolder folder= new TemporaryFolder();
024 *
025 *  &#064;Test
026 *  public void testUsingTempFolder() throws IOException {
027 *      File createdFile= folder.newFile(&quot;myfile.txt&quot;);
028 *      File createdFolder= folder.newFolder(&quot;subfolder&quot;);
029 *      // ...
030 *     }
031 * }
032 * </pre>
033 *
034 * <p>TemporaryFolder rule supports assured deletion mode, which
035 * will fail the test in case deletion fails with {@link AssertionError}.
036 *
037 * <p>Creating TemporaryFolder with assured deletion:
038 * <pre>
039 *  &#064;Rule
040 *  public TemporaryFolder folder= TemporaryFolder.builder().assureDeletion().build();
041 * </pre>
042 *
043 * @since 4.7
044 */
045public class TemporaryFolder extends ExternalResource {
046    private final File parentFolder;
047    private final boolean assureDeletion;
048    private File folder;
049
050    private static final int TEMP_DIR_ATTEMPTS = 10000;
051    private static final String TMP_PREFIX = "junit";
052
053    /**
054     * Create a temporary folder which uses system default temporary-file 
055     * directory to create temporary resources.
056     */
057    public TemporaryFolder() {
058        this((File) null);
059    }
060
061    /**
062     * Create a temporary folder which uses the specified directory to create
063     * temporary resources.
064     *
065     * @param parentFolder folder where temporary resources will be created.
066     * If {@code null} then system default temporary-file directory is used.
067     */
068    public TemporaryFolder(File parentFolder) {
069        this.parentFolder = parentFolder;
070        this.assureDeletion = false;
071    }
072
073    /**
074     * Create a {@link TemporaryFolder} initialized with
075     * values from a builder.
076     */
077    protected TemporaryFolder(Builder builder) {
078        this.parentFolder = builder.parentFolder;
079        this.assureDeletion = builder.assureDeletion;
080    }
081
082    /**
083     * Returns a new builder for building an instance of {@link TemporaryFolder}.
084     *
085     * @since 4.13
086     */
087    public static Builder builder() {
088        return new Builder();
089    }
090
091    /**
092     * Builds an instance of {@link TemporaryFolder}.
093     * 
094     * @since 4.13
095     */
096    public static class Builder {
097        private File parentFolder;
098        private boolean assureDeletion;
099
100        protected Builder() {}
101
102        /**
103         * Specifies which folder to use for creating temporary resources.
104         * If {@code null} then system default temporary-file directory is
105         * used.
106         *
107         * @return this
108         */
109        public Builder parentFolder(File parentFolder) {
110            this.parentFolder = parentFolder;
111            return this;
112        }
113
114        /**
115         * Setting this flag assures that no resources are left undeleted. Failure
116         * to fulfill the assurance results in failure of tests with an
117         * {@link AssertionError}.
118         *
119         * @return this
120         */
121        public Builder assureDeletion() {
122            this.assureDeletion = true;
123            return this;
124        }
125
126        /**
127         * Builds a {@link TemporaryFolder} instance using the values in this builder.
128         */
129        public TemporaryFolder build() {
130            return new TemporaryFolder(this);
131        }
132    }
133
134    @Override
135    protected void before() throws Throwable {
136        create();
137    }
138
139    @Override
140    protected void after() {
141        delete();
142    }
143
144    // testing purposes only
145
146    /**
147     * for testing purposes only. Do not use.
148     */
149    public void create() throws IOException {
150        folder = createTemporaryFolderIn(parentFolder);
151    }
152
153    /**
154     * Returns a new fresh file with the given name under the temporary folder.
155     */
156    public File newFile(String fileName) throws IOException {
157        File file = new File(getRoot(), fileName);
158        if (!file.createNewFile()) {
159            throw new IOException(
160                    "a file with the name \'" + fileName + "\' already exists in the test folder");
161        }
162        return file;
163    }
164
165    /**
166     * Returns a new fresh file with a random name under the temporary folder.
167     */
168    public File newFile() throws IOException {
169        return File.createTempFile(TMP_PREFIX, null, getRoot());
170    }
171
172    /**
173     * Returns a new fresh folder with the given path under the temporary
174     * folder.
175     */
176    public File newFolder(String path) throws IOException {
177        return newFolder(new String[]{path});
178    }
179
180    /**
181     * Returns a new fresh folder with the given paths under the temporary
182     * folder. For example, if you pass in the strings {@code "parent"} and {@code "child"}
183     * then a directory named {@code "parent"} will be created under the temporary folder
184     * and a directory named {@code "child"} will be created under the newly-created
185     * {@code "parent"} directory.
186     */
187    public File newFolder(String... paths) throws IOException {
188        if (paths.length == 0) {
189            throw new IllegalArgumentException("must pass at least one path");
190        }
191
192        /*
193         * Before checking if the paths are absolute paths, check if create() was ever called,
194         * and if it wasn't, throw IllegalStateException.
195         */
196        File root = getRoot();
197        for (String path : paths) {
198            if (new File(path).isAbsolute()) {
199                throw new IOException("folder path \'" + path + "\' is not a relative path");
200            }
201        }
202
203        File relativePath = null;
204        File file = root;
205        boolean lastMkdirsCallSuccessful = true;
206        for (String path : paths) {
207            relativePath = new File(relativePath, path);
208            file = new File(root, relativePath.getPath());
209
210            lastMkdirsCallSuccessful = file.mkdirs();
211            if (!lastMkdirsCallSuccessful && !file.isDirectory()) {
212                if (file.exists()) {
213                    throw new IOException(
214                            "a file with the path \'" + relativePath.getPath() + "\' exists");
215                } else {
216                    throw new IOException(
217                            "could not create a folder with the path \'" + relativePath.getPath() + "\'");
218                }
219            }
220        }
221        if (!lastMkdirsCallSuccessful) {
222            throw new IOException(
223                    "a folder with the path \'" + relativePath.getPath() + "\' already exists");
224        }
225        return file;
226    }
227
228    /**
229     * Returns a new fresh folder with a random name under the temporary folder.
230     */
231    public File newFolder() throws IOException {
232        return createTemporaryFolderIn(getRoot());
233    }
234
235    private static File createTemporaryFolderIn(File parentFolder) throws IOException {
236        try {
237            return createTemporaryFolderWithNioApi(parentFolder);
238        } catch (ClassNotFoundException ignore) {
239            // Fallback for Java 5 and 6
240            return createTemporaryFolderWithFileApi(parentFolder);
241        } catch (InvocationTargetException e) {
242            Throwable cause = e.getCause();
243            if (cause instanceof IOException) {
244                throw (IOException) cause;
245            }
246            if (cause instanceof RuntimeException) {
247                throw (RuntimeException) cause;
248            }
249            IOException exception = new IOException("Failed to create temporary folder in " + parentFolder);
250            exception.initCause(cause);
251            throw exception;
252        } catch (Exception e) {
253            throw new RuntimeException("Failed to create temporary folder in " + parentFolder, e);
254        }
255    }
256
257    private static File createTemporaryFolderWithNioApi(File parentFolder) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException {
258        Class<?> filesClass = Class.forName("java.nio.file.Files");
259        Object fileAttributeArray = Array.newInstance(Class.forName("java.nio.file.attribute.FileAttribute"), 0);
260        Class<?> pathClass = Class.forName("java.nio.file.Path");
261        Object tempDir;
262        if (parentFolder != null) {
263            Method createTempDirectoryMethod = filesClass.getDeclaredMethod("createTempDirectory", pathClass, String.class, fileAttributeArray.getClass());
264            Object parentPath = File.class.getDeclaredMethod("toPath").invoke(parentFolder);
265            tempDir = createTempDirectoryMethod.invoke(null, parentPath, TMP_PREFIX, fileAttributeArray);
266        } else {
267            Method createTempDirectoryMethod = filesClass.getDeclaredMethod("createTempDirectory", String.class, fileAttributeArray.getClass());
268            tempDir = createTempDirectoryMethod.invoke(null, TMP_PREFIX, fileAttributeArray);
269        }
270        return (File) pathClass.getDeclaredMethod("toFile").invoke(tempDir);
271    }
272
273    private static File createTemporaryFolderWithFileApi(File parentFolder) throws IOException {
274        File createdFolder = null;
275        for (int i = 0; i < TEMP_DIR_ATTEMPTS; ++i) {
276            // Use createTempFile to get a suitable folder name.
277            String suffix = ".tmp";
278            File tmpFile = File.createTempFile(TMP_PREFIX, suffix, parentFolder);
279            String tmpName = tmpFile.toString();
280            // Discard .tmp suffix of tmpName.
281            String folderName = tmpName.substring(0, tmpName.length() - suffix.length());
282            createdFolder = new File(folderName);
283            if (createdFolder.mkdir()) {
284                tmpFile.delete();
285                return createdFolder;
286            }
287            tmpFile.delete();
288        }
289        throw new IOException("Unable to create temporary directory in: "
290            + parentFolder.toString() + ". Tried " + TEMP_DIR_ATTEMPTS + " times. "
291            + "Last attempted to create: " + createdFolder.toString());
292    }
293
294    /**
295     * @return the location of this temporary folder.
296     */
297    public File getRoot() {
298        if (folder == null) {
299            throw new IllegalStateException(
300                    "the temporary folder has not yet been created");
301        }
302        return folder;
303    }
304
305    /**
306     * Delete all files and folders under the temporary folder. Usually not
307     * called directly, since it is automatically applied by the {@link Rule}.
308     *
309     * @throws AssertionError if unable to clean up resources
310     * and deletion of resources is assured.
311     */
312    public void delete() {
313        if (!tryDelete()) {
314            if (assureDeletion) {
315                fail("Unable to clean up temporary folder " + folder);
316            }
317        }
318    }
319
320    /**
321     * Tries to delete all files and folders under the temporary folder and
322     * returns whether deletion was successful or not.
323     *
324     * @return {@code true} if all resources are deleted successfully,
325     *         {@code false} otherwise.
326     */
327    private boolean tryDelete() {
328        if (folder == null) {
329            return true;
330        }
331        
332        return recursiveDelete(folder);
333    }
334
335    private boolean recursiveDelete(File file) {
336        // Try deleting file before assuming file is a directory
337        // to prevent following symbolic links.
338        if (file.delete()) {
339            return true;
340        }
341        File[] files = file.listFiles();
342        if (files != null) {
343            for (File each : files) {
344                if (!recursiveDelete(each)) {
345                    return false;
346                }
347            }
348        }
349        return file.delete();
350    }
351}