/* Copyright (c) 2007 Timothy Wall, All Rights Reserved
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
* <p/>
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*/
package com.sun.jna.examples;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.EventObject;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import com.sun.jna.examples.win32.Kernel32;
import com.sun.jna.examples.win32.Kernel32.FILE_NOTIFY_INFORMATION;
import com.sun.jna.examples.win32.Kernel32.OVERLAPPED;
import com.sun.jna.examples.win32.W32API.HANDLE;
import com.sun.jna.examples.win32.W32API.HANDLEByReference;
import com.sun.jna.ptr.IntByReference;
import com.sun.jna.ptr.PointerByReference;
/** Provides notification of file system changes. Actual capabilities may
* vary slightly by platform.
* <p>
* Watched files which are removed from the filesystem are no longer watched.
* @author twall
*/
public abstract class FileMonitor {
public static final int FILE_CREATED = 0x1;
public static final int FILE_DELETED = 0x2;
public static final int FILE_MODIFIED = 0x4;
public static final int FILE_ACCESSED = 0x8;
public static final int FILE_NAME_CHANGED_OLD = 0x10;
public static final int FILE_NAME_CHANGED_NEW = 0x20;
public static final int FILE_RENAMED = FILE_NAME_CHANGED_OLD|FILE_NAME_CHANGED_NEW;
public static final int FILE_SIZE_CHANGED = 0x40;
public static final int FILE_ATTRIBUTES_CHANGED = 0x80;
public static final int FILE_SECURITY_CHANGED = 0x100;
public static final int FILE_ANY = 0x1FF;
public interface FileListener {
public void fileChanged(FileEvent e);
}
public class FileEvent extends EventObject {
private final File file;
private final int type;
public FileEvent(File file, int type) {
super(FileMonitor.this);
this.file = file;
this.type = type;
}
public File getFile() { return file; }
public int getType() { return type; }
public String toString() {
return "FileEvent: " + file + ":" + type;
}
}
private final Map watched = new HashMap();
private List listeners = new ArrayList();
protected abstract void watch(File file, int mask, boolean recursive) throws IOException ;
protected abstract void unwatch(File file);
protected abstract void dispose();
public void addWatch(File dir) throws IOException {
addWatch(dir, FILE_ANY);
}
public void addWatch(File dir, int mask) throws IOException {
addWatch(dir, mask, dir.isDirectory());
}
public void addWatch(File dir, int mask, boolean recursive) throws IOException {
watched.put(dir, new Integer(mask));
watch(dir, mask, recursive);
}
public void removeWatch(File file) {
if (watched.remove(file) != null) {
unwatch(file);
}
}
protected void notify(FileEvent e) {
for (Iterator i=listeners.iterator();i.hasNext();) {
((FileListener)i.next()).fileChanged(e);
}
}
public synchronized void addFileListener(FileListener x) {
List list = new ArrayList(listeners);
list.add(x);
listeners = list;
}
public synchronized void removeFileListener(FileListener x) {
List list = new ArrayList(listeners);
list.remove(x);
listeners = list;
}
protected void finalize() {
for (Iterator i=watched.keySet().iterator();i.hasNext();) {
removeWatch((File)i.next());
}
dispose();
}
/** Canonical lazy loading of a singleton. */
private static class Holder {
public static final FileMonitor INSTANCE;
static {
String os = System.getProperty("os.name");
if (os.startsWith("Windows")) {
INSTANCE = new W32FileMonitor();
}
else {
throw new Error("FileMonitor not implemented for " + os);
}
}
}
public static FileMonitor getInstance() {
return Holder.INSTANCE;
}
private static class W32FileMonitor extends FileMonitor {
private static final int BUFFER_SIZE = 4096;
private class FileInfo {
public final File file;
public final HANDLE handle;
public final int notifyMask;
public final boolean recursive;
public final FILE_NOTIFY_INFORMATION info = new FILE_NOTIFY_INFORMATION(BUFFER_SIZE);
public final IntByReference infoLength = new IntByReference();
public final OVERLAPPED overlapped = new OVERLAPPED();
public FileInfo(File f, HANDLE h, int mask, boolean recurse) {
this.file = f;
this.handle = h;
this.notifyMask = mask;
this.recursive = recurse;
}
}
private Thread watcher;
private HANDLE port;
private final Map fileMap = new HashMap();
private final Map handleMap = new HashMap();
private void handleChanges(FileInfo finfo) throws IOException {
Kernel32 klib = Kernel32.INSTANCE;
FILE_NOTIFY_INFORMATION fni = finfo.info;
// Need an explicit read, since data was filled in asynchronously
fni.read();
do {
FileEvent event = null;
File file = new File(finfo.file, fni.getFilename());
switch(fni.Action) {
case Kernel32.FILE_ACTION_MODIFIED:
event = new FileEvent(file, FILE_MODIFIED); break;
case Kernel32.FILE_ACTION_ADDED:
event = new FileEvent(file, FILE_CREATED); break;
case Kernel32.FILE_ACTION_REMOVED:
event = new FileEvent(file, FILE_DELETED); break;
case Kernel32.FILE_ACTION_RENAMED_OLD_NAME:
event = new FileEvent(file, FILE_NAME_CHANGED_OLD); break;
case Kernel32.FILE_ACTION_RENAMED_NEW_NAME:
event = new FileEvent(file, FILE_NAME_CHANGED_NEW); break;
default:
// TODO: other actions...
System.err.println("Unrecognized file action '" + fni.Action + "'");
}
if (event != null)
notify(event);
fni = fni.next();
} while (fni != null);
// Trigger the next read
if (!finfo.file.exists()) {
unwatch(finfo.file);
return;
}
if (!klib.ReadDirectoryChangesW(finfo.handle, finfo.info,
finfo.info.size(), finfo.recursive,
finfo.notifyMask, finfo.infoLength,
finfo.overlapped, null)) {
int err = klib.GetLastError();
throw new IOException("ReadDirectoryChangesW failed on "
+ finfo.file + ": '"
+ getSystemError(err)
+ "' (" + err + ")");
}
}
private FileInfo waitForChange() {
Kernel32 klib = Kernel32.INSTANCE;
IntByReference rcount = new IntByReference();
HANDLEByReference rkey = new HANDLEByReference();
PointerByReference roverlap = new PointerByReference();
klib.GetQueuedCompletionStatus(port, rcount, rkey, roverlap, Kernel32.INFINITE);
synchronized (this) {
return (FileInfo)handleMap.get(rkey.getValue());
}
}
private int convertMask(int mask) {
int result = 0;
if ((mask & FILE_CREATED) != 0) {
result |= Kernel32.FILE_NOTIFY_CHANGE_CREATION;
}
if ((mask & FILE_DELETED) != 0) {
result |= Kernel32.FILE_NOTIFY_CHANGE_NAME;
}
if ((mask & FILE_MODIFIED) != 0) {
result |= Kernel32.FILE_NOTIFY_CHANGE_LAST_WRITE;
}
if ((mask & FILE_RENAMED) != 0) {
result |= Kernel32.FILE_NOTIFY_CHANGE_NAME;
}
if ((mask & FILE_SIZE_CHANGED) != 0) {
result |= Kernel32.FILE_NOTIFY_CHANGE_SIZE;
}
if ((mask & FILE_ACCESSED) != 0) {
result |= Kernel32.FILE_NOTIFY_CHANGE_LAST_ACCESS;
}
if ((mask & FILE_ATTRIBUTES_CHANGED) != 0) {
result |= Kernel32.FILE_NOTIFY_CHANGE_ATTRIBUTES;
}
if ((mask & FILE_SECURITY_CHANGED) != 0) {
result |= Kernel32.FILE_NOTIFY_CHANGE_SECURITY;
}
return result;
}
private static int watcherThreadID;
protected synchronized void watch(File file, int eventMask, boolean recursive) throws IOException {
File dir = file;
if (!dir.isDirectory()) {
recursive = false;
dir = file.getParentFile();
}
while (dir != null && !dir.exists()) {
recursive = true;
dir = dir.getParentFile();
}
if (dir == null) {
throw new FileNotFoundException("No ancestor found for " + file);
}
Kernel32 klib = Kernel32.INSTANCE;
int mask = Kernel32.FILE_SHARE_READ
| Kernel32.FILE_SHARE_WRITE | Kernel32.FILE_SHARE_DELETE;
int flags = Kernel32.FILE_FLAG_BACKUP_SEMANTICS
| Kernel32.FILE_FLAG_OVERLAPPED;
HANDLE handle = klib.CreateFile(file.getAbsolutePath(),
Kernel32.FILE_LIST_DIRECTORY,
mask, null, Kernel32.OPEN_EXISTING,
flags, null);
if (Kernel32.INVALID_HANDLE_VALUE.equals(handle)) {
throw new IOException("Unable to open " + file + " ("
+ klib.GetLastError() + ")");
}
int notifyMask = convertMask(eventMask);
FileInfo finfo = new FileInfo(file, handle, notifyMask, recursive);
fileMap.put(file, finfo);
handleMap.put(handle, finfo);
// Existing port is returned
port = klib.CreateIoCompletionPort(handle, port, handle.getPointer(), 0);
if (Kernel32.INVALID_HANDLE_VALUE.equals(port)) {
throw new IOException("Unable to create/use I/O Completion port "
+ "for " + file + " ("
+ klib.GetLastError() + ")");
}
// TODO: use FileIOCompletionRoutine callback method instead of a
// dedicated thread
if (!klib.ReadDirectoryChangesW(handle, finfo.info, finfo.info.size(),
recursive, notifyMask, finfo.infoLength,
finfo.overlapped, null)) {
int err = klib.GetLastError();
throw new IOException("ReadDirectoryChangesW failed on "
+ finfo.file + ", handle " + handle
+ ": '" + getSystemError(err)
+ "' (" + err + ")");
}
if (watcher == null) {
watcher = new Thread("W32 File Monitor-" + (watcherThreadID++)) {
public void run() {
FileInfo finfo;
while (true) {
finfo = waitForChange();
if (finfo == null) {
synchronized(W32FileMonitor.this) {
if (fileMap.isEmpty()) {
watcher = null;
break;
}
}
continue;
}
try {
handleChanges(finfo);
}
catch(IOException e) {
// TODO: how is this best handled?
e.printStackTrace();
}
}
}
};
watcher.setDaemon(true);
watcher.start();
}
}
protected synchronized void unwatch(File file) {
FileInfo finfo = (FileInfo)fileMap.remove(file);
if (finfo != null) {
handleMap.remove(finfo.handle);
Kernel32 klib = Kernel32.INSTANCE;
klib.CloseHandle(finfo.handle);
}
}
protected synchronized void dispose() {
// unwatch any remaining files in map, allows watcher thread to exit
int i = 0;
for (Object[] keys = fileMap.keySet().toArray(); !fileMap.isEmpty();) {
unwatch((File)keys[i++]);
}
Kernel32 klib = Kernel32.INSTANCE;
klib.PostQueuedCompletionStatus(port, 0, null, null);
klib.CloseHandle(port);
port = null;
watcher = null;
}
private String getSystemError(int code) {
Kernel32 lib = Kernel32.INSTANCE;
PointerByReference pref = new PointerByReference();
lib.FormatMessage(Kernel32.FORMAT_MESSAGE_ALLOCATE_BUFFER
|Kernel32.FORMAT_MESSAGE_FROM_SYSTEM
|Kernel32.FORMAT_MESSAGE_IGNORE_INSERTS,
null, code,
0, pref, 0, null);
String s = pref.getValue().getString(0, !Boolean.getBoolean("w32.ascii"));
s = s.replace(".\r",".").replace(".\n",".");
lib.LocalFree(pref.getValue());
return s;
}
}
private static class KQueueFileMonitor extends FileMonitor {
protected void watch(File file, int mask, boolean recursive) {
}
protected void unwatch(File file) {
}
protected void dispose() {
}
}
private static class INotifyFileMonitor extends FileMonitor {
protected void watch(File file, int mask, boolean recursive) {
}
protected void unwatch(File file) {
}
protected void dispose() {
}
}
}