Skip to content

Commit

Permalink
Merge pull request #22 from vania-pooh/master
Browse files Browse the repository at this point in the history
Added ResourcesWatcher implementation
  • Loading branch information
robot-bucket committed Dec 30, 2014
2 parents 17e8f3f + 85a3394 commit 3b186ae
Show file tree
Hide file tree
Showing 6 changed files with 376 additions and 49 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
package ru.meridor.stecker;

import ru.meridor.stecker.impl.PluginUtils;
import ru.meridor.stecker.interfaces.ResourceChangedHandler;

import java.io.Closeable;
import java.io.IOException;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.WatchKey;
import java.nio.file.WatchService;
import java.nio.file.attribute.FileTime;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CountDownLatch;
import java.util.stream.Collectors;

import static java.nio.file.StandardWatchEventKinds.ENTRY_MODIFY;

/**
* Allows you to watch for resource files changes
*/
public class ResourcesWatcher implements Closeable {

private volatile List<ResourceChangedHandler> handlers = new ArrayList<>();

private volatile boolean isStarted;

private Thread watcherThread;

public ResourcesWatcher(List<Path> resources) {
this.watcherThread = getWatcherThread(resources);
}

private Thread getWatcherThread(List<Path> resources) {
return isMacOs() ? getScanningWatcherThread(resources) : getWatchServiceWatcherThread(resources);
}

private static boolean isMacOs() {
//WatchService doesn't work correctly on MacOS
return System.getProperty("os.name").toLowerCase().contains("mac");
}

private Thread getWatchServiceWatcherThread(List<Path> resources) {
return new Thread() {
@Override
public void run() {
super.run();
try (WatchService watcher = FileSystems.getDefault().newWatchService()) {
final List<Path> alreadyRegisteredDirectories = new ArrayList<>();
List<Path> regularFilesAndDirectories = resources.stream()
.filter(r -> Files.isDirectory(r) || Files.isRegularFile(r))
.collect(Collectors.toList());
for (Path resource : regularFilesAndDirectories) {
Path pathToRegister = resource;
if (Files.isRegularFile(resource)) {
pathToRegister = resource.getParent();
}
if (!alreadyRegisteredDirectories.contains(pathToRegister)) {
pathToRegister.register(watcher, ENTRY_MODIFY);
alreadyRegisteredDirectories.add(pathToRegister);
}
}
while (true) {
WatchKey key;
try {
key = watcher.take();
} catch (InterruptedException e) {
break;
}
Path directory = (Path) key.watchable();
key.pollEvents()
.stream()
.filter(event -> ENTRY_MODIFY == event.kind())
.forEach(event -> {
for (ResourceChangedHandler handler : handlers) {
Path filePath = directory.resolve((Path) event.context());
handler.onResourceChanged(filePath);
}
});
if (!key.reset()){
break;
}
}
} catch (IOException e) {
throw new IllegalStateException(e);
}
}
};
}

private Thread getScanningWatcherThread(List<Path> resources) {
final long SCAN_DELAY = 1000;
return new Thread(){
@Override
public void run() {
super.run();
try {
Map<Path, FileTime> lastModifiedTimes = new HashMap<>();
for (Path resource : resources) {
lastModifiedTimes.put(resource, PluginUtils.getLastModificationTime(resource));
}

while (true) {
try {
for (Path resource : resources) {
FileTime previousLastModifiedTime = lastModifiedTimes.get(resource);
FileTime currentLastModifiedTime = PluginUtils.getLastModificationTime(resource);
if (currentLastModifiedTime.compareTo(previousLastModifiedTime) > 0) {
for (ResourceChangedHandler handler : handlers) {
handler.onResourceChanged(resource);
}
}
}
sleep(SCAN_DELAY);
} catch (InterruptedException e) {
break;
}
}

} catch (IOException e) {
throw new IllegalStateException(e);
}
}
};
}

public void addChangedHandler(ResourceChangedHandler handler) {
handlers.add(handler);
}

public void start() {
watcherThread.start();
isStarted = true;
}

public void stop() {
watcherThread.interrupt();
}

public void await(Path resource) throws InterruptedException {
if (!isStarted) {
start();
}
final CountDownLatch countDownLatch = new CountDownLatch(1);
addChangedHandler(r -> {
if (resource == null || r.equals(resource)) {
countDownLatch.countDown();
}
});
countDownLatch.await();
}

public void await() throws InterruptedException {
await(null);
}

@Override
public void close() throws IOException {
stop();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -82,10 +82,14 @@ private static void unpackJar(Path pluginFile, Path pluginStorageDirectory) thro
}

private static boolean fileIsNewerThan(Path file, Path anotherFile) throws IOException {
FileTime fileLastModificationTime = Files.readAttributes(file, BasicFileAttributes.class).lastModifiedTime();
FileTime anotherFileLastModificationTime = Files.readAttributes(anotherFile, BasicFileAttributes.class).lastModifiedTime();
FileTime fileLastModificationTime = getLastModificationTime(file);
FileTime anotherFileLastModificationTime = getLastModificationTime(anotherFile);
return fileLastModificationTime.compareTo(anotherFileLastModificationTime) > 0;
}

public static FileTime getLastModificationTime(Path path) throws IOException {
return Files.readAttributes(path, BasicFileAttributes.class).lastModifiedTime();
}

public static Path getPluginImplementationDirectory(Path unpackedPluginDirectory) {
return unpackedPluginDirectory.resolve(PLUGIN_IMPLEMENTATION_UNPACK_DIRECTORY);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package ru.meridor.stecker.interfaces;

import java.nio.file.Path;

public interface ResourceChangedHandler {

void onResourceChanged(Path resource);

}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package ru.meridor.stecker;

import org.junit.Rule;
import org.junit.Test;
import ru.meridor.stecker.impl.FileSystemHelper;
import ru.meridor.stecker.impl.ManifestField;
import ru.meridor.stecker.impl.data.AnnotatedImpl;
import ru.meridor.stecker.impl.data.TestAnnotation;
Expand All @@ -16,16 +16,28 @@
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.*;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.jar.Manifest;
import java.util.stream.Collectors;

import static org.hamcrest.Matchers.*;
import static org.junit.Assert.*;
import static org.hamcrest.Matchers.contains;
import static org.hamcrest.Matchers.containsInAnyOrder;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.hasSize;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.mock;

public class PluginLoaderTest {

@Rule
public TemporaryDirectory temporaryDirectory = new TemporaryDirectory();

@Test
public void testFluentApi() throws PluginException {
Path pluginDirectory = Paths.get("plugin-directory");
Expand Down Expand Up @@ -74,50 +86,47 @@ public void testLoad() throws Exception {
final String PLUGIN_NAME = "plugin-name";
final String PLUGIN_VERSION = "plugin-version";
Manifest manifest = createTestLoadManifest(PLUGIN_NAME, PLUGIN_VERSION);
Path tempDirectory = FileSystemHelper.createTempDirectory(); //Can't use in-memory filesystems (e.g. Google JimFS) here because they don't support java.net.URL for class loader

try {
assertNotNull(tempDirectory);
assertTrue(Files.exists(tempDirectory));

JarHelper.createTestPluginFile(
"some-plugin",
tempDirectory,
Optional.of(manifest)
);

PluginRegistry pluginRegistry = PluginLoader
.withPluginDirectory(tempDirectory)
.withExtensionPoints(TestExtensionPoint.class, TestAnnotation.class)
.withResourcesPatterns("glob:**/*.resource")
.load();

assertThat(pluginRegistry.getPluginNames(), hasSize(1));
assertThat(pluginRegistry.getPluginNames(), contains(PLUGIN_NAME));
assertTrue(pluginRegistry.getPlugin(PLUGIN_NAME).isPresent());
assertThat(pluginRegistry.getPlugin(PLUGIN_NAME).get().getVersion(), equalTo(PLUGIN_VERSION));

assertThat(pluginRegistry.getExtensionPoints(), hasSize(2));
assertThat(pluginRegistry.getExtensionPoints(), containsInAnyOrder(TestExtensionPoint.class, TestAnnotation.class));

assertThat(pluginRegistry.getImplementations(TestAnnotation.class), hasSize(1));
assertThat(pluginRegistry.getImplementations(TestAnnotation.class), contains(AnnotatedImpl.class));

assertThat(pluginRegistry.getImplementations(TestExtensionPoint.class), hasSize(1));
assertThat(pluginRegistry.getImplementations(TestExtensionPoint.class), contains(TestExtensionPointImpl.class));

assertThat(pluginRegistry.getResources(), hasSize(1));
assertThat(pluginRegistry.getResources("missing-plugin"), hasSize(0));
assertThat(pluginRegistry.getResources(PLUGIN_NAME), hasSize(1));
Path resourcePath = pluginRegistry.getResources(PLUGIN_NAME).get(0);
assertTrue(resourcePath.endsWith(JarHelper.TEST_RESOURCE_NAME));

try (InputStream inputStream = Files.newInputStream(resourcePath)) {
assertNotNull(inputStream); //We should be able to open resource
}
} finally {
FileSystemHelper.removeDirectory(tempDirectory);
Path tempDirectory = temporaryDirectory.getDirectory(); //Can't use in-memory filesystems (e.g. Google JimFS) here because they don't support java.net.URL for class loader

assertNotNull(tempDirectory);
assertTrue(Files.exists(tempDirectory));

JarHelper.createTestPluginFile(
"some-plugin",
tempDirectory,
Optional.of(manifest)
);

PluginRegistry pluginRegistry = PluginLoader
.withPluginDirectory(tempDirectory)
.withExtensionPoints(TestExtensionPoint.class, TestAnnotation.class)
.withResourcesPatterns("glob:**/*.resource")
.load();

assertThat(pluginRegistry.getPluginNames(), hasSize(1));
assertThat(pluginRegistry.getPluginNames(), contains(PLUGIN_NAME));
assertTrue(pluginRegistry.getPlugin(PLUGIN_NAME).isPresent());
assertThat(pluginRegistry.getPlugin(PLUGIN_NAME).get().getVersion(), equalTo(PLUGIN_VERSION));

assertThat(pluginRegistry.getExtensionPoints(), hasSize(2));
assertThat(pluginRegistry.getExtensionPoints(), containsInAnyOrder(TestExtensionPoint.class, TestAnnotation.class));

assertThat(pluginRegistry.getImplementations(TestAnnotation.class), hasSize(1));
assertThat(pluginRegistry.getImplementations(TestAnnotation.class), contains(AnnotatedImpl.class));

assertThat(pluginRegistry.getImplementations(TestExtensionPoint.class), hasSize(1));
assertThat(pluginRegistry.getImplementations(TestExtensionPoint.class), contains(TestExtensionPointImpl.class));

assertThat(pluginRegistry.getResources(), hasSize(1));
assertThat(pluginRegistry.getResources("missing-plugin"), hasSize(0));
assertThat(pluginRegistry.getResources(PLUGIN_NAME), hasSize(1));
Path resourcePath = pluginRegistry.getResources(PLUGIN_NAME).get(0);
assertTrue(resourcePath.endsWith(JarHelper.TEST_RESOURCE_NAME));

try (InputStream inputStream = Files.newInputStream(resourcePath)) {
assertNotNull(inputStream); //We should be able to open resource
}

}

private Manifest createTestLoadManifest(String pluginName, String pluginVersion) {
Expand Down
Loading

0 comments on commit 3b186ae

Please sign in to comment.