diff --git a/jimfs/src/main/java/com/google/common/jimfs/Configuration.java b/jimfs/src/main/java/com/google/common/jimfs/Configuration.java index e862c60a..1be79b31 100644 --- a/jimfs/src/main/java/com/google/common/jimfs/Configuration.java +++ b/jimfs/src/main/java/com/google/common/jimfs/Configuration.java @@ -35,6 +35,7 @@ import java.nio.file.FileSystem; import java.nio.file.InvalidPathException; import java.nio.file.SecureDirectoryStream; +import java.nio.file.WatchService; import java.nio.file.attribute.BasicFileAttributeView; import java.util.Arrays; import java.util.HashMap; @@ -209,6 +210,9 @@ public static Builder builder(PathType pathType) { final ImmutableSet attributeProviders; final ImmutableMap defaultAttributeValues; + // Watch service + final WatchServiceConfiguration watchServiceConfig; + // Other final ImmutableSet roots; final String workingDirectory; @@ -234,6 +238,7 @@ private Configuration(Builder builder) { builder.defaultAttributeValues == null ? ImmutableMap.of() : ImmutableMap.copyOf(builder.defaultAttributeValues); + this.watchServiceConfig = builder.watchServiceConfig; this.roots = builder.roots; this.workingDirectory = builder.workingDirectory; this.supportedFeatures = builder.supportedFeatures; @@ -276,6 +281,9 @@ public static final class Builder { private Set attributeProviders = null; private Map defaultAttributeValues; + // Watch service + private WatchServiceConfiguration watchServiceConfig = WatchServiceConfiguration.DEFAULT; + // Other private ImmutableSet roots = ImmutableSet.of(); private String workingDirectory; @@ -302,6 +310,7 @@ private Builder(Configuration configuration) { configuration.defaultAttributeValues.isEmpty() ? null : new HashMap<>(configuration.defaultAttributeValues); + this.watchServiceConfig = configuration.watchServiceConfig; this.roots = configuration.roots; this.workingDirectory = configuration.workingDirectory; this.supportedFeatures = configuration.supportedFeatures; @@ -607,6 +616,17 @@ public Builder setSupportedFeatures(Feature... features) { return this; } + /** + * Sets the configuration that {@link WatchService} instances created by the file system + * should use. The default configuration polls watched directories for changes every 5 seconds. + * + * @since 1.1 + */ + public Builder setWatchServiceConfiguration(WatchServiceConfiguration config) { + this.watchServiceConfig = checkNotNull(config); + return this; + } + /** * Creates a new immutable configuration object from this builder. */ diff --git a/jimfs/src/main/java/com/google/common/jimfs/JimfsFileSystem.java b/jimfs/src/main/java/com/google/common/jimfs/JimfsFileSystem.java index 4632bbdf..779d48df 100644 --- a/jimfs/src/main/java/com/google/common/jimfs/JimfsFileSystem.java +++ b/jimfs/src/main/java/com/google/common/jimfs/JimfsFileSystem.java @@ -180,17 +180,21 @@ final class JimfsFileSystem extends FileSystem { private final FileSystemView defaultView; + private final WatchServiceConfiguration watchServiceConfig; + JimfsFileSystem( JimfsFileSystemProvider provider, URI uri, JimfsFileStore fileStore, PathService pathService, - FileSystemView defaultView) { + FileSystemView defaultView, + WatchServiceConfiguration watchServiceConfig) { this.provider = checkNotNull(provider); this.uri = checkNotNull(uri); this.fileStore = checkNotNull(fileStore); this.pathService = checkNotNull(pathService); this.defaultView = checkNotNull(defaultView); + this.watchServiceConfig = checkNotNull(watchServiceConfig); } @Override @@ -296,7 +300,7 @@ public UserPrincipalLookupService getUserPrincipalLookupService() { @Override public WatchService newWatchService() throws IOException { - return new PollingWatchService(defaultView, pathService, fileStore.state()); + return watchServiceConfig.newWatchService(defaultView, pathService); } @Nullable private ExecutorService defaultThreadPool; diff --git a/jimfs/src/main/java/com/google/common/jimfs/JimfsFileSystems.java b/jimfs/src/main/java/com/google/common/jimfs/JimfsFileSystems.java index 7b3bbf62..9b60f5a7 100644 --- a/jimfs/src/main/java/com/google/common/jimfs/JimfsFileSystems.java +++ b/jimfs/src/main/java/com/google/common/jimfs/JimfsFileSystems.java @@ -90,9 +90,10 @@ public static JimfsFileSystem newFileSystem( JimfsFileStore fileStore = createFileStore(config, pathService, state); FileSystemView defaultView = createDefaultView(config, fileStore, pathService); + WatchServiceConfiguration watchServiceConfig = config.watchServiceConfig; JimfsFileSystem fileSystem = - new JimfsFileSystem(provider, uri, fileStore, pathService, defaultView); + new JimfsFileSystem(provider, uri, fileStore, pathService, defaultView, watchServiceConfig); pathService.setFileSystem(fileSystem); return fileSystem; diff --git a/jimfs/src/main/java/com/google/common/jimfs/PollingWatchService.java b/jimfs/src/main/java/com/google/common/jimfs/PollingWatchService.java index 9876fa41..ae14cd66 100644 --- a/jimfs/src/main/java/com/google/common/jimfs/PollingWatchService.java +++ b/jimfs/src/main/java/com/google/common/jimfs/PollingWatchService.java @@ -21,7 +21,6 @@ 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.util.concurrent.TimeUnit.SECONDS; import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.ImmutableMap; @@ -29,7 +28,6 @@ import com.google.common.util.concurrent.ThreadFactoryBuilder; import java.io.IOException; -import java.nio.file.NotDirectoryException; import java.nio.file.Path; import java.nio.file.WatchEvent; import java.nio.file.WatchService; @@ -73,30 +71,25 @@ final class PollingWatchService extends AbstractWatchService { private final PathService pathService; private final FileSystemState fileSystemState; - private final long pollingTime; - private final TimeUnit timeUnit; + @VisibleForTesting + final long interval; + @VisibleForTesting + final TimeUnit timeUnit; private ScheduledFuture pollingFuture; - public PollingWatchService( - FileSystemView view, PathService pathService, FileSystemState fileSystemState) { - this(view, pathService, fileSystemState, 5, SECONDS); - } - - // TODO(cgdecker): make user configurable somehow? meh - @VisibleForTesting PollingWatchService( FileSystemView view, PathService pathService, FileSystemState fileSystemState, - long pollingTime, + long interval, TimeUnit timeUnit) { this.view = checkNotNull(view); this.pathService = checkNotNull(pathService); this.fileSystemState = checkNotNull(fileSystemState); - checkArgument(pollingTime >= 0, "polling time (%s) may not be negative", pollingTime); - this.pollingTime = pollingTime; + checkArgument(interval >= 0, "interval (%s) may not be negative", interval); + this.interval = interval; this.timeUnit = checkNotNull(timeUnit); fileSystemState.register(this); @@ -168,7 +161,7 @@ public void close() { private void startPolling() { pollingFuture = - pollingService.scheduleAtFixedRate(pollingTask, pollingTime, pollingTime, timeUnit); + pollingService.scheduleAtFixedRate(pollingTask, interval, interval, timeUnit); } private void stopPolling() { diff --git a/jimfs/src/main/java/com/google/common/jimfs/WatchServiceConfiguration.java b/jimfs/src/main/java/com/google/common/jimfs/WatchServiceConfiguration.java new file mode 100644 index 00000000..8648fc3c --- /dev/null +++ b/jimfs/src/main/java/com/google/common/jimfs/WatchServiceConfiguration.java @@ -0,0 +1,81 @@ +/* + * Copyright 2016 Google Inc. + * + * 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 com.google.common.jimfs; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static java.util.concurrent.TimeUnit.SECONDS; + +import java.nio.file.WatchService; +import java.util.concurrent.TimeUnit; + +/** + * Configuration for the {@link WatchService} implementation used by a file system. + * + * @author Colin Decker + * @since 1.1 + */ +public abstract class WatchServiceConfiguration { + + /** + * The default configuration that's used if the user doesn't provide anything more specific. + */ + static final WatchServiceConfiguration DEFAULT = polling(5, SECONDS); + + /** + * Returns a configuration for a {@link WatchService} that polls watched directories for changes + * every {@code interval} of the given {@code timeUnit} (e.g. every 5 + * {@link TimeUnit#SECONDS seconds}). + */ + public static WatchServiceConfiguration polling(long interval, TimeUnit timeUnit) { + return new PollingConfig(interval, timeUnit); + } + + WatchServiceConfiguration() {} + + /** + * Creates a new {@link AbstractWatchService} implementation. + */ + // return type and parameters of this method subject to change if needed for any future + // implementations + abstract AbstractWatchService newWatchService(FileSystemView view, PathService pathService); + + /** + * Implementation for {@link #polling}. + */ + private static final class PollingConfig extends WatchServiceConfiguration { + + private final long interval; + private final TimeUnit timeUnit; + + private PollingConfig(long interval, TimeUnit timeUnit) { + checkArgument(interval > 0, "interval (%s) must be positive", interval); + this.interval = interval; + this.timeUnit = checkNotNull(timeUnit); + } + + @Override + AbstractWatchService newWatchService(FileSystemView view, PathService pathService) { + return new PollingWatchService(view, pathService, view.state(), interval, timeUnit); + } + + @Override + public String toString() { + return "WatchServiceConfiguration.polling(" + interval + ", " + timeUnit + ")"; + } + } +} diff --git a/jimfs/src/test/java/com/google/common/jimfs/ConfigurationTest.java b/jimfs/src/test/java/com/google/common/jimfs/ConfigurationTest.java index 2badc1d6..968feb3e 100644 --- a/jimfs/src/test/java/com/google/common/jimfs/ConfigurationTest.java +++ b/jimfs/src/test/java/com/google/common/jimfs/ConfigurationTest.java @@ -22,8 +22,11 @@ import static com.google.common.jimfs.PathNormalization.NFC; import static com.google.common.jimfs.PathNormalization.NFD; import static com.google.common.jimfs.PathSubject.paths; +import static com.google.common.jimfs.WatchServiceConfiguration.polling; import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assert_; +import static java.util.concurrent.TimeUnit.MILLISECONDS; +import static java.util.concurrent.TimeUnit.SECONDS; import static org.junit.Assert.fail; import com.google.common.collect.ImmutableList; @@ -38,7 +41,9 @@ import java.nio.file.FileSystem; import java.nio.file.Files; import java.nio.file.Path; +import java.nio.file.WatchService; import java.nio.file.attribute.PosixFilePermissions; +import java.util.concurrent.TimeUnit; /** * Tests for {@link Configuration}, {@link Configuration.Builder} and file systems created from @@ -337,6 +342,32 @@ public void testCreateFileSystemFromConfigurationWithWorkingDirectoryNotUnderCon } } + @Test + public void testFileSystemWithDefaultWatchService() throws IOException { + FileSystem fs = Jimfs.newFileSystem(Configuration.unix()); + + WatchService watchService = fs.newWatchService(); + assertThat(watchService).isInstanceOf(PollingWatchService.class); + + PollingWatchService pollingWatchService = (PollingWatchService) watchService; + assertThat(pollingWatchService.interval).isEqualTo(5); + assertThat(pollingWatchService.timeUnit).isEqualTo(SECONDS); + } + + @Test + public void testFileSystemWithCustomWatchServicePollingInterval() throws IOException { + FileSystem fs = Jimfs.newFileSystem(Configuration.unix().toBuilder() + .setWatchServiceConfiguration(polling(10, MILLISECONDS)) + .build()); + + WatchService watchService = fs.newWatchService(); + assertThat(watchService).isInstanceOf(PollingWatchService.class); + + PollingWatchService pollingWatchService = (PollingWatchService) watchService; + assertThat(pollingWatchService.interval).isEqualTo(10); + assertThat(pollingWatchService.timeUnit).isEqualTo(MILLISECONDS); + } + @Test public void testTurkishNormalization() { Configuration config = diff --git a/jimfs/src/test/java/com/google/common/jimfs/PollingWatchServiceTest.java b/jimfs/src/test/java/com/google/common/jimfs/PollingWatchServiceTest.java index f75443d5..f0fe7ca2 100644 --- a/jimfs/src/test/java/com/google/common/jimfs/PollingWatchServiceTest.java +++ b/jimfs/src/test/java/com/google/common/jimfs/PollingWatchServiceTest.java @@ -69,8 +69,11 @@ public void setUp() { } @After - public void tearDown() { + public void tearDown() throws IOException { watcher.close(); + fs.close(); + watcher = null; + fs = null; } @Test diff --git a/jimfs/src/test/java/com/google/common/jimfs/WatchServiceConfigurationTest.java b/jimfs/src/test/java/com/google/common/jimfs/WatchServiceConfigurationTest.java new file mode 100644 index 00000000..f10ecde3 --- /dev/null +++ b/jimfs/src/test/java/com/google/common/jimfs/WatchServiceConfigurationTest.java @@ -0,0 +1,79 @@ +/* + * Copyright 2016 Google Inc. + * + * 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 com.google.common.jimfs; + +import static com.google.common.truth.Truth.assertThat; +import static java.util.concurrent.TimeUnit.MILLISECONDS; +import static java.util.concurrent.TimeUnit.SECONDS; + +import com.google.common.truth.Truth; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +import java.io.IOException; +import java.nio.file.WatchService; +import java.util.concurrent.TimeUnit; + +/** + * Tests for {@link WatchServiceConfiguration}. + * + * @author Colin Decker + */ +@RunWith(JUnit4.class) +public class WatchServiceConfigurationTest { + + private JimfsFileSystem fs; + + @Before + public void setUp() { + // kind of putting the cart before the horse maybe, but it's the easiest way to get valid + // instances of both a FileSystemView and a PathService + fs = (JimfsFileSystem) Jimfs.newFileSystem(); + } + + @After + public void tearDown() throws IOException { + fs.close(); + fs = null; + } + + @Test + public void testPollingConfig() { + WatchServiceConfiguration polling = WatchServiceConfiguration.polling(50, MILLISECONDS); + WatchService watchService = polling.newWatchService(fs.getDefaultView(), fs.getPathService()); + assertThat(watchService).isInstanceOf(PollingWatchService.class); + + PollingWatchService pollingWatchService = (PollingWatchService) watchService; + assertThat(pollingWatchService.interval).isEqualTo(50); + assertThat(pollingWatchService.timeUnit).isEqualTo(MILLISECONDS); + } + + @Test + public void testDefaultConfig() { + WatchService watchService = WatchServiceConfiguration.DEFAULT + .newWatchService(fs.getDefaultView(), fs.getPathService()); + assertThat(watchService).isInstanceOf(PollingWatchService.class); + + PollingWatchService pollingWatchService = (PollingWatchService) watchService; + assertThat(pollingWatchService.interval).isEqualTo(5); + assertThat(pollingWatchService.timeUnit).isEqualTo(SECONDS); + } +}