diff --git a/src/share/classes/java/nio/file/FileTreeWalker.java b/src/share/classes/java/nio/file/FileTreeWalker.java index 7415f3e7047b6e5a3c5012b63f0613146020fd0a..8ce95bc82403438f62af136926f61c7b2aba9434 100644 --- a/src/share/classes/java/nio/file/FileTreeWalker.java +++ b/src/share/classes/java/nio/file/FileTreeWalker.java @@ -25,27 +25,147 @@ package java.nio.file; -import java.nio.file.attribute.*; +import java.nio.file.attribute.BasicFileAttributes; +import java.io.Closeable; import java.io.IOException; -import java.util.*; +import java.util.ArrayDeque; +import java.util.Iterator; +import java.util.Set; import sun.nio.fs.BasicFileAttributesHolder; /** - * Simple file tree walker that works in a similar manner to nftw(3C). + * Walks a file tree, generating a sequence of events corresponding to the files + * in the tree. + * + *
{@code
+ *     Path top = ...
+ *     Set options = ...
+ *     int maxDepth = ...
+ *
+ *     try (FileTreeWalker walker = new FileTreeWalker(options, maxDepth)) {
+ *         FileTreeWalker.Event ev = walker.walk(top);
+ *         do {
+ *             process(ev);
+ *             ev = walker.next();
+ *         } while (ev != null);
+ *     }
+ * }
* * @see Files#walkFileTree */ -class FileTreeWalker { +class FileTreeWalker implements Closeable { private final boolean followLinks; private final LinkOption[] linkOptions; - private final FileVisitor visitor; private final int maxDepth; + private final ArrayDeque stack = new ArrayDeque<>(); + private boolean closed; - FileTreeWalker(Set options, - FileVisitor visitor, - int maxDepth) - { + /** + * The element on the walking stack corresponding to a directory node. + */ + private static class DirectoryNode { + private final Path dir; + private final Object key; + private final DirectoryStream stream; + private final Iterator iterator; + private boolean skipped; + + DirectoryNode(Path dir, Object key, DirectoryStream stream) { + this.dir = dir; + this.key = key; + this.stream = stream; + this.iterator = stream.iterator(); + } + + Path directory() { + return dir; + } + + Object key() { + return key; + } + + DirectoryStream stream() { + return stream; + } + + Iterator iterator() { + return iterator; + } + + void skip() { + skipped = true; + } + + boolean skipped() { + return skipped; + } + } + + /** + * The event types. + */ + static enum EventType { + /** + * Start of a directory + */ + START_DIRECTORY, + /** + * End of a directory + */ + END_DIRECTORY, + /** + * An entry in a directory + */ + ENTRY; + } + + /** + * Events returned by the {@link #walk} and {@link #next} methods. + */ + static class Event { + private final EventType type; + private final Path file; + private final BasicFileAttributes attrs; + private final IOException ioe; + + private Event(EventType type, Path file, BasicFileAttributes attrs, IOException ioe) { + this.type = type; + this.file = file; + this.attrs = attrs; + this.ioe = ioe; + } + + Event(EventType type, Path file, BasicFileAttributes attrs) { + this(type, file, attrs, null); + } + + Event(EventType type, Path file, IOException ioe) { + this(type, file, null, ioe); + } + + EventType type() { + return type; + } + + Path file() { + return file; + } + + BasicFileAttributes attributes() { + return attrs; + } + + IOException ioeException() { + return ioe; + } + } + + /** + * Creates a {@code FileTreeWalker}. + */ + FileTreeWalker(Set options, int maxDepth) { boolean fl = false; for (FileVisitOption option: options) { // will throw NPE if options contains null @@ -58,191 +178,236 @@ class FileTreeWalker { this.followLinks = fl; this.linkOptions = (fl) ? new LinkOption[0] : new LinkOption[] { LinkOption.NOFOLLOW_LINKS }; - this.visitor = visitor; this.maxDepth = maxDepth; } /** - * Walk file tree starting at the given file - */ - void walk(Path start) throws IOException { - FileVisitResult result = walk(start, - 0, - new ArrayList()); - Objects.requireNonNull(result, "FileVisitor returned null"); - } - - /** - * @param file - * the directory to visit - * @param depth - * depth remaining - * @param ancestors - * use when cycle detection is enabled + * Returns the attributes of the given file, taking into account whether + * the walk is following sym links is not. The {@code canUseCached} + * argument determines whether this method can use cached attributes. */ - private FileVisitResult walk(Path file, - int depth, - List ancestors) + private BasicFileAttributes getAttributes(Path file, boolean canUseCached) throws IOException { // if attributes are cached then use them if possible - BasicFileAttributes attrs = null; - if ((depth > 0) && + if (canUseCached && (file instanceof BasicFileAttributesHolder) && (System.getSecurityManager() == null)) { BasicFileAttributes cached = ((BasicFileAttributesHolder)file).get(); - if (cached != null && (!followLinks || !cached.isSymbolicLink())) - attrs = cached; + if (cached != null && (!followLinks || !cached.isSymbolicLink())) { + return cached; + } } - IOException exc = null; // attempt to get attributes of file. If fails and we are following // links then a link target might not exist so get attributes of link - if (attrs == null) { - try { + BasicFileAttributes attrs; + try { + attrs = Files.readAttributes(file, BasicFileAttributes.class, linkOptions); + } catch (IOException ioe) { + if (!followLinks) + throw ioe; + + // attempt to get attrmptes without following links + attrs = Files.readAttributes(file, + BasicFileAttributes.class, + LinkOption.NOFOLLOW_LINKS); + } + return attrs; + } + + /** + * Returns true if walking into the given directory would result in a + * file system loop/cycle. + */ + private boolean wouldLoop(Path dir, Object key) { + // if this directory and ancestor has a file key then we compare + // them; otherwise we use less efficient isSameFile test. + for (DirectoryNode ancestor: stack) { + Object ancestorKey = ancestor.key(); + if (key != null && ancestorKey != null) { + if (key.equals(ancestorKey)) { + // cycle detected + return true; + } + } else { try { - attrs = Files.readAttributes(file, BasicFileAttributes.class, linkOptions); - } catch (IOException x1) { - if (followLinks) { - try { - attrs = Files.readAttributes(file, - BasicFileAttributes.class, - LinkOption.NOFOLLOW_LINKS); - } catch (IOException x2) { - exc = x2; - } - } else { - exc = x1; + if (Files.isSameFile(dir, ancestor.directory())) { + // cycle detected + return true; } + } catch (IOException | SecurityException x) { + // ignore } - } catch (SecurityException x) { - // If access to starting file is denied then SecurityException - // is thrown, otherwise the file is ignored. - if (depth == 0) - throw x; - return FileVisitResult.CONTINUE; } } + return false; + } - // unable to get attributes of file - if (exc != null) { - return visitor.visitFileFailed(file, exc); + /** + * Visits the given file, returning the {@code Event} corresponding to that + * visit. + * + * The {@code ignoreSecurityException} parameter determines whether + * any SecurityException should be ignored or not. If a SecurityException + * is thrown, and is ignored, then this method returns {@code null} to + * mean that there is no event corresponding to a visit to the file. + * + * The {@code canUseCached} parameter determines whether cached attributes + * for the file can be used or not. + */ + private Event visit(Path entry, boolean ignoreSecurityException, boolean canUseCached) { + // need the file attributes + BasicFileAttributes attrs; + try { + attrs = getAttributes(entry, canUseCached); + } catch (IOException ioe) { + return new Event(EventType.ENTRY, entry, ioe); + } catch (SecurityException se) { + if (ignoreSecurityException) + return null; + throw se; } // at maximum depth or file is not a directory + int depth = stack.size(); if (depth >= maxDepth || !attrs.isDirectory()) { - return visitor.visitFile(file, attrs); + return new Event(EventType.ENTRY, entry, attrs); } // check for cycles when following links - if (followLinks) { - Object key = attrs.fileKey(); - - // if this directory and ancestor has a file key then we compare - // them; otherwise we use less efficient isSameFile test. - for (AncestorDirectory ancestor: ancestors) { - Object ancestorKey = ancestor.fileKey(); - if (key != null && ancestorKey != null) { - if (key.equals(ancestorKey)) { - // cycle detected - return visitor.visitFileFailed(file, - new FileSystemLoopException(file.toString())); - } - } else { - boolean isSameFile = false; - try { - isSameFile = Files.isSameFile(file, ancestor.file()); - } catch (IOException x) { - // ignore - } catch (SecurityException x) { - // ignore - } - if (isSameFile) { - // cycle detected - return visitor.visitFileFailed(file, - new FileSystemLoopException(file.toString())); - } - } - } - - ancestors.add(new AncestorDirectory(file, key)); + if (followLinks && wouldLoop(entry, attrs.fileKey())) { + return new Event(EventType.ENTRY, entry, + new FileSystemLoopException(entry.toString())); } - // visit directory + // file is a directory, attempt to open it + DirectoryStream stream = null; try { - DirectoryStream stream = null; - FileVisitResult result; + stream = Files.newDirectoryStream(entry); + } catch (IOException ioe) { + return new Event(EventType.ENTRY, entry, ioe); + } catch (SecurityException se) { + if (ignoreSecurityException) + return null; + throw se; + } - // open the directory - try { - stream = Files.newDirectoryStream(file); - } catch (IOException x) { - return visitor.visitFileFailed(file, x); - } catch (SecurityException x) { - // ignore, as per spec - return FileVisitResult.CONTINUE; - } + // push a directory node to the stack and return an event + stack.push(new DirectoryNode(entry, attrs.fileKey(), stream)); + return new Event(EventType.START_DIRECTORY, entry, attrs); + } - // the exception notified to the postVisitDirectory method - IOException ioe = null; - // invoke preVisitDirectory and then visit each entry - try { - result = visitor.preVisitDirectory(file, attrs); - if (result != FileVisitResult.CONTINUE) { - return result; - } + /** + * Start walking from the given file. + */ + Event walk(Path file) { + if (closed) + throw new IllegalStateException("Closed"); - try { - for (Path entry: stream) { - result = walk(entry, depth+1, ancestors); + Event ev = visit(file, + false, // ignoreSecurityException + false); // canUseCached + assert ev != null; + return ev; + } - // returning null will cause NPE to be thrown - if (result == null || result == FileVisitResult.TERMINATE) - return result; + /** + * Returns the next Event or {@code null} if there are no more events or + * the walker is closed. + */ + Event next() { + DirectoryNode top = stack.peek(); + if (top == null) + return null; // stack is empty, we are done + + // continue iteration of the directory at the top of the stack + Event ev; + do { + Path entry = null; + IOException ioe = null; - // skip remaining siblings in this directory - if (result == FileVisitResult.SKIP_SIBLINGS) - break; + // get next entry in the directory + if (!top.skipped()) { + Iterator iterator = top.iterator(); + try { + if (iterator.hasNext()) { + entry = iterator.next(); } - } catch (DirectoryIteratorException e) { - // IOException will be notified to postVisitDirectory - ioe = e.getCause(); + } catch (DirectoryIteratorException x) { + ioe = x.getCause(); } - } finally { + } + + // no next entry so close and pop directory, creating corresponding event + if (entry == null) { try { - stream.close(); + top.stream().close(); } catch (IOException e) { - // IOException will be notified to postVisitDirectory - if (ioe == null) + if (ioe != null) { ioe = e; + } else { + ioe.addSuppressed(e); + } } + stack.pop(); + return new Event(EventType.END_DIRECTORY, top.directory(), ioe); } - // invoke postVisitDirectory last - return visitor.postVisitDirectory(file, ioe); + // visit the entry + ev = visit(entry, + true, // ignoreSecurityException + true); // canUseCached - } finally { - // remove key from trail if doing cycle detection - if (followLinks) { - ancestors.remove(ancestors.size()-1); - } - } + } while (ev == null); + + return ev; } - private static class AncestorDirectory { - private final Path dir; - private final Object key; - AncestorDirectory(Path dir, Object key) { - this.dir = dir; - this.key = key; + /** + * Pops the directory node that is the current top of the stack so that + * there are no more events for the directory (including no END_DIRECTORY) + * event. This method is a no-op if the stack is empty or the walker is + * closed. + */ + void pop() { + if (!stack.isEmpty()) { + DirectoryNode node = stack.pop(); + try { + node.stream().close(); + } catch (IOException ignore) { } } - Path file() { - return dir; + } + + /** + * Skips the remaining entries in the directory at the top of the stack. + * This method is a no-op if the stack is empty or the walker is closed. + */ + void skipRemainingSiblings() { + if (!stack.isEmpty()) { + stack.peek().skip(); } - Object fileKey() { - return key; + } + + /** + * Returns {@code true} if the walker is open. + */ + boolean isOpen() { + return !closed; + } + + /** + * Closes/pops all directories on the stack. + */ + @Override + public void close() { + if (!closed) { + while (!stack.isEmpty()) { + pop(); + } + closed = true; } } } diff --git a/src/share/classes/java/nio/file/Files.java b/src/share/classes/java/nio/file/Files.java index 2db7ba25c4a9ec09d248611ab25a9a12261d0a27..a935a58001e5902aa1f6d7ab2a041cea2795f925 100644 --- a/src/share/classes/java/nio/file/Files.java +++ b/src/share/classes/java/nio/file/Files.java @@ -2589,7 +2589,60 @@ public final class Files { { if (maxDepth < 0) throw new IllegalArgumentException("'maxDepth' is negative"); - new FileTreeWalker(options, visitor, maxDepth).walk(start); + + /** + * Create a FileTreeWalker to walk the file tree, invoking the visitor + * for each event. + */ + try (FileTreeWalker walker = new FileTreeWalker(options, maxDepth)) { + FileTreeWalker.Event ev = walker.walk(start); + do { + FileVisitResult result; + switch (ev.type()) { + case ENTRY : + IOException ioe = ev.ioeException(); + if (ioe == null) { + assert ev.attributes() != null; + result = visitor.visitFile(ev.file(), ev.attributes()); + } else { + result = visitor.visitFileFailed(ev.file(), ioe); + } + break; + + case START_DIRECTORY : + result = visitor.preVisitDirectory(ev.file(), ev.attributes()); + + // if SKIP_SIBLINGS and SKIP_SUBTREE is returned then + // there shouldn't be any more events for the current + // directory. + if (result == FileVisitResult.SKIP_SUBTREE || + result == FileVisitResult.SKIP_SIBLINGS) + walker.pop(); + break; + + case END_DIRECTORY : + result = visitor.postVisitDirectory(ev.file(), ev.ioeException()); + + // SKIP_SIBLINGS is a no-op for postVisitDirectory + if (result == FileVisitResult.SKIP_SIBLINGS) + result = FileVisitResult.CONTINUE; + break; + + default : + throw new AssertionError("Should not get here"); + } + + if (Objects.requireNonNull(result) != FileVisitResult.CONTINUE) { + if (result == FileVisitResult.TERMINATE) { + break; + } else if (result == FileVisitResult.SKIP_SIBLINGS) { + walker.skipRemainingSiblings(); + } + } + ev = walker.next(); + } while (ev != null); + } + return start; } diff --git a/test/java/nio/file/Files/walkFileTree/CreateFileTree.java b/test/java/nio/file/Files/walkFileTree/CreateFileTree.java index 9911bfa405aa52c71c09626d3170177a32397340..689c6ee7703eb7186f6b9a4aa2115481874ab488 100644 --- a/test/java/nio/file/Files/walkFileTree/CreateFileTree.java +++ b/test/java/nio/file/Files/walkFileTree/CreateFileTree.java @@ -32,9 +32,23 @@ import java.util.*; public class CreateFileTree { - static final Random rand = new Random(); + private static final Random rand = new Random(); - public static void main(String[] args) throws IOException { + private static boolean supportsLinks(Path dir) { + Path link = dir.resolve("testlink"); + Path target = dir.resolve("testtarget"); + try { + Files.createSymbolicLink(link, target); + Files.delete(link); + return true; + } catch (UnsupportedOperationException x) { + return false; + } catch (IOException x) { + return false; + } + } + + static Path create() throws IOException { Path top = Files.createTempDirectory("tree"); List dirs = new ArrayList(); @@ -53,7 +67,6 @@ public class CreateFileTree { dirs.add(subdir); } } - assert dirs.size() >= 2; // create a few regular files in the file tree int files = dirs.size() * 3; @@ -64,20 +77,26 @@ public class CreateFileTree { } // create a few sym links in the file tree so as to create cycles - int links = 1 + rand.nextInt(5); - for (int i=0; i opts = Collections.emptySet(); diff --git a/test/java/nio/file/Files/walkFileTree/SkipSiblings.java b/test/java/nio/file/Files/walkFileTree/SkipSiblings.java index cca004427c44429e1bd05f1e118fb6293a8d41e9..5224ee41601e9727951c6c9eef9af03c877013fe 100644 --- a/test/java/nio/file/Files/walkFileTree/SkipSiblings.java +++ b/test/java/nio/file/Files/walkFileTree/SkipSiblings.java @@ -21,15 +21,18 @@ * questions. */ +/* + * @test + * @summary Unit test for Files.walkFileTree to test SKIP_SIBLINGS return value + * @compile SkipSiblings.java CreateFileTree.java + * @run main SkipSiblings + */ + import java.nio.file.*; import java.nio.file.attribute.*; import java.io.IOException; import java.util.*; -/** - * Unit test for Files.walkFileTree to test SKIP_SIBLINGS return value. - */ - public class SkipSiblings { static final Random rand = new Random(); @@ -52,7 +55,7 @@ public class SkipSiblings { } public static void main(String[] args) throws Exception { - Path dir = Paths.get(args[0]); + Path dir = CreateFileTree.create(); Files.walkFileTree(dir, new SimpleFileVisitor() { @Override @@ -74,7 +77,11 @@ public class SkipSiblings { if (x != null) throw new RuntimeException(x); check(dir); - return FileVisitResult.CONTINUE; + if (rand.nextBoolean()) { + return FileVisitResult.CONTINUE; + } else { + return FileVisitResult.SKIP_SIBLINGS; + } } }); } diff --git a/test/java/nio/file/Files/walkFileTree/SkipSubtree.java b/test/java/nio/file/Files/walkFileTree/SkipSubtree.java new file mode 100644 index 0000000000000000000000000000000000000000..6332e0741bd17c5fa8bc41b85c4b420134e04a0d --- /dev/null +++ b/test/java/nio/file/Files/walkFileTree/SkipSubtree.java @@ -0,0 +1,85 @@ +/* + * Copyright (c) 2013, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +/* + * @test + * @summary Unit test for Files.walkFileTree to test SKIP_SUBTREE return value + * @compile SkipSubtree.java CreateFileTree.java + * @run main SkipSubtree + */ +import java.nio.file.*; +import java.nio.file.attribute.BasicFileAttributes; +import java.io.IOException; +import java.util.HashSet; +import java.util.Random; +import java.util.Set; + +public class SkipSubtree { + + static final Random rand = new Random(); + static final Set skipped = new HashSet<>(); + + // check if this path should have been skipped + static void check(Path path) { + do { + if (skipped.contains(path)) + throw new RuntimeException(path + " should not have been visited"); + path = path.getParent(); + } while (path != null); + } + + // indicates if the subtree should be skipped + static boolean skip(Path path) { + if (rand.nextInt(3) == 0) { + skipped.add(path); + return true; + } + return false; + } + + public static void main(String[] args) throws Exception { + Path dir = CreateFileTree.create(); + + Files.walkFileTree(dir, new SimpleFileVisitor() { + @Override + public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) { + check(dir); + if (skip(dir)) + return FileVisitResult.SKIP_SUBTREE; + return FileVisitResult.CONTINUE; + } + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) { + check(file); + return FileVisitResult.CONTINUE; + } + @Override + public FileVisitResult postVisitDirectory(Path dir, IOException x) { + if (x != null) + throw new RuntimeException(x); + check(dir); + return FileVisitResult.CONTINUE; + } + }); + } +} diff --git a/test/java/nio/file/Files/walkFileTree/TerminateWalk.java b/test/java/nio/file/Files/walkFileTree/TerminateWalk.java index 134d68b749d2da25717d628c25fcf45bd1bee250..65409877a315683f4484494c273c704377d9129a 100644 --- a/test/java/nio/file/Files/walkFileTree/TerminateWalk.java +++ b/test/java/nio/file/Files/walkFileTree/TerminateWalk.java @@ -21,15 +21,18 @@ * questions. */ +/* + * @test + * @summary Unit test for Files.walkFileTree to test TERMINATE return value + * @compile TerminateWalk.java CreateFileTree.java + * @run main TerminateWalk + */ + import java.nio.file.*; import java.nio.file.attribute.*; import java.io.IOException; import java.util.*; -/** - * Unit test for Files.walkFileTree to test TERMINATE return value - */ - public class TerminateWalk { static final Random rand = new Random(); @@ -47,7 +50,7 @@ public class TerminateWalk { } public static void main(String[] args) throws Exception { - Path dir = Paths.get(args[0]); + Path dir = CreateFileTree.create(); Files.walkFileTree(dir, new SimpleFileVisitor() { @Override diff --git a/test/java/nio/file/Files/walkFileTree/walk_file_tree.sh b/test/java/nio/file/Files/walkFileTree/find.sh similarity index 83% rename from test/java/nio/file/Files/walkFileTree/walk_file_tree.sh rename to test/java/nio/file/Files/walkFileTree/find.sh index 6c9d7dc89d3dfe754b39d93d2d6a7fb304c8011c..9a99147fda079b946eb6e942916ee3d0d56adea2 100644 --- a/test/java/nio/file/Files/walkFileTree/walk_file_tree.sh +++ b/test/java/nio/file/Files/walkFileTree/find.sh @@ -23,9 +23,9 @@ # @test # @bug 4313887 6907737 -# @summary Unit test for walkFileTree method -# @build CreateFileTree PrintFileTree SkipSiblings TerminateWalk MaxDepth -# @run shell walk_file_tree.sh +# @summary Tests that walkFileTree is consistent with the native find program +# @build CreateFileTree PrintFileTree +# @run shell find.sh # if TESTJAVA isn't set then we assume an interactive run. @@ -76,18 +76,6 @@ if [ $? != 0 ]; if [ $? != 0 ]; then failures=`expr $failures + 1`; fi fi -# test SKIP_SIBLINGS -$JAVA ${TESTVMOPTS} SkipSiblings "$ROOT" -if [ $? != 0 ]; then failures=`expr $failures + 1`; fi - -# test TERMINATE -$JAVA ${TESTVMOPTS} TerminateWalk "$ROOT" -if [ $? != 0 ]; then failures=`expr $failures + 1`; fi - -# test maxDepth -$JAVA ${TESTVMOPTS} MaxDepth "$ROOT" -if [ $? != 0 ]; then failures=`expr $failures + 1`; fi - # clean-up rm -r "$ROOT"