1 /*
   2  * Copyright (c) 2013, 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.io.Closeable;
  29 import java.io.IOException;
  30 import java.io.UncheckedIOException;
  31 import java.util.ArrayDeque;
  32 import java.util.Iterator;
  33 import java.util.NoSuchElementException;
  34 import java.util.Objects;
  35 import java.nio.file.attribute.BasicFileAttributes;
  36 
  37 class FileTreeIterator implements Iterator<FileTreeIterator.Entry>, Closeable {
  38 
  39     /**
  40      * A pair of {@code Path} and its {@code BasicFileAttributes}.
  41      */
  42     static class Entry {
  43         private final Path file;
  44         private final BasicFileAttributes attrs;
  45         // Latched exception thrown when tried to read the BasicFileAttributes
  46         private RuntimeException latched_ex;
  47 
  48         Entry(Path file, BasicFileAttributes attrs) {
  49             this.file = Objects.requireNonNull(file);
  50             this.attrs = attrs;
  51             latched_ex = null;
  52         }
  53 
  54         Entry(Path file, RuntimeException ex) {
  55             this.file = Objects.requireNonNull(file);
  56             this.latched_ex = Objects.requireNonNull(ex);
  57             attrs = null;
  58         }
  59 
  60         static Entry make(Path file, boolean followLinks) {
  61             Objects.requireNonNull(file);
  62             BasicFileAttributes attrs;
  63             try {
  64                 if (followLinks) {
  65                     try {
  66                         attrs = Files.readAttributes(file, BasicFileAttributes.class);
  67                         return new Entry(file, attrs);
  68                     } catch (IOException notcare) {
  69                         // ignore, try not to follow link
  70                     }
  71                 }
  72                 attrs = Files.readAttributes(file, BasicFileAttributes.class,
  73                                              LinkOption.NOFOLLOW_LINKS);
  74                 return new Entry(file, attrs);
  75             } catch (IOException ioe) {
  76                 return new Entry(file, new UncheckedIOException(ioe));
  77             } catch (RuntimeException ex) {
  78                 return new Entry(file, ex);
  79             }
  80         }
  81 
  82         public Entry ignoreException() {
  83             latched_ex = null;
  84             return this;
  85         }
  86 
  87         public Path getPath() {
  88             return file;
  89         }
  90 
  91         /**
  92          * Could return null if ignoreException
  93          */
  94         public BasicFileAttributes getFileAttributes() {
  95             if (latched_ex != null) {
  96                 throw latched_ex;
  97             }
  98             return attrs;
  99         }
 100 
 101         public void checkException() throws IOException {
 102             if (latched_ex != null) {
 103                 if (latched_ex instanceof UncheckedIOException) {
 104                     throw ((UncheckedIOException) latched_ex).getCause();
 105                 } else {
 106                     throw latched_ex;
 107                 }
 108             }
 109         }
 110     }
 111 
 112     private static class Context {
 113         final Path file;
 114         final BasicFileAttributes attrs;
 115         final DirectoryStream<Path> ds;
 116         final Iterator<Path> itor;
 117 
 118         Context(Path file, BasicFileAttributes attrs, DirectoryStream<Path> ds, Iterator<Path> itor) {
 119             this.file = file;
 120             this.attrs = attrs;
 121             this.ds = ds;
 122             this.itor = itor;
 123         }
 124     }
 125 
 126     private static class VisitorException extends RuntimeException {
 127         VisitorException(IOException ioe) {
 128             super(ioe);
 129         }
 130 
 131         @Override
 132         public IOException getCause() {
 133             return (IOException) super.getCause();
 134         }
 135     }
 136 
 137     private final boolean followLinks;
 138     private final int maxDepth;
 139     private final ArrayDeque<Context> stack = new ArrayDeque<>();
 140 
 141     private FileVisitor<Path> visitorProxy;
 142     private Entry next;
 143 
 144     private FileTreeIterator(int maxDepth,
 145                              FileVisitOption... options) {
 146         this.maxDepth = maxDepth;
 147 
 148         boolean follow = false;
 149         for (FileVisitOption opt : options) {
 150             switch(opt) {
 151                 case FOLLOW_LINKS:
 152                     follow = true;
 153                     break;
 154                 default:
 155                     // nothing should be here
 156                     break;
 157             }
 158         }
 159         this.followLinks = follow;
 160     }
 161 
 162     private FileTreeIterator init(Path start, FileVisitor<? super Path> visitor) throws IOException {
 163         next = Entry.make(start, followLinks);
 164         try {
 165             next.checkException();
 166         } catch (SecurityException se) {
 167             // First level, re-throw it.
 168             throw se;
 169         } catch (IOException ioe) {
 170             if (visitor != null) {
 171                 visitor.visitFileFailed(start, ioe);
 172             } else {
 173                 throw ioe;
 174             }
 175         }
 176 
 177         // Wrap IOException in VisitorException so we can throw from hasNext()
 178         // and distinguish them for re-throw later.
 179         // For non-proxy mode, exception come in should be re-thrown so the caller know
 180         // it is not processed and deal with it accordingly.
 181         visitorProxy = new FileVisitor<Path>() {
 182             public FileVisitResult preVisitDirectory(Path path, BasicFileAttributes attrs) {
 183                 if (visitor != null) {
 184                     try {
 185                         return visitor.preVisitDirectory(path, attrs);
 186                     } catch (IOException ex) {
 187                         throw new VisitorException(ex);
 188                     }
 189                 }
 190                 return FileVisitResult.CONTINUE;
 191             }
 192 
 193             public FileVisitResult postVisitDirectory(Path path, IOException exc) throws IOException {
 194                 if (visitor != null) {
 195                     try {
 196                         return visitor.postVisitDirectory(path, exc);
 197                     } catch (IOException ex) {
 198                         throw new VisitorException(ex);
 199                     }
 200                 } else if (exc != null) {
 201                     throw exc;
 202                 }
 203                 return FileVisitResult.CONTINUE;
 204             }
 205 
 206             public FileVisitResult visitFile(Path path, BasicFileAttributes attrs) {
 207                 if (visitor != null) {
 208                     try {
 209                         return visitor.visitFile(path, attrs);
 210                     } catch (IOException ex) {
 211                         throw new VisitorException(ex);
 212                     }
 213                 }
 214                 return FileVisitResult.CONTINUE;
 215             }
 216 
 217             public FileVisitResult visitFileFailed(Path path, IOException exc) throws IOException {
 218                 if (visitor != null) {
 219                     try {
 220                         return visitor.visitFileFailed(path, exc);
 221                     } catch (IOException ex) {
 222                         throw new VisitorException(ex);
 223                     }
 224                 } else if (exc != null) {
 225                     throw exc;
 226                 }
 227                 return FileVisitResult.CONTINUE;
 228             }
 229         };
 230 
 231         // Setup first visit for directory
 232         visitNext();
 233 
 234         return this;
 235     }
 236 
 237     public static FileTreeIterator iterator(Path start, int maxDepth,
 238                                             FileVisitOption... options) throws IOException {
 239         return new FileTreeIterator(maxDepth, options).init(start, null);
 240     }
 241 
 242     public static void walkThrough(Path start, int maxDepth,
 243                                    FileVisitor visitor,
 244                                    FileVisitOption... options) throws IOException {
 245         Objects.requireNonNull(visitor);
 246         FileTreeIterator itor = new FileTreeIterator(maxDepth, options).init(start, visitor);
 247         try {
 248             while (itor.hasNext()) {
 249                 itor.next();
 250             }
 251         } catch (VisitorException ex) {
 252             // Only VisitorException is processed here as others should be
 253             // handled by FileVisitor already.
 254             throw ex.getCause();
 255         }
 256     }
 257 
 258     private boolean detectLoop(Path dir, BasicFileAttributes attrs) {
 259         Object key = attrs.fileKey();
 260         for (Context ctx : stack) {
 261             Object ancestorKey = ctx.attrs.fileKey();
 262             if (key != null && ancestorKey != null) {
 263                 if (key.equals(ancestorKey)) {
 264                     return true;
 265                 }
 266             } else {
 267                 boolean isSameFile = false;
 268                 try {
 269                     isSameFile = Files.isSameFile(dir, ctx.file);
 270                 } catch (IOException x) {
 271                     // ignore
 272                 } catch (SecurityException x) {
 273                     // ignore
 274                 }
 275                 if (isSameFile) {
 276                     return true;
 277                 }
 278             }
 279         }
 280 
 281         return false;
 282     }
 283 
 284     private void evalVisitorResult(FileVisitResult result) {
 285         Objects.requireNonNull(result);
 286         switch (result) {
 287             case TERMINATE:
 288                 try {
 289                     close();
 290                 } catch (IOException ioe) {
 291                     // ignore
 292                 }
 293                 break;
 294             case SKIP_SIBLINGS:
 295             case SKIP_SUBTREE:
 296                 // stop iterate in the containing folder
 297                 if (! stack.isEmpty()) {
 298                     exitDirectory(null);
 299                 }
 300                 break;
 301             case CONTINUE:
 302                 break;
 303         }
 304     }
 305 
 306     private void enteringDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
 307         // Detect loop when follow links
 308         if (followLinks && detectLoop(dir, attrs)) {
 309             // Loop detected
 310             throw new FileSystemLoopException(dir.toString());
 311             // ?? skip is better ??
 312             // return;
 313         }
 314 
 315         DirectoryStream<Path> ds = Files.newDirectoryStream(dir);
 316         stack.push(new Context(dir, attrs, ds, ds.iterator()));
 317     }
 318 
 319     private void exitDirectory(DirectoryIteratorException die) {
 320         Context ctx = stack.pop();
 321         IOException failure = (die == null) ? null : die.getCause();
 322 
 323         try {
 324             ctx.ds.close();
 325         } catch (IOException ioe) {
 326             if (failure != null) {
 327                 failure = ioe;
 328             }
 329         }
 330 
 331         try {
 332             evalVisitorResult(visitorProxy.postVisitDirectory(ctx.file, failure));
 333         } catch (IOException ex) {
 334             throw new UncheckedIOException(ex);
 335             // retain DirectoryIteratorException information ?
 336             // throw (die == null) ? new UncheckedIOException(ex) : die;
 337         }
 338     }
 339 
 340     private void visitNext() {
 341         Path p = next.file;
 342         try {
 343             BasicFileAttributes attrs = next.getFileAttributes();
 344             if (attrs.isDirectory() && stack.size() < maxDepth) {
 345                 enteringDirectory(p, attrs);
 346                 FileVisitResult result = visitorProxy.preVisitDirectory(p, attrs);
 347                 // Simply undo enter, not calling postVisitDirectory
 348                 if (FileVisitResult.CONTINUE != result) {
 349                     Context ctx = stack.pop();
 350                     try {
 351                         ctx.ds.close();
 352                     } catch (IOException ioe) {
 353                         // ignore
 354                     }
 355                 }
 356                 // deal result from containing folder
 357                 evalVisitorResult(result);
 358             } else {
 359                 evalVisitorResult(visitorProxy.visitFile(p, attrs));
 360             }
 361         } catch (IOException ioe) {
 362             try {
 363                 evalVisitorResult(visitorProxy.visitFileFailed(p, ioe));
 364             } catch (IOException ioe2) {
 365                 throw new UncheckedIOException(ioe2);
 366             }
 367         }
 368     }
 369 
 370     /**
 371      * When there is an exception occurred, we will try to resume the iteration
 372      * to next element. So the exception is thrown, and next call to hasNext()
 373      * will continue the iteration.
 374      */
 375     public boolean hasNext() {
 376         // next was read-ahead, not yet fetched.
 377         if (next != null) {
 378             return true;
 379         }
 380 
 381         // Check if iterator had been closed.
 382         if (stack.isEmpty()) {
 383             return false;
 384         }
 385 
 386         Iterator<Path> itor = stack.peek().itor;
 387         try {
 388             Path p = itor.next();
 389             next = Entry.make(p, followLinks);
 390             visitNext();
 391         } catch (SecurityException se) {
 392             // ignore and skip this file
 393             next = null;
 394             return hasNext();
 395         } catch (DirectoryIteratorException die) {
 396             // try to resume from level above
 397             exitDirectory(die);
 398         } catch (NoSuchElementException nsee) {
 399             // nothing left at this level
 400             exitDirectory(null);
 401         }
 402         return stack.isEmpty() ? false : hasNext();
 403     }
 404 
 405     public Entry next() {
 406         if (next != null || hasNext()) {
 407             try {
 408                 return next;
 409             } finally {
 410                 next = null;
 411             }
 412         } else {
 413             throw new NoSuchElementException();
 414         }
 415     }
 416 
 417     public void close() throws IOException {
 418         IOException ioe = null;
 419 
 420         for (Context ctx : stack) {
 421             try {
 422                 ctx.ds.close();
 423             } catch (IOException ex) {
 424                 // ignore so we try to close all DirectoryStream
 425                 // keep the last exception to throw later
 426                 ioe = ex;
 427             }
 428         }
 429 
 430         next = null;
 431         stack.clear();
 432 
 433         if (ioe != null) {
 434             // Throw at least one if there is any
 435             throw ioe;
 436         }
 437     }
 438 }