Skip to content

Commit 825060a

Browse files
committed
Initial commit
1 parent 721fe69 commit 825060a

12 files changed

+530
-0
lines changed

.gitignore

+2
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,5 @@ pom.xml.releaseBackup
44
pom.xml.versionsBackup
55
pom.xml.next
66
release.properties
7+
*.iml
8+
.idea/

README.md

+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# java-file-change-watcher
2+
3+
The JavaFileChangeWatcher is a small programm (Jar File: ~10Kb) which watches a single file for changes and executes
4+
a command if the file was created or changed.
5+
6+
The program aims to be similar to [inotifywatch](https://github.com/rvoicilas/inotify-tools) but platform independent
7+
so that it works on Windows as well.
8+
9+
# Features
10+
11+
- ...uses the Watcher-API introduced in Java 7 which should make it get along with low system resources.
12+
- ...after a file change was detected waits another second before executing. If another change is detected during that
13+
second start to wait another second to reduce command executions if many alterations happen to a file in a small time.
14+
15+
# Usage
16+
17+
java -jar filewatch.jar [-v] <fileToWatch> <commandToExecute>
18+
19+
Remember that "commandToExecute" is not executed in a shell, so you cannot do any fancy stuff like string expansion (*).
20+
I suggest creating a script file (.bat on Windows, .sh on Linux) with your fancy things and just let this program call
21+
that script.
22+
23+
# Alternatives
24+
25+
The following alternatives I encountered:
26+
27+
- [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)
28+
- [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)
29+
- [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)
30+
31+
If you know another tool which you think may be helpful for others to find, feel free to create a pull request.
32+
33+
# Contributing
34+
35+
Feel free to create a pull request if you think something important is missing. Please submit tests with your code and
36+
remember that this tool is meant to be lightweight, so no huge add ons.

pom.xml

+51
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<project xmlns="http://maven.apache.org/POM/4.0.0"
3+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
4+
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
5+
<modelVersion>4.0.0</modelVersion>
6+
7+
<groupId>de.softwertiger.filewatch</groupId>
8+
<artifactId>filewatch</artifactId>
9+
<version>1.0</version>
10+
11+
<build>
12+
<plugins>
13+
<plugin>
14+
<groupId>org.apache.maven.plugins</groupId>
15+
<artifactId>maven-compiler-plugin</artifactId>
16+
<version>3.1</version>
17+
<configuration>
18+
<source>1.8</source>
19+
<target>1.8</target>
20+
</configuration>
21+
</plugin>
22+
<plugin>
23+
<groupId>org.apache.maven.plugins</groupId>
24+
<artifactId>maven-jar-plugin</artifactId>
25+
<configuration>
26+
<archive>
27+
<manifest>
28+
<addClasspath>true</addClasspath>
29+
<mainClass>de.softwertiger.filewatch.cli.Main</mainClass>
30+
</manifest>
31+
</archive>
32+
</configuration>
33+
</plugin>
34+
</plugins>
35+
</build>
36+
37+
<dependencies>
38+
<dependency>
39+
<groupId>org.testng</groupId>
40+
<artifactId>testng</artifactId>
41+
<version>6.8.8</version>
42+
<scope>test</scope>
43+
</dependency>
44+
<dependency>
45+
<groupId>org.mockito</groupId>
46+
<artifactId>mockito-all</artifactId>
47+
<version>1.9.5</version>
48+
<scope>test</scope>
49+
</dependency>
50+
</dependencies>
51+
</project>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package de.softwertiger.filewatch;
2+
3+
import java.util.concurrent.atomic.AtomicInteger;
4+
5+
public class DelayCallMerger implements Runnable {
6+
private final Runnable delegate;
7+
private volatile AtomicInteger runCounter = new AtomicInteger(0);
8+
9+
public DelayCallMerger(final Runnable delegate) {
10+
this.delegate = delegate;
11+
}
12+
13+
@Override
14+
public void run() {
15+
int id = runCounter.incrementAndGet();
16+
new Thread() {
17+
@Override
18+
public void run() {
19+
try {
20+
Thread.sleep(1000);
21+
} catch (InterruptedException e) {
22+
e.printStackTrace();
23+
return;
24+
}
25+
synchronized (DelayCallMerger.this) {
26+
if (id == runCounter.get()) {
27+
delegate.run();
28+
}
29+
}
30+
}
31+
}.start();
32+
}
33+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
package de.softwertiger.filewatch;
2+
3+
import java.io.IOException;
4+
import java.nio.file.FileSystems;
5+
import java.nio.file.Path;
6+
import java.nio.file.WatchEvent;
7+
import java.nio.file.WatchKey;
8+
import java.nio.file.WatchService;
9+
10+
import static java.nio.file.StandardWatchEventKinds.ENTRY_CREATE;
11+
import static java.nio.file.StandardWatchEventKinds.ENTRY_MODIFY;
12+
13+
public class OnFileChangeRunner {
14+
private final Path watchDir;
15+
private final Path watchFile;
16+
private final WatchService watchService;
17+
18+
private OnFileChangeRunner(final Path watchFile) throws IOException {
19+
this.watchFile = watchFile;
20+
watchDir = watchFile.getParent();
21+
watchService = FileSystems.getDefault().newWatchService();
22+
watchDir.register(watchService, ENTRY_CREATE, ENTRY_MODIFY);
23+
}
24+
25+
public static OnFileChangeRunner registerForFile(final Path watchFile) throws IOException {
26+
return new OnFileChangeRunner(watchFile);
27+
}
28+
29+
public void runOnFileChange(final Runnable runnable) {
30+
try {
31+
tryRunOnFileChange(runnable);
32+
} catch (IOException e) {
33+
throw new Error(e);
34+
}
35+
}
36+
37+
private void tryRunOnFileChange(final Runnable runnable) throws IOException {
38+
try {
39+
final WatchKey take = watchService.take();
40+
while (!Thread.interrupted()) {
41+
for (final WatchEvent<?> watchEvent : take.pollEvents()) {
42+
final WatchEvent<Path> ev = cast(watchEvent);
43+
if (watchFile.equals(watchDir.resolve(ev.context()))) {
44+
runnable.run();
45+
}
46+
}
47+
}
48+
} catch (InterruptedException e) {
49+
// Ignore
50+
}
51+
}
52+
53+
@SuppressWarnings("unchecked")
54+
private static <T> WatchEvent<T> cast(WatchEvent<?> event) {
55+
return (WatchEvent<T>) event;
56+
}
57+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package de.softwertiger.filewatch;
2+
3+
import java.io.IOException;
4+
import java.io.InputStream;
5+
import java.io.PrintStream;
6+
import java.util.concurrent.ThreadFactory;
7+
import java.util.concurrent.atomic.AtomicInteger;
8+
9+
public class StreamUtil {
10+
private final ThreadFactory threadFactory;
11+
private final int bufferSize;
12+
13+
public StreamUtil() {
14+
this(new ThreadFactory() {
15+
private final AtomicInteger threadNumber = new AtomicInteger(1);
16+
@Override
17+
public Thread newThread(final Runnable r) {
18+
return new Thread(r, "stream-util-" + threadNumber);
19+
}
20+
}, 8192);
21+
}
22+
23+
StreamUtil(final ThreadFactory threadFactory, final int bufferSize) {
24+
this.threadFactory = threadFactory;
25+
this.bufferSize = bufferSize;
26+
}
27+
28+
public Thread forwardStream(final InputStream in, final PrintStream out) {
29+
Thread thread = threadFactory.newThread(() -> {
30+
byte[] buffer = new byte[bufferSize];
31+
int read;
32+
try {
33+
while((read = in.read(buffer)) > -1) {
34+
out.write(buffer, 0, read);
35+
}
36+
} catch (IOException e) {
37+
e.printStackTrace();
38+
}
39+
});
40+
thread.start();
41+
return thread;
42+
}
43+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
package de.softwertiger.filewatch.cli;
2+
3+
public class IllegalCommandLineOptionsException extends RuntimeException {
4+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package de.softwertiger.filewatch.cli;
2+
3+
import de.softwertiger.filewatch.DelayCallMerger;
4+
import de.softwertiger.filewatch.OnFileChangeRunner;
5+
import de.softwertiger.filewatch.StreamUtil;
6+
7+
import java.io.IOException;
8+
9+
public class Main {
10+
private static final StreamUtil streamUtil = new StreamUtil();
11+
private final Options options;
12+
13+
public Main(final Options options) throws IOException {
14+
this.options = options;
15+
if (options.isVerbose()) {
16+
System.out.println("Watching <" + options.getFileToWatch() + "> for changes");
17+
}
18+
OnFileChangeRunner
19+
.registerForFile(options.getFileToWatch())
20+
.runOnFileChange(new DelayCallMerger(this::tryRunCommand));
21+
}
22+
23+
public static void main(String[] args) throws Exception {
24+
try {
25+
new Main(Options.parseCommandLineOptions(args));
26+
} catch (IllegalCommandLineOptionsException e) {
27+
System.out.println("Usage: fileWatcher [-v] <fileToWatch> <commandToExecute>");
28+
}
29+
}
30+
31+
private void tryRunCommand() {
32+
if (options.isVerbose()) {
33+
System.out.println("File has changed. Executing...");
34+
}
35+
try {
36+
final Process process = Runtime.getRuntime().exec(options.getCommand());
37+
streamUtil.forwardStream(process.getInputStream(), System.out);
38+
streamUtil.forwardStream(process.getErrorStream(), System.err);
39+
} catch (IOException e) {
40+
throw new Error(e);
41+
}
42+
}
43+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package de.softwertiger.filewatch.cli;
2+
3+
import java.nio.file.Path;
4+
import java.nio.file.Paths;
5+
6+
public class Options {
7+
private final boolean verbose;
8+
private final Path fileToWatch;
9+
private final String command;
10+
11+
private Options(final boolean verbose, final Path fileToWatch, final String command) {
12+
if (fileToWatch == null || command == null) {
13+
throw new IllegalCommandLineOptionsException();
14+
}
15+
this.verbose = verbose;
16+
this.fileToWatch = fileToWatch;
17+
this.command = command;
18+
}
19+
20+
public static Options parseCommandLineOptions(final String[] args) {
21+
boolean verbose = false;
22+
Path fileToWatch = null;
23+
String command = null;
24+
int unnamedPos = 0;
25+
for (final String arg : args) {
26+
if (arg.equals("-v")) {
27+
verbose = true;
28+
} else {
29+
if (unnamedPos == 0) {
30+
fileToWatch = Paths.get(System.getProperty("user.dir")).resolve(arg).normalize();
31+
} else if (unnamedPos == 1) {
32+
command = arg;
33+
} else {
34+
throw new IllegalCommandLineOptionsException(); // too many args
35+
}
36+
++unnamedPos;
37+
}
38+
}
39+
40+
return new Options(verbose, fileToWatch, command);
41+
}
42+
43+
public String getCommand() {
44+
return command;
45+
}
46+
47+
public Path getFileToWatch() {
48+
return fileToWatch;
49+
}
50+
51+
public boolean isVerbose() {
52+
return verbose;
53+
}
54+
}

0 commit comments

Comments
 (0)