Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

WatchService and SwingWorker: how to do it correctly?

WatchService sounded like an exciting idea ... unfortunately it seems to be as low-level as warned in the tutorial/api plus doesn't really fit into the Swing event model (or I'm missing something obvious, a not-zero probability

Taking the code from WatchDir example in the tutorial (simplyfied to handle a single directory only), I basically ended up

  • extend SwingWorker
  • do the registration stuff in the constructor
  • put the endless loop waiting for a key in doInBackground
  • publish each WatchEvent when retrieved via key.pollEvents()
  • process the chunks by firing propertyChangeEvents with the deleted/created files as newValue

    @SuppressWarnings("unchecked")
    public class FileWorker extends SwingWorker<Void, WatchEvent<Path>> {
    
        public static final String DELETED = "deletedFile";
        public static final String CREATED = "createdFile";
    
        private Path directory;
        private WatchService watcher;
    
        public FileWorker(File file) throws IOException {
            directory = file.toPath();
            watcher = FileSystems.getDefault().newWatchService();
            directory.register(watcher, ENTRY_CREATE, ENTRY_DELETE, ENTRY_MODIFY);
        }
    
        @Override
        protected Void doInBackground() throws Exception {
            for (;;) {
                // wait for key to be signalled
                WatchKey key;
                try {
                    key = watcher.take();
                } catch (InterruptedException x) {
                    return null;
                }
    
                for (WatchEvent<?> event : key.pollEvents()) {
                    WatchEvent.Kind<?> kind = event.kind();
                    // TBD - provide example of how OVERFLOW event is handled
                    if (kind == OVERFLOW) {
                        continue;
                    }
                    publish((WatchEvent<Path>) event);
                }
    
                // reset key return if directory no longer accessible
                boolean valid = key.reset();
                if (!valid) {
                    break;
                }
            }
            return null;
        }
    
        @Override
        protected void process(List<WatchEvent<Path>> chunks) {
            super.process(chunks);
            for (WatchEvent<Path> event : chunks) {
                WatchEvent.Kind<?> kind = event.kind();
                Path name = event.context();
                Path child = directory.resolve(name);
                File file = child.toFile();
                if (StandardWatchEventKinds.ENTRY_DELETE == kind) {
                    firePropertyChange(DELETED, null, file);
                } else if (StandardWatchEventKinds.ENTRY_CREATE == kind) {
                    firePropertyChange(CREATED, null, file);
                }
            }
        }
    
    }
    

The basic idea is to make using code blissfully un-aware of the slimy details: it listens to the property changes and f.i. updates arbitrary models as appropriate:

    String testDir = "D:\\scans\\library";
    File directory = new File(testDir);
    final DefaultListModel<File> model = new DefaultListModel<File>();
    for (File file : directory.listFiles()) {
        model.addElement(file);
    }
    final FileWorker worker = new FileWorker(directory);
    PropertyChangeListener l = new PropertyChangeListener() {

        @Override
        public void propertyChange(PropertyChangeEvent evt) {
            if (FileWorker.DELETED == evt.getPropertyName()) {
                model.removeElement(evt.getNewValue());
            } else if (FileWorker.CREATED == evt.getPropertyName()) {
                model.addElement((File) evt.getNewValue());
            }
        }
    };
    worker.addPropertyChangeListener(l);
    JXList list = new JXList(model);

Seems to work, but I feel uncomfortable

  • Outing myself as the thread agnostic I am: all example snippets I have seen so far do block the waiting thread by using watcher.take(). Why do they do it? Would expect at least some use watcher.poll() and sleep a bit.
  • the SwingWorker publish method doesn't quite seem to fit: for now it's okay, as I'm watching one directory only (didn't want to galopp too far into the wrong direction :) When trying to watch several directories (as in the original WatchDir example) there are several keys and the WatchEvent relative to one of those. To resolve the path, I would need both the event and the directory [A] the key is watching - but can pass on only one. Most probably got the distribution of logic wrong, though

[A] Edited (triggered by @trashgods's comment) - it's actually not the key I have to pass around along with the event, it's the directory it's reporting the changes on. Changed the question accordingly

FYI, this question is cross-posted to the OTN swing forum

Addendum

Reading the api doc of WatchKey:

Where there are several threads retrieving signalled keys from a watch service then care should be taken to ensure that the reset method is only invoked after the events for the object have been processed.

seems to imply that the events should

  1. be processed on the same thread that retrieved the WatchKey
  2. shouldn't be touched after the key is reset

Not entirely sure, but combined with the (future) requirement to recursively watching directories (more than one) decided to follow @Eels advice, kind of - will soon post the code I settled on

EDIT just accepted my own answer - will humbly revert that if anybody has reasonable objections

like image 317
kleopatra Avatar asked Oct 16 '11 14:10

kleopatra


1 Answers

Because your background thread is devoted entirely to watching, take() is the right choice. It effectively hides the platform dependent implementation, which may either forward or poll. One of the poll() methods would be appropriate if, for example, your background thread also needed to examine other queues in series with the WatchService.

Addendum: Because the WatchKey has state, it should probably not be forwarded to process(). The context() of a WatchEvent is a "relative path between the directory registered with the watch service and the entry that is created, deleted, or modified." One of the resolve() methods should work if the directories share a common root.

like image 95
trashgod Avatar answered Oct 01 '22 09:10

trashgod