DO NOT REPLY TO THIS EMAIL, BUT PLEASE POST YOUR BUG·
RELATED COMMENTS THROUGH THE WEB INTERFACE AVAILABLE AT
<http://issues.apache.org/bugzilla/show_bug.cgi?id=35117>.
ANY REPLY MADE TO THIS MESSAGE WILL NOT BE COLLECTED AND·
INSERTED IN THE BUG DATABASE.

http://issues.apache.org/bugzilla/show_bug.cgi?id=35117

           Summary: [vfs][PATCH] External File Monitor
           Product: Commons
           Version: unspecified
          Platform: Other
        OS/Version: other
            Status: NEW
          Severity: normal
          Priority: P2
         Component: VFS
        AssignedTo: commons-dev@jakarta.apache.org
        ReportedBy: [EMAIL PROTECTED]


The VFS default external file monitor uses a fixed thread sleep time. This isn't
a problem if there are a small number of files monitored. If the number grows
however, the CPU usage grows rapidly as the processing required to check the
file states increases between calls to Thread.sleep. Monitoring 10 files per
second is fine, 1000 or 10000 files per second however starts getting too
intensive. This modification of the default monitor sleeps a modulus of the time
to sleep in milliseconds. This means that for 2000 files being monitored, a
worst case scenario would take 2 seconds before a file modification event was
broadcast, for 3000 files, 3 seconds and so on. This reduces the CPU load
considerably while still giving adequate performance for the number of files
monitored. Basically I added a sleep call if (iterFileNames % getDelay()) == 0).


/*
 * Copyright 2002, 2003, 2004, 2005 The Apache Software Foundation.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.commons.vfs.impl;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.commons.vfs.FileListener;
import org.apache.commons.vfs.FileMonitor;
import org.apache.commons.vfs.FileName;
import org.apache.commons.vfs.FileObject;
import org.apache.commons.vfs.FileSystemException;
import org.apache.commons.vfs.FileType;
import org.apache.commons.vfs.provider.AbstractFileSystem;

import java.util.HashMap;
import java.util.Map;
import java.util.Stack;


/**
 * A polling [EMAIL PROTECTED] FileMonitor} implementation.<br />
 * <br />
 * The DefaultFileMonitor is a Thread based polling file system monitor with a 1
second delay.<br />
 * <br />
 * <b>Design:</b>
 * <p/>
 * There is a Map of monitors known as FileMonitorAgents. With the thread 
running,
 * each FileMonitorAgent object is asked to "check" on the file it is
responsible for.
 * To do this check, the cache is cleared.
 * </p>
 * <ul>
 * <li>If the file existed before the refresh and it no longer exists, a delete
event is fired.</li>
 * <li>If the file existed before the refresh and it still exists, check the
last modified timestamp to see if that has changed.</li>
 * <li>If it has, fire a change event.</li>
 * </ul>
 * <p/>
 * With each file delete, the FileMonitorAgent of the parent is asked to
re-build its
 * list of children, so that they can be accurately checked when there are new
children.<br/>
 * New files are detected during each "check" as each file does a check for new
children.
 * If new children are found, create events are fired recursively if recursive
descent is
 * enabled.
 * </p>
 * <p>
 * For performance reasons, added a delay that increases as the number of files
monitored
 * increases. The default is a delay of 1 second for every 1000 files processed.
 * </p>
 * <p/>
 * <br /><b>Example usage:</b><pre>
 * FileSystemManager fsManager = VFS.getManager();
 * FileObject listendir = fsManager.resolveFile("/home/username/monitored/");
 * <p/>
 * DefaultFileMonitor fm = new DefaultFileMonitor(new CustomFileListener());
 * fm.setRecursive(true);
 * fm.addFile(listendir);
 * fm.start();
 * </pre>
 * <i>(where CustomFileListener is a class that implements the FileListener
interface.)</i>
 *
 * @author <a href="mailto:[EMAIL PROTECTED]">Christopher Ottley</a>
 * @version $Revision: 1.4 $ $Date: 2004/12/29 19:47:34 $
 */
