From 1441c175fa216e928f45644b01768633d502c486 Mon Sep 17 00:00:00 2001
From: Jan Vales <jan@jvales.net>
Date: Fri, 4 May 2018 03:53:50 +0200
Subject: [PATCH] [2.3.1] working plugin finder and executor.
---
.../dst/ass2/aop/PluginExecutorFactory.java | 5 +-
.../dst/ass2/aop/impl/PluginExecutor.java | 197 ++++++++++++++++++
2 files changed, 200 insertions(+), 2 deletions(-)
create mode 100644 ass2-aop/src/main/java/dst/ass2/aop/impl/PluginExecutor.java
diff --git a/ass2-aop/src/main/java/dst/ass2/aop/PluginExecutorFactory.java b/ass2-aop/src/main/java/dst/ass2/aop/PluginExecutorFactory.java
index f0fcf1e..56afd8a 100644
--- a/ass2-aop/src/main/java/dst/ass2/aop/PluginExecutorFactory.java
+++ b/ass2-aop/src/main/java/dst/ass2/aop/PluginExecutorFactory.java
@@ -1,10 +1,11 @@
package dst.ass2.aop;
+import dst.ass2.aop.impl.PluginExecutor;
+
public class PluginExecutorFactory {
public static IPluginExecutor createPluginExecutor() {
- // TODO
- return null;
+ return new PluginExecutor();
}
}
diff --git a/ass2-aop/src/main/java/dst/ass2/aop/impl/PluginExecutor.java b/ass2-aop/src/main/java/dst/ass2/aop/impl/PluginExecutor.java
new file mode 100644
index 0000000..c79efdf
--- /dev/null
+++ b/ass2-aop/src/main/java/dst/ass2/aop/impl/PluginExecutor.java
@@ -0,0 +1,197 @@
+package dst.ass2.aop.impl;
+
+import dst.ass2.aop.IPluginExecutable;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.net.URLClassLoader;
+import java.nio.file.*;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipInputStream;
+
+import static java.nio.file.StandardWatchEventKinds.*;
+
+public class PluginExecutor implements dst.ass2.aop.IPluginExecutor, Runnable {
+ private final WatchService watcher;
+ private final Map<WatchKey, Path> keys;
+ private final Map<File, String> startedPlugins;
+ private Thread myThread;
+ private ExecutorService executorTP = Executors.newFixedThreadPool(5);
+
+ public PluginExecutor() {
+ try {
+ watcher = FileSystems.getDefault().newWatchService();
+ keys = new ConcurrentHashMap<>();
+ startedPlugins = new ConcurrentHashMap<>();
+ } catch (IOException e) {
+ throw new RuntimeException("Most likely irrecoverable error when creating watch service.", e);
+ }
+ }
+
+ private static String getFileChecksum(MessageDigest digest, File file) throws IOException {
+ FileInputStream fis = new FileInputStream(file);
+ byte[] byteArray = new byte[1024];
+ int bytesCount = 0;
+ while ((bytesCount = fis.read(byteArray)) != -1) {
+ digest.update(byteArray, 0, bytesCount);
+ }
+ fis.close();
+ byte[] bytes = digest.digest();
+ StringBuilder sb = new StringBuilder();
+ for (byte aByte : bytes) {
+ sb.append(Integer.toString((aByte & 0xff) + 0x100, 16).substring(1));
+ }
+ return sb.toString();
+ }
+
+ @Override
+ public void monitor(File dir) {
+ try {
+ Path dirPath = FileSystems.getDefault().getPath(dir.getAbsolutePath());
+ WatchKey key = dirPath.register(watcher, ENTRY_CREATE, ENTRY_MODIFY, ENTRY_DELETE);
+ keys.put(key, dirPath);
+ } catch (IOException e) {
+ throw new RuntimeException("Could not listen on directory: " + dir.getAbsolutePath(), e);
+ }
+ }
+
+ @Override
+ public void stopMonitoring(File dir) {
+ for (WatchKey key : keys.keySet()) {
+ if (keys.get(key).toAbsolutePath().equals(dir.toPath().toAbsolutePath())) {
+ key.cancel();
+ keys.remove(key);
+ System.out.println("stopMonitoring(): " + dir.getAbsolutePath());
+ return;
+ }
+ }
+ }
+
+ @Override
+ public void start() {
+ for (Path path : keys.values()) {
+ for (File file : path.toAbsolutePath().toFile().listFiles()) {
+ execPlugin(file);
+ }
+ }
+ System.out.println("start(): existing files checked. starting watcher.");
+ // Unsure if interrupted is alive.
+ // should not matter, but we handle that case in run() anyway.
+ if (myThread == null || !myThread.isAlive()) {
+ myThread = new Thread(this);
+ myThread.start();
+ }
+ }
+
+ @Override
+ public void stop() {
+ for (WatchKey key : keys.keySet()) {
+ stopMonitoring(keys.get(key).toAbsolutePath().toFile());
+ }
+ System.out.println("stop()");
+ }
+
+ @Override
+ public void run() {
+ while (!keys.isEmpty() && myThread == Thread.currentThread()) {
+ WatchKey key;
+ try {
+ key = watcher.take();
+ } catch (InterruptedException x) {
+ return;
+ }
+
+ Path dir = keys.get(key);
+ if (dir == null) {
+ System.out.println("WatchKey not recognized!");
+ continue;
+ }
+
+ for (WatchEvent<?> event : key.pollEvents()) {
+ @SuppressWarnings("rawtypes")
+ WatchEvent.Kind kind = event.kind();
+
+ // Context for directory entry event is the file name of entry
+ @SuppressWarnings("unchecked")
+ Path name = dir.resolve(((WatchEvent<Path>) event).context());
+
+ // print out event
+ System.out.format("%s: %s\n", event.kind().name(), name);
+
+ if (kind == ENTRY_CREATE || kind == ENTRY_MODIFY) {
+ execPlugin(name.toAbsolutePath().toFile());
+ } else if (kind == ENTRY_DELETE) {
+ startedPlugins.remove(name.toAbsolutePath().toFile());
+ }
+ }
+
+ // reset key and remove from set if directory no longer accessible
+ boolean valid = key.reset();
+ if (!valid) {
+ keys.remove(key);
+
+ }
+ }
+ System.out.println("run(): ended - keys.isEmpty() == true");
+ }
+
+ private void execPlugin(File file) {
+ if (file.isDirectory() || !file.getAbsolutePath().endsWith(".jar")) {
+ return;
+ }
+
+ // we use SHA-1 to prevent multiple executions of the same plugin.
+ String currFileChecksum = "";
+ try {
+ currFileChecksum = getFileChecksum(MessageDigest.getInstance("SHA-1"), file);
+ } catch (IOException | NoSuchAlgorithmException e) {
+ e.printStackTrace();
+ }
+
+ String startedFileChecksum = startedPlugins.get(file.getAbsoluteFile());
+ if (currFileChecksum.equals(startedFileChecksum)) {
+ return;
+ }
+
+ // now we are finally set to execute the plugin...
+ System.out.println("execPlugin(): " + file.getAbsolutePath());
+ startedPlugins.put(file.getAbsoluteFile(), currFileChecksum);
+
+ // For each class in jar, try to load and cast it to IPluginExecutable.
+ // Ignore errors.
+ // If successful (=not aborted by exception) execute IPluginExecutable.execute() in a threadpool.
+ try {
+ ZipInputStream zip = new ZipInputStream(new FileInputStream(file.getAbsoluteFile()));
+ for (ZipEntry entry = zip.getNextEntry(); entry != null; entry = zip.getNextEntry()) {
+ if (!entry.isDirectory() && entry.getName().endsWith(".class")) {
+ String className = entry.getName().replace('/', '.');
+ className = className.substring(0, className.length() - ".class".length());
+ try {
+ ClassLoader cl = new URLClassLoader(new URL[]{file.toURL()});
+ IPluginExecutable extension = (IPluginExecutable) cl.loadClass(className).newInstance();
+ executorTP.execute(new Thread() {
+ @Override
+ public void run() {
+ extension.execute();
+ }
+ });
+ } catch (MalformedURLException e) {
+ e.printStackTrace();
+ } catch (IllegalAccessException | InstantiationException | ClassNotFoundException | ClassCastException ignored) {
+ }
+ }
+ }
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+}
--
2.47.3