jdk/src/share/classes/sun/nio/fs/PollingWatchService.java
changeset 2057 3acf8e5e2ca0
child 3065 452aaa2899fc
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/jdk/src/share/classes/sun/nio/fs/PollingWatchService.java	Sun Feb 15 12:25:54 2009 +0000
@@ -0,0 +1,429 @@
+/*
+ * Copyright 2008-2009 Sun Microsystems, Inc.  All Rights Reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.  Sun designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Sun in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa Clara,
+ * CA 95054 USA or visit www.sun.com if you need additional information or
+ * have any questions.
+ */
+
+package sun.nio.fs;
+
+import java.nio.file.*;
+import java.nio.file.attribute.*;
+import java.security.AccessController;
+import java.security.PrivilegedAction;
+import java.security.PrivilegedExceptionAction;
+import java.security.PrivilegedActionException;
+import java.io.IOException;
+import java.util.*;
+import java.util.concurrent.*;
+import com.sun.nio.file.SensitivityWatchEventModifier;
+
+/**
+ * Simple WatchService implementation that uses periodic tasks to poll
+ * registered directories for changes.  This implementation is for use on
+ * operating systems that do not have native file change notification support.
+ */
+
+class PollingWatchService
+    extends AbstractWatchService
+{
+    // map of registrations
+    private final Map<Object,PollingWatchKey> map =
+        new HashMap<Object,PollingWatchKey>();
+
+    // used to execute the periodic tasks that poll for changes
+    private final ScheduledExecutorService scheduledExecutor;
+
+    PollingWatchService() {
+        // TBD: Make the number of threads configurable
+        scheduledExecutor = Executors
+            .newSingleThreadScheduledExecutor(new ThreadFactory() {
+                 @Override
+                 public Thread newThread(Runnable r) {
+                     Thread t = new Thread(r);
+                     t.setDaemon(true);
+                     return t;
+                 }});
+    }
+
+    /**
+     * Register the given file with this watch service
+     */
+    @Override
+    WatchKey register(final Path path,
+                      WatchEvent.Kind<?>[] events,
+                      WatchEvent.Modifier... modifiers)
+         throws IOException
+    {
+        // check events - CCE will be thrown if there are invalid elements
+        if (events.length == 0)
+            throw new IllegalArgumentException("No events to register");
+        final Set<WatchEvent.Kind<?>> eventSet =
+            new HashSet<WatchEvent.Kind<?>>(events.length);
+        for (WatchEvent.Kind<?> event: events) {
+            // standard events
+            if (event == StandardWatchEventKind.ENTRY_CREATE ||
+                event == StandardWatchEventKind.ENTRY_MODIFY ||
+                event == StandardWatchEventKind.ENTRY_DELETE)
+            {
+                eventSet.add(event);
+                continue;
+            }
+
+            // OVERFLOW is ignored
+            if (event == StandardWatchEventKind.OVERFLOW) {
+                if (events.length == 1)
+                    throw new IllegalArgumentException("No events to register");
+                continue;
+            }
+
+            // null/unsupported
+            if (event == null)
+                throw new NullPointerException("An element in event set is 'null'");
+            throw new UnsupportedOperationException(event.name());
+        }
+
+        // A modifier may be used to specify the sensitivity level
+        SensitivityWatchEventModifier sensivity = SensitivityWatchEventModifier.MEDIUM;
+        if (modifiers.length > 0) {
+            for (WatchEvent.Modifier modifier: modifiers) {
+                if (modifier == null)
+                    throw new NullPointerException();
+                if (modifier instanceof SensitivityWatchEventModifier) {
+                    sensivity = (SensitivityWatchEventModifier)modifier;
+                    continue;
+                }
+                throw new UnsupportedOperationException("Modifier not supported");
+            }
+        }
+
+        // check if watch service is closed
+        if (!isOpen())
+            throw new ClosedWatchServiceException();
+
+        // registration is done in privileged block as it requires the
+        // attributes of the entries in the directory.
+        try {
+            final SensitivityWatchEventModifier s = sensivity;
+            return AccessController.doPrivileged(
+                new PrivilegedExceptionAction<PollingWatchKey>() {
+                    @Override
+                    public PollingWatchKey run() throws IOException {
+                        return doPrivilegedRegister(path, eventSet, s);
+                    }
+                });
+        } catch (PrivilegedActionException pae) {
+            Throwable cause = pae.getCause();
+            if (cause != null && cause instanceof IOException)
+                throw (IOException)cause;
+            throw new AssertionError(pae);
+        }
+    }
+
+    // registers directory returning a new key if not already registered or
+    // existing key if already registered
+    private PollingWatchKey doPrivilegedRegister(Path path,
+                                                 Set<? extends WatchEvent.Kind<?>> events,
+                                                 SensitivityWatchEventModifier sensivity)
+        throws IOException
+    {
+        // check file is a directory and get its file key if possible
+        BasicFileAttributes attrs = Attributes.readBasicFileAttributes(path);
+        if (!attrs.isDirectory()) {
+            throw new NotDirectoryException(path.toString());
+        }
+        Object fileKey = attrs.fileKey();
+        if (fileKey == null)
+            throw new AssertionError("File keys must be supported");
+
+        // grab close lock to ensure that watch service cannot be closed
+        synchronized (closeLock()) {
+            if (!isOpen())
+                throw new ClosedWatchServiceException();
+
+            PollingWatchKey watchKey;
+            synchronized (map) {
+                watchKey = map.get(fileKey);
+                if (watchKey == null) {
+                    // new registration
+                    watchKey = new PollingWatchKey(this, path, fileKey);
+                    map.put(fileKey, watchKey);
+                } else {
+                    // update to existing registration
+                    watchKey.disable();
+                }
+            }
+            watchKey.enable(events, sensivity.sensitivityValueInSeconds());
+            return watchKey;
+        }
+
+    }
+
+    @Override
+    void implClose() throws IOException {
+        synchronized (map) {
+            for (Map.Entry<Object,PollingWatchKey> entry: map.entrySet()) {
+                PollingWatchKey watchKey = entry.getValue();
+                watchKey.disable();
+                watchKey.invalidate();
+            }
+            map.clear();
+        }
+        AccessController.doPrivileged(new PrivilegedAction<Void>() {
+            @Override
+            public Void run() {
+                scheduledExecutor.shutdown();
+                return null;
+            }
+         });
+    }
+
+    /**
+     * Entry in directory cache to record file last-modified-time and tick-count
+     */
+    private static class CacheEntry {
+        private long lastModified;
+        private int lastTickCount;
+
+        CacheEntry(long lastModified, int lastTickCount) {
+            this.lastModified = lastModified;
+            this.lastTickCount = lastTickCount;
+        }
+
+        int lastTickCount() {
+            return lastTickCount;
+        }
+
+        long lastModified() {
+            return lastModified;
+        }
+
+        void update(long lastModified, int tickCount) {
+            this.lastModified = lastModified;
+            this.lastTickCount = tickCount;
+        }
+    }
+
+    /**
+     * WatchKey implementation that encapsulates a map of the entries of the
+     * entries in the directory. Polling the key causes it to re-scan the
+     * directory and queue keys when entries are added, modified, or deleted.
+     */
+    private class PollingWatchKey extends AbstractWatchKey {
+        private final Path dir;
+        private final Object fileKey;
+
+        // current event set
+        private Set<? extends WatchEvent.Kind<?>> events;
+
+        // the result of the periodic task that causes this key to be polled
+        private ScheduledFuture<?> poller;
+
+        // indicates if the key is valid
+        private volatile boolean valid;
+
+        // used to detect files that have been deleted
+        private int tickCount;
+
+        // map of entries in directory
+        private Map<Path,CacheEntry> entries;
+
+        PollingWatchKey(PollingWatchService watcher,
+                        Path dir,
+                        Object fileKey)
+            throws IOException
+        {
+            super(watcher);
+            this.dir = dir;
+            this.fileKey = fileKey;
+            this.valid = true;
+            this.tickCount = 0;
+            this.entries = new HashMap<Path,CacheEntry>();
+
+            // get the initial entries in the directory
+            DirectoryStream<Path> stream = dir.newDirectoryStream();
+            try {
+                for (Path entry: stream) {
+                    // don't follow links
+                    long lastModified = Attributes
+                        .readBasicFileAttributes(entry, LinkOption.NOFOLLOW_LINKS)
+                        .lastModifiedTime();
+                    entries.put(entry.getName(),
+                                new CacheEntry(lastModified, tickCount));
+                }
+            } catch (ConcurrentModificationException cme) {
+                // thrown if directory iteration fails
+                Throwable cause = cme.getCause();
+                if (cause != null && cause instanceof IOException)
+                    throw (IOException)cause;
+                throw new AssertionError(cme);
+            } finally {
+                stream.close();
+            }
+        }
+
+        FileRef directory() {
+            return dir;
+        }
+
+        Object fileKey() {
+            return fileKey;
+        }
+
+        @Override
+        public boolean isValid() {
+            return valid;
+        }
+
+        void invalidate() {
+            valid = false;
+        }
+
+        // enables periodic polling
+        void enable(Set<? extends WatchEvent.Kind<?>> events, long period) {
+            synchronized (this) {
+                // update the events
+                this.events = events;
+
+                // create the periodic task
+                Runnable thunk = new Runnable() { public void run() { poll(); }};
+                this.poller = scheduledExecutor
+                    .scheduleAtFixedRate(thunk, period, period, TimeUnit.SECONDS);
+            }
+        }
+
+        // disables periodic polling
+        void disable() {
+            synchronized (this) {
+                if (poller != null)
+                    poller.cancel(false);
+            }
+        }
+
+        @Override
+        public void cancel() {
+            valid = false;
+            synchronized (map) {
+                map.remove(fileKey());
+            }
+            disable();
+        }
+
+        /**
+         * Polls the directory to detect for new files, modified files, or
+         * deleted files.
+         */
+        synchronized void poll() {
+            if (!valid) {
+                return;
+            }
+
+            // update tick
+            tickCount++;
+
+            // open directory
+            DirectoryStream<Path> stream = null;
+            try {
+                stream = dir.newDirectoryStream();
+            } catch (IOException x) {
+                // directory is no longer accessible so cancel key
+                cancel();
+                signal();
+                return;
+            }
+
+            // iterate over all entries in directory
+            try {
+                for (Path entry: stream) {
+                    long lastModified = 0L;
+                    try {
+                        lastModified = Attributes
+                            .readBasicFileAttributes(entry, LinkOption.NOFOLLOW_LINKS)
+                            .lastModifiedTime();
+                    } catch (IOException x) {
+                        // unable to get attributes of entry. If file has just
+                        // been deleted then we'll report it as deleted on the
+                        // next poll
+                        continue;
+                    }
+
+                    // lookup cache
+                    CacheEntry e = entries.get(entry.getName());
+                    if (e == null) {
+                        // new file found
+                        entries.put(entry.getName(),
+                                     new CacheEntry(lastModified, tickCount));
+
+                        // queue ENTRY_CREATE if event enabled
+                        if (events.contains(StandardWatchEventKind.ENTRY_CREATE)) {
+                            signalEvent(StandardWatchEventKind.ENTRY_CREATE, entry.getName());
+                            continue;
+                        } else {
+                            // if ENTRY_CREATE is not enabled and ENTRY_MODIFY is
+                            // enabled then queue event to avoid missing out on
+                            // modifications to the file immediately after it is
+                            // created.
+                            if (events.contains(StandardWatchEventKind.ENTRY_MODIFY)) {
+                                signalEvent(StandardWatchEventKind.ENTRY_MODIFY, entry.getName());
+                            }
+                        }
+                        continue;
+                    }
+
+                    // check if file has changed
+                    if (e.lastModified != lastModified) {
+                        if (events.contains(StandardWatchEventKind.ENTRY_MODIFY)) {
+                            signalEvent(StandardWatchEventKind.ENTRY_MODIFY, entry.getName());
+                        }
+                    }
+                    // entry in cache so update poll time
+                    e.update(lastModified, tickCount);
+
+                }
+            } catch (ConcurrentModificationException x) {
+                // FIXME - should handle this
+            } finally {
+
+                // close directory stream
+                try {
+                    stream.close();
+                } catch (IOException x) {
+                    // ignore
+                }
+            }
+
+            // iterate over cache to detect entries that have been deleted
+            Iterator<Map.Entry<Path,CacheEntry>> i = entries.entrySet().iterator();
+            while (i.hasNext()) {
+                Map.Entry<Path,CacheEntry> mapEntry = i.next();
+                CacheEntry entry = mapEntry.getValue();
+                if (entry.lastTickCount() != tickCount) {
+                    Path name = mapEntry.getKey();
+                    // remove from map and queue delete event (if enabled)
+                    i.remove();
+                    if (events.contains(StandardWatchEventKind.ENTRY_DELETE)) {
+                        signalEvent(StandardWatchEventKind.ENTRY_DELETE, name);
+                    }
+                }
+            }
+        }
+    }
+}