001: /*
002:
003: This software is OSI Certified Open Source Software.
004: OSI Certified is a certification mark of the Open Source Initiative.
005:
006: The license (Mozilla version 1.0) can be read at the MMBase site.
007: See http://www.MMBase.org/license
008:
009: */
010:
011: package org.mmbase.util;
012:
013: import java.io.File;
014: import java.util.*;
015: import org.mmbase.util.logging.*;
016: import org.mmbase.util.xml.UtilReader;
017: import java.util.concurrent.CopyOnWriteArraySet;
018:
019: /**
020: * Original javadoc.
021:
022: * This will run as a thread after it has been started.
023: * It will check every interval if one of it's files has been changed.
024: * When one of them has been changed, the onChange method will be called, with the file that
025: * was changed. After that the thread will stop.
026: * To stop a running thread, call the method exit();
027: *
028: * Example:
029: * <code style="white-space:pre;">
030: * class FooFileWatcher extends FileWatcher {
031: *
032: * public FooFileWatcher() {
033: * super(true); // true: keep reading.
034: * }
035: *
036: * public void onChange(File file) {
037: * System.out.println(file.getAbsolutePath());
038: * }
039: * }
040: *
041: * // create new instance
042: * FooFileWatcher watcher = new FooFileWatcher();
043: * // set inteval
044: * watcher.setDelay(10 * 1000);
045: * watcher.add(new File("/tmp/foo.txt"));
046: * watcher.start();
047: * watcher.add(new File("/tmp/foo.txt"));
048: * wait(100*1000);
049: * watcher.exit();
050: * </code>
051: *
052: * Thanks to contributions by Mathias Bogaert.
053: * Licence was changed from apache 1.1 to Mozilla.
054: *
055: * MMBase javadoc
056: *
057: * This code was originally borrowed from the log4j project (as can still be seen from the authors),
058: * it was however quite heavily adapted. You are probably better of using a {@link ResourceWatcher}
059: * (since MMBase 1.8), because that does not watch only files. Its implementation does of course use
060: * FileWatcher, for the 'file' part of the watching.
061: *
062: * @author Ceki Gülcü
063: * @author Eduard Witteveen
064: * @author Michiel Meeuwissen
065: * @since MMBase-1.4
066: * @version $Id: FileWatcher.java,v 1.48 2007/06/21 15:50:22 nklasens Exp $
067: */
068: public abstract class FileWatcher {
069: private static Logger log = Logging
070: .getLoggerInstance(FileWatcher.class);
071:
072: private static FileWatcherRunner fileWatchers = new FileWatcherRunner();
073: static {
074: fileWatchers.start();
075: }
076:
077: /**
078: * The default delay between every file modification check, set to 60
079: * seconds.
080: */
081: static final public long DEFAULT_DELAY = 60000;
082:
083: /**
084: * The one thread doing al the work also needs a delay.
085: */
086: static public long THREAD_DELAY = 10000;
087:
088: private static Map<String, String> props;
089:
090: /**
091: * @since MMBase-1.8
092: */
093: static private Runnable watcher = new Runnable() {
094: public void run() {
095: try {
096: String delay = props.get("delay");
097: if (delay != null) {
098: THREAD_DELAY = Integer.parseInt(delay);
099: log.service("Set thread delay time to "
100: + THREAD_DELAY);
101: }
102: } catch (Exception e) {
103: log.error(e);
104: }
105: }
106: };
107:
108: static {
109: props = new UtilReader("resourcewatcher.xml", watcher)
110: .getProperties();
111: watcher.run();
112: }
113:
114: /**
115: * @since MMBase-1.8
116: */
117: public static void shutdown() {
118: fileWatchers.run = false;
119: fileWatchers.interrupt();
120: log.service("Shut down file watcher thread");
121: }
122:
123: /**
124: * The delay to observe between every check. By default set {@link
125: * #DEFAULT_DELAY}.
126: */
127: private long delay = DEFAULT_DELAY;
128:
129: private Set<FileEntry> files = new LinkedHashSet<FileEntry>();
130: private Set<File> fileSet = new FileSet(); // (automaticly) wraps 'files'.
131: private Set<File> removeFiles = new HashSet<File>();
132: private boolean stop = false;
133: private boolean continueAfterChange = false;
134: private long lastCheck = 0;
135:
136: protected FileWatcher() {
137: this (true);
138: }
139:
140: protected FileWatcher(boolean c) {
141: // make it end when parent treath ends..
142: continueAfterChange = c;
143: }
144:
145: public void start() {
146: fileWatchers.add(this );
147: }
148:
149: /**
150: * Put here the stuff that has to be executed, when a file has been changed.
151: * @param file The file that was changed..
152: */
153: abstract public void onChange(File file);
154:
155: /**
156: * Set the delay to observe between each check of the file changes.
157: * @param delay The delay in milliseconds
158: */
159: public void setDelay(long delay) {
160: this .delay = delay;
161: if (delay < THREAD_DELAY) {
162: log
163: .service("Delay of "
164: + this
165: + " ("
166: + delay
167: + " ms) is smaller than the delay of the watching thread. Will not watch more often then once per "
168: + THREAD_DELAY + " ms.");
169: }
170: }
171:
172: /**
173: * Add's a file to be checked...
174: * @param file The file which has to be monitored..
175: * @throws RuntimeException If file is null
176: */
177: public void add(File file) {
178: FileEntry fe = new FileEntry(file);
179: synchronized (this ) {
180: files.add(fe);
181: if (removeFiles.remove(fe)) {
182: log.service("Canceling removal from filewatcher " + fe);
183: }
184: }
185: }
186:
187: /**
188: * Wether the file is being watched or not.
189: * @param file the file to be checked.
190: * @since MMBase-1.6
191: */
192: public boolean contains(File file) {
193: return files.contains(new FileEntry(file));
194: }
195:
196: /**
197: * Remove file from the watch-list
198: */
199: public void remove(File file) {
200: synchronized (this ) {
201: removeFiles.add(file);
202: }
203: }
204:
205: /**
206: * Returns a (modifiable) Set of all files (File object) of this FileWatcher. If you change it, you change the
207: * FileWatcher. The order of the Set is predictable (backed by a {@link java.util.LinkedHashSet}).
208: *
209: * @since MMBase-1.8.
210: */
211: public Set<File> getFiles() {
212: return fileSet;
213: }
214:
215: /**
216: * Removes all files, this watcher will end up watching nothing.
217: * @since MMBase-1.8
218: */
219: public void clear() {
220: fileSet.clear();
221: }
222:
223: /**
224: * Stops watching.
225: */
226: public void exit() {
227: synchronized (this ) {
228: stop = true;
229: }
230: }
231:
232: /**
233: * Shows the 'contents' of the filewatcher. It shows a list of files/last modified timestamps.
234: */
235: public String toString() {
236: return files.toString();
237: }
238:
239: /**
240: * Looks if a file has changed. If it is, the 'onChance' for this file is called.
241: *
242: * Before doing so, it removes the files which were requested for
243: * removal from the watchlist.
244: *
245: */
246: private boolean changed() {
247: synchronized (this ) {
248: for (FileEntry fe : files) {
249: if (fe.changed()) {
250: log.debug("the file :"
251: + fe.getFile().getAbsolutePath()
252: + " has changed.");
253: try {
254: onChange(fe.getFile());
255: } catch (Throwable e) {
256: log.warn("onChange of "
257: + fe.getFile().getName()
258: + " lead to exception:");
259: log.warn(Logging.stackTrace(e));
260: }
261: if (continueAfterChange) {
262: fe.updated(); // onChange was called now, it can be marked up-to-date again
263: } else { //
264: return true; // stop watching
265: }
266: }
267: }
268: }
269: return false;
270: }
271:
272: private void removeFiles() {
273: synchronized (this ) {
274: // remove files if necessary
275: for (File f : removeFiles) {
276: FileEntry found = null;
277: // search the file
278: for (FileEntry fe : files) {
279: if (fe.getFile().equals(f)) {
280: if (log.isDebugEnabled()) {
281: log.debug("removing file["
282: + fe.getFile().getName() + "]");
283: }
284: found = fe;
285: break;
286: }
287: }
288: if (found != null) {
289: files.remove(found);
290: log.service("Removed " + found + " from watchlist");
291: }
292: }
293: removeFiles.clear();
294: }
295: }
296:
297: /**
298: * Looks if we have to stop
299: */
300: private boolean mustStop() {
301: synchronized (this ) {
302: return stop;
303: }
304: }
305:
306: public boolean equals(Object o) {
307: if (o == this )
308: return true;
309: if (o == null)
310: return false;
311: if (getClass().equals(o.getClass())) {
312: FileWatcher f = (FileWatcher) o;
313: return this .files.equals(f.files);
314: }
315: return false;
316: }
317:
318: /**
319: * @see java.lang.Object#hashCode()
320: */
321: public int hashCode() {
322: return files == null ? 0 : files.hashCode();
323: }
324:
325: /**
326: * @javadoc
327: */
328: public static void main(String[] args) {
329:
330: // start some filewatchers
331: for (int i = 0; i < 100; i++) {
332: FileWatcher w = new TestFileWatcher();
333: // add some files
334: for (int j = 0; j < 4; j++) {
335: try {
336: w.add(File.createTempFile("filewatchertestfile",
337: ".txt"));
338: } catch (Exception e) {
339: System.out.println(e);
340: }
341: }
342: //set a delay and start it.
343: w.setDelay(1 * 1000); // 1 s
344: w.start();
345: //this.wait(123); // make all watchers out sync
346: }
347:
348: System.out.println("Starting");
349: // ok, a lot of those are running now, let time something else, and see if it suffers.
350: long start = System.currentTimeMillis();
351: long k;
352: for (k = 0; k < 400000000;) {
353: k++;
354: }
355: System.out.println("\ntook "
356: + (System.currentTimeMillis() - start) + " ms");
357: }
358:
359: /**
360: * The one thread to handle all FileWatchers. In earlier implementation every FileWatcher had
361: * it's own thread, but that is avoided now.
362: */
363: private static class FileWatcherRunner extends Thread {
364:
365: boolean run = true;
366: /**
367: * Set of file-watchers, which are currently active.
368: */
369: private Set<FileWatcher> watchers = new CopyOnWriteArraySet<FileWatcher>();
370:
371: FileWatcherRunner() {
372: super ("MMBase FileWatcher thread");
373: log.service("Starting the file-watcher thread");
374: setPriority(MIN_PRIORITY);
375: setDaemon(true);
376: }
377:
378: void add(FileWatcher f) {
379: watchers.add(f);
380: }
381:
382: /**
383: * Main loop, will check every watched file every amount of time.
384: * It will never stop, this thread is a daemon.
385: */
386: public void run() {
387: // todo: how to stop this thread except through interrupting it?
388: List<FileWatcher> removed = new ArrayList<FileWatcher>();
389: while (run) {
390: try {
391: long now = System.currentTimeMillis();
392: for (FileWatcher f : watchers) {
393: if (now - f.lastCheck > f.delay) {
394: if (log.isTraceEnabled()) {
395: log
396: .trace("Filewatcher will sleep for : "
397: + f.delay
398: / 1000
399: + " s. "
400: + "Currently watching: "
401: + f.getClass()
402: .getName()
403: + " " + f.toString());
404: }
405: // System.out.print(".");
406: f.removeFiles();
407: //changed returns true if we can stop watching
408: if (f.changed() || f.mustStop()) {
409: if (log.isDebugEnabled()) {
410: log.debug("Removing filewatcher "
411: + f + " " + f.mustStop());
412: }
413: removed.add(f);
414: }
415: f.lastCheck = now;
416: }
417: }
418: watchers.removeAll(removed);
419: removed.clear();
420: if (log.isTraceEnabled()) {
421: log.trace("Sleeping " + THREAD_DELAY + " ms");
422: }
423: Thread.sleep(THREAD_DELAY);
424: } catch (InterruptedException e) {
425: Thread ct = Thread.currentThread();
426: log.debug((ct != null ? ct.getName() : "MMBase")
427: + " was interrupted.");
428: break; // likely interrupted due to MMBase going down - break out of loop
429: } catch (Throwable ex) {
430: // unexpected exception?? This run method should never interrupt, so we catch everything.
431: log.error("Exception: " + ex.getClass().getName()
432: + ": " + ex.getMessage()
433: + Logging.stackTrace(ex));
434: }
435: // when we found a change, we exit..
436: }
437: }
438: }
439:
440: /**
441: * Performance test
442: */
443: private static class TestFileWatcher extends FileWatcher {
444: int i = 0;
445:
446: public void onChange(java.io.File f) {
447: // do something..
448: i++;
449: }
450:
451: protected void finalize() {
452: System.out.println(this .toString() + ":" + i);
453: }
454: }
455:
456: /**
457: * Object used in file-lists of the FileWatcher. It wraps a File object, but adminstrates
458: * lastmodified an existence seperately (to compare with the actual values of the File).
459: */
460: private class FileEntry {
461: // static final Logger log = Logging.getLoggerInstance(FileWatcher.class.getName());
462: private long lastModified = -1;
463: private boolean exists = false;
464: private final File file;
465:
466: public FileEntry(File file) {
467: if (file == null) {
468: throw new IllegalArgumentException();
469: }
470: exists = file.exists();
471: lastModified = getLastModified(file);
472: this .file = file;
473: }
474:
475: /**
476: * Returns the last modification time of a file, or -1 if the file does not exists, or the
477: * last modification time of the last modified file in it, if it is a directory.
478: * @since MMBase-1.9
479: */
480: protected long getLastModified(File f) {
481: long lm;
482: if (!f.exists()) {
483: // file does not exist. A change will be triggered
484: // once the file comes into existence
485: if (log.isDebugEnabled()) {
486: log.debug("file :" + f.getAbsolutePath()
487: + " did not exist (yet)");
488: }
489: lm = -1;
490: } else {
491: lm = f.lastModified();
492: if (f.isDirectory() && f.canRead()) {
493: // in that case, we take the last modified file in it, and return that.
494: // TODO, we may need a flag to also enable _not_ doing this, or at least not recursively
495: for (File child : f.listFiles()) {
496: try {
497: long childLastModified = getLastModified(child);
498: if (childLastModified > lm) {
499: lm = childLastModified;
500: }
501: } catch (SecurityException se) {
502: // never mind
503: }
504: }
505: }
506: }
507: return lm;
508: }
509:
510: /**
511: * Returns true if the file was modified, added, or removed. If the change is handled, then
512: * call {@link #updated}.
513: * @return <code>true</code> if the file was changed
514: */
515: public boolean changed() {
516: if (file.exists()) {
517: if (!exists) {
518: log.info("File " + file.getAbsolutePath()
519: + " added");
520: return true;
521: } else {
522: boolean result = lastModified < getLastModified(file);
523: if (result) {
524: log.info("File " + file.getAbsolutePath()
525: + " changed");
526: }
527: return result;
528: }
529: } else {
530: if (exists) {
531: log.info("File " + file.getAbsolutePath()
532: + " removed");
533: }
534: return exists;
535: }
536: }
537:
538: /**
539: * Call this if changes were treated. It resets the state, and after that {@link #changed}
540: * will return <code>false</code> again.
541: */
542: public void updated() {
543: exists = file.exists();
544: if (exists) {
545: lastModified = getLastModified(file);
546: } else {
547: lastModified = -1;
548: }
549: }
550:
551: public File getFile() {
552: return file;
553: }
554:
555: public String toString() {
556: return file.toString() + ":" + lastModified;
557: }
558:
559: public boolean equals(Object o) {
560: if (o instanceof FileEntry) {
561: FileEntry fe = (FileEntry) o;
562: return file.equals(fe.file);
563: } else if (o instanceof File) {
564: return file.equals(o);
565: }
566: return false;
567: }
568:
569: public int hashCode() {
570: return file.hashCode();
571: }
572:
573: }
574:
575: /**
576: * This FileSet makes the 'files' object of the FileWatcher look like a Set of File rather then Set of FileEntry's.
577: * @since MMBase-1.8
578: */
579: private class FileSet extends AbstractSet<File> {
580: public int size() {
581: return FileWatcher.this .files.size();
582: }
583:
584: public Iterator<File> iterator() {
585: return new FileIterator();
586: }
587:
588: public boolean add(File o) {
589: int s = size();
590: FileWatcher.this .add(o);
591: return s != size();
592: }
593: }
594:
595: /**
596: * The iterator belonging to FileSet.
597: * @since MMBase-1.8
598: */
599: private class FileIterator implements Iterator<File> {
600: Iterator<FileEntry> it;
601: File lastFile;
602:
603: FileIterator() {
604: it = FileWatcher.this .files.iterator();
605: }
606:
607: public boolean hasNext() {
608: return it.hasNext();
609: }
610:
611: public File next() {
612: FileEntry f = it.next();
613: lastFile = f.getFile();
614: return lastFile;
615: }
616:
617: public void remove() {
618: FileWatcher.this.remove(lastFile);
619: }
620:
621: }
622:
623: }
|