2013-10-20 5 views
6

Ich habe Herumspielen gerade mit dem Java 7 WatchService für eine Datei für den Wandel zu überwachen.Java 7 watchservice get-Datei ändern Offset

Hier ist ein wenig Code, den ich oben geklopft:

WatchService watcher = FileSystems.getDefault().newWatchService(); 

    Path path = Paths.get("c:\\testing"); 

    path.register(watcher, StandardWatchEventKinds.ENTRY_MODIFY); 

    while (true) { 
     WatchKey key = watcher.take(); 

     for (WatchEvent event : key.pollEvents()) { 
      System.out.println(event.kind() + ":" + event.context()); 
     } 

     boolean valid = key.reset(); 
     if (!valid) { 
      break; 
     } 
    } 

Dies scheint zu funktionieren, und ich Benachrichtigungen, wann eine Datei ‚changethis.txt‘ geändert wird.

zusätzlich jedoch benachrichtigen zu können, eine Datei ändert, wenn, ist es trotzdem innerhalb der Datei über den Ort informiert zu werden, dass die Änderung aufgetreten?

Ich habe einen Blick durch die Java-Dokumentation hatte, aber ich kann nicht finden etwas zu sein scheinen.

Ist dies möglich, die WatchService, oder würde etwas Brauch umgesetzt werden müssen?

Dank

+2

So etwas ist mit 'WatchService' nicht möglich. –

+0

Danke. Gibt es etwas in Java 7/NIO, das es könnte? – Tony

+0

Nicht, dass ich mir dessen bewusst bin. Sie müssen vorher einen eigenen Scan der Klasse durchführen. Ein 'WatchService' wäre für dieses Imo nicht ideal. –

Antwort

4

Für was es wert ist, habe ich ein wenig Proof of Concept gehackt, die

  • detect hinzugefügt

    der Lage ist, modifiziert und gelöschte Dateien in einem überwachten Verzeichnis,
  • einheitliche Anzeige Diffs für jede Änderung (auch vollständige Diffs, wenn Dateien hinzugefügt/gelöscht wurden),
  • Verfolgung der sukzessiven Änderungen durch eine Schattenkopie des Quellverzeichnisses,
  • Arbeit in einem benutzerdefinierten Rhythmus (die Standardeinstellung ist 5 Sekunden), um nicht zu viele kleine diffs in kurzer Zeit zu drucken, sondern etwas größere einmal in einer Weile.

Es gibt mehrere Einschränkungen, die Hindernisse in Produktionsumgebungen sein würde:

  • Um den Beispielcode mehr als nötig, um nicht zu erschweren, werden Verzeichnisse am Anfang kopiert, wenn das Schatten-Verzeichnis erstellt wird (weil Ich habe eine vorhandene Methode zum Erstellen einer tiefen Verzeichniskopie wiederverwendet, aber während der Laufzeit ignoriert. Nur Dateien direkt unter dem überwachten Verzeichnis werden überwacht, um Rekursionen zu vermeiden.
  • Ihre Anforderung, keine externen Bibliotheken zu verwenden, wird nicht erfüllt, weil ich wirklich vermeiden wollte, das Rad für die einheitliche Diff-Erstellung neu zu erfinden.
  • Der größte Vorteil dieser Lösung - es ist in der Lage, Änderungen in einer Textdatei zu erkennen, nicht nur am Ende der Datei wie tail -f - ist auch der größte Nachteil: Wenn eine Datei ändert, muss es vollständig Schatten kopiert werden, da die anderen Programm kann die nachfolgende Änderung nicht erkennen. Daher würde ich diese Lösung für sehr große Dateien nicht empfehlen.

Wie bauen:

<?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> 

Quellcode (sorry, etwas langatmig):

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(); 
    } 
} 

Ich empfehle die Standardeinstellungen zu verwenden (zB ein Quellverzeichnis verwenden den Namen „Uhr -dir ") und spielen, um mit ihm für eine Weile und beobachtete die Ausgabe der Konsole, wie Sie einige Textdateien in einem Editor erstellen und bearbeiten. Es hilft, die inneren Mechanismen der Software zu verstehen. Wenn etwas schief geht, z.B.innerhalb eines 5-Sekunden-Rhythmus wird eine Datei erstellt, aber auch schnell wieder gelöscht, es gibt nichts zu kopieren oder zu diff. Das Programm druckt also nur einen Stack-Trace auf System.err.

+0

+1 und danke für eine gute und umfassende Antwort. Ich kann die Verwendung eines externen Diff-Tools in diesem Fall verstehen. In meinem Fall würde Inhalt nur angehängt, so dass ein Unterschied viel einfacher wäre. Auf jeden Fall mag ich den Ansatz nicht, einfach eine Kopie der Datei zu haben. Ich hoffe immer noch, dass es eine bessere Lösung gibt, obwohl ich bezweifle, dass es da ist :-) – Simon

+0

Nun, Simon, du bist nicht der Autor der Frage, und wahrscheinlich war dein "ein Diff leichter" war ein Tippfehler und du wolltest sagen " Schwanz "statt" diff ". Es gibt auch eine Lösung dafür, und ich denke, wir wollen plattformunabhängig bleiben (zB gibt es kein diff/tail auf Windows vorinstalliert): https://github.com/dpillay/tail4j (ungetestet) – kriegaex

+0

Ja, ich weiß. .. Damit? :-) Ein Diff auf Dateien, wo nur Inhalte angehängt werden, ist eigentlich ein Schwanz, den ich sagen würde :-) Wie auch immer, ich werde noch ein paar Tage warten, um zu sehen, ob es mehr Antworten gibt und wenn nicht, werde ich Ihnen das Kopfgeld geben. – Simon

3

Okay, hier ist eine andere Antwort als eine Variation meiner vorherigen für Änderungen an jeder Dateiposition (Diff). Der etwas einfachere Fall ist, dass Dateien nur angehängt werden (Tail).

Wie bauen:

<?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>commons-io</groupId> 
      <artifactId>commons-io</artifactId> 
      <!-- Use snapshot because of the UTF-8 problem in https://issues.apache.org/jira/browse/IO-354 --> 
      <version>2.5-SNAPSHOT</version> 
     </dependency> 
    </dependencies> 

    <repositories> 
     <repository> 
      <id>apache.snapshots</id> 
      <url>http://repository.apache.org/snapshots/</url> 
     </repository> 
    </repositories> 
</project> 

Wie Sie sehen können, verwenden wir Apache Commons IO hier. (? Warum eine Snapshot-Version Folgen Sie dem Link in der XML-Kommentar, wenn Sie interessiert sind.)

Quellcode:

package de.scrum_master.app; 

import org.apache.commons.io.input.Tailer; 
import org.apache.commons.io.input.TailerListenerAdapter; 

import java.io.IOException; 
import java.nio.charset.Charset; 
import java.nio.file.*; 

import static java.nio.file.StandardWatchEventKinds.ENTRY_CREATE; 

public class FileTailWatcher { 
    public static final String DEFAULT_WATCH_DIR = "watch-dir"; 
    public static final int DEFAULT_WATCH_INTERVAL = 5; 

    private Path watchDir; 
    private int watchInterval; 
    private WatchService watchService; 

    public FileTailWatcher(Path watchDir, int watchInterval) throws IOException { 
     if (!Files.isDirectory(watchDir)) 
      throw new IllegalArgumentException("Path '" + watchDir + "' is not a directory"); 
     this.watchDir = watchDir; 
     this.watchInterval = watchInterval; 
     watchService = FileSystems.getDefault().newWatchService(); 
    } 

    public static class MyTailerListener extends TailerListenerAdapter { 
     public void handle(String line) { 
      System.out.println(line); 
     } 
    } 

    public void run() throws InterruptedException, IOException { 
     try (DirectoryStream<Path> dirEntries = Files.newDirectoryStream(watchDir)) { 
      for (Path file : dirEntries) 
       createTailer(file); 
     } 
     watchDir.register(watchService, ENTRY_CREATE); 
     while (true) { 
      WatchKey watchKey = watchService.take(); 
      for (WatchEvent<?> event : watchKey.pollEvents()) 
       createTailer(watchDir.resolve((Path) event.context())); 
      watchKey.reset(); 
      Thread.sleep(1000 * watchInterval); 
     } 
    } 

    private Tailer createTailer(Path path) { 
     if (Files.isDirectory(path)) 
      return null; 
     System.out.println("Creating tailer: " + path); 
     return Tailer.create(
      path.toFile(),    // File to be monitored 
      Charset.defaultCharset(), // Character set (available since Commons IO 2.5) 
      new MyTailerListener(), // What should happen for new tail events? 
      1000,      // Delay between checks in ms 
      true,      // Tail from end of file, not from beginning 
      true,      // Close & reopen files in between reads, 
             // otherwise file is locked on Windows and cannot be deleted 
      4096      // Read buffer size 
     ); 
    } 

    public static void main(String[] args) throws IOException, InterruptedException { 
     String watchDirName = args.length > 0 ? args[0] : DEFAULT_WATCH_DIR; 
     int watchInterval = args.length > 2 ? Integer.getInteger(args[2]) : DEFAULT_WATCH_INTERVAL; 
     new FileTailWatcher(Paths.get(watchDirName), watchInterval).run(); 
    } 
} 

Nun versuchen, vorhandene Dateien anhängen und/oder neue zu schaffen. Alles wird auf Standardausgabe gedruckt. In einer Produktionsumgebung würden Sie möglicherweise mehrere Fenster oder Registerkarten anzeigen, eines für jede Protokolldatei. Was auch immer ...

@Simon: Ich hoffe, dass dieses Ihrer Situation besser als der allgemeinere Fall entspricht und ein Kopfgeld wert ist. :-)

+0

Vielen Dank. Die Kombination der beiden Antworten ist großartig. Sie könnten erwägen, dieses in das akzeptierte zu integrieren. – Simon

+0

Nein, die Anwendungsfälle sind zu unterschiedlich und jede Antwort selbst ist schon zu ausführlich. ;-) – kriegaex