diff --git a/bundles/runtime/org.eclipse.fx.core/src/org/eclipse/fx/core/internal/FileSystemServiceImpl.java b/bundles/runtime/org.eclipse.fx.core/src/org/eclipse/fx/core/internal/FileSystemServiceImpl.java new file mode 100644 index 000000000..cfbb01ed1 --- /dev/null +++ b/bundles/runtime/org.eclipse.fx.core/src/org/eclipse/fx/core/internal/FileSystemServiceImpl.java @@ -0,0 +1,214 @@ +/******************************************************************************* + * Copyright (c) 2015 BestSolution.at and others. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Tom Schindl - initial API and implementation + *******************************************************************************/ +package org.eclipse.fx.core.internal; + +import java.io.IOException; +import java.net.URISyntaxException; +import java.nio.file.ClosedWatchServiceException; +import java.nio.file.FileSystem; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardWatchEventKinds; +import java.nio.file.WatchEvent; +import java.nio.file.WatchKey; +import java.nio.file.WatchService; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; +import java.util.function.BiConsumer; + +import org.eclipse.fx.core.FilesystemService; +import org.eclipse.fx.core.Subscription; +import org.eclipse.fx.core.URI; +import org.eclipse.fx.core.log.LoggerCreator; +import org.osgi.service.component.annotations.Component; + +/** + * Implementation of a file system + * + * @since 1.2 + */ +@Component +public class FileSystemServiceImpl implements FilesystemService { + private CheckThread thread; + + @Override + public boolean applies(URI path) { + return path.toString().startsWith("file:"); //$NON-NLS-1$ + } + + @SuppressWarnings("all") + @Override + public Subscription observePath(URI uri, BiConsumer consumer) { + try { + return observePath(Paths.get(new java.net.URI(uri.toString())), + (k, p) -> consumer.accept(k, URI.create(p.toUri().toString()))); + } catch (URISyntaxException e) { + throw new RuntimeException(e); + } + } + + @Override + public Subscription observePath(Path path, BiConsumer consumer) { + if (this.thread == null || !this.thread.isAlive()) { + this.thread = new CheckThread(path.getFileSystem()); + this.thread.start(); + } + try { + return this.thread.registerURI(path, consumer); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + static class CheckThread extends Thread { + public final Map> subscriptions = new HashMap<>(); + private final WatchService watcher; + private final Executor dispatcher = Executors.newCachedThreadPool(); + private final Thread shutdownCleanup; + + public CheckThread(FileSystem fileSystem) { + setDaemon(true); + try { + this.watcher = fileSystem.newWatchService(); + } catch (IOException e) { + throw new IllegalStateException(); + } + this.shutdownCleanup = new Thread(() -> { + try { + this.watcher.close(); + } catch (Exception e) { + // do not care about + } + }); + Runtime.getRuntime().addShutdownHook(this.shutdownCleanup); + } + + @Override + protected void finalize() throws Throwable { + super.finalize(); + } + + public Subscription registerURI(Path path, BiConsumer consumer) throws IOException { + WatchKey register = path.register(this.watcher, StandardWatchEventKinds.ENTRY_CREATE, + StandardWatchEventKinds.ENTRY_DELETE, StandardWatchEventKinds.ENTRY_MODIFY); + + PathSubscription s = new PathSubscription(this, path, register, consumer); + synchronized (this.subscriptions) { + List list = this.subscriptions.get(register); + if (list == null) { + list = new CopyOnWriteArrayList<>(); + this.subscriptions.put(register, list); + } + list.add(s); + } + + return s; + } + + @SuppressWarnings("null") + @Override + public void run() { + while (true) { + WatchKey key; + try { + key = this.watcher.take(); + } catch (Exception x) { + if( x instanceof ClosedWatchServiceException ) { + // nothing to be done + } else { + LoggerCreator.createLogger(getClass()).warning("File watcher failed. Watching ended", x); //$NON-NLS-1$ + } + + return; + } + + for (WatchEvent event : key.pollEvents()) { + @SuppressWarnings("unchecked") + WatchEvent e = (WatchEvent) event; + WatchEvent.Kind kind = e.kind(); + if (kind == StandardWatchEventKinds.OVERFLOW) { + continue; + } + Path context = e.context(); + synchronized (this.subscriptions) { + List pathSubscriptionList = this.subscriptions.get(key); + + if (pathSubscriptionList != null) { + for (PathSubscription pathSubscription : pathSubscriptionList) { + PathSubscription fp = pathSubscription; + this.dispatcher.execute((() -> fp.consumer.accept(toKind(kind), + fp.path.resolve(context)))); + } + } + } + } + + key.reset(); + } + } + + synchronized void removeSubscription(PathSubscription subscription) { + synchronized (this.subscriptions) { + List list = this.subscriptions.get(subscription.register); + + if( list != null ) { + list.remove(subscription); + if( list.isEmpty() ) { + this.subscriptions.remove(subscription.register); + } + } + + if (this.subscriptions.isEmpty()) { + try { + Runtime.getRuntime().removeShutdownHook(this.shutdownCleanup); + this.watcher.close(); + } catch (IOException e) { + // ignore + } + } + } + } + + private static Kind toKind(WatchEvent.Kind kind) { + if (kind == StandardWatchEventKinds.ENTRY_CREATE) { + return Kind.CREATE; + } else if (kind == StandardWatchEventKinds.ENTRY_DELETE) { + return Kind.DELETE; + } else { + return Kind.MODIFY; + } + } + } + + static class PathSubscription implements Subscription { + public final Path path; + public final WatchKey register; + public final CheckThread thread; + public final BiConsumer consumer; + + public PathSubscription(CheckThread thread, Path path, WatchKey register, BiConsumer consumer) { + this.thread = thread; + this.path = path; + this.register = register; + this.consumer = consumer; + } + + @Override + public void dispose() { + this.register.cancel(); + this.thread.removeSubscription(this); + } + } +} diff --git a/modules/core/org.eclipse.fx.core/src/main/java/org/eclipse/fx/core/internal/FileSystemServiceImpl.java b/modules/core/org.eclipse.fx.core/src/main/java/org/eclipse/fx/core/internal/FileSystemServiceImpl.java index d8066e180..cfbb01ed1 100644 --- a/modules/core/org.eclipse.fx.core/src/main/java/org/eclipse/fx/core/internal/FileSystemServiceImpl.java +++ b/modules/core/org.eclipse.fx.core/src/main/java/org/eclipse/fx/core/internal/FileSystemServiceImpl.java @@ -13,7 +13,7 @@ import java.io.IOException; import java.net.URISyntaxException; import java.nio.file.ClosedWatchServiceException; -import java.nio.file.FileSystems; +import java.nio.file.FileSystem; import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.StandardWatchEventKinds; @@ -62,7 +62,7 @@ public Subscription observePath(URI uri, BiConsumer consumer) { @Override public Subscription observePath(Path path, BiConsumer consumer) { if (this.thread == null || !this.thread.isAlive()) { - this.thread = new CheckThread(); + this.thread = new CheckThread(path.getFileSystem()); this.thread.start(); } try { @@ -78,10 +78,10 @@ static class CheckThread extends Thread { private final Executor dispatcher = Executors.newCachedThreadPool(); private final Thread shutdownCleanup; - public CheckThread() { + public CheckThread(FileSystem fileSystem) { setDaemon(true); try { - this.watcher = FileSystems.getDefault().newWatchService(); + this.watcher = fileSystem.newWatchService(); } catch (IOException e) { throw new IllegalStateException(); }