All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.helidon.config.FileSystemWatcher Maven / Gradle / Ivy

There is a newer version: 4.1.1
Show newest version
/*
 * Copyright (c) 2020, 2022 Oracle and/or its affiliates.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package io.helidon.config;

import java.io.IOException;
import java.lang.System.Logger.Level;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.WatchEvent;
import java.nio.file.WatchKey;
import java.nio.file.WatchService;
import java.time.Duration;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;

import io.helidon.config.spi.ChangeEventType;
import io.helidon.config.spi.ChangeWatcher;

import static java.nio.file.StandardWatchEventKinds.ENTRY_CREATE;
import static java.nio.file.StandardWatchEventKinds.ENTRY_DELETE;
import static java.nio.file.StandardWatchEventKinds.ENTRY_MODIFY;
import static java.nio.file.StandardWatchEventKinds.OVERFLOW;

/**
 * This change watcher is backed by {@link WatchService} to fire a polling event with every change on monitored {@link Path}.
 * 

* When a parent directory of the {@code path} is not available, or becomes unavailable later, a new attempt to register {@code * WatchService} is scheduled again and again until the directory finally exists and the registration is successful. *

* This {@link io.helidon.config.spi.ChangeWatcher} might be initialized with a custom {@link ScheduledExecutorService executor} * or the {@link Executors#newSingleThreadScheduledExecutor()} is used if none is explicitly configured. *

* This watcher notifies with appropriate change event in the following cases: *

    *
  • The watched directory is gone
  • *
  • The watched directory appears
  • *
  • A file in the watched directory is deleted, created or modified
  • *
*

* A single file system watcher may be used to watch multiple targets. In such a case, if {@link #stop()} is invoked, it stops * watching all of these targets. * * @see WatchService */ public final class FileSystemWatcher implements ChangeWatcher { private static final System.Logger LOGGER = System.getLogger(FileSystemWatcher.class.getName()); /* * Configurable options through builder. */ private final List watchServiceModifiers = new LinkedList<>(); private ScheduledExecutorService executor; private final boolean defaultExecutor; private final long initialDelay; private final long delay; private final TimeUnit timeUnit; /* * Runtime options. */ private final List runtimes = Collections.synchronizedList(new LinkedList<>()); private FileSystemWatcher(Builder builder) { ScheduledExecutorService executor = builder.executor; if (executor == null) { this.executor = Executors.newSingleThreadScheduledExecutor(new ConfigThreadFactory("file-watch-polling")); this.defaultExecutor = true; } else { this.executor = executor; this.defaultExecutor = false; } this.watchServiceModifiers.addAll(builder.watchServiceModifiers); this.initialDelay = builder.initialDelay; this.delay = builder.delay; this.timeUnit = builder.timeUnit; } /** * Fluent API builder for {@link io.helidon.config.FileSystemWatcher}. * @return a new builder instance */ public static Builder builder() { return new Builder(); } /** * Create a new file watcher with default configuration. * * @return a new file watcher */ public static FileSystemWatcher create() { return builder().build(); } @Override public synchronized void start(Path target, Consumer> listener) { if (defaultExecutor && executor.isShutdown()) { executor = Executors.newSingleThreadScheduledExecutor(new ConfigThreadFactory("file-watch-polling")); } if (executor.isShutdown()) { throw new ConfigException("Cannot start a watcher for path " + target + ", as the executor service is shutdown"); } Monitor monitor = new Monitor( listener, target, watchServiceModifiers); ScheduledFuture future = executor.scheduleWithFixedDelay(monitor, initialDelay, delay, timeUnit); this.runtimes.add(new TargetRuntime(monitor, future)); } @Override public synchronized void stop() { runtimes.forEach(TargetRuntime::stop); if (defaultExecutor) { ConfigUtils.shutdownExecutor(executor); } } @Override public Class type() { return Path.class; } /** * Add modifiers to be used when registering the {@link WatchService}. * See {@link Path#register(WatchService, WatchEvent.Kind[], * WatchEvent.Modifier...) Path.register}. * * @param modifiers the modifiers to add */ public void initWatchServiceModifiers(WatchEvent.Modifier... modifiers) { watchServiceModifiers.addAll(Arrays.asList(modifiers)); } private static final class TargetRuntime { private final Monitor monitor; private final ScheduledFuture future; private TargetRuntime(Monitor monitor, ScheduledFuture future) { this.monitor = monitor; this.future = future; } public void stop() { monitor.stop(); future.cancel(true); } } private static final class Monitor implements Runnable { private final WatchService watchService; private final Consumer> listener; private final Path target; private final List watchServiceModifiers; private final boolean watchingFile; private final Path watchedDir; /* * Runtime handling */ // we have failed - retry registration on next trigger private volatile boolean failed = true; // maybe we were stopped, do not do anything (the scheduled future will be cancelled shortly) private volatile boolean shouldStop = false; // last file information private volatile boolean fileExists; private WatchKey watchKey; private Monitor(Consumer> listener, Path target, List watchServiceModifiers) { try { this.watchService = FileSystems.getDefault().newWatchService(); } catch (IOException e) { throw new ConfigException("Cannot obtain WatchService.", e); } this.listener = listener; this.target = target; this.watchServiceModifiers = watchServiceModifiers; this.fileExists = Files.exists(target); this.watchingFile = !Files.isDirectory(target); this.watchedDir = watchingFile ? target.getParent() : target; } @SuppressWarnings("unchecked") @Override public void run() { if (shouldStop) { return; } if (failed) { register(); } if (failed) { return; } // if we used `take`, we would block the thread forever. This way we can use the same thread to handle // multiple targets WatchKey key = watchService.poll(); if (null == key) { return; } List> watchEvents = key.pollEvents(); if (watchEvents.isEmpty()) { // something happened, cannot get details key.cancel(); listener.accept(ChangeEvent.create(target, ChangeEventType.CHANGED)); failed = true; return; } // we actually have some changes for (WatchEvent watchEvent : watchEvents) { WatchEvent event = (WatchEvent) watchEvent; Path eventPath = event.context(); // as we watch on whole directory // make sure this is the watched file (if only interested in a single file) if (watchingFile && !target.endsWith(eventPath)) { continue; } eventPath = watchedDir.resolve(eventPath); WatchEvent.Kind kind = event.kind(); if (kind.equals(OVERFLOW)) { LOGGER.log(Level.DEBUG, "Overflow event on path: " + eventPath); continue; } if (kind.equals(ENTRY_CREATE)) { LOGGER.log(Level.DEBUG, "Entry created. Path: " + eventPath); listener.accept(ChangeEvent.create(eventPath, ChangeEventType.CREATED)); } else if (kind == ENTRY_DELETE) { LOGGER.log(Level.DEBUG, "Entry deleted. Path: " + eventPath); listener.accept(ChangeEvent.create(eventPath, ChangeEventType.DELETED)); } else if (kind == ENTRY_MODIFY) { LOGGER.log(Level.DEBUG, "Entry changed. Path: " + eventPath); listener.accept(ChangeEvent.create(eventPath, ChangeEventType.CHANGED)); } } if (!key.reset()) { LOGGER.log(Level.TRACE, "Directory of '" + target + "' is no more valid to be watched."); failed = true; } } private void fire(Path target, ChangeEventType eventType) { listener.accept(ChangeEvent.create(target, eventType)); } private synchronized void register() { if (shouldStop) { failed = true; return; } boolean oldFileExists = fileExists; try { Path cleanTarget = target(this.target); Path watchedDirectory = Files.isDirectory(cleanTarget) ? cleanTarget : parentDir(cleanTarget); WatchKey oldWatchKey = watchKey; watchKey = watchedDirectory.register(watchService, new WatchEvent.Kind[] {ENTRY_CREATE, ENTRY_MODIFY, ENTRY_DELETE}, watchServiceModifiers.toArray(new WatchEvent.Modifier[0])); failed = false; if (null != oldWatchKey) { oldWatchKey.cancel(); } } catch (IOException e) { LOGGER.log(Level.TRACE, "Failed to register watch service", e); this.failed = true; } // in either case, let's see if our target has changed this.fileExists = Files.exists(target); if (fileExists != oldFileExists) { if (fileExists) { fire(this.target, ChangeEventType.CREATED); } else { fire(this.target, ChangeEventType.DELETED); } } } private synchronized void stop() { this.shouldStop = true; if (null != watchKey) { watchKey.cancel(); } try { watchService.close(); } catch (IOException e) { LOGGER.log(Level.TRACE, "Failed to close watch service", e); } } private Path target(Path path) throws IOException { Path target = path; while (Files.isSymbolicLink(target)) { target = target.toRealPath(); } return target; } private Path parentDir(Path path) { Path parent = path.toAbsolutePath().getParent(); if (parent == null) { throw new ConfigException( String.format("Cannot find parent directory for '%s' to register watch service.", path)); } return parent; } } /** * Fluent API builder for {@link FileSystemWatcher}. */ public static final class Builder implements io.helidon.common.Builder { private final List watchServiceModifiers = new LinkedList<>(); private ScheduledExecutorService executor; private long initialDelay = 1000; private long delay = 100; private TimeUnit timeUnit = TimeUnit.MILLISECONDS; private Builder() { } @Override public FileSystemWatcher build() { return new FileSystemWatcher(this); } /** * Update this builder from meta configuration. *

* Currently these options are supported: *

    *
  • {@code initial-delay-millis} - delay between the time this watcher is started * and the time the first check is triggered
  • *
  • {@code delay-millis} - how often do we check the watcher service for changes
  • *
* As the watcher is implemented as non-blocking, a single watcher can be used to watch multiple * directories using the same thread. * * @param metaConfig configuration of file system watcher * @return updated builder instance */ public Builder config(Config metaConfig) { metaConfig.get("initial-delay-millis") .asLong() .ifPresent(initDelay -> initialDelay = timeUnit.convert(initDelay, TimeUnit.MILLISECONDS)); metaConfig.get("delay-millis") .asLong() .ifPresent(delayMillis -> delay = timeUnit.convert(delayMillis, TimeUnit.MILLISECONDS)); return this; } /** * Executor to use for this watcher. * The task is scheduled for regular execution and is only blocking a thread for the time needed * to process changed files. * * @param executor executor service to use * @return updated builder instance */ public Builder executor(ScheduledExecutorService executor) { this.executor = executor; return this; } /** * Configure schedule of the file watcher. * * @param initialDelay initial delay before regular scheduling starts * @param delay delay between schedules * @param timeUnit time unit of the delays * @return updated builder instance * @deprecated use {@link #initialDelay(Duration)} and {@link #delay(Duration)} instead */ @Deprecated(since = "4.0.0") public Builder schedule(long initialDelay, long delay, TimeUnit timeUnit) { this.initialDelay = initialDelay; this.delay = delay; this.timeUnit = timeUnit; return this; } /** * Configure an initial delay before regular scheduling starts. * * @param initialDelay initial delay * @return updated builder instance */ public Builder initialDelay(Duration initialDelay) { this.initialDelay = initialDelay.toMillis(); this.timeUnit = TimeUnit.MILLISECONDS; return this; } /** * Configure a delay between schedules. * * @param delay delay between schedules * @return updated builder instance */ public Builder delay(Duration delay) { this.delay = delay.toMillis(); this.timeUnit = TimeUnit.MILLISECONDS; return this; } /** * Add a modifier of the watch service. * Currently only implementation specific modifier are available, such as * {@code com.sun.nio.file.SensitivityWatchEventModifier}. * * @param modifier modifier to use * @return updated builder instance */ public Builder addWatchServiceModifier(WatchEvent.Modifier modifier) { this.watchServiceModifiers.add(modifier); return this; } /** * Set modifiers to use for the watch service. * Currently only implementation specific modifier are available, such as * {@code com.sun.nio.file.SensitivityWatchEventModifier}. * * @param modifiers modifiers to use (replacing current configuration) * @return updated builder instance */ public Builder watchServiceModifiers(List modifiers) { this.watchServiceModifiers.clear(); this.watchServiceModifiers.addAll(modifiers); return this; } } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy