View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *      https://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.apache.commons.io;
18  
19  import static org.junit.jupiter.api.Assertions.assertEquals;
20  import static org.junit.jupiter.api.Assertions.assertFalse;
21  import static org.junit.jupiter.api.Assertions.assertNotNull;
22  import static org.junit.jupiter.api.Assertions.assertThrows;
23  import static org.junit.jupiter.api.Assertions.assertTrue;
24  import static org.junit.jupiter.api.Assertions.fail;
25  import static org.junit.jupiter.api.Assumptions.assumeFalse;
26  
27  import java.io.File;
28  import java.io.IOException;
29  import java.nio.charset.StandardCharsets;
30  import java.nio.file.Files;
31  import java.nio.file.Path;
32  import java.util.ArrayList;
33  import java.util.Collection;
34  import java.util.Iterator;
35  import java.util.List;
36  import java.util.UUID;
37  import java.util.concurrent.CompletableFuture;
38  import java.util.concurrent.ExecutionException;
39  import java.util.stream.Collectors;
40  import java.util.stream.Stream;
41  
42  import org.apache.commons.io.file.PathUtils;
43  import org.apache.commons.io.filefilter.FileFilterUtils;
44  import org.apache.commons.io.filefilter.IOFileFilter;
45  import org.apache.commons.lang3.JavaVersion;
46  import org.apache.commons.lang3.SystemUtils;
47  import org.apache.commons.lang3.function.Consumers;
48  import org.junit.jupiter.api.BeforeEach;
49  import org.junit.jupiter.api.Test;
50  import org.junit.jupiter.api.io.TempDir;
51  
52  /**
53   * Tests {@link FileUtils#listFiles(File, IOFileFilter, IOFileFilter)} and friends.
54   */
55  class FileUtilsListFilesTest {
56  
57      @TempDir
58      public File temporaryFolder;
59  
60      @BeforeEach
61      public void setUp() throws Exception {
62          File dir = temporaryFolder;
63          File file = new File(dir, "dummy-build.xml");
64          FileUtils.touch(file);
65          file = new File(dir, "README");
66          FileUtils.touch(file);
67  
68          dir = new File(dir, "subdir1");
69          dir.mkdirs();
70          file = new File(dir, "dummy-build.xml");
71          FileUtils.touch(file);
72          file = new File(dir, "dummy-readme.txt");
73          FileUtils.touch(file);
74  
75          dir = new File(dir, "subsubdir1");
76          dir.mkdirs();
77          file = new File(dir, "dummy-file.txt");
78          FileUtils.touch(file);
79          file = new File(dir, "dummy-index.html");
80          FileUtils.touch(file);
81          file = new File(dir, "dummy-indexhtml");
82          FileUtils.touch(file);
83  
84          dir = dir.getParentFile();
85          dir = new File(dir, "CVS");
86          dir.mkdirs();
87          file = new File(dir, "Entries");
88          FileUtils.touch(file);
89          file = new File(dir, "Repository");
90          FileUtils.touch(file);
91      }
92  
93      @Test
94      void testIterateFilesByExtension() {
95          final String[] extensions = { "xml", "txt" };
96  
97          Iterator<File> files = FileUtils.iterateFiles(temporaryFolder, extensions, false);
98          try {
99              final Collection<String> fileNames = toFileNames(files);
100             assertEquals(1, fileNames.size());
101             assertTrue(fileNames.contains("dummy-build.xml"));
102             assertFalse(fileNames.contains("README"));
103             assertFalse(fileNames.contains("dummy-file.txt"));
104         } finally {
105             // Backstop in case filesToFilenames() failure.
106             files.forEachRemaining(Consumers.nop());
107         }
108 
109         try {
110             files = FileUtils.iterateFiles(temporaryFolder, extensions, true);
111             final Collection<String> fileNames = toFileNames(files);
112             assertEquals(4, fileNames.size());
113             assertTrue(fileNames.contains("dummy-file.txt"));
114             assertFalse(fileNames.contains("dummy-index.html"));
115         } finally {
116             // Backstop in case filesToFilenames() failure.
117             files.forEachRemaining(Consumers.nop());
118         }
119 
120         files = FileUtils.iterateFiles(temporaryFolder, null, false);
121         try {
122             final Collection<String> fileNames = toFileNames(files);
123             assertEquals(2, fileNames.size());
124             assertTrue(fileNames.contains("dummy-build.xml"));
125             assertTrue(fileNames.contains("README"));
126             assertFalse(fileNames.contains("dummy-file.txt"));
127         } finally {
128             // Backstop in case filesToFilenames() failure.
129             files.forEachRemaining(Consumers.nop());
130         }
131     }
132 
133     @Test
134     void testListFiles() {
135         Collection<File> files;
136         Collection<String> fileNames;
137         IOFileFilter fileFilter;
138         IOFileFilter dirFilter;
139         //
140         // First, find non-recursively
141         fileFilter = FileFilterUtils.trueFileFilter();
142         files = FileUtils.listFiles(temporaryFolder, fileFilter, null);
143         fileNames = toFileNames(files);
144         assertTrue(fileNames.contains("dummy-build.xml"), "'dummy-build.xml' is missing");
145         assertFalse(fileNames.contains("dummy-index.html"), "'dummy-index.html' shouldn't be found");
146         assertFalse(fileNames.contains("Entries"), "'Entries' shouldn't be found");
147         //
148         // Second, find recursively
149         fileFilter = FileFilterUtils.trueFileFilter();
150         dirFilter = FileFilterUtils.notFileFilter(FileFilterUtils.nameFileFilter("CVS"));
151         files = FileUtils.listFiles(temporaryFolder, fileFilter, dirFilter);
152         fileNames = toFileNames(files);
153         assertTrue(fileNames.contains("dummy-build.xml"), "'dummy-build.xml' is missing");
154         assertTrue(fileNames.contains("dummy-index.html"), "'dummy-index.html' is missing");
155         assertFalse(fileNames.contains("Entries"), "'Entries' shouldn't be found");
156         //
157         // Do the same as above but now with the filter coming from FileFilterUtils
158         fileFilter = FileFilterUtils.trueFileFilter();
159         dirFilter = FileFilterUtils.makeCVSAware(null);
160         files = FileUtils.listFiles(temporaryFolder, fileFilter, dirFilter);
161         fileNames = toFileNames(files);
162         assertTrue(fileNames.contains("dummy-build.xml"), "'dummy-build.xml' is missing");
163         assertTrue(fileNames.contains("dummy-index.html"), "'dummy-index.html' is missing");
164         assertFalse(fileNames.contains("Entries"), "'Entries' shouldn't be found");
165         //
166         // Again with the CVS filter but now with a non-null parameter
167         fileFilter = FileFilterUtils.trueFileFilter();
168         dirFilter = FileFilterUtils.prefixFileFilter("sub");
169         dirFilter = FileFilterUtils.makeCVSAware(dirFilter);
170         files = FileUtils.listFiles(temporaryFolder, fileFilter, dirFilter);
171         fileNames = toFileNames(files);
172         assertTrue(fileNames.contains("dummy-build.xml"), "'dummy-build.xml' is missing");
173         assertTrue(fileNames.contains("dummy-index.html"), "'dummy-index.html' is missing");
174         assertFalse(fileNames.contains("Entries"), "'Entries' shouldn't be found");
175         // Edge case
176         assertThrows(NullPointerException.class, () -> FileUtils.listFiles(temporaryFolder, null, null));
177     }
178 
179     @Test
180     void testListFilesByExtension() {
181         final String[] extensions = {"xml", "txt"};
182 
183         Collection<File> files = FileUtils.listFiles(temporaryFolder, extensions, false);
184         assertEquals(1, files.size());
185         Collection<String> fileNames = toFileNames(files);
186         assertTrue(fileNames.contains("dummy-build.xml"));
187         assertFalse(fileNames.contains("README"));
188         assertFalse(fileNames.contains("dummy-file.txt"));
189 
190         files = FileUtils.listFiles(temporaryFolder, extensions, true);
191         fileNames = toFileNames(files);
192         assertEquals(4, fileNames.size(), fileNames::toString);
193         assertTrue(fileNames.contains("dummy-file.txt"));
194         assertFalse(fileNames.contains("dummy-index.html"));
195 
196         files = FileUtils.listFiles(temporaryFolder, null, false);
197         assertEquals(2, files.size(), files::toString);
198         fileNames = toFileNames(files);
199         assertTrue(fileNames.contains("dummy-build.xml"));
200         assertTrue(fileNames.contains("README"));
201         assertFalse(fileNames.contains("dummy-file.txt"));
202 
203         final File directory = new File(temporaryFolder, "subdir1/subsubdir1");
204         files = FileUtils.listFiles(directory, new String[] { "html" }, false);
205         fileNames = toFileNames(files);
206         assertFalse(files.isEmpty(), directory::toString);
207         assertTrue(fileNames.contains("dummy-index.html"));
208         assertFalse(fileNames.contains("dummy-indexhtml"));
209         files = FileUtils.listFiles(temporaryFolder, new String[] { "html" }, true);
210         fileNames = toFileNames(files);
211         assertFalse(files.isEmpty(), temporaryFolder::toString);
212         assertTrue(fileNames.contains("dummy-index.html"));
213         assertFalse(fileNames.contains("dummy-indexhtml"));
214     }
215 
216     @Test
217     void testListFilesMissing() {
218         assertTrue(FileUtils.listFiles(new File(temporaryFolder, "dir/does/not/exist/at/all"), null, false).isEmpty());
219     }
220 
221     /**
222      * Tests <a href="https://issues.apache.org/jira/browse/IO-856">IO-856</a> ListFiles should not fail on vanishing files.
223      */
224     @Test
225     void testListFilesWithDeletionThreaded() throws ExecutionException, InterruptedException {
226         // test for IO-856
227         // create random directory in tmp, create the directory if it does not exist
228         final Path tempPath = PathUtils.getTempDirectory().resolve("IO-856");
229         final File tempDir = tempPath.toFile();
230         if (!tempDir.exists() && !tempDir.mkdirs()) {
231             fail("Could not create file path: " + tempDir.getAbsolutePath());
232         }
233         final int waitTime = 10_000;
234         final int maxFiles = 500;
235         final byte[] bytes = "TEST".getBytes(StandardCharsets.UTF_8);
236         final CompletableFuture<Void> c1 = CompletableFuture.runAsync(() -> {
237             final long endTime = System.currentTimeMillis() + waitTime;
238             int count = 0;
239             while (System.currentTimeMillis() < endTime && count < maxFiles) {
240                 final File file = new File(tempDir.getAbsolutePath(), UUID.randomUUID() + ".deletetester");
241                 file.deleteOnExit();
242                 try {
243                     Files.write(file.toPath(), bytes);
244                     count++;
245                 } catch (final Exception e) {
246                     fail("Could not create test file: '" + file.getAbsolutePath() + "': " + e, e);
247                 }
248                 if (!file.delete()) {
249                     fail("Could not delete test file: '" + file.getAbsolutePath() + "'");
250                 }
251             }
252             // System.out.printf("Created %,d%n", count);
253         });
254         final CompletableFuture<Void> c2 = CompletableFuture.runAsync(() -> {
255             final long endTime = System.currentTimeMillis() + waitTime;
256             int max = 0;
257             try {
258                 while (System.currentTimeMillis() < endTime) {
259                     final Collection<File> files = FileUtils.listFiles(tempDir, new String[] { ".deletetester" }, false);
260                     assertNotNull(files);
261                     max = Math.max(max, files.size());
262                 }
263             } catch (final Exception e) {
264                 System.out.printf("List size max %,d%n", max);
265                 fail("IO-856 test failure: " + e, e);
266                 // The exception can be hidden.
267                 e.printStackTrace();
268             }
269             // System.out.printf("List size max %,d%n", max);
270         });
271         // wait for the threads to finish
272         c1.get();
273         c2.get();
274     }
275 
276     @Test
277     void testStreamFilesWithDeletionCollect() throws IOException {
278         final String[] extensions = {"xml", "txt"};
279         final File xFile = new File(temporaryFolder, "x.xml");
280         if (!xFile.createNewFile()) {
281             fail("could not create test file: " + xFile);
282         }
283         final Collection<File> files = FileUtils.listFiles(temporaryFolder, extensions, true);
284         assertEquals(5, files.size());
285         final List<File> list;
286         try (Stream<File> stream = FileUtils.streamFiles(temporaryFolder, true, extensions)) {
287             assertTrue(xFile.delete());
288             // TODO? Should we create a custom stream to ignore missing files for Java 24 and up?
289             // collect() will fail on Java 24 and up here
290             // GitHub CI:
291             // Fails on Java 24 macOS, but OK on Windows and Ubuntu
292             // Fails on Java 25-EA Windows and macOS, but OK on Ubuntu
293             // forEach() will fail on Java 24 and up here
294             assumeFalse(SystemUtils.isJavaVersionAtLeast(JavaVersion.JAVA_24));
295             list = stream.collect(Collectors.toList());
296             assertFalse(list.contains(xFile), list::toString);
297         }
298         assertEquals(4, list.size());
299     }
300 
301     @Test
302     void testStreamFilesWithDeletionForEach() throws IOException {
303         final String[] extensions = {"xml", "txt"};
304         final File xFile = new File(temporaryFolder, "x.xml");
305         if (!xFile.createNewFile()) {
306             fail("could not create test file: " + xFile);
307         }
308         final Collection<File> files = FileUtils.listFiles(temporaryFolder, extensions, true);
309         assertEquals(5, files.size());
310         final List<File> list;
311         try (Stream<File> stream = FileUtils.streamFiles(temporaryFolder, true, extensions)) {
312             assertTrue(xFile.delete());
313             list = new ArrayList<>();
314             // TODO? Should we create a custom stream to ignore missing files for Java 24 and up?
315             // forEach() will fail on Java 24 and up here
316             // GitHub CI:
317             // Fails on Java 24 macOS, but OK on Windows and Ubuntu
318             // Fails on Java 25-EA Windows and macOS, but OK on Ubuntu
319             // forEach() will fail on Java 24 and up here
320             assumeFalse(SystemUtils.isJavaVersionAtLeast(JavaVersion.JAVA_24));
321             stream.forEach(list::add);
322             assertFalse(list.contains(xFile), list::toString);
323         }
324         assertEquals(4, list.size());
325     }
326 
327     @Test
328     void testStreamFilesWithDeletionIterator() throws IOException {
329         final String[] extensions = {"xml", "txt"};
330         final File xFile = new File(temporaryFolder, "x.xml");
331         if (!xFile.createNewFile()) {
332             fail("could not create test file: " + xFile);
333         }
334         final Collection<File> files = FileUtils.listFiles(temporaryFolder, extensions, true);
335         assertEquals(5, files.size());
336         final List<File> list;
337         try (Stream<File> stream = FileUtils.streamFiles(temporaryFolder, true, extensions)) {
338             assertTrue(xFile.delete());
339             list = new ArrayList<>();
340             final Iterator<File> iterator = stream.iterator();
341             // TODO? Should we create a custom stream to ignore missing files for Java 24 and up?
342             // hasNext() will fail on Java 24 and up here
343             // GitHub CI:
344             // Fails on Java 24 macOS, but OK on Windows and Ubuntu
345             // Fails on Java 25-EA Windows and macOS, but OK on Ubuntu
346             // forEach() will fail on Java 24 and up here
347             assumeFalse(SystemUtils.isJavaVersionAtLeast(JavaVersion.JAVA_24));
348             while (iterator.hasNext()) {
349                 list.add(iterator.next());
350             }
351             assertFalse(list.contains(xFile), list::toString);
352         }
353         assertEquals(4, list.size());
354     }
355 
356     private Collection<String> toFileNames(final Collection<File> files) {
357         return files.stream().map(File::getName).collect(Collectors.toList());
358     }
359 
360     /**
361      * Consumes and closes the underlying stream.
362      *
363      * @param files The iterator to consume.
364      * @return a new collection.
365      */
366     private Collection<String> toFileNames(final Iterator<File> files) {
367         final Collection<String> fileNames = new ArrayList<>();
368         // Iterator.forEachRemaining() closes the underlying stream.
369         files.forEachRemaining(f -> fileNames.add(f.getName()));
370         return fileNames;
371     }
372 
373 }