public class DefaultFileMonitor implements Runnable, FileMonitor
{
    private final static Log log = LogFactory.getLog(DefaultFileMonitor.class);

    /**
     * Map from FileName to FileObject being monitored.
     */
    private final Map monitorMap = new HashMap();

    /**
     * The low priority thread used for checking the files being monitored.
     */
    private Thread monitorThread;

    /**
     * File objects to be removed from the monitor map.
     */
    private Stack deleteStack = new Stack();

    /**
     * File objects to be added to the monitor map.
     */
    private Stack addStack = new Stack();

    /**
     * A flag used to determine if the monitor thread should be running.
     */
    private boolean shouldRun = true;

    /**
     * A flag used to determine if adding files to be monitored should be 
recursive.
     */
    private boolean recursive = false;

    /**
     * Set the delay between checks
     */
    private long delay = 1000;

    /**
     * A listener object that if set, is notified on file creation and deletion.
     */
    private final FileListener listener;

    public DefaultFileMonitor(final FileListener listener)
    {
        this.listener = listener;
    }

    /**
     * Access method to get the recursive setting when adding files for 
monitoring.
     */
    public boolean isRecursive()
    {
        return this.recursive;
    }

    /**
     * Access method to set the recursive setting when adding files for 
monitoring.
     */
    public void setRecursive(final boolean newRecursive)
    {
        this.recursive = newRecursive;
    }

    /**
     * Access method to get the current FileListener object notified when there
     * are changes with the files added.
     */
    FileListener getFileListener()
    {
        return this.listener;
    }

    /**
     * Adds a file to be monitored.
     */
    public void addFile(final FileObject file)
    {
        synchronized (this.monitorMap)
        {
            if (this.monitorMap.get(file.getName()) == null)
            {
                this.monitorMap.put(file.getName(), new FileMonitorAgent(this,
file));

                try
                {
                    if (this.listener != null)
                    {
                        file.getFileSystem().addListener(file, this.listener);
                    }

                    if (file.getType().hasChildren() && this.recursive)
                    {
                        // Traverse the children
                        final FileObject[] children = file.getChildren();
                        for (int i = 0; i < children.length; i++)
                        {
                            this.addFile(children[i]); // Add depth first
                        }
                    }

                }
                catch (FileSystemException fse)
                {
                    log.error(fse.getLocalizedMessage(), fse);
                }

            }
        }
    }

    /**
     * Removes a file from being monitored.
     */
    public void removeFile(final FileObject file)
    {
        synchronized (this.monitorMap)
        {
            FileName fn = file.getName();
            if (this.monitorMap.get(fn) != null)
            {
                FileObject parent;
                try
                {
                    parent = file.getParent();
                }
                catch (FileSystemException fse)
                {
                    parent = null;
                }

                this.monitorMap.remove(fn);

                if (parent != null)
                { // Not the root
                    FileMonitorAgent parentAgent =
                        (FileMonitorAgent) 
this.monitorMap.get(parent.getName());
                    if (parentAgent != null)
                    {
                        parentAgent.resetChildrenList();
                    }
                }
            }
        }
    }

    /**
     * Queues a file for removal from being monitored.
     */
    protected void queueRemoveFile(final FileObject file)
    {
        this.deleteStack.push(file);
    }

    /**
     * Get the delay between runs
     */
    public long getDelay()
    {
        return delay;
    }

    /**
     * Set the delay between runs
     */
    public void setDelay(long delay)
    {
        if (delay > 0) {
          this.delay = delay;
        } else {
          this.delay = 1000;
        }
    }

    /**
     * Queues a file for addition to be monitored.
     */
    protected void queueAddFile(final FileObject file)
    {
        this.addStack.push(file);
    }

