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.