commit 9bebe997d3af4589f3f6607dc86be18b56953da2
Author: Pavel Fidranský <fidransky@outlook.com>
Date:   Mon Feb 11 20:52:32 2019 +0100

    dynamic module loading with SPI

diff --git a/sources/imiger-core/src/main/java/cz/zcu/kiv/offscreen/modularization/DynamicServiceLoader.java b/sources/imiger-core/src/main/java/cz/zcu/kiv/offscreen/modularization/DynamicServiceLoader.java
new file mode 100644
index 0000000..b1e9fca
--- /dev/null
+++ b/sources/imiger-core/src/main/java/cz/zcu/kiv/offscreen/modularization/DynamicServiceLoader.java
@@ -0,0 +1,150 @@
+package cz.zcu.kiv.offscreen.modularization;
+
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.apache.logging.log4j.message.ParameterizedMessage;
+
+import java.io.IOException;
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.net.URLClassLoader;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.FileSystem;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.nio.file.spi.FileSystemProvider;
+import java.util.*;
+import java.util.function.Predicate;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+/**
+ * @author Tomáš Šimandl
+ */
+class DynamicServiceLoader<S> {
+
+    private static final Logger logger = LogManager.getLogger();
+    /** Valid file extension of Java module. */
+    private static final String VALID_MODULE_EXTENSION = "jar";
+    /** Filter to get only jar files from modules folder. */
+    private static final Predicate<Path> MODULE_FILTER = path -> path.toString().toLowerCase().endsWith(VALID_MODULE_EXTENSION);
+    /** Interface or abstract class representing the service to be loaded. */
+    private final Class<S> service;
+    /** Instance of static SPI loader. */
+    private ServiceLoader<S> serviceLoader;
+    /** Path to folder where are modules. Path is relative to resources. */
+    private final String servicesPath;
+    /** Instance of file system provider capable of exploring jar files. */
+    private final FileSystemProvider provider;
+
+    /**
+     * @param service Interface or abstract class representing the service to be loaded.
+     * @param servicesPath Path to folder to load services from. Path is relative to resources.
+     */
+    DynamicServiceLoader(Class<S> service, String servicesPath) {
+        this.service = service;
+        this.serviceLoader = ServiceLoader.load(service);
+        this.servicesPath = servicesPath;
+        this.provider = getZipFileSystemProvider();
+
+        logger.info("Initialized new DynamicServiceLoader with folder path: " + servicesPath);
+    }
+
+    /**
+     * Loads services implements {@link S} interface.
+     * @return Iterator of loaded services.
+     */
+    public Iterator<S> load() {
+        return serviceLoader.iterator();
+    }
+
+    /**
+     * Dynamically reloads services from services path.
+     */
+    public void reload() {
+        logger.info("Reloading all modules from path.");
+
+        Optional<Path> servicesPath = getServicesPath();
+        if (servicesPath.isPresent()) {
+            List<URL> serviceURLs = new ArrayList<>();
+            List<String> implementationsToLoad = new ArrayList<>();
+
+            try (Stream<Path> paths = Files.walk(servicesPath.get())) {
+                List<Path> jarPaths = paths.filter(MODULE_FILTER).collect(Collectors.toList());
+                for (Path jarPath : jarPaths) {
+                    FileSystem fileSystem = provider.newFileSystem(jarPath, new HashMap<>());
+                    Path serviceFilePath = fileSystem.getPath("/META-INF", "services", service.getCanonicalName());
+
+                    if (Files.exists(serviceFilePath)) {
+                        serviceURLs.add(jarPath.toUri().toURL());
+
+                        List<String> serviceLines = Files.readAllLines(serviceFilePath, StandardCharsets.UTF_8);
+                        serviceLines.removeIf(line -> line.startsWith("#"));
+                        implementationsToLoad.addAll(serviceLines);
+                    }
+                }
+            } catch (IOException e) {
+                logger.warn("Can not open services directory or service jar file.", e);
+            }
+
+            ClassLoader classLoader = URLClassLoader.newInstance(serviceURLs.toArray(new URL[0]), getClass().getClassLoader());
+            implementationsToLoad.forEach(className -> loadClass(classLoader, className));
+
+            serviceLoader = ServiceLoader.load(service, classLoader);
+            serviceLoader.reload();
+        }
+    }
+
+    /**
+     * Checks if modules path exists and if it is a directory.
+     * On success return Optional of opened folder otherwise returns empty optional.
+     * @return Optional of opened folder otherwise returns empty optional.
+     */
+    Optional<Path> getServicesPath() {
+        final URL servicesURL = getClass().getClassLoader().getResource(servicesPath);
+        if (servicesURL == null) {
+            logger.warn("Cannot open services directory.");
+            return Optional.empty();
+        }
+
+        try {
+            final Path servicesPath = Paths.get(servicesURL.toURI());
+
+            if (!Files.exists(servicesPath) || !Files.isDirectory(servicesPath)) {
+                logger.warn("Services path does not exist or it is not a directory.");
+                return Optional.empty();
+            }
+            return Optional.of(servicesPath);
+
+        } catch (URISyntaxException e) {
+            logger.warn("Can not open services directory", e);
+            return Optional.empty();
+        }
+    }
+
+    /**
+     * @return File system provider capable of exploring jar files.
+     */
+    private FileSystemProvider getZipFileSystemProvider() {
+        for (FileSystemProvider provider : FileSystemProvider.installedProviders()) {
+            if (VALID_MODULE_EXTENSION.equals(provider.getScheme())) {
+                return provider;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Dynamically loads class using classloader.
+     * @param classLoader Class loader used for loading class.
+     * @param className Fully qualified class name.
+     */
+    private void loadClass(ClassLoader classLoader, String className) {
+        try {
+            classLoader.loadClass(className);
+        } catch (ClassNotFoundException e) {
+            logger.warn(new ParameterizedMessage("Cannot load class {}.", className), e);
+        }
+    }
+}
diff --git a/sources/imiger-core/src/main/java/cz/zcu/kiv/offscreen/modularization/ModuleProvider.java b/sources/imiger-core/src/main/java/cz/zcu/kiv/offscreen/modularization/ModuleProvider.java
index 873f750..606b6b0 100644
--- a/sources/imiger-core/src/main/java/cz/zcu/kiv/offscreen/modularization/ModuleProvider.java
+++ b/sources/imiger-core/src/main/java/cz/zcu/kiv/offscreen/modularization/ModuleProvider.java
@@ -5,12 +5,10 @@ import org.apache.logging.log4j.LogManager;
 import org.apache.logging.log4j.Logger;
 
 import java.io.IOException;
-import java.net.URISyntaxException;
 import java.nio.file.*;
-import java.util.HashMap;
 import java.util.Iterator;
 import java.util.Map;
-import java.util.ServiceLoader;
+import java.util.Optional;
 import java.util.concurrent.*;
 
 /**
@@ -19,13 +17,15 @@ import java.util.concurrent.*;
 public class ModuleProvider {
 
     private static final Logger logger = LogManager.getLogger();
+    /** Path to folder with modules relative to resources */
+    private static final String MODULES_PATH = "/../lib/";
     /** Instance of this class used for singleton pattern. */
     private static ModuleProvider instance = null;
 
     /** Queue containing actual loaded modules. */
     private Map<String, IModule> modules = new ConcurrentHashMap<>();
-    /** Instance of ServiceLoader. */
-    private final ServiceLoader<IModule> loader;
+    /** Instance of DynamicServiceLoader. */
+    private final DynamicServiceLoader<IModule> serviceLoader;
 
     /** Instance of ScheduledExecutorService used for scheduling of module folder watcher. */
     private ScheduledExecutorService executor;
@@ -53,9 +53,9 @@ public class ModuleProvider {
      * When watcher fails than is automatically starts new watcher after 5 minutes timeout.
      */
     private ModuleProvider() {
-        this.loader = ServiceLoader.load(IModule.class);
+        serviceLoader = new DynamicServiceLoader<>(IModule.class, MODULES_PATH);
 
-        processModules(loader.iterator());
+        processModules(serviceLoader.load());
 
         executor = Executors.newSingleThreadScheduledExecutor();
         Runnable task = this::initModulesWatcher;
@@ -91,11 +91,17 @@ public class ModuleProvider {
      * and storing (method processModules) of all modules.
      */
     private void initModulesWatcher() {
+        Optional<Path> modulesFolderOptional = serviceLoader.getServicesPath();
+        if (!modulesFolderOptional.isPresent()) {
+            return;
+        }
+
         try {
             logger.debug("Initializing new WatcherService for modules directory");
 
-            Path path = Paths.get(getClass().getClassLoader().getResource("/../lib/").toURI());
             watcher = FileSystems.getDefault().newWatchService();
+
+            Path path = modulesFolderOptional.get();
             path.register(watcher,
                     StandardWatchEventKinds.ENTRY_CREATE,
                     StandardWatchEventKinds.ENTRY_DELETE,
@@ -113,8 +119,8 @@ public class ModuleProvider {
                         continue;
                     }
 
-                    loader.reload();
-                    processModules(loader.iterator());
+                    serviceLoader.reload();
+                    processModules(serviceLoader.load());
                     break; // watching only one folder and loading all files every loop => Only one iteration is needed.
                 }
 
@@ -124,7 +130,7 @@ public class ModuleProvider {
                 }
             }
 
-        } catch (URISyntaxException | IOException | InterruptedException e) {
+        } catch (IOException | InterruptedException e) {
             logger.error("Modules directory watcher throw an exception: ", e);
         }
     }
@@ -138,14 +144,11 @@ public class ModuleProvider {
      */
     private void processModules(Iterator<IModule> unprocessedModules) {
         long startTime = System.nanoTime();
-        Map<String, IModule> localModules = new HashMap<>();
-
         logger.info("Processing modules.");
 
-        unprocessedModules.forEachRemaining(module -> localModules.put(String.valueOf(module.getModuleName().hashCode()), module));
-
         modules.clear();
-        modules.putAll(localModules);
+        unprocessedModules.forEachRemaining(module -> modules.put(String.valueOf(module.getModuleName().hashCode()), module));
+
         logger.debug("Modules were loaded and processed in " + (System.nanoTime() - startTime) / 1000000d + " milliseconds");
     }
 
diff --git a/sources/imiger-plugin-template/pom.xml b/sources/imiger-plugin-template/pom.xml
index ee1b042..21552d2 100644
--- a/sources/imiger-plugin-template/pom.xml
+++ b/sources/imiger-plugin-template/pom.xml
@@ -26,8 +26,21 @@
         <plugins>
             <plugin>
                 <groupId>org.apache.maven.plugins</groupId>
-                <artifactId>maven-jar-plugin</artifactId>
-                <version>3.1.1</version>
+                <artifactId>maven-assembly-plugin</artifactId>
+                <configuration>
+                    <descriptorRefs>
+                        <descriptorRef>jar-with-dependencies</descriptorRef>
+                    </descriptorRefs>
+                </configuration>
+                <executions>
+                    <execution>
+                        <id>make-assembly</id> <!-- this is used for inheritance merges -->
+                        <phase>package</phase> <!-- bind to the packaging phase -->
+                        <goals>
+                            <goal>single</goal>
+                        </goals>
+                    </execution>
+                </executions>
             </plugin>
         </plugins>
     </build>
diff --git a/sources/imiger-spade-converter/pom.xml b/sources/imiger-spade-converter/pom.xml
index e426975..0eda568 100644
--- a/sources/imiger-spade-converter/pom.xml
+++ b/sources/imiger-spade-converter/pom.xml
@@ -19,8 +19,21 @@
         <plugins>
             <plugin>
                 <groupId>org.apache.maven.plugins</groupId>
-                <artifactId>maven-jar-plugin</artifactId>
-                <version>3.1.1</version>
+                <artifactId>maven-assembly-plugin</artifactId>
+                <configuration>
+                    <descriptorRefs>
+                        <descriptorRef>jar-with-dependencies</descriptorRef>
+                    </descriptorRefs>
+                </configuration>
+                <executions>
+                    <execution>
+                        <id>make-assembly</id> <!-- this is used for inheritance merges -->
+                        <phase>package</phase> <!-- bind to the packaging phase -->
+                        <goals>
+                            <goal>single</goal>
+                        </goals>
+                    </execution>
+                </executions>
             </plugin>
         </plugins>
     </build>