    /**
     * Starts monitoring the files that have been added.
     */
    public void start()
    {
        if (this.monitorThread == null)
        {
            this.monitorThread = new Thread(this);
            this.monitorThread.setDaemon(true);
            this.monitorThread.setPriority(Thread.MIN_PRIORITY);
        }
        this.monitorThread.start();
    }

    /**
     * Stops monitoring the files that have been added.
     */
    public void stop()
    {
        this.shouldRun = false;
    }

    /**
     * Asks the agent for each file being monitored to check its file for 
changes.
     */
    public void run()
    {
        mainloop: while (!Thread.currentThread().isInterrupted() && 
this.shouldRun)
        {
            while (!this.deleteStack.empty())
            {
                this.removeFile((FileObject) this.deleteStack.pop());
            }

            // For each entry in the map
            Object fileNames[];
            synchronized (this.monitorMap)
            {
                fileNames = this.monitorMap.keySet().toArray();
            }
            for (int iterFileNames = 0; iterFileNames < fileNames.length;
iterFileNames++)
            {
                FileName fileName = (FileName) fileNames[iterFileNames];
                FileMonitorAgent agent;
                synchronized (this.monitorMap)
                {
                    agent = (FileMonitorAgent) this.monitorMap.get(fileName);
                }
                if (agent != null)
                {
                    agent.check();
                }
                
                if ((iterFileNames % getDelay()) == 0) {
                  try
                  {
                      Thread.sleep(getDelay());
                  }
                  catch (InterruptedException e)
                  {
                  
                  }                                
                }                

                if (Thread.currentThread().isInterrupted() || !this.shouldRun)
                {
                    continue mainloop;
                }
            }

            while (!this.addStack.empty())
            {
                this.addFile((FileObject) this.addStack.pop());
            }

            try
            {
                Thread.sleep(getDelay());
            }
            catch (InterruptedException e)
            {
                continue;
            }
        }

        this.shouldRun = true;
    }

    /**
     * File monitor agent.
     */
    private static class FileMonitorAgent
    {
        private final FileObject file;
        private final DefaultFileMonitor fm;

        private boolean exists;
        private long timestamp;
        private Map children = null;

        private FileMonitorAgent(DefaultFileMonitor fm, FileObject file)
        {
            this.fm = fm;
            this.file = file;

            this.refresh();
            this.resetChildrenList();

            try
            {
                this.exists = this.file.exists();
            }
            catch (FileSystemException fse)
            {
                this.exists = false;
            }

            try
            {
                this.timestamp = this.file.getContent().getLastModifiedTime();
            }
            catch (FileSystemException fse)
            {
                this.timestamp = -1;
            }

        }

        private void resetChildrenList()
        {
            try
            {
                if (this.file.getType() == FileType.FOLDER)
                {
                    this.children = new HashMap();
                    FileObject[] childrenList = this.file.getChildren();
                    for (int i = 0; i < childrenList.length; i++)
                    {
                        this.children.put(childrenList[i].getName(), new
Object()); // null?
                    }
                }
            }
            catch (FileSystemException fse)
            {
                this.children = null;
            }
        }


        /**
         * Clear the cache and re-request the file object
         */
        private void refresh()
        {
            try
            {
                // this.file = ((AbstractFileSystem)
this.file.getFileSystem()).resolveFile(this.file.getName(), false);

                // close the file - this will detach and reattach its resources
(for this thread) on the
                // next access
                this.file.close();
            }
            catch (FileSystemException fse)
            {
                log.error(fse.getLocalizedMessage(), fse);
            }
        }


