/** * Copyright (c) 2005-2013 by Appcelerator, Inc. All Rights Reserved. * Licensed under the terms of the Eclipse Public License (EPL). * Please see the license.txt included with this distribution for details. * Any modifications to this file must keep this entire header intact. */ package org.python.pydev.shared_core.path_watch; import java.io.File; import java.io.FileFilter; import java.nio.file.Path; import java.nio.file.WatchKey; import java.util.HashMap; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import org.eclipse.core.runtime.Assert; import org.python.pydev.shared_core.callbacks.ListenerList; import org.python.pydev.shared_core.io.FileUtils; import org.python.pydev.shared_core.log.Log; import org.python.pydev.shared_core.string.FastStringBuffer; import org.python.pydev.shared_core.structure.OrderedMap; /** * This object will stack many ADD/REMOVE changes into a single change. It also deals with OVERFLOW changes, which * mean that too many changes occurred and thus can't be properly mapped to the actual events. In this case, * a notification that the base path was removed and then added again is issued (listener clients must take care * of properly dealing with this notification, as no events of added/removed children will be issued in this case). * * @author fabioz */ public class EventsStackerRunnable implements Runnable { public final static int ADDED = 0; public final static int REMOVED = 1; /** * As we only get notifications in directories from the files beneath it, or files within a folder * within it, we should hear 2 levels to get modifications. */ public final static int LEVELS_TO_GET_MODIFIED_TIME = 2; /** * May be null! */ /*default*/volatile WatchKey key; public final ListenerList<IFilesystemChangesListener> list; public final Path watchedPath; /** * Lock for dealing with fileToEvent and overflow. */ private final Object lock = new Object(); /** * The file mapping to the last event recorded in it. */ private Map<File, Integer> fileToEvent = new OrderedMap<File, Integer>(); /** * The directory being watched, where the overflow occurred. */ private volatile File overflow = null; /** * The file related to the watchedPath we're listening. */ private final File file; /** * This is the time of the last modified file we're interested in in the directory we're watching. * * If not a directory, it's not considered. */ private final Map<File, Long> internalDirToLastModifiedTime = new HashMap<File, Long>(); /** * Identifies whether we're watching a dir or not. */ private final boolean isDir; /** * The file filter we're dealing with. */ private final FileFilter fileFilter; /** * The filter for directories. */ private final FileFilter dirFilter; private static final Long DIRECTORY_WITH_NOTHING_INTERESTING = 0L; private volatile boolean initializationFinished = false; private final Object lockInitialization = new Object(); /** * Creates the events stacker based on the key, path and listeners related (the contents of the listeners may * change later on, but the actual key and path may not change). * @param fileFilter * @param path */ public EventsStackerRunnable(WatchKey key, Path watchedPath, ListenerList<IFilesystemChangesListener> list, final File file, final FileFilter fileFilter, final FileFilter dirFilter) { Assert.isNotNull(list); Assert.isNotNull(watchedPath); this.list = list; this.key = key; //the key may be null! this.watchedPath = watchedPath; this.file = file; isDir = file.isDirectory(); this.fileFilter = fileFilter; this.dirFilter = dirFilter; if (isDir) { new Thread() { @Override public void run() { try { File[] listFiles = file.listFiles(); if (listFiles != null) { for (File f : listFiles) { if (f.isDirectory()) { if (dirFilter.accept(f)) { long lastModifiedTimeFromDir = FileUtils.getLastModifiedTimeFromDir(f, fileFilter, dirFilter, LEVELS_TO_GET_MODIFIED_TIME); if (lastModifiedTimeFromDir != 0) { internalDirToLastModifiedTime.put( f, lastModifiedTimeFromDir); } else { internalDirToLastModifiedTime.put( f, DIRECTORY_WITH_NOTHING_INTERESTING); } } } } } } finally { initializationFinished = true; } }; }.start(); } else { initializationFinished = true; } } /** * When run, it'll notify clients about the events that were stacked as it makes sense (i.e.: if multiple additions * or removals of a file were issued, only the last one will actually be seen by clients). */ @Override public void run() { while (!initializationFinished) { synchronized (lockInitialization) { try { lockInitialization.wait(100); } catch (InterruptedException e) { } } } Map<File, Integer> currentFileToEvent; File currentOverflow; boolean dirExists = true; synchronized (lock) { currentFileToEvent = fileToEvent; fileToEvent = new OrderedMap<File, Integer>(); currentOverflow = overflow; overflow = null; } IFilesystemChangesListener[] listeners = list.getListeners(); if (listeners.length == 0) { return; } if (isDir) { dirExists = file.exists(); if (!dirExists) { //Special case if we were watching a directory and it no longer exists... for (IFilesystemChangesListener iFilesystemChangesListener : listeners) { try { iFilesystemChangesListener.removed(file); } catch (Exception e) { Log.log(e); } } //Directory no longer exists: just bail out! return; } } if (currentOverflow != null) { for (IFilesystemChangesListener iFilesystemChangesListener : listeners) { //Say that the dir was removed... File watched = file; iFilesystemChangesListener.removed(watched); if (watched.exists()) { //And later added again (without notifying about inner contents!!) iFilesystemChangesListener.added(watched); } } return; } Set<Entry<File, Integer>> entrySet = currentFileToEvent.entrySet(); for (Entry<File, Integer> entry : entrySet) { Integer value = entry.getValue(); File currKey = entry.getKey(); if (isDir) { Long lastModifiedTime = internalDirToLastModifiedTime.get(currKey); if (currKey.isDirectory()) { long newLast = FileUtils .getLastModifiedTimeFromDir(currKey, fileFilter, dirFilter, LEVELS_TO_GET_MODIFIED_TIME); if (lastModifiedTime != null && newLast == lastModifiedTime) { continue; //nothing interesting changed, just go on... } if (newLast == 0 && lastModifiedTime == null) { //nothing interesting added either... //Note: register as seen but with nothing interesting. internalDirToLastModifiedTime.put(currKey, DIRECTORY_WITH_NOTHING_INTERESTING); continue; } if (newLast == 0 && lastModifiedTime != null) { //interesting content was removed (notify about it). internalDirToLastModifiedTime.put(currKey, DIRECTORY_WITH_NOTHING_INTERESTING); } else { //interesting content changed internalDirToLastModifiedTime.put(currKey, newLast); } } else { if (lastModifiedTime != null) { if (value == ADDED && !currKey.exists()) { //we have an add notification from addition of a directory that no longer exists. //Don't notify: just wait for the remove notification. continue; } //The internal directory was removed. internalDirToLastModifiedTime.remove(currKey); if (lastModifiedTime == DIRECTORY_WITH_NOTHING_INTERESTING) { continue; } } else { //Ok, it's really a file inside a directory: let's check if it's interesting... if (!fileFilter.accept(currKey)) { continue; } } } } switch (value) { case ADDED: for (IFilesystemChangesListener iFilesystemChangesListener : listeners) { try { iFilesystemChangesListener.added(currKey); } catch (Exception e) { Log.log(e); } } break; case REMOVED: for (IFilesystemChangesListener iFilesystemChangesListener : listeners) { try { iFilesystemChangesListener.removed(currKey); } catch (Exception e) { Log.log(e); } } break; } } } /** * On overflow we'll clear all the other events and just send the overflow (any subsequent event is * ignored until the overflow is signaled). */ public void overflow(File file) { synchronized (lock) { this.overflow = file; fileToEvent.clear(); } } public void added(File file) { synchronized (lock) { if (overflow == null) { fileToEvent.put(file, ADDED); } } } public void removed(File file) { synchronized (lock) { if (overflow == null) { fileToEvent.put(file, REMOVED); } } } /* (non-Javadoc) * @see java.lang.Object#toString() */ @Override public String toString() { return new FastStringBuffer().append("EventsStackerRunnable(key=").appendObject(this.key) .append(";watchedPath=").appendObject(this.watchedPath).append(";overflow=") .appendObject(this.overflow).append(";fileToEvent=").appendObject(this.fileToEvent) .append(";listeners=").appendObject(this.list.getListeners()).append(")").toString(); } }