3 Replies Latest reply: Oct 21, 2011 7:35 AM by Kleopatra RSS

    WatchService and SwingWorker: how to do it correctly?

    Kleopatra
      cross-posted to SOF:

      http://stackoverflow.com/questions/7784909/watchservice-and-swingworker-how-to-do-it-correctly

      For maximum feedback (though many regulars roam everywhere :-), here's a copy

      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 (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 key - but can pass on only one. Most probably got the distribution of logic wrong, though

      Feedback (here or there, will take all :-) highly welcome!

      Cheers
      Jeanette
        • 1. Re: WatchService and SwingWorker: how to do it correctly?
          EJP
          Polling and sleeping would still block the thread, just like take() does.
          • 2. Re: WatchService and SwingWorker: how to do it correctly?
            Kleopatra
            you'r right, of course - my brain occasionally refuses to work when confronted with threaded thoughts ;-)

            thanks for pointing it out again
            Jeanette
            • 3. Re: WatchService and SwingWorker: how to do it correctly?
              Kleopatra
              finally settled on a version that's good enough (for now, at least), published over at SOF, copied here:

              Actually, @Eels's comment didn't stop knocking in the back of my head - and finally registered: it's the way to go, but there is no need for any "artificial" struct, because we already have the perfect candidate - it's the PropertyChangeEvent itself :-)

              Taking the overall process description from my question, the first three bullets remain the same

              - same: extend SwingWorker
              - same: do the registration stuff in the constructor
              - same: put the endless loop waiting for a key in doInBackground
              - changed: create the appropriate PropertyChangeEvent from each WatchEvent when retrieved via key.pollEvents and publish the PropertyChangeEvent
              - changed: fire the previously created event in process(chunks)
              @SuppressWarnings("unchecked")
              public class FileWorker extends SwingWorker<Void, PropertyChangeEvent> {
                  
                  public static final String FILE_DELETED = StandardWatchEventKinds.ENTRY_DELETE.name();
                  public static final String FILE_CREATED = StandardWatchEventKinds.ENTRY_CREATE.name();
                  public static final String FILE_MODIFIED = StandardWatchEventKinds.ENTRY_MODIFY.name();
                  // will change to a map of key/directories, just as the tutorial example 
                  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(createChangeEvent((WatchEvent<Path>) event, key));
                          }
              
                          // reset key return if directory no longer accessible
                          boolean valid = key.reset();
                          if (!valid) {
                              break;
                          }
                      }
                      return null;
                  }
                  
                  /**
                   * Creates and returns the change notification. This method is called from the 
                   * worker thread while looping through the events as received from the Watchkey.
                   * 
                   * @param event
                   * @param key
                   */
                  protected PropertyChangeEvent createChangeEvent(WatchEvent<Path> event, WatchKey key) {
                      Path name = event.context();
                      // evolve into looking up the directoy from the key/directory map
                      Path child = directory.resolve(name);
                      PropertyChangeEvent e = new PropertyChangeEvent(this, event.kind().name(), null, child.toFile());
                      return e;
                  }
                  
                  @Override
                  protected void process(List<PropertyChangeEvent> chunks) {
                      super.process(chunks);
                      for (PropertyChangeEvent event : chunks) {
                          getPropertyChangeSupport().firePropertyChange(event);
                      }
                  }
                  
              }
              Feedback still highly welcome, of course, especially if there's something wrong :-)

              Thanks
              Jeanette