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 }