1/*
2 * Copyright (c) 2008, 2011, Oracle and/or its affiliates. All rights reserved.
3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4 *
5 * This code is free software; you can redistribute it and/or modify it
6 * under the terms of the GNU General Public License version 2 only, as
7 * published by the Free Software Foundation.  Oracle designates this
8 * particular file as subject to the "Classpath" exception as provided
9 * by Oracle in the LICENSE file that accompanied this code.
10 *
11 * This code is distributed in the hope that it will be useful, but WITHOUT
12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
14 * version 2 for more details (a copy is included in the LICENSE file that
15 * accompanied this code).
16 *
17 * You should have received a copy of the GNU General Public License version
18 * 2 along with this work; if not, write to the Free Software Foundation,
19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
20 *
21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
22 * or visit www.oracle.com if you need additional information or have any
23 * questions.
24 */
25
26package sun.nio.fs;
27
28import java.nio.file.*;
29import java.nio.file.attribute.*;
30import java.security.AccessController;
31import java.security.PrivilegedAction;
32import java.security.PrivilegedExceptionAction;
33import java.security.PrivilegedActionException;
34import java.io.IOException;
35import java.util.*;
36import java.util.concurrent.*;
37import com.sun.nio.file.SensitivityWatchEventModifier;
38
39/**
40 * Simple WatchService implementation that uses periodic tasks to poll
41 * registered directories for changes.  This implementation is for use on
42 * operating systems that do not have native file change notification support.
43 */
44
45class PollingWatchService
46    extends AbstractWatchService
47{
48    // map of registrations
49    private final Map<Object,PollingWatchKey> map =
50        new HashMap<Object,PollingWatchKey>();
51
52    // used to execute the periodic tasks that poll for changes
53    private final ScheduledExecutorService scheduledExecutor;
54
55    PollingWatchService() {
56        // TBD: Make the number of threads configurable
57        scheduledExecutor = Executors
58            .newSingleThreadScheduledExecutor(new ThreadFactory() {
59                 @Override
60                 public Thread newThread(Runnable r) {
61                     Thread t = new Thread(r);
62                     t.setDaemon(true);
63                     return t;
64                 }});
65    }
66
67    /**
68     * Register the given file with this watch service
69     */
70    @Override
71    WatchKey register(final Path path,
72                      WatchEvent.Kind<?>[] events,
73                      WatchEvent.Modifier... modifiers)
74         throws IOException
75    {
76        // check events - CCE will be thrown if there are invalid elements
77        final Set<WatchEvent.Kind<?>> eventSet =
78            new HashSet<WatchEvent.Kind<?>>(events.length);
79        for (WatchEvent.Kind<?> event: events) {
80            // standard events
81            if (event == StandardWatchEventKinds.ENTRY_CREATE ||
82                event == StandardWatchEventKinds.ENTRY_MODIFY ||
83                event == StandardWatchEventKinds.ENTRY_DELETE)
84            {
85                eventSet.add(event);
86                continue;
87            }
88
89            // OVERFLOW is ignored
90            if (event == StandardWatchEventKinds.OVERFLOW) {
91                continue;
92            }
93
94            // null/unsupported
95            if (event == null)
96                throw new NullPointerException("An element in event set is 'null'");
97            throw new UnsupportedOperationException(event.name());
98        }
99        if (eventSet.isEmpty())
100            throw new IllegalArgumentException("No events to register");
101
102        // A modifier may be used to specify the sensitivity level
103        SensitivityWatchEventModifier sensivity = SensitivityWatchEventModifier.MEDIUM;
104        if (modifiers.length > 0) {
105            for (WatchEvent.Modifier modifier: modifiers) {
106                if (modifier == null)
107                    throw new NullPointerException();
108                if (modifier instanceof SensitivityWatchEventModifier) {
109                    sensivity = (SensitivityWatchEventModifier)modifier;
110                    continue;
111                }
112                throw new UnsupportedOperationException("Modifier not supported");
113            }
114        }
115
116        // check if watch service is closed
117        if (!isOpen())
118            throw new ClosedWatchServiceException();
119
120        // registration is done in privileged block as it requires the
121        // attributes of the entries in the directory.
122        try {
123            final SensitivityWatchEventModifier s = sensivity;
124            return AccessController.doPrivileged(
125                new PrivilegedExceptionAction<PollingWatchKey>() {
126                    @Override
127                    public PollingWatchKey run() throws IOException {
128                        return doPrivilegedRegister(path, eventSet, s);
129                    }
130                });
131        } catch (PrivilegedActionException pae) {
132            Throwable cause = pae.getCause();
133            if (cause != null && cause instanceof IOException)
134                throw (IOException)cause;
135            throw new AssertionError(pae);
136        }
137    }
138
139    // registers directory returning a new key if not already registered or
140    // existing key if already registered
141    private PollingWatchKey doPrivilegedRegister(Path path,
142                                                 Set<? extends WatchEvent.Kind<?>> events,
143                                                 SensitivityWatchEventModifier sensivity)
144        throws IOException
145    {
146        // check file is a directory and get its file key if possible
147        BasicFileAttributes attrs = Files.readAttributes(path, BasicFileAttributes.class);
148        if (!attrs.isDirectory()) {
149            throw new NotDirectoryException(path.toString());
150        }
151        Object fileKey = attrs.fileKey();
152        if (fileKey == null)
153            throw new AssertionError("File keys must be supported");
154
155        // grab close lock to ensure that watch service cannot be closed
156        synchronized (closeLock()) {
157            if (!isOpen())
158                throw new ClosedWatchServiceException();
159
160            PollingWatchKey watchKey;
161            synchronized (map) {
162                watchKey = map.get(fileKey);
163                if (watchKey == null) {
164                    // new registration
165                    watchKey = new PollingWatchKey(path, this, fileKey);
166                    map.put(fileKey, watchKey);
167                } else {
168                    // update to existing registration
169                    watchKey.disable();
170                }
171            }
172            watchKey.enable(events, sensivity.sensitivityValueInSeconds());
173            return watchKey;
174        }
175
176    }
177
178    @Override
179    void implClose() throws IOException {
180        synchronized (map) {
181            for (Map.Entry<Object,PollingWatchKey> entry: map.entrySet()) {
182                PollingWatchKey watchKey = entry.getValue();
183                watchKey.disable();
184                watchKey.invalidate();
185            }
186            map.clear();
187        }
188        AccessController.doPrivileged(new PrivilegedAction<Void>() {
189            @Override
190            public Void run() {
191                scheduledExecutor.shutdown();
192                return null;
193            }
194         });
195    }
196
197    /**
198     * Entry in directory cache to record file last-modified-time and tick-count
199     */
200    private static class CacheEntry {
201        private long lastModified;
202        private int lastTickCount;
203
204        CacheEntry(long lastModified, int lastTickCount) {
205            this.lastModified = lastModified;
206            this.lastTickCount = lastTickCount;
207        }
208
209        int lastTickCount() {
210            return lastTickCount;
211        }
212
213        long lastModified() {
214            return lastModified;
215        }
216
217        void update(long lastModified, int tickCount) {
218            this.lastModified = lastModified;
219            this.lastTickCount = tickCount;
220        }
221    }
222
223    /**
224     * WatchKey implementation that encapsulates a map of the entries of the
225     * entries in the directory. Polling the key causes it to re-scan the
226     * directory and queue keys when entries are added, modified, or deleted.
227     */
228    private class PollingWatchKey extends AbstractWatchKey {
229        private final Object fileKey;
230
231        // current event set
232        private Set<? extends WatchEvent.Kind<?>> events;
233
234        // the result of the periodic task that causes this key to be polled
235        private ScheduledFuture<?> poller;
236
237        // indicates if the key is valid
238        private volatile boolean valid;
239
240        // used to detect files that have been deleted
241        private int tickCount;
242
243        // map of entries in directory
244        private Map<Path,CacheEntry> entries;
245
246        PollingWatchKey(Path dir, PollingWatchService watcher, Object fileKey)
247            throws IOException
248        {
249            super(dir, watcher);
250            this.fileKey = fileKey;
251            this.valid = true;
252            this.tickCount = 0;
253            this.entries = new HashMap<Path,CacheEntry>();
254
255            // get the initial entries in the directory
256            try (DirectoryStream<Path> stream = Files.newDirectoryStream(dir)) {
257                for (Path entry: stream) {
258                    // don't follow links
259                    long lastModified =
260                        Files.getLastModifiedTime(entry, LinkOption.NOFOLLOW_LINKS).toMillis();
261                    entries.put(entry.getFileName(), new CacheEntry(lastModified, tickCount));
262                }
263            } catch (DirectoryIteratorException e) {
264                throw e.getCause();
265            }
266        }
267
268        Object fileKey() {
269            return fileKey;
270        }
271
272        @Override
273        public boolean isValid() {
274            return valid;
275        }
276
277        void invalidate() {
278            valid = false;
279        }
280
281        // enables periodic polling
282        void enable(Set<? extends WatchEvent.Kind<?>> events, long period) {
283            synchronized (this) {
284                // update the events
285                this.events = events;
286
287                // create the periodic task
288                Runnable thunk = new Runnable() { public void run() { poll(); }};
289                this.poller = scheduledExecutor
290                    .scheduleAtFixedRate(thunk, period, period, TimeUnit.SECONDS);
291            }
292        }
293
294        // disables periodic polling
295        void disable() {
296            synchronized (this) {
297                if (poller != null)
298                    poller.cancel(false);
299            }
300        }
301
302        @Override
303        public void cancel() {
304            valid = false;
305            synchronized (map) {
306                map.remove(fileKey());
307            }
308            disable();
309        }
310
311        /**
312         * Polls the directory to detect for new files, modified files, or
313         * deleted files.
314         */
315        synchronized void poll() {
316            if (!valid) {
317                return;
318            }
319
320            // update tick
321            tickCount++;
322
323            // open directory
324            DirectoryStream<Path> stream = null;
325            try {
326                stream = Files.newDirectoryStream(watchable());
327            } catch (IOException x) {
328                // directory is no longer accessible so cancel key
329                cancel();
330                signal();
331                return;
332            }
333
334            // iterate over all entries in directory
335            try {
336                for (Path entry: stream) {
337                    long lastModified = 0L;
338                    try {
339                        lastModified =
340                            Files.getLastModifiedTime(entry, LinkOption.NOFOLLOW_LINKS).toMillis();
341                    } catch (IOException x) {
342                        // unable to get attributes of entry. If file has just
343                        // been deleted then we'll report it as deleted on the
344                        // next poll
345                        continue;
346                    }
347
348                    // lookup cache
349                    CacheEntry e = entries.get(entry.getFileName());
350                    if (e == null) {
351                        // new file found
352                        entries.put(entry.getFileName(),
353                                     new CacheEntry(lastModified, tickCount));
354
355                        // queue ENTRY_CREATE if event enabled
356                        if (events.contains(StandardWatchEventKinds.ENTRY_CREATE)) {
357                            signalEvent(StandardWatchEventKinds.ENTRY_CREATE, entry.getFileName());
358                            continue;
359                        } else {
360                            // if ENTRY_CREATE is not enabled and ENTRY_MODIFY is
361                            // enabled then queue event to avoid missing out on
362                            // modifications to the file immediately after it is
363                            // created.
364                            if (events.contains(StandardWatchEventKinds.ENTRY_MODIFY)) {
365                                signalEvent(StandardWatchEventKinds.ENTRY_MODIFY, entry.getFileName());
366                            }
367                        }
368                        continue;
369                    }
370
371                    // check if file has changed
372                    if (e.lastModified != lastModified) {
373                        if (events.contains(StandardWatchEventKinds.ENTRY_MODIFY)) {
374                            signalEvent(StandardWatchEventKinds.ENTRY_MODIFY,
375                                        entry.getFileName());
376                        }
377                    }
378                    // entry in cache so update poll time
379                    e.update(lastModified, tickCount);
380
381                }
382            } catch (DirectoryIteratorException e) {
383                // ignore for now; if the directory is no longer accessible
384                // then the key will be cancelled on the next poll
385            } finally {
386
387                // close directory stream
388                try {
389                    stream.close();
390                } catch (IOException x) {
391                    // ignore
392                }
393            }
394
395            // iterate over cache to detect entries that have been deleted
396            Iterator<Map.Entry<Path,CacheEntry>> i = entries.entrySet().iterator();
397            while (i.hasNext()) {
398                Map.Entry<Path,CacheEntry> mapEntry = i.next();
399                CacheEntry entry = mapEntry.getValue();
400                if (entry.lastTickCount() != tickCount) {
401                    Path name = mapEntry.getKey();
402                    // remove from map and queue delete event (if enabled)
403                    i.remove();
404                    if (events.contains(StandardWatchEventKinds.ENTRY_DELETE)) {
405                        signalEvent(StandardWatchEventKinds.ENTRY_DELETE, name);
406                    }
407                }
408            }
409        }
410    }
411}
412