From 1441c175fa216e928f45644b01768633d502c486 Mon Sep 17 00:00:00 2001 From: Jan Vales 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 keys; + private final Map 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) 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