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.43.0