14.3 Directory Traversal and File Watching
Master directory operations, tree walking, filtering, and real-time file system monitoring.
Listing Directory Contents
Basic Directory Listing:
import java.nio.file.*;
import java.io.IOException;
import java.util.stream.Stream;
Path directory = Path.of("/var/log");
// List direct children (not recursive)
try (Stream<Path> stream = Files.list(directory)) {
stream.forEach(System.out::println);
}
// Filter directories only
try (Stream<Path> stream = Files.list(directory)) {
stream.filter(Files::isDirectory)
.forEach(System.out::println);
}
// Filter files only
try (Stream<Path> stream = Files.list(directory)) {
stream.filter(Files::isRegularFile)
.forEach(System.out::println);
}
// Collect to list
try (Stream<Path> stream = Files.list(directory)) {
List<Path> files = stream
.filter(Files::isRegularFile)
.collect(Collectors.toList());
System.out.println("Found " + files.size() + " files");
}
DirectoryStream (Iterator Pattern):
// Alternative to Files.list() for iteration
try (DirectoryStream<Path> stream = Files.newDirectoryStream(directory)) {
for (Path entry : stream) {
System.out.println(entry.getFileName());
}
}
// With glob pattern filter
try (DirectoryStream<Path> stream =
Files.newDirectoryStream(directory, "*.log")) {
for (Path logFile : stream) {
System.out.println(logFile);
}
}
// With custom filter
DirectoryStream.Filter<Path> sizeFilter = path ->
Files.size(path) > 1024 * 1024; // Files > 1MB
try (DirectoryStream<Path> stream =
Files.newDirectoryStream(directory, sizeFilter)) {
for (Path largeFile : stream) {
System.out.println(largeFile + " - " + Files.size(largeFile) + " bytes");
}
}
Walking Directory Trees
Basic Tree Walking:
Path rootDir = Path.of("/home/user/projects");
// Walk entire tree (depth-first)
try (Stream<Path> stream = Files.walk(rootDir)) {
stream.forEach(System.out::println);
}
// Limit depth
try (Stream<Path> stream = Files.walk(rootDir, 2)) {
// Only go 2 levels deep
stream.forEach(System.out::println);
}
// Filter and collect
try (Stream<Path> stream = Files.walk(rootDir)) {
List<Path> javaFiles = stream
.filter(Files::isRegularFile)
.filter(p -> p.toString().endsWith(".java"))
.collect(Collectors.toList());
System.out.println("Found " + javaFiles.size() + " Java files");
}
// Calculate total size
try (Stream<Path> stream = Files.walk(rootDir)) {
long totalSize = stream
.filter(Files::isRegularFile)
.mapToLong(p -> {
try {
return Files.size(p);
} catch (IOException e) {
return 0L;
}
})
.sum();
System.out.println("Total size: " + totalSize + " bytes");
}
FileVisitor Pattern:
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
/**
* Custom file visitor
*/
class MyFileVisitor extends SimpleFileVisitor<Path> {
private int fileCount = 0;
private int dirCount = 0;
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
fileCount++;
System.out.println("File: " + file);
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) {
dirCount++;
System.out.println("Entering directory: " + dir);
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult visitFileFailed(Path file, IOException exc) {
System.err.println("Failed to access: " + file + " - " + exc.getMessage());
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult postVisitDirectory(Path dir, IOException exc) {
if (exc != null) {
System.err.println("Error in directory: " + dir + " - " + exc.getMessage());
}
return FileVisitResult.CONTINUE;
}
public void printStats() {
System.out.println("Files: " + fileCount);
System.out.println("Directories: " + dirCount);
}
}
// Usage
Path root = Path.of("/var/data");
MyFileVisitor visitor = new MyFileVisitor();
Files.walkFileTree(root, visitor);
visitor.printStats();
Skip Subtrees:
class SelectiveVisitor extends SimpleFileVisitor<Path> {
@Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) {
// Skip hidden directories
if (dir.getFileName().toString().startsWith(".")) {
System.out.println("Skipping: " + dir);
return FileVisitResult.SKIP_SUBTREE;
}
// Skip node_modules
if (dir.getFileName().toString().equals("node_modules")) {
return FileVisitResult.SKIP_SUBTREE;
}
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
System.out.println(file);
return FileVisitResult.CONTINUE;
}
}
Files.walkFileTree(Path.of("."), new SelectiveVisitor());
Terminate Walking:
class FindFileVisitor extends SimpleFileVisitor<Path> {
private final String targetName;
private Path foundPath = null;
public FindFileVisitor(String targetName) {
this.targetName = targetName;
}
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
if (file.getFileName().toString().equals(targetName)) {
foundPath = file;
return FileVisitResult.TERMINATE; // Stop walking
}
return FileVisitResult.CONTINUE;
}
public Path getFoundPath() {
return foundPath;
}
}
// Find specific file
FindFileVisitor visitor = new FindFileVisitor("config.xml");
Files.walkFileTree(Path.of("/etc"), visitor);
if (visitor.getFoundPath() != null) {
System.out.println("Found: " + visitor.getFoundPath());
} else {
System.out.println("Not found");
}
Path Matchers (Glob and Regex)
Glob Pattern Matching:
FileSystem fs = FileSystems.getDefault();
// Match Java files
PathMatcher javaFileMatcher = fs.getPathMatcher("glob:**.java");
// Match in specific directory
PathMatcher srcMatcher = fs.getPathMatcher("glob:/src/**/*.java");
// Match multiple extensions
PathMatcher docMatcher = fs.getPathMatcher("glob:**.{md,txt,doc}");
// Match pattern with ?
PathMatcher singleChar = fs.getPathMatcher("glob:file?.txt"); // file1.txt, fileA.txt
// Use matcher
Path file = Path.of("src/main/java/App.java");
if (javaFileMatcher.matches(file)) {
System.out.println("Java file: " + file);
}
// Find all matching files
try (Stream<Path> stream = Files.walk(Path.of("."))) {
stream.filter(javaFileMatcher::matches)
.forEach(System.out::println);
}
Regex Pattern Matching:
// Match with regex (more powerful)
PathMatcher regexMatcher = fs.getPathMatcher("regex:.*\\.java$");
// Match version numbers
PathMatcher versionMatcher = fs.getPathMatcher("regex:.*-\\d+\\.\\d+\\.\\d+\\.jar");
// Complex pattern
PathMatcher complexMatcher = fs.getPathMatcher(
"regex:src/(main|test)/java/.*\\.java"
);
// Test matches
Path jar = Path.of("library-1.2.3.jar");
if (versionMatcher.matches(jar)) {
System.out.println("Versioned JAR: " + jar);
}
Custom Filter Combinations:
/**
* Find files matching multiple criteria
*/
public static List<Path> findFiles(
Path start,
PathMatcher matcher,
long minSize,
long maxAge) throws IOException {
long cutoffTime = System.currentTimeMillis() - maxAge;
try (Stream<Path> stream = Files.walk(start)) {
return stream
.filter(Files::isRegularFile)
.filter(matcher::matches)
.filter(p -> {
try {
return Files.size(p) >= minSize &&
Files.getLastModifiedTime(p).toMillis() > cutoffTime;
} catch (IOException e) {
return false;
}
})
.collect(Collectors.toList());
}
}
// Usage
PathMatcher logMatcher = FileSystems.getDefault()
.getPathMatcher("glob:**.log");
List<Path> recentLargeLogs = findFiles(
Path.of("/var/log"),
logMatcher,
1024 * 1024, // 1 MB minimum
24 * 60 * 60 * 1000 // 24 hours
);
WatchService (File System Monitoring)
Basic File Watching:
import java.nio.file.*;
import static java.nio.file.StandardWatchEventKinds.*;
/**
* Monitor directory for changes
*/
public class DirectoryWatcher {
public static void watchDirectory(Path directory) throws IOException, InterruptedException {
// Create watch service
try (WatchService watchService = FileSystems.getDefault().newWatchService()) {
// Register directory for events
directory.register(
watchService,
ENTRY_CREATE,
ENTRY_DELETE,
ENTRY_MODIFY
);
System.out.println("Watching: " + directory);
// Event loop
while (true) {
// Wait for event (blocks)
WatchKey key = watchService.take();
// Process all events
for (WatchEvent<?> event : key.pollEvents()) {
WatchEvent.Kind<?> kind = event.kind();
// Overflow event
if (kind == OVERFLOW) {
System.out.println("Events overflow");
continue;
}
// Get the file name
@SuppressWarnings("unchecked")
WatchEvent<Path> ev = (WatchEvent<Path>) event;
Path fileName = ev.context();
// Resolve full path
Path fullPath = directory.resolve(fileName);
// Handle event
if (kind == ENTRY_CREATE) {
System.out.println("Created: " + fullPath);
} else if (kind == ENTRY_DELETE) {
System.out.println("Deleted: " + fullPath);
} else if (kind == ENTRY_MODIFY) {
System.out.println("Modified: " + fullPath);
}
}
// Reset key (IMPORTANT!)
boolean valid = key.reset();
if (!valid) {
System.out.println("Watch key no longer valid");
break;
}
}
}
}
public static void main(String[] args) throws Exception {
watchDirectory(Path.of("/tmp/watched"));
}
}
Recursive Directory Watching:
import java.util.concurrent.ConcurrentHashMap;
import java.util.Map;
/**
* Watch directory tree recursively
*/
public class RecursiveWatcher {
private final WatchService watchService;
private final Map<WatchKey, Path> keyToPath = new ConcurrentHashMap<>();
public RecursiveWatcher() throws IOException {
this.watchService = FileSystems.getDefault().newWatchService();
}
/**
* Register directory and all subdirectories
*/
public void registerRecursive(Path start) throws IOException {
Files.walkFileTree(start, new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs)
throws IOException {
registerDirectory(dir);
return FileVisitResult.CONTINUE;
}
});
}
/**
* Register single directory
*/
private void registerDirectory(Path dir) throws IOException {
WatchKey key = dir.register(
watchService,
ENTRY_CREATE,
ENTRY_DELETE,
ENTRY_MODIFY
);
keyToPath.put(key, dir);
System.out.println("Registered: " + dir);
}
/**
* Process events
*/
public void processEvents() throws InterruptedException {
while (true) {
WatchKey key = watchService.take();
Path dir = keyToPath.get(key);
if (dir == null) {
continue;
}
for (WatchEvent<?> event : key.pollEvents()) {
WatchEvent.Kind<?> kind = event.kind();
if (kind == OVERFLOW) {
continue;
}
@SuppressWarnings("unchecked")
WatchEvent<Path> ev = (WatchEvent<Path>) event;
Path fileName = ev.context();
Path fullPath = dir.resolve(fileName);
System.out.println(kind.name() + ": " + fullPath);
// If new directory created, register it
if (kind == ENTRY_CREATE) {
try {
if (Files.isDirectory(fullPath)) {
registerRecursive(fullPath);
}
} catch (IOException e) {
System.err.println("Failed to register: " + fullPath);
}
}
}
boolean valid = key.reset();
if (!valid) {
keyToPath.remove(key);
if (keyToPath.isEmpty()) {
break;
}
}
}
}
public void close() throws IOException {
watchService.close();
}
public static void main(String[] args) throws Exception {
RecursiveWatcher watcher = new RecursiveWatcher();
watcher.registerRecursive(Path.of("/tmp/watched"));
watcher.processEvents();
}
}
Filtered File Watching:
/**
* Watch specific file types
*/
public class FilteredWatcher {
private final WatchService watchService;
private final PathMatcher matcher;
public FilteredWatcher(String pattern) throws IOException {
this.watchService = FileSystems.getDefault().newWatchService();
this.matcher = FileSystems.getDefault().getPathMatcher("glob:" + pattern);
}
public void watch(Path directory) throws IOException, InterruptedException {
directory.register(watchService, ENTRY_CREATE, ENTRY_MODIFY, ENTRY_DELETE);
System.out.println("Watching for: " + matcher);
while (true) {
WatchKey key = watchService.take();
for (WatchEvent<?> event : key.pollEvents()) {
if (event.kind() == OVERFLOW) continue;
@SuppressWarnings("unchecked")
WatchEvent<Path> ev = (WatchEvent<Path>) event;
Path fileName = ev.context();
// Filter by pattern
if (matcher.matches(fileName)) {
Path fullPath = directory.resolve(fileName);
handleEvent(event.kind(), fullPath);
}
}
if (!key.reset()) break;
}
}
private void handleEvent(WatchEvent.Kind<?> kind, Path path) {
System.out.println(kind.name() + ": " + path);
// Custom handling
if (kind == ENTRY_CREATE) {
onFileCreated(path);
} else if (kind == ENTRY_MODIFY) {
onFileModified(path);
} else if (kind == ENTRY_DELETE) {
onFileDeleted(path);
}
}
private void onFileCreated(Path path) {
System.out.println("Processing new file: " + path);
}
private void onFileModified(Path path) {
System.out.println("File modified: " + path);
}
private void onFileDeleted(Path path) {
System.out.println("File deleted: " + path);
}
public static void main(String[] args) throws Exception {
// Watch only .txt files
FilteredWatcher watcher = new FilteredWatcher("*.txt");
watcher.watch(Path.of("/tmp/docs"));
}
}
Real-World Example: File System Monitor
import java.io.IOException;
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
import java.time.Instant;
import java.util.*;
import java.util.concurrent.*;
import java.util.function.Consumer;
/**
* Comprehensive file system monitoring system
*/
public class FileSystemMonitor implements AutoCloseable {
private final WatchService watchService;
private final Map<WatchKey, Path> watchKeys = new ConcurrentHashMap<>();
private final Map<String, List<Consumer<FileEvent>>> listeners = new ConcurrentHashMap<>();
private final ExecutorService executor = Executors.newSingleThreadExecutor();
private volatile boolean running = false;
public enum EventType {
CREATED, MODIFIED, DELETED
}
public static class FileEvent {
public final EventType type;
public final Path path;
public final Instant timestamp;
public FileEvent(EventType type, Path path) {
this.type = type;
this.path = path;
this.timestamp = Instant.now();
}
@Override
public String toString() {
return String.format("%s: %s at %s", type, path, timestamp);
}
}
public FileSystemMonitor() throws IOException {
this.watchService = FileSystems.getDefault().newWatchService();
}
/**
* Watch directory (non-recursive)
*/
public void watchDirectory(Path directory) throws IOException {
if (!Files.isDirectory(directory)) {
throw new IllegalArgumentException("Not a directory: " + directory);
}
WatchKey key = directory.register(
watchService,
StandardWatchEventKinds.ENTRY_CREATE,
StandardWatchEventKinds.ENTRY_MODIFY,
StandardWatchEventKinds.ENTRY_DELETE
);
watchKeys.put(key, directory);
System.out.println("Watching: " + directory);
}
/**
* Watch directory recursively
*/
public void watchDirectoryRecursive(Path directory) throws IOException {
Files.walkFileTree(directory, new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs)
throws IOException {
watchDirectory(dir);
return FileVisitResult.CONTINUE;
}
});
}
/**
* Register listener for all events
*/
public void addListener(Consumer<FileEvent> listener) {
addListener("*", listener);
}
/**
* Register listener for specific pattern
*/
public void addListener(String pattern, Consumer<FileEvent> listener) {
listeners.computeIfAbsent(pattern, k -> new CopyOnWriteArrayList<>())
.add(listener);
}
/**
* Start monitoring
*/
public void start() {
if (running) {
return;
}
running = true;
executor.submit(this::monitorLoop);
System.out.println("Monitor started");
}
/**
* Stop monitoring
*/
public void stop() {
running = false;
executor.shutdown();
System.out.println("Monitor stopped");
}
/**
* Main monitoring loop
*/
private void monitorLoop() {
while (running) {
try {
WatchKey key = watchService.poll(100, TimeUnit.MILLISECONDS);
if (key == null) {
continue;
}
Path dir = watchKeys.get(key);
if (dir == null) {
continue;
}
for (WatchEvent<?> event : key.pollEvents()) {
processEvent(dir, event);
}
boolean valid = key.reset();
if (!valid) {
watchKeys.remove(key);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
}
}
/**
* Process single event
*/
private void processEvent(Path dir, WatchEvent<?> event) {
WatchEvent.Kind<?> kind = event.kind();
if (kind == StandardWatchEventKinds.OVERFLOW) {
System.err.println("Event overflow");
return;
}
@SuppressWarnings("unchecked")
WatchEvent<Path> ev = (WatchEvent<Path>) event;
Path fileName = ev.context();
Path fullPath = dir.resolve(fileName);
// Determine event type
EventType eventType;
if (kind == StandardWatchEventKinds.ENTRY_CREATE) {
eventType = EventType.CREATED;
// Register new subdirectories
try {
if (Files.isDirectory(fullPath)) {
watchDirectoryRecursive(fullPath);
}
} catch (IOException e) {
System.err.println("Failed to watch new directory: " + fullPath);
}
} else if (kind == StandardWatchEventKinds.ENTRY_MODIFY) {
eventType = EventType.MODIFIED;
} else if (kind == StandardWatchEventKinds.ENTRY_DELETE) {
eventType = EventType.DELETED;
} else {
return;
}
// Create event
FileEvent fileEvent = new FileEvent(eventType, fullPath);
// Notify listeners
notifyListeners(fileEvent);
}
/**
* Notify all matching listeners
*/
private void notifyListeners(FileEvent event) {
// Global listeners
List<Consumer<FileEvent>> globalListeners = listeners.get("*");
if (globalListeners != null) {
globalListeners.forEach(listener -> {
try {
listener.accept(event);
} catch (Exception e) {
System.err.println("Listener error: " + e.getMessage());
}
});
}
// Pattern-specific listeners
listeners.entrySet().stream()
.filter(entry -> !entry.getKey().equals("*"))
.forEach(entry -> {
String pattern = entry.getKey();
PathMatcher matcher = FileSystems.getDefault()
.getPathMatcher("glob:" + pattern);
if (matcher.matches(event.path.getFileName())) {
entry.getValue().forEach(listener -> {
try {
listener.accept(event);
} catch (Exception e) {
System.err.println("Listener error: " + e.getMessage());
}
});
}
});
}
@Override
public void close() throws IOException {
stop();
try {
executor.awaitTermination(5, TimeUnit.SECONDS);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
watchService.close();
}
// Example usage
public static void main(String[] args) throws Exception {
try (FileSystemMonitor monitor = new FileSystemMonitor()) {
// Watch directories
monitor.watchDirectoryRecursive(Path.of("/tmp/watched"));
// Add global listener
monitor.addListener(event -> {
System.out.println("Global: " + event);
});
// Add pattern-specific listeners
monitor.addListener("*.txt", event -> {
System.out.println("Text file " + event.type + ": " + event.path);
});
monitor.addListener("*.log", event -> {
if (event.type == EventType.CREATED) {
System.out.println("New log file: " + event.path);
}
});
// Start monitoring
monitor.start();
// Run for 60 seconds
Thread.sleep(60000);
}
}
}
Best Practices
1. Always Close Streams:
// Always use try-with-resources
try (Stream<Path> stream = Files.walk(root)) {
stream.forEach(System.out::println);
}
2. Handle Exceptions in Visitors:
@Override
public FileVisitResult visitFileFailed(Path file, IOException exc) {
System.err.println("Failed: " + file + " - " + exc);
return FileVisitResult.CONTINUE; // Keep going
}
3. Reset WatchKey:
// Always reset the key
boolean valid = key.reset();
if (!valid) {
// Key no longer valid - stop watching
break;
}
4. Filter Early:
// Filter in stream for performance
try (Stream<Path> stream = Files.walk(root)) {
stream.filter(Files::isRegularFile) // Filter early
.filter(matcher::matches)
.forEach(this::process);
}
5. Use Appropriate Depth:
// Limit depth to avoid scanning too much
try (Stream<Path> stream = Files.walk(root, 3)) {
// Only 3 levels deep
}
These directory traversal and watching techniques enable powerful file system monitoring and management in modern Java applications.