001: /*
002: * Licensed to the Apache Software Foundation (ASF) under one or more
003: * contributor license agreements. See the NOTICE file distributed with
004: * this work for additional information regarding copyright ownership.
005: * The ASF licenses this file to You under the Apache License, Version 2.0
006: * (the "License"); you may not use this file except in compliance with
007: * the License. You may obtain a copy of the License at
008: *
009: * http://www.apache.org/licenses/LICENSE-2.0
010: *
011: * Unless required by applicable law or agreed to in writing, software
012: * distributed under the License is distributed on an "AS IS" BASIS,
013: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014: * See the License for the specific language governing permissions and
015: * limitations under the License.
016: */
017: package org.apache.commons.vfs.impl;
018:
019: import org.apache.commons.logging.Log;
020: import org.apache.commons.logging.LogFactory;
021: import org.apache.commons.vfs.FileListener;
022: import org.apache.commons.vfs.FileMonitor;
023: import org.apache.commons.vfs.FileName;
024: import org.apache.commons.vfs.FileObject;
025: import org.apache.commons.vfs.FileSystemException;
026: import org.apache.commons.vfs.provider.AbstractFileSystem;
027:
028: import java.util.HashMap;
029: import java.util.Map;
030: import java.util.Stack;
031:
032: /**
033: * A polling {@link FileMonitor} implementation.<br />
034: * <br />
035: * The DefaultFileMonitor is a Thread based polling file system monitor with a 1
036: * second delay.<br />
037: * <br />
038: * <b>Design:</b>
039: * <p/>
040: * There is a Map of monitors known as FileMonitorAgents. With the thread running,
041: * each FileMonitorAgent object is asked to "check" on the file it is
042: * responsible for.
043: * To do this check, the cache is cleared.
044: * </p>
045: * <ul>
046: * <li>If the file existed before the refresh and it no longer exists, a delete
047: * event is fired.</li>
048: * <li>If the file existed before the refresh and it still exists, check the
049: * last modified timestamp to see if that has changed.</li>
050: * <li>If it has, fire a change event.</li>
051: * </ul>
052: * <p/>
053: * With each file delete, the FileMonitorAgent of the parent is asked to
054: * re-build its
055: * list of children, so that they can be accurately checked when there are new
056: * children.<br/>
057: * New files are detected during each "check" as each file does a check for new
058: * children.
059: * If new children are found, create events are fired recursively if recursive
060: * descent is
061: * enabled.
062: * </p>
063: * <p/>
064: * For performance reasons, added a delay that increases as the number of files
065: * monitored
066: * increases. The default is a delay of 1 second for every 1000 files processed.
067: * </p>
068: * <p/>
069: * <br /><b>Example usage:</b><pre>
070: * FileSystemManager fsManager = VFS.getManager();
071: * FileObject listendir = fsManager.resolveFile("/home/username/monitored/");
072: * <p/>
073: * DefaultFileMonitor fm = new DefaultFileMonitor(new CustomFileListener());
074: * fm.setRecursive(true);
075: * fm.addFile(listendir);
076: * fm.start();
077: * </pre>
078: * <i>(where CustomFileListener is a class that implements the FileListener
079: * interface.)</i>
080: *
081: * @author <a href="mailto:xknight@users.sourceforge.net">Christopher Ottley</a>
082: * @version $Revision: 537938 $ $Date: 2007-05-14 11:26:40 -0700 (Mon, 14 May 2007) $
083: */
084: public class DefaultFileMonitor implements Runnable, FileMonitor {
085: private final static Log log = LogFactory
086: .getLog(DefaultFileMonitor.class);
087:
088: /**
089: * Map from FileName to FileObject being monitored.
090: */
091: private final Map monitorMap = new HashMap();
092:
093: /**
094: * The low priority thread used for checking the files being monitored.
095: */
096: private Thread monitorThread;
097:
098: /**
099: * File objects to be removed from the monitor map.
100: */
101: private Stack deleteStack = new Stack();
102:
103: /**
104: * File objects to be added to the monitor map.
105: */
106: private Stack addStack = new Stack();
107:
108: /**
109: * A flag used to determine if the monitor thread should be running.
110: */
111: private boolean shouldRun = true;
112:
113: /**
114: * A flag used to determine if adding files to be monitored should be recursive.
115: */
116: private boolean recursive = false;
117:
118: /**
119: * Set the delay between checks
120: */
121: private long delay = 1000;
122:
123: /**
124: * Set the number of files to check until a delay will be inserted
125: */
126: private int checksPerRun = 1000;
127:
128: /**
129: * A listener object that if set, is notified on file creation and deletion.
130: */
131: private final FileListener listener;
132:
133: public DefaultFileMonitor(final FileListener listener) {
134: this .listener = listener;
135: }
136:
137: /**
138: * Access method to get the recursive setting when adding files for monitoring.
139: */
140: public boolean isRecursive() {
141: return this .recursive;
142: }
143:
144: /**
145: * Access method to set the recursive setting when adding files for monitoring.
146: */
147: public void setRecursive(final boolean newRecursive) {
148: this .recursive = newRecursive;
149: }
150:
151: /**
152: * Access method to get the current FileListener object notified when there
153: * are changes with the files added.
154: */
155: FileListener getFileListener() {
156: return this .listener;
157: }
158:
159: /**
160: * Adds a file to be monitored.
161: */
162: public void addFile(final FileObject file) {
163: _addFile(file);
164: try {
165: // add all direct children too
166: if (file.getType().hasChildren()) {
167: // Traverse the children
168: final FileObject[] children = file.getChildren();
169: for (int i = 0; i < children.length; i++) {
170: _addFile(children[i]);
171: }
172: }
173: } catch (FileSystemException fse) {
174: log.error(fse.getLocalizedMessage(), fse);
175: }
176: }
177:
178: /**
179: * Adds a file to be monitored.
180: */
181: private void _addFile(final FileObject file) {
182: synchronized (this .monitorMap) {
183: if (this .monitorMap.get(file.getName()) == null) {
184: this .monitorMap.put(file.getName(),
185: new FileMonitorAgent(this , file));
186:
187: try {
188: if (this .listener != null) {
189: file.getFileSystem().addListener(file,
190: this .listener);
191: }
192:
193: if (file.getType().hasChildren() && this .recursive) {
194: // Traverse the children
195: final FileObject[] children = file
196: .getChildren();
197: for (int i = 0; i < children.length; i++) {
198: this .addFile(children[i]); // Add depth first
199: }
200: }
201:
202: } catch (FileSystemException fse) {
203: log.error(fse.getLocalizedMessage(), fse);
204: }
205:
206: }
207: }
208: }
209:
210: /**
211: * Removes a file from being monitored.
212: */
213: public void removeFile(final FileObject file) {
214: synchronized (this .monitorMap) {
215: FileName fn = file.getName();
216: if (this .monitorMap.get(fn) != null) {
217: FileObject parent;
218: try {
219: parent = file.getParent();
220: } catch (FileSystemException fse) {
221: parent = null;
222: }
223:
224: this .monitorMap.remove(fn);
225:
226: if (parent != null) { // Not the root
227: FileMonitorAgent parentAgent = (FileMonitorAgent) this .monitorMap
228: .get(parent.getName());
229: if (parentAgent != null) {
230: parentAgent.resetChildrenList();
231: }
232: }
233: }
234: }
235: }
236:
237: /**
238: * Queues a file for removal from being monitored.
239: */
240: protected void queueRemoveFile(final FileObject file) {
241: this .deleteStack.push(file);
242: }
243:
244: /**
245: * Get the delay between runs
246: */
247: public long getDelay() {
248: return delay;
249: }
250:
251: /**
252: * Set the delay between runs
253: */
254: public void setDelay(long delay) {
255: if (delay > 0) {
256: this .delay = delay;
257: } else {
258: this .delay = 1000;
259: }
260: }
261:
262: /**
263: * get the number of files to check per run
264: */
265: public int getChecksPerRun() {
266: return checksPerRun;
267: }
268:
269: /**
270: * set the number of files to check per run.
271: * a additional delay will be added if there are more files to check
272: *
273: * @param checksPerRun a value less than 1 will disable this feature
274: */
275: public void setChecksPerRun(int checksPerRun) {
276: this .checksPerRun = checksPerRun;
277: }
278:
279: /**
280: * Queues a file for addition to be monitored.
281: */
282: protected void queueAddFile(final FileObject file) {
283: this .addStack.push(file);
284: }
285:
286: /**
287: * Starts monitoring the files that have been added.
288: */
289: public void start() {
290: if (this .monitorThread == null) {
291: this .monitorThread = new Thread(this );
292: this .monitorThread.setDaemon(true);
293: this .monitorThread.setPriority(Thread.MIN_PRIORITY);
294: }
295: this .monitorThread.start();
296: }
297:
298: /**
299: * Stops monitoring the files that have been added.
300: */
301: public void stop() {
302: this .shouldRun = false;
303: }
304:
305: /**
306: * Asks the agent for each file being monitored to check its file for changes.
307: */
308: public void run() {
309: mainloop: while (!monitorThread.isInterrupted()
310: && this .shouldRun) {
311: while (!this .deleteStack.empty()) {
312: this .removeFile((FileObject) this .deleteStack.pop());
313: }
314:
315: // For each entry in the map
316: Object fileNames[];
317: synchronized (this .monitorMap) {
318: fileNames = this .monitorMap.keySet().toArray();
319: }
320: for (int iterFileNames = 0; iterFileNames < fileNames.length; iterFileNames++) {
321: FileName fileName = (FileName) fileNames[iterFileNames];
322: FileMonitorAgent agent;
323: synchronized (this .monitorMap) {
324: agent = (FileMonitorAgent) this .monitorMap
325: .get(fileName);
326: }
327: if (agent != null) {
328: agent.check();
329: }
330:
331: if (getChecksPerRun() > 0) {
332: if ((iterFileNames % getChecksPerRun()) == 0) {
333: try {
334: Thread.sleep(getDelay());
335: } catch (InterruptedException e) {
336:
337: }
338: }
339: }
340:
341: if (monitorThread.isInterrupted() || !this .shouldRun) {
342: continue mainloop;
343: }
344: }
345:
346: while (!this .addStack.empty()) {
347: this .addFile((FileObject) this .addStack.pop());
348: }
349:
350: try {
351: Thread.sleep(getDelay());
352: } catch (InterruptedException e) {
353: continue;
354: }
355: }
356:
357: this .shouldRun = true;
358: }
359:
360: /**
361: * File monitor agent.
362: */
363: private static class FileMonitorAgent {
364: private final FileObject file;
365: private final DefaultFileMonitor fm;
366:
367: private boolean exists;
368: private long timestamp;
369: private Map children = null;
370:
371: private FileMonitorAgent(DefaultFileMonitor fm, FileObject file) {
372: this .fm = fm;
373: this .file = file;
374:
375: this .refresh();
376: this .resetChildrenList();
377:
378: try {
379: this .exists = this .file.exists();
380: } catch (FileSystemException fse) {
381: this .exists = false;
382: }
383:
384: try {
385: this .timestamp = this .file.getContent()
386: .getLastModifiedTime();
387: } catch (FileSystemException fse) {
388: this .timestamp = -1;
389: }
390:
391: }
392:
393: private void resetChildrenList() {
394: try {
395: if (this .file.getType().hasChildren()) {
396: this .children = new HashMap();
397: FileObject[] childrenList = this .file.getChildren();
398: for (int i = 0; i < childrenList.length; i++) {
399: this .children.put(childrenList[i].getName(),
400: new Object()); // null?
401: }
402: }
403: } catch (FileSystemException fse) {
404: this .children = null;
405: }
406: }
407:
408: /**
409: * Clear the cache and re-request the file object
410: */
411: private void refresh() {
412: try {
413: this .file.refresh();
414: } catch (FileSystemException fse) {
415: log.error(fse.getLocalizedMessage(), fse);
416: }
417: }
418:
419: /**
420: * Recursively fires create events for all children if recursive descent is
421: * enabled. Otherwise the create event is only fired for the initial
422: * FileObject.
423: */
424: private void fireAllCreate(FileObject child) {
425: // Add listener so that it can be triggered
426: if (this .fm.getFileListener() != null) {
427: child.getFileSystem().addListener(child,
428: this .fm.getFileListener());
429: }
430:
431: ((AbstractFileSystem) child.getFileSystem())
432: .fireFileCreated(child);
433:
434: // Remove it because a listener is added in the queueAddFile
435: if (this .fm.getFileListener() != null) {
436: child.getFileSystem().removeListener(child,
437: this .fm.getFileListener());
438: }
439:
440: this .fm.queueAddFile(child); // Add
441:
442: try {
443:
444: if (this .fm.isRecursive()) {
445: if (child.getType().hasChildren()) {
446: FileObject[] newChildren = child.getChildren();
447: for (int i = 0; i < newChildren.length; i++) {
448: fireAllCreate(newChildren[i]);
449: }
450: }
451: }
452:
453: } catch (FileSystemException fse) {
454: log.error(fse.getLocalizedMessage(), fse);
455: }
456: }
457:
458: /**
459: * Only checks for new children. If children are removed, they'll
460: * eventually be checked.
461: */
462: private void checkForNewChildren() {
463: try {
464: if (this .file.getType().hasChildren()) {
465: FileObject[] newChildren = this .file.getChildren();
466: if (this .children != null) {
467: // See which new children are not listed in the current children map.
468: Map newChildrenMap = new HashMap();
469: Stack missingChildren = new Stack();
470:
471: for (int i = 0; i < newChildren.length; i++) {
472: newChildrenMap.put(
473: newChildren[i].getName(),
474: new Object()); // null ?
475: // If the child's not there
476: if (!this .children
477: .containsKey(newChildren[i]
478: .getName())) {
479: missingChildren.push(newChildren[i]);
480: }
481: }
482:
483: this .children = newChildrenMap;
484:
485: // If there were missing children
486: if (!missingChildren.empty()) {
487:
488: while (!missingChildren.empty()) {
489: FileObject child = (FileObject) missingChildren
490: .pop();
491: this .fireAllCreate(child);
492: }
493: }
494:
495: } else {
496: // First set of children - Break out the cigars
497: if (newChildren.length > 0) {
498: this .children = new HashMap();
499: }
500: for (int i = 0; i < newChildren.length; i++) {
501: this .children.put(newChildren[i].getName(),
502: new Object()); // null?
503: this .fireAllCreate(newChildren[i]);
504: }
505: }
506: }
507: } catch (FileSystemException fse) {
508: log.error(fse.getLocalizedMessage(), fse);
509: }
510: }
511:
512: private void check() {
513: this .refresh();
514:
515: try {
516: // If the file existed and now doesn't
517: if (this .exists && !this .file.exists()) {
518: this .exists = this .file.exists();
519: this .timestamp = -1;
520:
521: // Fire delete event
522:
523: ((AbstractFileSystem) this .file.getFileSystem())
524: .fireFileDeleted(this .file);
525:
526: // Remove listener in case file is re-created. Don't want to fire twice.
527: if (this .fm.getFileListener() != null) {
528: this .file.getFileSystem().removeListener(
529: this .file, this .fm.getFileListener());
530: }
531:
532: // Remove from map
533: this .fm.queueRemoveFile(this .file);
534: } else if (this .exists && this .file.exists()) {
535:
536: // Check the timestamp to see if it has been modified
537: if (this .timestamp != this .file.getContent()
538: .getLastModifiedTime()) {
539: this .timestamp = this .file.getContent()
540: .getLastModifiedTime();
541: // Fire change event
542:
543: // Don't fire if it's a folder because new file children
544: // and deleted files in a folder have their own event triggered.
545: if (!this .file.getType().hasChildren()) {
546: ((AbstractFileSystem) this .file
547: .getFileSystem())
548: .fireFileChanged(this .file);
549: }
550: }
551:
552: }
553:
554: this .checkForNewChildren();
555:
556: } catch (FileSystemException fse) {
557: log.error(fse.getLocalizedMessage(), fse);
558: }
559: }
560:
561: }
562:
563: }
|