        /**
         * Recursively fires create events for all children if recursive 
descent is
         * enabled. Otherwise the create event is only fired for the initial
         * FileObject.
         */
        private void fireAllCreate(FileObject child)
        {
            // Add listener so that it can be triggered
            if (this.fm.getFileListener() != null)
            {
                child.getFileSystem().addListener(child, 
this.fm.getFileListener());
            }

            ((AbstractFileSystem) child.getFileSystem()).fireFileCreated(child);

            // Remove it because a listener is added in the queueAddFile
            if (this.fm.getFileListener() != null)
            {
                child.getFileSystem().removeListener(child,
this.fm.getFileListener());
            }

            this.fm.queueAddFile(child); // Add

            try
            {

                if (this.fm.isRecursive())
                {
                    if (child.getType() == FileType.FOLDER)
                    {
                        FileObject[] newChildren = child.getChildren();
                        for (int i = 0; i < newChildren.length; i++)
                        {
                            fireAllCreate(newChildren[i]);
                        }
                    }
                }

            }
            catch (FileSystemException fse)
            {
                log.error(fse.getLocalizedMessage(), fse);
            }
        }

        /**
         * Only checks for new children. If children are removed, they'll
         * eventually be checked.
         */
        private void checkForNewChildren()
        {
            try
            {
                if (this.file.getType() == FileType.FOLDER)
                {
                    FileObject[] newChildren = this.file.getChildren();
                    if (this.children != null)
                    {
                        // See which new children are not listed in the current
children map.
                        Map newChildrenMap = new HashMap();
                        Stack missingChildren = new Stack();

                        for (int i = 0; i < newChildren.length; i++)
                        {
                            newChildrenMap.put(newChildren[i].getName(), new
Object()); // null ?
                            // If the child's not there
                            if
(!this.children.containsKey(newChildren[i].getName()))
                            {
                                missingChildren.push(newChildren[i]);
                            }
                        }

                        this.children = newChildrenMap;

                        // If there were missing children
                        if (!missingChildren.empty())
                        {

                            while (!missingChildren.empty())
                            {
                                FileObject child = (FileObject)
missingChildren.pop();
                                this.fireAllCreate(child);
                            }
                        }

                    }
                    else
                    {
                        // First set of children - Break out the cigars
                        if (newChildren.length > 0)
                        {
                            this.children = new HashMap();
                        }
                        for (int i = 0; i < newChildren.length; i++)
                        {
                            this.children.put(newChildren[i].getName(), new
Object()); // null?
                            this.fireAllCreate(newChildren[i]);
                        }
                    }
                }
            }
            catch (FileSystemException fse)
            {
                log.error(fse.getLocalizedMessage(), fse);
            }
        }

        private void check()
        {
            this.refresh();

            try
            {
                // If the file existed and now doesn't
                if (this.exists && !this.file.exists())
                {
                    this.exists = this.file.exists();
                    this.timestamp = -1;

                    // Fire delete event

                    ((AbstractFileSystem)
this.file.getFileSystem()).fireFileDeleted(this.file);

                    // Remove listener in case file is re-created. Don't want to
fire twice.
                    if (this.fm.getFileListener() != null)
                    {
                        this.file.getFileSystem().removeListener(this.file,
                            this.fm.getFileListener());
                    }

                    // Remove from map
                    this.fm.queueRemoveFile(this.file);
                }
                else if (this.exists && this.file.exists())
                {

                    // Check the timestamp to see if it has been modified
                    if (this.timestamp !=
this.file.getContent().getLastModifiedTime())
                    {
                        this.timestamp =
this.file.getContent().getLastModifiedTime();
                        // Fire change event

                        // Don't fire if it's a folder because new file children
                        // and deleted files in a folder have their own event
triggered.
                        if (this.file.getType() != FileType.FOLDER)
                        {
                            ((AbstractFileSystem)
this.file.getFileSystem()).fireFileChanged(this.file);
                        }
                    }

                }

                this.checkForNewChildren();

            }
            catch (FileSystemException fse)
            {
                log.error(fse.getLocalizedMessage(), fse);
            }
        }

    }

}

-- 
Configure bugmail: http://issues.apache.org/bugzilla/userprefs.cgi?tab=email
------- You are receiving this mail because: -------
You are the assignee for the bug, or are watching the assignee.

---------------------------------------------------------------------
To unsubscribe, e-mail: [EMAIL PROTECTED]
For additional commands, e-mail: [EMAIL PROTECTED]

Reply via email to