All Downloads are FREE. Search and download functionalities are using the official Maven repository.

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