Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
yankee42 committed Mar 2, 2015
1 parent 721fe69 commit 825060a
Show file tree
Hide file tree
Showing 12 changed files with 530 additions and 0 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,5 @@ pom.xml.releaseBackup
pom.xml.versionsBackup
pom.xml.next
release.properties
*.iml
.idea/
36 changes: 36 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# java-file-change-watcher

The JavaFileChangeWatcher is a small programm (Jar File: ~10Kb) which watches a single file for changes and executes
a command if the file was created or changed.

The program aims to be similar to [inotifywatch](https://github.com/rvoicilas/inotify-tools) but platform independent
so that it works on Windows as well.

# Features

- ...uses the Watcher-API introduced in Java 7 which should make it get along with low system resources.
- ...after a file change was detected waits another second before executing. If another change is detected during that
second start to wait another second to reduce command executions if many alterations happen to a file in a small time.

# Usage

java -jar filewatch.jar [-v] <fileToWatch> <commandToExecute>

Remember that "commandToExecute" is not executed in a shell, so you cannot do any fancy stuff like string expansion (*).
I suggest creating a script file (.bat on Windows, .sh on Linux) with your fancy things and just let this program call
that script.

# Alternatives

The following alternatives I encountered:

- [Using the PowerShell on Windows](http://blogs.technet.com/b/heyscriptingguy/archive/2004/10/11/how-can-i-automatically-run-a-script-any-time-a-file-is-added-to-a-folder.aspx) (I could not get that one to work, but I never worked with the MS Powershell before so it's probably just me)
- [Belvedere](http://ca.lifehacker.com/341950/belvedere-automates-your-self+cleaning-pc) (Is Windows only and I wanted something that works on Linux as well for testing. Additionally it is not lightweight)
- [when_changed](https://github.com/benblamey/when_changed) (Similar to this one, but written in C#. Maybe it works with Mono on Linux, I did not test)

If you know another tool which you think may be helpful for others to find, feel free to create a pull request.

# Contributing

Feel free to create a pull request if you think something important is missing. Please submit tests with your code and
remember that this tool is meant to be lightweight, so no huge add ons.
51 changes: 51 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<?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.softwertiger.filewatch</groupId>
<artifactId>filewatch</artifactId>
<version>1.0</version>

<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<configuration>
<archive>
<manifest>
<addClasspath>true</addClasspath>
<mainClass>de.softwertiger.filewatch.cli.Main</mainClass>
</manifest>
</archive>
</configuration>
</plugin>
</plugins>
</build>

<dependencies>
<dependency>
<groupId>org.testng</groupId>
<artifactId>testng</artifactId>
<version>6.8.8</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-all</artifactId>
<version>1.9.5</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>
33 changes: 33 additions & 0 deletions src/main/java/de/softwertiger/filewatch/DelayCallMerger.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package de.softwertiger.filewatch;

import java.util.concurrent.atomic.AtomicInteger;

public class DelayCallMerger implements Runnable {
private final Runnable delegate;
private volatile AtomicInteger runCounter = new AtomicInteger(0);

public DelayCallMerger(final Runnable delegate) {
this.delegate = delegate;
}

@Override
public void run() {
int id = runCounter.incrementAndGet();
new Thread() {
@Override
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
return;
}
synchronized (DelayCallMerger.this) {
if (id == runCounter.get()) {
delegate.run();
}
}
}
}.start();
}
}
57 changes: 57 additions & 0 deletions src/main/java/de/softwertiger/filewatch/OnFileChangeRunner.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package de.softwertiger.filewatch;

import java.io.IOException;
import java.nio.file.FileSystems;
import java.nio.file.Path;
import java.nio.file.WatchEvent;
import java.nio.file.WatchKey;
import java.nio.file.WatchService;

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

public class OnFileChangeRunner {
private final Path watchDir;
private final Path watchFile;
private final WatchService watchService;

private OnFileChangeRunner(final Path watchFile) throws IOException {
this.watchFile = watchFile;
watchDir = watchFile.getParent();
watchService = FileSystems.getDefault().newWatchService();
watchDir.register(watchService, ENTRY_CREATE, ENTRY_MODIFY);
}

public static OnFileChangeRunner registerForFile(final Path watchFile) throws IOException {
return new OnFileChangeRunner(watchFile);
}

public void runOnFileChange(final Runnable runnable) {
try {
tryRunOnFileChange(runnable);
} catch (IOException e) {
throw new Error(e);
}
}

private void tryRunOnFileChange(final Runnable runnable) throws IOException {
try {
final WatchKey take = watchService.take();
while (!Thread.interrupted()) {
for (final WatchEvent<?> watchEvent : take.pollEvents()) {
final WatchEvent<Path> ev = cast(watchEvent);
if (watchFile.equals(watchDir.resolve(ev.context()))) {
runnable.run();
}
}
}
} catch (InterruptedException e) {
// Ignore
}
}

@SuppressWarnings("unchecked")
private static <T> WatchEvent<T> cast(WatchEvent<?> event) {
return (WatchEvent<T>) event;
}
}
43 changes: 43 additions & 0 deletions src/main/java/de/softwertiger/filewatch/StreamUtil.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package de.softwertiger.filewatch;

import java.io.IOException;
import java.io.InputStream;
import java.io.PrintStream;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.atomic.AtomicInteger;

public class StreamUtil {
private final ThreadFactory threadFactory;
private final int bufferSize;

public StreamUtil() {
this(new ThreadFactory() {
private final AtomicInteger threadNumber = new AtomicInteger(1);
@Override
public Thread newThread(final Runnable r) {
return new Thread(r, "stream-util-" + threadNumber);
}
}, 8192);
}

StreamUtil(final ThreadFactory threadFactory, final int bufferSize) {
this.threadFactory = threadFactory;
this.bufferSize = bufferSize;
}

public Thread forwardStream(final InputStream in, final PrintStream out) {
Thread thread = threadFactory.newThread(() -> {
byte[] buffer = new byte[bufferSize];
int read;
try {
while((read = in.read(buffer)) > -1) {
out.write(buffer, 0, read);
}
} catch (IOException e) {
e.printStackTrace();
}
});
thread.start();
return thread;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package de.softwertiger.filewatch.cli;

public class IllegalCommandLineOptionsException extends RuntimeException {
}
43 changes: 43 additions & 0 deletions src/main/java/de/softwertiger/filewatch/cli/Main.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package de.softwertiger.filewatch.cli;

import de.softwertiger.filewatch.DelayCallMerger;
import de.softwertiger.filewatch.OnFileChangeRunner;
import de.softwertiger.filewatch.StreamUtil;

import java.io.IOException;

public class Main {
private static final StreamUtil streamUtil = new StreamUtil();
private final Options options;

public Main(final Options options) throws IOException {
this.options = options;
if (options.isVerbose()) {
System.out.println("Watching <" + options.getFileToWatch() + "> for changes");
}
OnFileChangeRunner
.registerForFile(options.getFileToWatch())
.runOnFileChange(new DelayCallMerger(this::tryRunCommand));
}

public static void main(String[] args) throws Exception {
try {
new Main(Options.parseCommandLineOptions(args));
} catch (IllegalCommandLineOptionsException e) {
System.out.println("Usage: fileWatcher [-v] <fileToWatch> <commandToExecute>");
}
}

private void tryRunCommand() {
if (options.isVerbose()) {
System.out.println("File has changed. Executing...");
}
try {
final Process process = Runtime.getRuntime().exec(options.getCommand());
streamUtil.forwardStream(process.getInputStream(), System.out);
streamUtil.forwardStream(process.getErrorStream(), System.err);
} catch (IOException e) {
throw new Error(e);
}
}
}
54 changes: 54 additions & 0 deletions src/main/java/de/softwertiger/filewatch/cli/Options.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package de.softwertiger.filewatch.cli;

import java.nio.file.Path;
import java.nio.file.Paths;

public class Options {
private final boolean verbose;
private final Path fileToWatch;
private final String command;

private Options(final boolean verbose, final Path fileToWatch, final String command) {
if (fileToWatch == null || command == null) {
throw new IllegalCommandLineOptionsException();
}
this.verbose = verbose;
this.fileToWatch = fileToWatch;
this.command = command;
}

public static Options parseCommandLineOptions(final String[] args) {
boolean verbose = false;
Path fileToWatch = null;
String command = null;
int unnamedPos = 0;
for (final String arg : args) {
if (arg.equals("-v")) {
verbose = true;
} else {
if (unnamedPos == 0) {
fileToWatch = Paths.get(System.getProperty("user.dir")).resolve(arg).normalize();
} else if (unnamedPos == 1) {
command = arg;
} else {
throw new IllegalCommandLineOptionsException(); // too many args
}
++unnamedPos;
}
}

return new Options(verbose, fileToWatch, command);
}

public String getCommand() {
return command;
}

public Path getFileToWatch() {
return fileToWatch;
}

public boolean isVerbose() {
return verbose;
}
}
Loading

0 comments on commit 825060a

Please sign in to comment.