For what it's worth, I hacked a little evidence of a concept that is capable of
- detect added, modified and deleted files in the monitored directory,
- display of uniform differences for each change (also a complete difference when adding / deleting files),
- tracking successive changes by storing a shadow copy of the source directory,
- work in a user-defined rhythm (5 seconds by default) so as not to print too many small differences in a short period of time, and sometimes a few large ones.
There are several limitations that can be barriers in production environments:
- In order not to complicate the sample code more than necessary, the subdirectories are copied at the beginning when creating the shadow directory (because I reworked the existing method to create a deep copy of the directory), but are ignored at runtime. Only files directly below the directory being watched are tracked to avoid recursion.
- Your requirement not to use external libraries is not fulfilled, because I really wanted not to reinvent the wheel to create uniform differences.
- The biggest advantage of this solution - it is able to detect changes anywhere in the text file, and not just at the end of the file, for example
tail -f - is also its biggest drawback: whenever the file changes, it should be completely shadowed, because otherwise, the Program cannot detect a subsequent change. Therefore, I would not recommend this solution for very large files.
How to build:
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>de.scrum-master.tools</groupId> <artifactId>SO_WatchServiceChangeLocationInFile</artifactId> <version>1.0-SNAPSHOT</version> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> </properties> <build> <plugins> <plugin> <artifactId>maven-compiler-plugin</artifactId> <version>3.1</version> <configuration> <source>1.7</source> <target>1.7</target> </configuration> </plugin> </plugins> </build> <dependencies> <dependency> <groupId>com.googlecode.java-diff-utils</groupId> <artifactId>diffutils</artifactId> <version>1.3.0</version> </dependency> </dependencies> </project>
Source code (sorry, a little long):
package de.scrum_master.app; import difflib.DiffUtils; import java.io.BufferedReader; import java.io.FileReader; import java.io.IOException; import java.nio.file.*; import java.nio.file.attribute.BasicFileAttributes; import java.util.LinkedList; import java.util.List; import static java.nio.file.StandardWatchEventKinds.*; public class FileChangeWatcher { public static final String DEFAULT_WATCH_DIR = "watch-dir"; public static final String DEFAULT_SHADOW_DIR = "shadow-dir"; public static final int DEFAULT_WATCH_INTERVAL = 5; private Path watchDir; private Path shadowDir; private int watchInterval; private WatchService watchService; public FileChangeWatcher(Path watchDir, Path shadowDir, int watchInterval) throws IOException { this.watchDir = watchDir; this.shadowDir = shadowDir; this.watchInterval = watchInterval; watchService = FileSystems.getDefault().newWatchService(); } public void run() throws InterruptedException, IOException { prepareShadowDir(); watchDir.register(watchService, ENTRY_CREATE, ENTRY_MODIFY, ENTRY_DELETE); while (true) { WatchKey watchKey = watchService.take(); for (WatchEvent<?> event : watchKey.pollEvents()) { Path oldFile = shadowDir.resolve((Path) event.context()); Path newFile = watchDir.resolve((Path) event.context()); List<String> oldContent; List<String> newContent; WatchEvent.Kind<?> eventType = event.kind(); if (!(Files.isDirectory(newFile) || Files.isDirectory(oldFile))) { if (eventType == ENTRY_CREATE) { if (!Files.isDirectory(newFile)) Files.createFile(oldFile); } else if (eventType == ENTRY_MODIFY) { Thread.sleep(200); oldContent = fileToLines(oldFile); newContent = fileToLines(newFile); printUnifiedDiff(newFile, oldFile, oldContent, newContent); try { Files.copy(newFile, oldFile, StandardCopyOption.REPLACE_EXISTING); } catch (Exception e) { e.printStackTrace(); } } else if (eventType == ENTRY_DELETE) { try { oldContent = fileToLines(oldFile); newContent = new LinkedList<>(); printUnifiedDiff(newFile, oldFile, oldContent, newContent); Files.deleteIfExists(oldFile); } catch (Exception e) { e.printStackTrace(); } } } } watchKey.reset(); Thread.sleep(1000 * watchInterval); } } private void prepareShadowDir() throws IOException { recursiveDeleteDir(shadowDir); Runtime.getRuntime().addShutdownHook( new Thread() { @Override public void run() { try { System.out.println("Cleaning up shadow directory " + shadowDir); recursiveDeleteDir(shadowDir); } catch (IOException e) { e.printStackTrace(); } } } ); recursiveCopyDir(watchDir, shadowDir); } public static void recursiveDeleteDir(Path directory) throws IOException { if (!directory.toFile().exists()) return; Files.walkFileTree(directory, new SimpleFileVisitor<Path>() { @Override public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { Files.delete(file); return FileVisitResult.CONTINUE; } @Override public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException { Files.delete(dir); return FileVisitResult.CONTINUE; } }); } public static void recursiveCopyDir(final Path sourceDir, final Path targetDir) throws IOException { Files.walkFileTree(sourceDir, new SimpleFileVisitor<Path>() { @Override public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { Files.copy(file, Paths.get(file.toString().replace(sourceDir.toString(), targetDir.toString()))); return FileVisitResult.CONTINUE; } @Override public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException { Files.createDirectories(Paths.get(dir.toString().replace(sourceDir.toString(), targetDir.toString()))); return FileVisitResult.CONTINUE; } }); } private static List<String> fileToLines(Path path) throws IOException { List<String> lines = new LinkedList<>(); String line; try (BufferedReader reader = new BufferedReader(new FileReader(path.toFile()))) { while ((line = reader.readLine()) != null) lines.add(line); } catch (Exception e) {} return lines; } private static void printUnifiedDiff(Path oldPath, Path newPath, List<String> oldContent, List<String> newContent) { List<String> diffLines = DiffUtils.generateUnifiedDiff( newPath.toString(), oldPath.toString(), oldContent, DiffUtils.diff(oldContent, newContent), 3 ); System.out.println(); for (String diffLine : diffLines) System.out.println(diffLine); } public static void main(String[] args) throws IOException, InterruptedException { String watchDirName = args.length > 0 ? args[0] : DEFAULT_WATCH_DIR; String shadowDirName = args.length > 1 ? args[1] : DEFAULT_SHADOW_DIR; int watchInterval = args.length > 2 ? Integer.getInteger(args[2]) : DEFAULT_WATCH_INTERVAL; new FileChangeWatcher(Paths.get(watchDirName), Paths.get(shadowDirName), watchInterval).run(); } }
I recommend that you use the default settings (for example, use the source directory called "watch-dir") and play with it for a while, watching the console output when you create and edit some text files in the editor. This helps to understand the internal mechanics of software. If something goes wrong, for example, within one 5-second rhythm, the file is created, but also quickly deleted again, there is nothing to copy or analyze, so the program simply prints the stack trace in System.err .