/** * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2016 Maxence Bernard * * muCommander 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 3 of the License, or * (at your option) any later version. * * muCommander 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. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package com.mucommander.commons.file.util; import com.mucommander.commons.file.AbstractFile; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.WeakHashMap; /** * <code>FileMonitor</code> allows to monitor a file and detect changes in the file's attributes and notify registered * {@link FileChangeListener} listeners accordingly. * * <p> * FileMonitor detects attributes changes by polling the file's attributes at a given frequency and comparing their * values with the previous ones. If any of the monitored attributes has changed, {@link FileChangeListener#fileChanged(AbstractFile, int)} * is called on each of the registered listeners to notify them of the file attributes that have changed. * <br>Here's the list of file attributes that can be monitored: * <ul> * <li>{@link #DATE_ATTRIBUTE} * <li>{@link #SIZE_ATTRIBUTE} * <li>{@link #PERMISSIONS_ATTRIBUTE} * <li>{@link #IS_DIRECTORY_ATTRIBUTE} * <li>{@link #EXISTS_ATTRIBUTE} * </ul> * </p> * * <p>The polling frequency is controlled by the poll period. This parameter determines how often the file's attributes * are checked. The lower this period is, the faster changes will be reported to listeners, but also the higher the * impact on I/O and CPU. This parameter should be carefully specified to avoid hogging resources excessively.</p> * * <p>Note that FileMonitor uses file attributes polling because the Java API doesn't currently provide any better way * to do detect file changes. If Java ever does provide a callback mechanism for detecting file changes, this class * will be modified to take advantage of it. Another possible improvement would be to add JNI hooks for platform-specific * filesystem events such as 'inotify' (Linux Kernel), 'kqueue' (BSD, Mac OS X), PAM (Solaris), ...</p> * * @see FileChangeListener * @author Maxence Bernard */ public class FileMonitor implements FileMonitorConstants, Runnable { private static final Logger LOGGER = LoggerFactory.getLogger(FileMonitor.class); /** Monitored file */ private AbstractFile file; /** Monitored attributes */ private int attributes; /** Poll period in milliseconds, i.e. the time to elapse between two file attributes polls */ private long pollPeriod; /** The thread that actually does the file attributes polling and event firing */ private Thread monitorThread; /** * True once this monitor is ready to catch file changes, that is when the monitor thread has been started and * initial file attributes have been fetched. */ private boolean isInitialized; /** Registered FileChangeListener instances, stored as weak references */ private WeakHashMap<FileChangeListener, ?> listeners = new WeakHashMap<FileChangeListener, Object>(); /** * Creates a new FileMonitor that monitors the given file for changes, using the default attribute set (as defined * by {@link #DEFAULT_ATTRIBUTES}) and default poll period (as defined by {@link #DEFAULT_POLL_PERIOD}). * * <p>See the general constructor {@link #FileMonitor(AbstractFile, int, long)} for more information. * * @param file the AbstractFile to monitor for changes */ public FileMonitor(AbstractFile file) { this(file, DEFAULT_ATTRIBUTES, DEFAULT_POLL_PERIOD); } /** * Creates a new FileMonitor that monitors the given file for changes, using the specified attribute set and * default poll period as defined by {@link #DEFAULT_POLL_PERIOD}. * * <p>See the general constructor {@link #FileMonitor(AbstractFile, int, long)} for more information. * * @param file the AbstractFile to monitor for changes * @param attributes the set of attributes to monitor, see constant fields for a list of possible attributes */ public FileMonitor(AbstractFile file, int attributes) { this(file, attributes, DEFAULT_POLL_PERIOD); } /** * Creates a new FileMonitor that monitors the given file for changes, using the specified poll period and * default attribute set as defined by {@link #DEFAULT_ATTRIBUTES}). * * <p>See the general constructor {@link #FileMonitor(AbstractFile, int, long)} for more information. * * @param file the AbstractFile to monitor for changes * @param pollPeriod number of milliseconds between two file attributes polls */ public FileMonitor(AbstractFile file, long pollPeriod) { this(file, DEFAULT_ATTRIBUTES, pollPeriod); } /** * Creates a new FileMonitor that monitors the given file for changes, using the specified attribute set * and poll period. * * <p>Note that monitoring will only start after {@link #startMonitoring()} has been called.</p> * * <p> * The following attributes can be monitored: * <ul> * <li>{@link #DATE_ATTRIBUTE} * <li>{@link #SIZE_ATTRIBUTE} * <li>{@link #PERMISSIONS_ATTRIBUTE} * <li>{@link #IS_DIRECTORY_ATTRIBUTE} * <li>{@link #EXISTS_ATTRIBUTE} * </ul> * Several attributes can be specified by combining them with the binary OR operator. * </p> * * <p> * The poll period specified in the constructor determines how often the file's attributes will be checked. * The lower this period is, the faster changes will be reported to registered listeners, but also the higher the * impact on I/O and CPU. * <br>Note that the time spent for polling is taken into account for the poll period. For example, if the poll * period is 1000ms, and polling the file's attributes took 50ms, the next poll will happen in 950ms. * </p> * * @param file the AbstractFile to monitor for changes * @param attributes the set of attributes to monitor, see constant fields for a list of possible attributes * @param pollPeriod number of milliseconds between two file attributes polls */ public FileMonitor(AbstractFile file, int attributes, long pollPeriod) { this.file = file; this.attributes = attributes; this.pollPeriod = pollPeriod; } /** * Adds the given {@link FileChangeListener} instance to the list of registered listeners. * * <p>Listeners are stored as weak references so {@link #removeFileChangeListener(FileChangeListener)} * doesn't need to be called for listeners to be garbage collected when they're not used anymore.</p> * * @param listener the FileChangeListener to add to the list of registered listeners. * @see #removeFileChangeListener(FileChangeListener) */ public void addFileChangeListener(FileChangeListener listener) { listeners.put(listener, null); } /** * Removes the given {@link FileChangeListener} instance to the list of registered listeners. * * @param listener the FileChangeListener to remove from the list of registered listeners. * @see #addFileChangeListener(FileChangeListener) */ public void removeFileChangeListener(FileChangeListener listener) { listeners.remove(listener); } /** * Starts monitoring the monitored file in a dedicated thread. Does nothing if monitoring has already been started * and not stopped yet. Calling this method after {@link #stopMonitoring()} has been called will resume monitoring. * <p>Once started, the monitoring thread will check for changes in the monitored file attributes specified in * the constructor, and call registered {@link FileChangeListener} instances whenever a change in one or several * attributes has been detected. The poll period specified in the constructor determines how often the file's * attributes will be checked.</p> * * <p>This method waits until the thread is started effectively and the monitor is ready to monitor file changes. * This guarantees that all changes made to the monitored file after this method returns will be caught and properly * reported to listeners.</p> * * <p><code>FileMonitor</code> will keep monitoring the file until {@link #stopMonitoring()} is called, even if the * monitored file doesn't exist anymore. Thus, it is important not to forget to call {@link #stopMonitoring()} when * monitoring is not needed anymore, in order to prevent unnecessary resource hogging.</p> */ public synchronized void startMonitoring() { if(monitorThread ==null) { monitorThread = new Thread(this); monitorThread.start(); isInitialized = false; // Wait until the thread has been started and initial file attributes have been fetched while(!isInitialized) { try { wait(); // run() will notify when initialization is complete } catch(InterruptedException e) {} } } } /** * Stops monitoring the monitored file. Does nothing if monitoring has not yet been started. */ public synchronized void stopMonitoring() { monitorThread = null; } /** * Returns <code>true</code> if this FileMonitor is currently monitoring the file. * * @return true if this FileMonitor is currently monitoring the file. */ public synchronized boolean isMonitoring() { return monitorThread!=null; } /** * Notifies all registered FileChangeListener instances that the monitored file has changed, specifying which * file attributes have changed. * * @param changedAttributes the set of attributes that have changed */ private void fireFileChangeEvent(int changedAttributes) { LOGGER.info("firing an event to registered listeners, changed attributes={}", changedAttributes); // Iterate on all listeners for(FileChangeListener listener : listeners.keySet()) listener.fileChanged(file, changedAttributes); } ///////////////////////////// // Runnable implementation // ///////////////////////////// public void run() { Thread thisThread = monitorThread; long lastDate = (attributes&DATE_ATTRIBUTE)!=0?file.getDate():0; long lastSize = (attributes&SIZE_ATTRIBUTE)!=0?file.getSize():0; int lastPermissions = (attributes&PERMISSIONS_ATTRIBUTE)!=0?file.getPermissions().getIntValue():0; boolean lastIsDirectory = (attributes&IS_DIRECTORY_ATTRIBUTE)!=0 && file.isDirectory(); boolean lastExists = (attributes&EXISTS_ATTRIBUTE)!=0 && file.exists(); synchronized(this) { // We are now ready to detect file changes, notify the thread that started this thread isInitialized = true; notify(); } long now; int changedAttributes; long tempLong; int tempInt; boolean tempBool; while(monitorThread ==thisThread) { changedAttributes = 0; now = System.currentTimeMillis(); if((attributes&DATE_ATTRIBUTE)!=0) { if((tempLong=file.getDate())!=lastDate) { lastDate = tempLong; changedAttributes |= DATE_ATTRIBUTE; } } if(monitorThread ==thisThread && (attributes&SIZE_ATTRIBUTE)!=0) { if((tempLong=file.getSize())!=lastSize) { lastSize = tempLong; changedAttributes |= SIZE_ATTRIBUTE; } } if(monitorThread ==thisThread && (attributes&PERMISSIONS_ATTRIBUTE)!=0) { if((tempInt=file.getPermissions().getIntValue())!=lastPermissions) { lastPermissions = tempInt; changedAttributes |= PERMISSIONS_ATTRIBUTE; } } if(monitorThread ==thisThread && (attributes& IS_DIRECTORY_ATTRIBUTE)!=0) { if((tempBool=file.isDirectory())!=lastIsDirectory) { lastIsDirectory = tempBool; changedAttributes |= IS_DIRECTORY_ATTRIBUTE; } } if(monitorThread ==thisThread && (attributes&EXISTS_ATTRIBUTE)!=0) { if((tempBool=file.exists())!=lastExists) { lastExists = tempBool; changedAttributes |= EXISTS_ATTRIBUTE; } } if(changedAttributes!=0) fireFileChangeEvent(changedAttributes); // Get some well-deserved rest: sleep for the specified poll period minus the time we spent // for this iteration try { Thread.sleep(Math.max(pollPeriod-(System.currentTimeMillis()-now), 0)); } catch(InterruptedException e) { } } } }