001    package org.junit.rules;
002    
003    import static org.junit.Assert.fail;
004    
005    import java.io.File;
006    import java.io.IOException;
007    import java.lang.reflect.Array;
008    import java.lang.reflect.InvocationTargetException;
009    import java.lang.reflect.Method;
010    
011    import 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     */
045    public 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    }