[7937] | 1 | // License: GPL. For details, see LICENSE file.
|
---|
| 2 | package org.openstreetmap.josm.io;
|
---|
| 3 |
|
---|
| 4 | import java.io.File;
|
---|
| 5 | import java.io.IOException;
|
---|
| 6 | import java.nio.file.FileSystems;
|
---|
[12814] | 7 | import java.nio.file.Files;
|
---|
[7937] | 8 | import java.nio.file.Path;
|
---|
| 9 | import java.nio.file.StandardWatchEventKinds;
|
---|
| 10 | import java.nio.file.WatchEvent;
|
---|
| 11 | import java.nio.file.WatchEvent.Kind;
|
---|
| 12 | import java.nio.file.WatchKey;
|
---|
| 13 | import java.nio.file.WatchService;
|
---|
[12825] | 14 | import java.util.EnumMap;
|
---|
[7937] | 15 | import java.util.HashMap;
|
---|
| 16 | import java.util.Map;
|
---|
[12825] | 17 | import java.util.Objects;
|
---|
| 18 | import java.util.function.Consumer;
|
---|
[7937] | 19 |
|
---|
[12649] | 20 | import org.openstreetmap.josm.data.preferences.sources.SourceEntry;
|
---|
[12825] | 21 | import org.openstreetmap.josm.data.preferences.sources.SourceType;
|
---|
[7937] | 22 | import org.openstreetmap.josm.tools.CheckParameterUtil;
|
---|
[12620] | 23 | import org.openstreetmap.josm.tools.Logging;
|
---|
[7937] | 24 |
|
---|
| 25 | /**
|
---|
| 26 | * Background thread that monitors certain files and perform relevant actions when they change.
|
---|
| 27 | * @since 7185
|
---|
| 28 | */
|
---|
| 29 | public class FileWatcher {
|
---|
| 30 |
|
---|
| 31 | private WatchService watcher;
|
---|
[9205] | 32 | private Thread thread;
|
---|
[7937] | 33 |
|
---|
[12825] | 34 | private static final Map<SourceType, Consumer<SourceEntry>> loaderMap = new EnumMap<>(SourceType.class);
|
---|
| 35 | private final Map<Path, SourceEntry> sourceMap = new HashMap<>();
|
---|
[7937] | 36 |
|
---|
| 37 | /**
|
---|
| 38 | * Constructs a new {@code FileWatcher}.
|
---|
| 39 | */
|
---|
| 40 | public FileWatcher() {
|
---|
| 41 | try {
|
---|
| 42 | watcher = FileSystems.getDefault().newWatchService();
|
---|
[10615] | 43 | thread = new Thread((Runnable) this::processEvents, "File Watcher");
|
---|
[7937] | 44 | } catch (IOException e) {
|
---|
[12620] | 45 | Logging.error(e);
|
---|
[7937] | 46 | }
|
---|
| 47 | }
|
---|
| 48 |
|
---|
| 49 | /**
|
---|
[9205] | 50 | * Starts the File Watcher thread.
|
---|
| 51 | */
|
---|
| 52 | public final void start() {
|
---|
[11589] | 53 | if (thread != null && !thread.isAlive()) {
|
---|
[9205] | 54 | thread.start();
|
---|
| 55 | }
|
---|
| 56 | }
|
---|
| 57 |
|
---|
| 58 | /**
|
---|
[12825] | 59 | * Registers a source for local file changes, allowing dynamic reloading.
|
---|
| 60 | * @param src The source to watch
|
---|
| 61 | * @throws IllegalArgumentException if {@code rule} is null or if it does not provide a local file
|
---|
| 62 | * @throws IllegalStateException if the watcher service failed to start
|
---|
| 63 | * @throws IOException if an I/O error occurs
|
---|
| 64 | * @since 12825
|
---|
| 65 | */
|
---|
| 66 | public void registerSource(SourceEntry src) throws IOException {
|
---|
| 67 | CheckParameterUtil.ensureParameterNotNull(src, "src");
|
---|
[7937] | 68 | if (watcher == null) {
|
---|
| 69 | throw new IllegalStateException("File watcher is not available");
|
---|
| 70 | }
|
---|
| 71 | // Get local file, as this method is only called for local style sources
|
---|
[12825] | 72 | File file = new File(src.url);
|
---|
[7937] | 73 | // Get parent directory as WatchService allows only to monitor directories, not single files
|
---|
| 74 | File dir = file.getParentFile();
|
---|
| 75 | if (dir == null) {
|
---|
[12825] | 76 | throw new IllegalArgumentException("Resource "+src+" does not have a parent directory");
|
---|
[7937] | 77 | }
|
---|
[8510] | 78 | synchronized (this) {
|
---|
[7937] | 79 | // Register directory. Can be called several times for a same directory without problem
|
---|
| 80 | // (it returns the same key so it should not send events several times)
|
---|
| 81 | dir.toPath().register(watcher, StandardWatchEventKinds.ENTRY_MODIFY, StandardWatchEventKinds.ENTRY_CREATE);
|
---|
[12825] | 82 | sourceMap.put(file.toPath(), src);
|
---|
[7937] | 83 | }
|
---|
| 84 | }
|
---|
| 85 |
|
---|
| 86 | /**
|
---|
[12825] | 87 | * Registers a source loader, allowing dynamic reloading when an entry changes.
|
---|
| 88 | * @param type the source type for which the loader operates
|
---|
| 89 | * @param loader the loader in charge of reloading any source of given type when it changes
|
---|
| 90 | * @return the previous loader registered for this source type, if any
|
---|
| 91 | * @since 12825
|
---|
| 92 | */
|
---|
| 93 | public static Consumer<SourceEntry> registerLoader(SourceType type, Consumer<SourceEntry> loader) {
|
---|
| 94 | return loaderMap.put(Objects.requireNonNull(type, "type"), Objects.requireNonNull(loader, "loader"));
|
---|
| 95 | }
|
---|
| 96 |
|
---|
| 97 | /**
|
---|
[7937] | 98 | * Process all events for the key queued to the watcher.
|
---|
| 99 | */
|
---|
| 100 | private void processEvents() {
|
---|
[12620] | 101 | Logging.debug("File watcher thread started");
|
---|
[7937] | 102 | while (true) {
|
---|
| 103 |
|
---|
| 104 | // wait for key to be signaled
|
---|
| 105 | WatchKey key;
|
---|
| 106 | try {
|
---|
| 107 | key = watcher.take();
|
---|
[11620] | 108 | } catch (InterruptedException ex) {
|
---|
[11535] | 109 | Thread.currentThread().interrupt();
|
---|
[7937] | 110 | return;
|
---|
| 111 | }
|
---|
| 112 |
|
---|
| 113 | for (WatchEvent<?> event: key.pollEvents()) {
|
---|
| 114 | Kind<?> kind = event.kind();
|
---|
| 115 |
|
---|
| 116 | if (StandardWatchEventKinds.OVERFLOW.equals(kind)) {
|
---|
| 117 | continue;
|
---|
| 118 | }
|
---|
| 119 |
|
---|
| 120 | // The filename is the context of the event.
|
---|
| 121 | @SuppressWarnings("unchecked")
|
---|
[8510] | 122 | WatchEvent<Path> ev = (WatchEvent<Path>) event;
|
---|
[7937] | 123 | Path filename = ev.context();
|
---|
| 124 | if (filename == null) {
|
---|
| 125 | continue;
|
---|
| 126 | }
|
---|
| 127 |
|
---|
| 128 | // Only way to get full path (http://stackoverflow.com/a/7802029/2257172)
|
---|
[8510] | 129 | Path fullPath = ((Path) key.watchable()).resolve(filename);
|
---|
[7937] | 130 |
|
---|
[12814] | 131 | try {
|
---|
| 132 | // Some filesystems fire two events when a file is modified. Skip first event (file is empty)
|
---|
| 133 | if (Files.size(fullPath) == 0) {
|
---|
| 134 | continue;
|
---|
| 135 | }
|
---|
| 136 | } catch (IOException ex) {
|
---|
[12827] | 137 | Logging.trace(ex);
|
---|
[12814] | 138 | continue;
|
---|
| 139 | }
|
---|
| 140 |
|
---|
[8510] | 141 | synchronized (this) {
|
---|
[12825] | 142 | SourceEntry source = sourceMap.get(fullPath);
|
---|
| 143 | if (source != null) {
|
---|
| 144 | Consumer<SourceEntry> loader = loaderMap.get(source.type);
|
---|
| 145 | if (loader != null) {
|
---|
| 146 | Logging.info("Source "+source.getDisplayString()+" has been modified. Reloading it...");
|
---|
| 147 | loader.accept(source);
|
---|
| 148 | } else {
|
---|
| 149 | Logging.warn("Received {0} event for unregistered source type: {1}", kind.name(), source.type);
|
---|
[7937] | 150 | }
|
---|
[12620] | 151 | } else if (Logging.isDebugEnabled()) {
|
---|
| 152 | Logging.debug("Received {0} event for unregistered file: {1}", kind.name(), fullPath);
|
---|
[7937] | 153 | }
|
---|
| 154 | }
|
---|
| 155 | }
|
---|
| 156 |
|
---|
| 157 | // Reset the key -- this step is critical to receive
|
---|
| 158 | // further watch events. If the key is no longer valid, the directory
|
---|
| 159 | // is inaccessible so exit the loop.
|
---|
| 160 | if (!key.reset()) {
|
---|
| 161 | break;
|
---|
| 162 | }
|
---|
| 163 | }
|
---|
| 164 | }
|
---|
| 165 | }
|
---|