
net.contentobjects.jnotify.macosx.JNotifyAdapterMacOSX Maven / Gradle / Ivy
The newest version!
package net.contentobjects.jnotify.macosx;
import java.io.File;
import java.io.IOException;
import java.util.Comparator;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.Map;
import java.util.TreeMap;
import java.util.TreeSet;
import net.contentobjects.jnotify.IJNotify;
import net.contentobjects.jnotify.JNotify;
import net.contentobjects.jnotify.JNotifyException;
import net.contentobjects.jnotify.JNotifyListener;
public class JNotifyAdapterMacOSX implements IJNotify
{
/**
* A JNFile uniquely identifies a file and stores it's mtime.
*/
private static class JNFile implements Comparable
{
long mtime;
int deviceid;
long inode;
// load the stat function
static
{
System.loadLibrary("jnotify"); //$NON-NLS-1$
}
JNFile(File f) throws IOException
{
mtime = f.lastModified();
stat(f.getAbsolutePath());
}
private native void stat(String absolutePath) throws IOException;
/**
* Compares the deviceid and inode of two JNFiles.
*/
public int compareTo(JNFile o)
{
if (o.deviceid != deviceid)
{
return deviceid - o.deviceid;
}
if (inode < o.inode)
{
return -1;
}
if (inode == o.inode)
{
return 0;
}
return 1;
}
/**
* Returns true if o refers to the same file as this.
*/
@Override
public boolean equals(Object o)
{
if (!(o instanceof JNFile))
{
return false;
}
JNFile j = (JNFile) o;
return j.inode == inode && j.deviceid == deviceid;
}
@Override
public int hashCode()
{
return (inode + "," + deviceid).hashCode(); //$NON-NLS-1$
}
@Override
public String toString()
{
return String.format("%08x.%016x - %d", deviceid, inode, mtime); //$NON-NLS-1$
}
}
/**
* Store information about each watch ID.
*/
private Hashtable _id2Data;
public JNotifyAdapterMacOSX()
{
JNotify_macosx.setNotifyListener(new FSEventListener()
{
public void notifyChange(int wd, String rootPath, String filePath,
boolean recurse)
{
notifyChangeEvent(wd, rootPath, filePath, recurse);
}
public void batchStart(int wd)
{
batchStartEvent(wd);
}
public void batchEnd(int wd)
{
batchEndEvent(wd);
}
});
_id2Data = new Hashtable();
}
public int addWatch(String path, int mask, boolean watchSubtree,
JNotifyListener listener) throws JNotifyException
{
File f;
try
{
f = new File(path).getCanonicalFile();
}
catch (IOException e)
{
throw new JNotifyException_macosx(
"Could not resolve canonical path for " + path);
}
int wd = JNotify_macosx.addWatch(f.getPath());
_id2Data.put(Integer.valueOf(wd), new WatchData(wd, mask, listener,
path, f, watchSubtree));
return wd;
}
public boolean removeWatch(int wd) throws JNotifyException
{
synchronized (_id2Data)
{
boolean removed = _id2Data.remove(Integer.valueOf(wd)) != null;
if (removed)
{
JNotify_macosx.removeWatch(wd);
}
return removed;
}
}
/**
* A path to scan and whether it needs recursed.
*/
private static class ScanJob
{
String path;
boolean recursive;
ScanJob(String path, boolean recursive)
{
this.path = path;
this.recursive = recursive;
}
@Override
public String toString()
{
return path + " " + recursive; //$NON-NLS-1$
}
}
/**
* A set of changes detected in a batch.
*/
private static class JNEvents
{
TreeMap> created;
TreeSet modified;
TreeMap> deleted;
TreeMap renamed;
JNEvents(int mask) {
if ((mask & JNotify.FILE_MODIFIED) != 0)
{
modified = new TreeSet();
}
if ((mask & (JNotify.FILE_CREATED | JNotify.FILE_DELETED | JNotify.FILE_RENAMED)) != 0)
{
created = new TreeMap>();
deleted = new TreeMap>();
renamed = new TreeMap();
}
}
}
private static T pollFirst(TreeSet set)
{
T result = set.first();
if (result != null)
{
set.remove(result);
}
return result;
}
/**
* Data associated with a watch.
*/
private static class WatchData
{
int _wd;
int _mask;
JNotifyListener _notifyListener;
TreeMap> paths;
TreeMap jnfiles;
LinkedList toScan;
String path;
String fullpath;
boolean watchSubtree;
WatchData(int wd, int mask, JNotifyListener listener, String path,
File pathFile, boolean watchSubtree)
{
_wd = wd;
_mask = mask;
_notifyListener = listener;
this.path = path;
this.fullpath = pathFile.getPath() + "/"; //$NON-NLS-1$
this.watchSubtree = watchSubtree;
paths = new TreeMap>();
jnfiles = new TreeMap();
scan(pathFile, true, null);
toScan = new LinkedList();
}
public String toString()
{
return "wd=" + _wd; //$NON-NLS-1$
}
/**
* Checks a directory for changes.
* @param job the directory to scan
* @param events the set to place the changes in
*/
private void scan(ScanJob job, JNEvents events)
{
scan(new File(job.path), job.recursive, events);
}
private static TreeMap> groupByNextComponent(File root, Map input)
{
TreeMap> grouped = new TreeMap>();
String lastDir = null;
TreeMap lastGroup = null;
String rootPath = root.getAbsolutePath() + "/";
for (Map.Entry entry : input.entrySet())
{
if (!entry.getKey().startsWith(rootPath))
{
continue;
}
String dir = entry.getKey().substring(rootPath.length());
int slashIndex = dir.indexOf('/');
if (slashIndex != -1)
{
dir = dir.substring(0, slashIndex);
}
if (lastDir == null || !lastDir.equals(dir))
{
lastDir = dir;
lastGroup = grouped.get(dir);
if (lastGroup == null)
{
lastGroup = new TreeMap();
grouped.put(dir, lastGroup);
}
}
lastGroup.put(entry.getKey(), entry.getValue());
}
return grouped;
}
@SuppressWarnings("unchecked")
private static Map.Entry floorEntry(TreeMap map, K target)
{
Map.Entry result = null;
Comparator super K> compare = map.comparator();
for (Map.Entry entry : map.entrySet())
{
if (compare == null)
{
if (((Comparable)target).compareTo(entry.getKey()) < 0)
{
break;
}
}
else
{
if (compare.compare(target, entry.getKey()) < 0)
{
break;
}
}
result = entry;
}
return result;
}
/**
* Checks a directory for changes.
* @param root the directory to scan
* @param recursive whether subdirectories should be scanned
* @param events the set to place the changes in
*/
private void scan(File root, boolean recursive, JNEvents events)
{
File[] files = root.listFiles();
Map existingfiles = jnfiles.tailMap(root.getAbsolutePath());
TreeSet stillAlive = null;
String rootPath = root.getAbsolutePath() + "/";
// check for created/modified/recreated
if (files != null)
{
// store all the entries for later
stillAlive = new TreeSet();
for (int i = 0; i < files.length; i++)
{
// get the path relative to rootPath
String filePath = files[i].getAbsolutePath();
filePath = filePath.substring(rootPath.length());
// use only the next path component
int slashindex = filePath.indexOf("/"); //$NON-NLS-1$
if (slashindex >= 0)
{
filePath = filePath.substring(0, slashindex);
}
// store for later(when looking for deletions)
stillAlive.add(filePath);
try
{
JNFile jnf = new JNFile(files[i]);
// check if this inode is already known
Map.Entry> oldEntry = floorEntry(paths, jnf);
TreeSet plist;
if (oldEntry == null || !jnf.equals(oldEntry.getKey()))
{
// new inode
plist = new TreeSet();
paths.put(jnf, plist);
}
else
{
// we've seen this inode before
plist = oldEntry.getValue();
JNFile oldKey = oldEntry.getKey();
if (oldKey.mtime != jnf.mtime)
{
// file modified
oldKey.mtime = jnf.mtime;
// record the changes in events
// don't do this with directories!
if (events != null && events.modified != null && !files[i].isDirectory())
{
for (String path : plist)
{
events.modified.add(path);
}
}
}
}
String path = files[i].getAbsolutePath();
boolean isNewPath = !plist.contains(path);
if (isNewPath)
{
// new file
// might not be a new inode
// add path to inode in map
plist.add(path);
// record change in events
if (events != null && events.created != null) {
TreeSet eplist = events.created.get(jnf);
if (eplist == null)
{
eplist = new TreeSet();
events.created.put(jnf, eplist);
}
eplist.add(path);
}
}
// update the inode and check if the inode has changed
JNFile oldjnf = jnfiles.put(path, jnf);
if (oldjnf != null && !jnf.equals(oldjnf))
{
// deleted and recreated
// remove this path from its old inode
TreeSet oldPaths = paths.get(oldjnf);
if (oldPaths == null)
{
// this shouldn't happen!
}
else
{
if (!oldPaths.remove(path))
{
// this shouldn't happen!
}
if (oldPaths.size() == 0) {
// inode is gone
paths.remove(oldjnf);
}
}
// record this change in events
// the create event is recorded earlier
if (events != null && events.deleted != null)
{
TreeSet eplist = events.deleted.get(oldjnf);
if (eplist == null)
{
eplist = new TreeSet();
events.deleted.put(oldjnf, eplist);
}
eplist.add(path);
}
}
boolean recurse = isNewPath || recursive;
if (watchSubtree && recurse && files[i].isDirectory())
{
scan(files[i], recurse, events);
}
}
catch(IOException e)
{
// should be fine here
e.printStackTrace();
}
}
}
// check for deleted files
TreeMap> grouped = groupByNextComponent(root, existingfiles);
Iterator stillAliveit = null;
String lastAlive = null;
if (stillAlive != null)
{
stillAliveit = stillAlive.iterator();
if (stillAliveit.hasNext())
{
lastAlive = stillAliveit.next();
}
}
for (Map.Entry> entry : grouped.entrySet())
{
String jnp = entry.getKey();
if (lastAlive != null)
{
int compare = -1;
while (lastAlive != null && (compare = jnp.compareTo(lastAlive)) > 0)
{
if (stillAliveit.hasNext())
{
lastAlive = stillAliveit.next();
}
else
{
lastAlive = null;
}
}
if (compare == 0 && lastAlive != null)
{
// this file still exists
continue;
}
}
// file no longer exists
for (Map.Entry jnf : entry.getValue().entrySet())
{
TreeSet oldPaths = paths.get(jnf.getValue());
// remove this path from its inode
if (oldPaths == null)
{
// this shouldn't happen!
}
else
{
if (!oldPaths.remove(jnf.getKey()))
{
// this shouldn't happen!
}
if (oldPaths.size() == 0) {
// inode is gone
paths.remove(jnf.getValue());
}
}
// remove this path from the map
existingfiles.remove(jnf.getKey());
// record this change in events
if (events != null && events.deleted != null)
{
TreeSet eplist = events.deleted.get(jnf.getValue());
if (eplist == null)
{
eplist = new TreeSet();
events.deleted.put(jnf.getValue(), eplist);
}
eplist.add(jnf.getKey());
}
}
}
}
}
void notifyChangeEvent(int wd, String rootPath, String filePath,
boolean recurse)
{
synchronized (_id2Data)
{
WatchData watchData = _id2Data.get(Integer.valueOf(wd));
if (watchData != null)
{
// queue this job for when the batch ends
watchData.toScan.add(new ScanJob(filePath, recurse));
}
}
}
void batchStartEvent(int wd)
{
WatchData watchData = _id2Data.get(Integer.valueOf(wd));
if (watchData != null)
{
watchData.toScan.clear();
}
}
void batchEndEvent(int wd)
{
WatchData watchData = _id2Data.get(Integer.valueOf(wd));
if (watchData != null)
{
JNEvents e = new JNEvents(watchData._mask);
// scan all jobs
ScanJob job;
while((job = watchData.toScan.poll()) != null)
{
if (watchData.watchSubtree || watchData.fullpath.equals(job.path))
{
watchData.scan(job, e);
}
}
// check for renames
// these are guesses
// we can't handle a file being renamed and hardlinked in a single batch
// we can't detect files being renamed into our directory from outside
if (e.created != null && e.deleted != null && e.renamed != null)
{
Iterator>> createdIt = e.created.entrySet().iterator();
Iterator>> deletedIt = e.deleted.entrySet().iterator();
Map.Entry> created = null;
if (createdIt.hasNext())
created = createdIt.next();
Map.Entry> deleted = null;
if (deletedIt.hasNext())
deleted = deletedIt.next();
while (created != null && deleted != null)
{
int compare = created.getKey().compareTo(deleted.getKey());
if (compare < 0)
{
if (createdIt.hasNext())
{
created = createdIt.next();
continue;
}
break;
}
if (compare > 0)
{
if (deletedIt.hasNext())
{
deleted = deletedIt.next();
continue;
}
break;
}
// a deleted file and a created file have the same inode
// merge into a rename event
String newpath = pollFirst(created.getValue());
String oldpath = pollFirst(deleted.getValue());
e.renamed.put(oldpath, newpath);
// this inode is no longer associated with anything
if (created.getValue().size() == 0)
{
createdIt.remove();
created = null;
}
if (deleted.getValue().size() == 0)
{
deletedIt.remove();
deleted = null;
}
// try to get new inodes to compare
if (created == null)
{
if (createdIt.hasNext())
{
created = createdIt.next();
}
else
{
// impossible to find more matches
break;
}
}
if (deleted == null)
{
if (deletedIt.hasNext())
{
deleted = deletedIt.next();
}
else
{
// impossible to find more matches
break;
}
}
}
}
// send events
if ((watchData._mask & JNotify.FILE_CREATED) != 0)
{
for (TreeSet cpaths : e.created.values())
{
for (String path : cpaths)
{
watchData._notifyListener.fileCreated(wd, watchData.path, path.substring(watchData.fullpath.length()));
}
}
}
if ((watchData._mask & JNotify.FILE_DELETED) != 0)
{
for (TreeSet cpaths : e.deleted.values())
{
for (String path : cpaths)
{
watchData._notifyListener.fileDeleted(wd, watchData.path, path.substring(watchData.fullpath.length()));
}
}
}
if ((watchData._mask & JNotify.FILE_MODIFIED) != 0)
{
for (String path : e.modified)
{
watchData._notifyListener.fileModified(wd, watchData.path, path.substring(watchData.fullpath.length()));
}
}
if ((watchData._mask & JNotify.FILE_RENAMED) != 0)
{
for (Map.Entry entry : e.renamed.entrySet())
{
watchData._notifyListener.fileRenamed(wd, watchData.path, entry.getKey().substring(watchData.fullpath.length()), entry.getValue().substring(watchData.fullpath.length()));
}
}
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy