001/*
002 * Syncany, www.syncany.org
003 * Copyright (C) 2011-2016 Philipp C. Heckel <philipp.heckel@gmail.com>
004 *
005 * This program is free software: you can redistribute it and/or modify
006 * it under the terms of the GNU General Public License as published by
007 * the Free Software Foundation, either version 3 of the License, or
008 * (at your option) any later version.
009 *
010 * This program is distributed in the hope that it will be useful,
011 * but WITHOUT ANY WARRANTY; without even the implied warranty of
012 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
013 * GNU General Public License for more details.
014 *
015 * You should have received a copy of the GNU General Public License
016 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
017 */
018package org.syncany.operations.watch;
019
020import static java.nio.file.StandardWatchEventKinds.ENTRY_CREATE;
021import static java.nio.file.StandardWatchEventKinds.ENTRY_DELETE;
022import static java.nio.file.StandardWatchEventKinds.ENTRY_MODIFY;
023import static java.nio.file.StandardWatchEventKinds.OVERFLOW;
024
025import java.io.IOException;
026import java.nio.file.FileSystems;
027import java.nio.file.FileVisitResult;
028import java.nio.file.FileVisitor;
029import java.nio.file.Files;
030import java.nio.file.LinkOption;
031import java.nio.file.Path;
032import java.nio.file.WatchKey;
033import java.nio.file.WatchService;
034import java.nio.file.attribute.BasicFileAttributes;
035import java.util.HashMap;
036import java.util.HashSet;
037import java.util.List;
038import java.util.Map;
039import java.util.Set;
040import java.util.logging.Level;
041
042/**
043 * The default recursive file watcher monitors a folder (and its sub-folders)
044 * by registering a watch on each of the sub-folders. This class is used on
045 * Linux/Unix-based operating systems and uses the Java 7 {@link WatchService}.
046 *
047 * <p>The class walks through the file tree and registers to a watch to every sub-folder.
048 * For new folders, a new watch is registered, and stale watches are removed.
049 *
050 * <p>When a file event occurs, a timer is started to wait for the file operations
051 * to settle. It is reset whenever a new event occurs. When the timer times out,
052 * an event is thrown through the {@link WatchListener}.
053 *
054 * @author Philipp C. Heckel (philipp.heckel@gmail.com)
055 */
056public class DefaultRecursiveWatcher extends RecursiveWatcher {
057        private WatchService watchService;
058        private Map<Path, WatchKey> watchPathKeyMap;
059
060        public DefaultRecursiveWatcher(Path root, List<Path> ignorePaths, int settleDelay, WatchListener listener) {
061                super(root, ignorePaths, settleDelay, listener);
062
063                this.watchService = null;
064                this.watchPathKeyMap = new HashMap<Path, WatchKey>();
065        }
066
067        @Override
068        public void beforeStart() throws Exception {
069                watchService = FileSystems.getDefault().newWatchService();
070        }
071
072        @Override
073        protected void beforePollEventLoop() {
074                walkTreeAndSetWatches();
075        }
076
077        @Override
078        protected boolean pollEvents() throws InterruptedException {
079                // Take events, but don't care what they are!
080                WatchKey watchKey = watchService.take();
081
082                watchKey.pollEvents();
083                watchKey.reset();
084
085                // Events are always relevant; ignored paths are not monitored
086                return true;
087        }
088
089        @Override
090        protected void watchEventsOccurred() {
091                walkTreeAndSetWatches();
092                unregisterStaleWatches();
093        }
094
095        @Override
096        public void afterStop() throws IOException {
097                watchService.close();
098        }
099
100        private synchronized void walkTreeAndSetWatches() {
101                logger.log(Level.INFO, "Registering new folders at watch service ...");
102
103                try {
104                        Files.walkFileTree(root, new FileVisitor<Path>() {
105                                @Override
106                                public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
107                                        if (ignorePaths.contains(dir)) {
108                                                return FileVisitResult.SKIP_SUBTREE;
109                                        }
110                                        else {
111                                                registerWatch(dir);
112                                                return FileVisitResult.CONTINUE;
113                                        }
114                                }
115
116                                @Override
117                                public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
118                                        return FileVisitResult.CONTINUE;
119                                }
120
121                                @Override
122                                public FileVisitResult visitFileFailed(Path file, IOException exc) throws IOException {
123                                        return FileVisitResult.CONTINUE;
124                                }
125
126                                @Override
127                                public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
128                                        return FileVisitResult.CONTINUE;
129                                }
130                        });
131                }
132                catch (IOException e) {
133                        logger.log(Level.FINE, "IO failed", e);
134                }
135        }
136
137        private synchronized void unregisterStaleWatches() {
138                Set<Path> paths = new HashSet<Path>(watchPathKeyMap.keySet());
139                Set<Path> stalePaths = new HashSet<Path>();
140
141                for (Path path : paths) {
142                        if (!Files.exists(path, LinkOption.NOFOLLOW_LINKS)) {
143                                stalePaths.add(path);
144                        }
145                }
146
147                if (stalePaths.size() > 0) {
148                        logger.log(Level.INFO, "Cancelling stale path watches ...");
149
150                        for (Path stalePath : stalePaths) {
151                                unregisterWatch(stalePath);
152                        }
153                }
154        }
155
156        private synchronized void registerWatch(Path dir) {
157                if (!watchPathKeyMap.containsKey(dir)) {
158                        logger.log(Level.INFO, "- Registering " + dir);
159
160                        try {
161                                WatchKey watchKey = dir.register(watchService, ENTRY_CREATE, ENTRY_DELETE, ENTRY_MODIFY, OVERFLOW);
162                                watchPathKeyMap.put(dir, watchKey);
163                        }
164                        catch (IOException e) {
165                                logger.log(Level.FINE, "IO Failed", e);
166                        }
167                }
168        }
169
170        private synchronized void unregisterWatch(Path dir) {
171                WatchKey watchKey = watchPathKeyMap.get(dir);
172
173                if (watchKey != null) {
174                        logger.log(Level.INFO, "- Cancelling " + dir);
175
176                        watchKey.cancel();
177                        watchPathKeyMap.remove(dir);
178                }
179        }
180}