    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    }