1 /*
   2  * Copyright (c) 2007, 2012, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.  Oracle designates this
   8  * particular file as subject to the "Classpath" exception as provided
   9  * by Oracle in the LICENSE file that accompanied this code.
  10  *
  11  * This code is distributed in the hope that it will be useful, but WITHOUT
  12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  14  * version 2 for more details (a copy is included in the LICENSE file that
  15  * accompanied this code).
  16  *
  17  * You should have received a copy of the GNU General Public License version
  18  * 2 along with this work; if not, write to the Free Software Foundation,
  19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  20  *
  21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  22  * or visit www.oracle.com if you need additional information or have any
  23  * questions.
  24  */
  25 
  26 package java.nio.file;
  27 
  28 import java.nio.file.attribute.BasicFileAttributes;
  29 import java.io.Closeable;
  30 import java.io.IOException;
  31 import java.util.ArrayDeque;
  32 import java.util.Iterator;
  33 import java.util.Set;
  34 import sun.nio.fs.BasicFileAttributesHolder;
  35 
  36 /**
  37  * Walks a file tree, generating a sequence of events corresponding to the files
  38  * in the tree.
  39  *
  40  * <pre>{@code
  41  *     Path top = ...
  42  *     Set<FileVisitOption> options = ...
  43  *     int maxDepth = ...
  44  *
  45  *     try (FileTreeWalker walker = new FileTreeWalker(options, maxDepth)) {
  46  *         FileTreeWalker.Event ev = walker.walk(top);
  47  *         do {
  48  *             process(ev);
  49  *             ev = walker.next();
  50  *         } while (ev != null);
  51  *     }
  52  * }</pre>
  53  *
  54  * @see Files#walkFileTree
  55  */
  56 
  57 class FileTreeWalker implements Closeable {
  58     private final boolean followLinks;
  59     private final LinkOption[] linkOptions;
  60     private final int maxDepth;
  61     private final ArrayDeque<DirectoryNode> stack = new ArrayDeque<>();
  62     private boolean closed;
  63 
  64     /**
  65      * The element on the walking stack corresponding to a directory node.
  66      */
  67     private static class DirectoryNode {
  68         private final Path dir;
  69         private final Object key;
  70         private final DirectoryStream<Path> stream;
  71         private final Iterator<Path> iterator;
  72         private boolean skipped;
  73 
  74         DirectoryNode(Path dir, Object key, DirectoryStream<Path> stream) {
  75             this.dir = dir;
  76             this.key = key;
  77             this.stream = stream;
  78             this.iterator = stream.iterator();
  79         }
  80 
  81         Path directory() {
  82             return dir;
  83         }
  84 
  85         Object key() {
  86             return key;
  87         }
  88 
  89         DirectoryStream<Path> stream() {
  90             return stream;
  91         }
  92 
  93         Iterator<Path> iterator() {
  94             return iterator;
  95         }
  96 
  97         void skip() {
  98             skipped = true;
  99         }
 100 
 101         boolean skipped() {
 102             return skipped;
 103         }
 104     }
 105 
 106     /**
 107      * The event types.
 108      */
 109     static enum EventType {
 110         /**
 111          * Start of a directory
 112          */
 113         START_DIRECTORY,
 114         /**
 115          * End of a directory
 116          */
 117         END_DIRECTORY,
 118         /**
 119          * An entry in a directory
 120          */
 121         ENTRY;
 122     }
 123 
 124     /**
 125      * Events returned by the {@link #walk} and {@link #next} methods.
 126      */
 127     static class Event {
 128         private final EventType type;
 129         private final Path file;
 130         private final BasicFileAttributes attrs;
 131         private final IOException ioe;
 132 
 133         private Event(EventType type, Path file, BasicFileAttributes attrs, IOException ioe) {
 134             this.type = type;
 135             this.file = file;
 136             this.attrs = attrs;
 137             this.ioe = ioe;
 138         }
 139 
 140         Event(EventType type, Path file, BasicFileAttributes attrs) {
 141             this(type, file, attrs, null);
 142         }
 143 
 144         Event(EventType type, Path file, IOException ioe) {
 145             this(type, file, null, ioe);
 146         }
 147 
 148         EventType type() {
 149             return type;
 150         }
 151 
 152         Path file() {
 153             return file;
 154         }
 155 
 156         BasicFileAttributes attributes() {
 157             return attrs;
 158         }
 159 
 160         IOException ioeException() {
 161             return ioe;
 162         }
 163     }
 164 
 165     /**
 166      * Creates a {@code FileTreeWalker}.
 167      */
 168     FileTreeWalker(Set<FileVisitOption> options, int maxDepth) {
 169         boolean fl = false;
 170         for (FileVisitOption option: options) {
 171             // will throw NPE if options contains null
 172             switch (option) {
 173                 case FOLLOW_LINKS : fl = true; break;
 174                 default:
 175                     throw new AssertionError("Should not get here");
 176             }
 177         }
 178         this.followLinks = fl;
 179         this.linkOptions = (fl) ? new LinkOption[0] :
 180             new LinkOption[] { LinkOption.NOFOLLOW_LINKS };
 181         this.maxDepth = maxDepth;
 182     }
 183 
 184     /**
 185      * Returns the attributes of the given file, taking into account whether
 186      * the walk is following sym links is not. The {@code canUseCached}
 187      * argument determines whether this method can use cached attributes.
 188      */
 189     private BasicFileAttributes getAttributes(Path file, boolean canUseCached)
 190         throws IOException
 191     {
 192         // if attributes are cached then use them if possible
 193         if (canUseCached &&
 194             (file instanceof BasicFileAttributesHolder) &&
 195             (System.getSecurityManager() == null))
 196         {
 197             BasicFileAttributes cached = ((BasicFileAttributesHolder)file).get();
 198             if (cached != null && (!followLinks || !cached.isSymbolicLink())) {
 199                 return cached;
 200             }
 201         }
 202 
 203         // attempt to get attributes of file. If fails and we are following
 204         // links then a link target might not exist so get attributes of link
 205         BasicFileAttributes attrs;
 206         try {
 207             attrs = Files.readAttributes(file, BasicFileAttributes.class, linkOptions);
 208         } catch (IOException ioe) {
 209             if (!followLinks)
 210                 throw ioe;
 211 
 212             // attempt to get attrmptes without following links
 213             attrs = Files.readAttributes(file,
 214                                          BasicFileAttributes.class,
 215                                          LinkOption.NOFOLLOW_LINKS);
 216         }
 217         return attrs;
 218     }
 219 
 220     /**
 221      * Returns true if walking into the given directory would result in a
 222      * file system loop/cycle.
 223      */
 224     private boolean wouldLoop(Path dir, Object key) {
 225         // if this directory and ancestor has a file key then we compare
 226         // them; otherwise we use less efficient isSameFile test.
 227         for (DirectoryNode ancestor: stack) {
 228             Object ancestorKey = ancestor.key();
 229             if (key != null && ancestorKey != null) {
 230                 if (key.equals(ancestorKey)) {
 231                     // cycle detected
 232                     return true;
 233                 }
 234             } else {
 235                 try {
 236                     if (Files.isSameFile(dir, ancestor.directory())) {
 237                         // cycle detected
 238                         return true;
 239                     }
 240                 } catch (IOException | SecurityException x) {
 241                     // ignore
 242                 }
 243             }
 244         }
 245         return false;
 246     }
 247 
 248     /**
 249      * Visits the given file, returning the {@code Event} corresponding to that
 250      * visit.
 251      *
 252      * The {@code ignoreSecurityException} parameter determines whether
 253      * any SecurityException should be ignored or not. If a SecurityException
 254      * is thrown, and is ignored, then this method returns {@code null} to
 255      * mean that there is no event corresponding to a visit to the file.
 256      *
 257      * The {@code canUseCached} parameter determines whether cached attributes
 258      * for the file can be used or not.
 259      */
 260     private Event visit(Path entry, boolean ignoreSecurityException, boolean canUseCached) {
 261         // need the file attributes
 262         BasicFileAttributes attrs;
 263         try {
 264             attrs = getAttributes(entry, canUseCached);
 265         } catch (IOException ioe) {
 266             return new Event(EventType.ENTRY, entry, ioe);
 267         } catch (SecurityException se) {
 268             if (ignoreSecurityException)
 269                 return null;
 270             throw se;
 271         }
 272 
 273         // at maximum depth or file is not a directory
 274         int depth = stack.size();
 275         if (depth >= maxDepth || !attrs.isDirectory()) {
 276             return new Event(EventType.ENTRY, entry, attrs);
 277         }
 278 
 279         // check for cycles when following links
 280         if (followLinks && wouldLoop(entry, attrs.fileKey())) {
 281             return new Event(EventType.ENTRY, entry,
 282                              new FileSystemLoopException(entry.toString()));
 283         }
 284 
 285         // file is a directory, attempt to open it
 286         DirectoryStream<Path> stream = null;
 287         try {
 288             stream = Files.newDirectoryStream(entry);
 289         } catch (IOException ioe) {
 290             return new Event(EventType.ENTRY, entry, ioe);
 291         } catch (SecurityException se) {
 292             if (ignoreSecurityException)
 293                 return null;
 294             throw se;
 295         }
 296 
 297         // push a directory node to the stack and return an event
 298         stack.push(new DirectoryNode(entry, attrs.fileKey(), stream));
 299         return new Event(EventType.START_DIRECTORY, entry, attrs);
 300     }
 301 
 302 
 303     /**
 304      * Start walking from the given file.
 305      */
 306     Event walk(Path file) {
 307         if (closed)
 308             throw new IllegalStateException("Closed");
 309 
 310         Event ev = visit(file,
 311                          false,   // ignoreSecurityException
 312                          false);  // canUseCached
 313         assert ev != null;
 314         return ev;
 315     }
 316 
 317     /**
 318      * Returns the next Event or {@code null} if there are no more events or
 319      * the walker is closed.
 320      */
 321     Event next() {
 322         DirectoryNode top = stack.peek();
 323         if (top == null)
 324             return null;      // stack is empty, we are done
 325 
 326         // continue iteration of the directory at the top of the stack
 327         Event ev;
 328         do {
 329             Path entry = null;
 330             IOException ioe = null;
 331 
 332             // get next entry in the directory
 333             if (!top.skipped()) {
 334                 Iterator<Path> iterator = top.iterator();
 335                 try {
 336                     if (iterator.hasNext()) {
 337                         entry = iterator.next();
 338                     }
 339                 } catch (DirectoryIteratorException x) {
 340                     ioe = x.getCause();
 341                 }
 342             }
 343 
 344             // no next entry so close and pop directory, creating corresponding event
 345             if (entry == null) {
 346                 try {
 347                     top.stream().close();
 348                 } catch (IOException e) {
 349                     if (ioe != null) {
 350                         ioe = e;
 351                     } else {
 352                         ioe.addSuppressed(e);
 353                     }
 354                 }
 355                 stack.pop();
 356                 return new Event(EventType.END_DIRECTORY, top.directory(), ioe);
 357             }
 358 
 359             // visit the entry
 360             ev = visit(entry,
 361                        true,   // ignoreSecurityException
 362                        true);  // canUseCached
 363 
 364         } while (ev == null);
 365 
 366         return ev;
 367     }
 368 
 369     /**
 370      * Pops the directory node that is the current top of the stack so that
 371      * there are no more events for the directory (including no END_DIRECTORY)
 372      * event. This method is a no-op if the stack is empty or the walker is
 373      * closed.
 374      */
 375     void pop() {
 376         if (!stack.isEmpty()) {
 377             DirectoryNode node = stack.pop();
 378             try {
 379                 node.stream().close();
 380             } catch (IOException ignore) { }
 381         }
 382     }
 383 
 384     /**
 385      * Skips the remaining entries in the directory at the top of the stack.
 386      * This method is a no-op if the stack is empty or the walker is closed.
 387      */
 388     void skipRemainingSiblings() {
 389         if (!stack.isEmpty()) {
 390             stack.peek().skip();
 391         }
 392     }
 393 
 394     /**
 395      * Returns {@code true} if the walker is open.
 396      */
 397     boolean isOpen() {
 398         return !closed;
 399     }
 400 
 401     /**
 402      * Closes/pops all directories on the stack.
 403      */
 404     @Override
 405     public void close() {
 406         if (!closed) {
 407             while (!stack.isEmpty()) {
 408                 pop();
 409             }
 410             closed = true;
 411         }
 412     }
 413 }