001/* 002 * This file is part of McIDAS-V 003 * 004 * Copyright 2007-2016 005 * Space Science and Engineering Center (SSEC) 006 * University of Wisconsin - Madison 007 * 1225 W. Dayton Street, Madison, WI 53706, USA 008 * https://www.ssec.wisc.edu/mcidas 009 * 010 * All Rights Reserved 011 * 012 * McIDAS-V is built on Unidata's IDV and SSEC's VisAD libraries, and 013 * some McIDAS-V source code is based on IDV and VisAD source code. 014 * 015 * McIDAS-V is free software; you can redistribute it and/or modify 016 * it under the terms of the GNU Lesser Public License as published by 017 * the Free Software Foundation; either version 3 of the License, or 018 * (at your option) any later version. 019 * 020 * McIDAS-V is distributed in the hope that it will be useful, 021 * but WITHOUT ANY WARRANTY; without even the implied warranty of 022 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 023 * GNU Lesser Public License for more details. 024 * 025 * You should have received a copy of the GNU Lesser Public License 026 * along with this program. If not, see http://www.gnu.org/licenses. 027 */ 028 029package edu.wisc.ssec.mcidasv.util.pathwatcher; 030 031import static java.nio.file.StandardWatchEventKinds.ENTRY_CREATE; 032import static java.nio.file.StandardWatchEventKinds.ENTRY_DELETE; 033import static java.nio.file.StandardWatchEventKinds.ENTRY_MODIFY; 034import static java.nio.file.StandardWatchEventKinds.OVERFLOW; 035 036import static edu.wisc.ssec.mcidasv.util.CollectionHelpers.concurrentMap; 037import static edu.wisc.ssec.mcidasv.util.CollectionHelpers.concurrentSet; 038 039import java.io.IOException; 040 041import java.nio.file.FileSystem; 042import java.nio.file.FileSystems; 043import java.nio.file.Files; 044import java.nio.file.Path; 045import java.nio.file.PathMatcher; 046import java.nio.file.Paths; 047import java.nio.file.WatchEvent; 048import java.nio.file.WatchKey; 049import java.nio.file.WatchService; 050 051import java.util.Arrays; 052import java.util.Map; 053import java.util.Set; 054import java.util.concurrent.atomic.AtomicBoolean; 055import java.util.stream.Collectors; 056 057import org.slf4j.Logger; 058import org.slf4j.LoggerFactory; 059 060// Adapted from https://gist.github.com/hindol-viz/394ebc553673e2cd0699 061 062/** 063 * A simple class which can monitor files and notify interested parties 064 * (i.e. listeners) of file changes. 065 * 066 * This class is kept lean by only keeping methods that are actually being 067 * called. 068 */ 069public class SimpleDirectoryWatchService implements DirectoryWatchService, 070 Runnable 071{ 072 073 /** Logging object. */ 074 private static final Logger logger = 075 LoggerFactory.getLogger(SimpleDirectoryWatchService.class); 076 077 /** 078 * {@code WatchService} used to monitor changes in various 079 * {@link Path Paths}. 080 */ 081 private final WatchService mWatchService; 082 083 /** Whether or not this {@link DirectoryWatchService} is running. */ 084 private final AtomicBoolean mIsRunning; 085 086 /** 087 * Mapping of monitoring {@literal "registration"} keys to the 088 * {@link Path} that it will be watching. 089 */ 090 private final Map<WatchKey, Path> mWatchKeyToDirPathMap; 091 092 /** 093 * Mapping of {@link Path Paths} to the {@link Set} of 094 * {@link OnFileChangeListener OnFileChangeListeners} listening for 095 * changes to the associated {@code Path}. 096 */ 097 private final Map<Path, Set<OnFileChangeListener>> mDirPathToListenersMap; 098 099 /** 100 * Mapping of {@link OnFileChangeListener OnFileChangeListeners} to the 101 * {@link Set} of patterns being used to observe changes in 102 * {@link Path Paths} of interest. 103 */ 104 private final Map<OnFileChangeListener, Set<PathMatcher>> 105 mListenerToFilePatternsMap; 106 107 /** 108 * A simple no argument constructor for creating a 109 * {@code SimpleDirectoryWatchService}. 110 * 111 * @throws IOException If an I/O error occurs. 112 */ 113 public SimpleDirectoryWatchService() throws IOException { 114 mWatchService = FileSystems.getDefault().newWatchService(); 115 mIsRunning = new AtomicBoolean(false); 116 mWatchKeyToDirPathMap = concurrentMap(); 117 mDirPathToListenersMap = concurrentMap(); 118 mListenerToFilePatternsMap = concurrentMap(); 119 } 120 121 /** 122 * Utility method used to make {@literal "valid"} casts of the given 123 * {@code event} to a specific type of {@link WatchEvent}. 124 * 125 * @param <T> Type to which {@code event} will be casted. 126 * @param event Event to cast. 127 * 128 * @return {@code event} casted to {@code WatchEvent<T>}. 129 */ 130 @SuppressWarnings("unchecked") 131 private static <T> WatchEvent<T> cast(WatchEvent<?> event) { 132 return (WatchEvent<T>)event; 133 } 134 135 /** 136 * Returns a {@link PathMatcher} that performs {@literal "glob"} matches 137 * with the given {@code globPattern} against the {@code String} 138 * representation of {@link Path} objects. 139 * 140 * @param globPattern Pattern to match against. {@code null} or empty 141 * {@code String} values will be converted to {@code *}. 142 * 143 * @return Path matching object for the given {@code globPattern}. 144 * 145 * @throws IOException if there was a problem creating the 146 * {@code PathMatcher}. 147 */ 148 private static PathMatcher matcherForGlobExpression(String globPattern) 149 throws IOException 150 { 151 if ((globPattern == null) || globPattern.isEmpty()) { 152 globPattern = "*"; 153 } 154 155 return FileSystems.getDefault().getPathMatcher("glob:"+globPattern); 156 } 157 158 /** 159 * Check the given {@code input} {@link Path} against the given {@code 160 * pattern}. 161 * 162 * @param input Path to check. 163 * @param pattern Pattern to check against. Cannot be {@code null}. 164 * 165 * @return Whether or not {@code input} matches {@code pattern}. 166 */ 167 public static boolean matches(Path input, PathMatcher pattern) { 168 return pattern.matches(input); 169 } 170 171 /** 172 * Check the given {@code input} {@link Path} against <i>all</i> of the 173 * specified {@code patterns}. 174 * 175 * @param input Path to check. 176 * @param patterns {@link Set} of patterns to attempt to match 177 * {@code input} against. Cannot be {@code null}. 178 * 179 * @return Whether or not {@code input} matches any of the given 180 * {@code patterns}. 181 */ 182 private static boolean matchesAny(Path input, Set<PathMatcher> patterns) { 183 for (PathMatcher pattern : patterns) { 184 if (matches(input, pattern)) { 185 return true; 186 } 187 } 188 189 return false; 190 } 191 192 /** 193 * Get the path associated with the given {@link WatchKey}. 194 * 195 * @param key {@code WatchKey} whose corresponding {@link Path} is being 196 * requested. 197 * 198 * @return Either the correspond {@code Path} or {@code null}. 199 */ 200 private Path getDirPath(WatchKey key) { 201 return mWatchKeyToDirPathMap.get(key); 202 } 203 204 /** 205 * Get the {@link OnFileChangeListener OnFileChangeListeners} associated 206 * with the given {@code path}. 207 * 208 * @param path Path whose listeners should be returned. Cannot be 209 * {@code null}. 210 * 211 * @return Either the {@link Set} of listeners associated with {@code path} 212 * or {@code null}. 213 */ 214 private Set<OnFileChangeListener> getListeners(Path path) { 215 return mDirPathToListenersMap.get(path); 216 } 217 218 /** 219 * Get the {@link Set} of patterns associated with the given 220 * {@link OnFileChangeListener}. 221 * 222 * @param listener Listener of interest. 223 * 224 * @return Either the {@code Set} of patterns associated with 225 * {@code listener} or {@code null}. 226 */ 227 private Set<PathMatcher> getPatterns(OnFileChangeListener listener) { 228 return mListenerToFilePatternsMap.get(listener); 229 } 230 231 /** 232 * Get the {@link Path} associated with the given 233 * {@link OnFileChangeListener}. 234 * 235 * @param listener Listener whose path is requested. 236 * 237 * @return Either the {@code Path} associated with {@code listener} or 238 * {@code null}. 239 */ 240 private Path getDir(OnFileChangeListener listener) { 241 242 Set<Map.Entry<Path, Set<OnFileChangeListener>>> entries = 243 mDirPathToListenersMap.entrySet(); 244 245 Path result = null; 246 for (Map.Entry<Path, Set<OnFileChangeListener>> entry : entries) { 247 Set<OnFileChangeListener> listeners = entry.getValue(); 248 if (listeners.contains(listener)) { 249 result = entry.getKey(); 250 break; 251 } 252 } 253 254 return result; 255 } 256 257 /** 258 * Get the monitoring {@literal "registration"} key associated with the 259 * given {@link Path}. 260 * 261 * @param dir {@code Path} whose {@link WatchKey} is requested. 262 * 263 * @return Either the {@code WatchKey} corresponding to {@code dir} or 264 * {@code null}. 265 */ 266 private WatchKey getWatchKey(Path dir) { 267 Set<Map.Entry<WatchKey, Path>> entries = 268 mWatchKeyToDirPathMap.entrySet(); 269 270 WatchKey key = null; 271 for (Map.Entry<WatchKey, Path> entry : entries) { 272 if (entry.getValue().equals(dir)) { 273 key = entry.getKey(); 274 break; 275 } 276 } 277 278 return key; 279 } 280 281 /** 282 * Get the {@link Set} of 283 * {@link OnFileChangeListener OnFileChangeListeners} that should be 284 * notified that {@code file} has changed. 285 * 286 * @param dir Directory containing {@code file}. 287 * @param file File that changed. 288 * 289 * @return {@code Set} of listeners that should be notified that 290 * {@code file} has changed. 291 */ 292 private Set<OnFileChangeListener> matchedListeners(Path dir, Path file) { 293 return getListeners(dir) 294 .stream() 295 .filter(listener -> matchesAny(file, getPatterns(listener))) 296 .collect(Collectors.toSet()); 297 } 298 299 /** 300 * Method responsible for notifying listeners when a file matching their 301 * relevant pattern has changed. 302 * 303 * <p>Note: {@literal "change"} means one of: 304 * <ul> 305 * <li>file creation</li> 306 * <li>file removal</li> 307 * <li>file contents changing</li> 308 * </ul></p> 309 * 310 * @param key {@link #mWatchService} {@literal "registration"} key for 311 * one of the {@link Path Paths} being watched. Cannot be 312 * {@code null}. 313 * 314 * @see #run() 315 */ 316 private void notifyListeners(WatchKey key) { 317 for (WatchEvent<?> event : key.pollEvents()) { 318 WatchEvent.Kind eventKind = event.kind(); 319 320 // Overflow occurs when the watch event queue is overflown 321 // with events. 322 if (eventKind.equals(OVERFLOW)) { 323 // TODO: Notify all listeners. 324 return; 325 } 326 327 WatchEvent<Path> pathEvent = cast(event); 328 Path file = pathEvent.context(); 329 String completePath = getDirPath(key).resolve(file).toString(); 330 331 if (eventKind.equals(ENTRY_CREATE)) { 332 matchedListeners(getDirPath(key), file) 333 .forEach(l -> l.onFileCreate(completePath)); 334 } else if (eventKind.equals(ENTRY_MODIFY)) { 335 matchedListeners(getDirPath(key), file) 336 .forEach(l -> l.onFileModify(completePath)); 337 } else if (eventKind.equals(ENTRY_DELETE)) { 338 matchedListeners(getDirPath(key), file) 339 .forEach(l -> l.onFileDelete(completePath)); 340 } 341 } 342 } 343 344 /** 345 * {@inheritDoc} 346 */ 347 @Override public void register(OnFileChangeListener listener, 348 String dirPath, String... globPatterns) 349 throws IOException 350 { 351 Path dir = Paths.get(dirPath); 352 353 if (!Files.isDirectory(dir)) { 354 throw new IllegalArgumentException(dirPath + " not a directory."); 355 } 356 357 if (!mDirPathToListenersMap.containsKey(dir)) { 358 // May throw 359 WatchKey key = dir.register( 360 mWatchService, ENTRY_CREATE, ENTRY_MODIFY, ENTRY_DELETE 361 ); 362 363 mWatchKeyToDirPathMap.put(key, dir); 364 mDirPathToListenersMap.put(dir, concurrentSet()); 365 } 366 367 getListeners(dir).add(listener); 368 369 Set<PathMatcher> patterns = concurrentSet(); 370 371 for (String globPattern : globPatterns) { 372 patterns.add(matcherForGlobExpression(globPattern)); 373 } 374 375 if (patterns.isEmpty()) { 376 // Match everything if no filter is found 377 patterns.add(matcherForGlobExpression("*")); 378 } 379 380 mListenerToFilePatternsMap.put(listener, patterns); 381 382 logger.trace("Watching files matching {} under '{}' for changes", 383 Arrays.toString(globPatterns), dirPath); 384 } 385 386 /** 387 * {@inheritDoc} 388 */ 389 public void unregister(OnFileChangeListener listener) { 390 Path dir = getDir(listener); 391 392 mDirPathToListenersMap.get(dir).remove(listener); 393 394 // is this step truly needed? 395 if (mDirPathToListenersMap.get(dir).isEmpty()) { 396 mDirPathToListenersMap.remove(dir); 397 } 398 399 mListenerToFilePatternsMap.remove(listener); 400 401 WatchKey key = getWatchKey(dir); 402 if (key != null) { 403 mWatchKeyToDirPathMap.remove(key); 404 key.cancel(); 405 } 406 logger.trace("listener unregistered"); 407 } 408 409 /** 410 * {@inheritDoc} 411 */ 412 @Override public void unregisterAll() { 413 // can't simply clear the key->dir map; need to cancel 414 mWatchKeyToDirPathMap.keySet().forEach(WatchKey::cancel); 415 416 mWatchKeyToDirPathMap.clear(); 417 mDirPathToListenersMap.clear(); 418 mListenerToFilePatternsMap.clear(); 419 } 420 421 /** 422 * Start this {@code SimpleDirectoryWatchService} instance by spawning a 423 * new thread. 424 * 425 * @see #stop() 426 */ 427 @Override public void start() { 428 if (mIsRunning.compareAndSet(false, true)) { 429 String name = DirectoryWatchService.class.getSimpleName(); 430 Thread runnerThread = new Thread(this, name); 431 runnerThread.start(); 432 } 433 } 434 435 /** 436 * Stop this {@code SimpleDirectoryWatchService} thread. 437 * 438 * <p>The killing happens lazily, giving the running thread an opportunity 439 * to finish the work at hand.</p> 440 * 441 * @see #start() 442 */ 443 @Override public void stop() { 444 // Kill thread lazily 445 mIsRunning.set(false); 446 } 447 448 /** 449 * {@inheritDoc} 450 */ 451 @Override public boolean isRunning() { 452 return mIsRunning.get(); 453 } 454 455 /** 456 * {@inheritDoc} 457 */ 458 @Override public void run() { 459 logger.info("Starting file watcher service."); 460 461 while (mIsRunning.get()) { 462 WatchKey key; 463 try { 464 key = mWatchService.take(); 465 } catch (InterruptedException e) { 466 logger.trace("{} service interrupted.", 467 DirectoryWatchService.class.getSimpleName()); 468 break; 469 } 470 471 if (null == getDirPath(key)) { 472 logger.error("Watch key not recognized."); 473 continue; 474 } 475 476 notifyListeners(key); 477 478 // Reset key to allow further events for this key to be processed. 479 boolean valid = key.reset(); 480 if (!valid) { 481 mWatchKeyToDirPathMap.remove(key); 482 if (mWatchKeyToDirPathMap.isEmpty()) { 483 break; 484 } 485 } 486 } 487 488 mIsRunning.set(false); 489 logger.trace("Stopping file watcher service."); 490 } 491}