From e29627ba0dad0dfafb39ba510feb8b596e431f20 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Georg=20Gra=C3=9Fnick?=
 <georg.grassnick@mailbox.tu-dresden.de>
Date: Thu, 26 Sep 2019 19:33:41 +0200
Subject: [PATCH] Load native libraries from resources on runtime

---
 .gitignore                                    |   5 +
 build.gradle                                  |  17 +++
 .../de/tudresden/inf/mci/brailleplot/App.java |   2 -
 .../LiblouisBrailleTextRasterizer.java        |   9 ++
 .../brailleplot/util/NativeLibraryHelper.java | 106 ++++++++++++++++++
 .../util/NoSuchNativeLibraryException.java    |  23 ++++
 6 files changed, 160 insertions(+), 2 deletions(-)
 create mode 100644 src/main/java/de/tudresden/inf/mci/brailleplot/util/NativeLibraryHelper.java
 create mode 100644 src/main/java/de/tudresden/inf/mci/brailleplot/util/NoSuchNativeLibraryException.java

diff --git a/.gitignore b/.gitignore
index 9f25cc23..e113ca2e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -22,3 +22,8 @@ src/main/resources/mapping/liblouis
 
 # Exported SVG files
 *.svg
+
+# Native libraries
+*.so
+*.dynlib
+*.dll
diff --git a/build.gradle b/build.gradle
index 59cf0b22..d56141ec 100644
--- a/build.gradle
+++ b/build.gradle
@@ -68,6 +68,23 @@ clean {
     delete "$rootDir/src/main/resources/mapping/liblouis"
 }
 
+// Copy native liblouis libraries for major platforms to the resource folder
+task copyLibLouisBinary(type: Copy) {
+    from "$projectDir/third_party/liblouis_bin/bin/"
+    into "$rootDir/src/main/resources/native/liblouis"
+}
+processResources.dependsOn copyLibLouisBinary
+// Abort if files are missing
+gradle.taskGraph.afterTask { copyLibLouisBinary ->
+    if(copyLibLouisBinary.state.noSource){
+        throw new GradleException("Could not find liblouis native libraries in \"$projectDir/third_party/liblouis_bin/bin/\". Please make sure you updated all git submodules using \"git submodule update --init --recursive\" ")
+    }
+}
+// Delete libraries on "clean" task
+clean {
+    delete "$rootDir/src/main/resources/native/liblouis"
+}
+
 // Define the main class for the application
 mainClassName = 'de.tudresden.inf.mci.brailleplot.App'
 
diff --git a/src/main/java/de/tudresden/inf/mci/brailleplot/App.java b/src/main/java/de/tudresden/inf/mci/brailleplot/App.java
index ada22108..14225cbe 100644
--- a/src/main/java/de/tudresden/inf/mci/brailleplot/App.java
+++ b/src/main/java/de/tudresden/inf/mci/brailleplot/App.java
@@ -151,8 +151,6 @@ public final class App {
         try {
             // Logging example
             mLogger.info("Application started");
-            // Needed for Windows machines
-            System.setProperty("jna.library.path", System.getProperty("user.dir") + "/third_party");
             // Parse command line parameters
             CommandLineParser cliParser = new CommandLineParser();
             SettingsWriter settings = cliParser.parse(args);
diff --git a/src/main/java/de/tudresden/inf/mci/brailleplot/rendering/LiblouisBrailleTextRasterizer.java b/src/main/java/de/tudresden/inf/mci/brailleplot/rendering/LiblouisBrailleTextRasterizer.java
index d47dec2b..e9a28cb4 100644
--- a/src/main/java/de/tudresden/inf/mci/brailleplot/rendering/LiblouisBrailleTextRasterizer.java
+++ b/src/main/java/de/tudresden/inf/mci/brailleplot/rendering/LiblouisBrailleTextRasterizer.java
@@ -7,6 +7,8 @@ import de.tudresden.inf.mci.brailleplot.layout.InsufficientRenderingAreaExceptio
 import de.tudresden.inf.mci.brailleplot.layout.RasterCanvas;
 import de.tudresden.inf.mci.brailleplot.layout.Rectangle;
 import de.tudresden.inf.mci.brailleplot.printerbackend.NotSupportedFileExtensionException;
+import de.tudresden.inf.mci.brailleplot.util.NativeLibraryHelper;
+import de.tudresden.inf.mci.brailleplot.util.NoSuchNativeLibraryException;
 import org.liblouis.DisplayException;
 import org.liblouis.DisplayTable;
 import org.liblouis.TranslationException;
@@ -42,6 +44,13 @@ public class LiblouisBrailleTextRasterizer implements Rasterizer<BrailleText> {
      * @param printer Needed to get the semantictable according to the printer config.
      */
     public LiblouisBrailleTextRasterizer(final Printer printer) {
+
+        try {
+            NativeLibraryHelper.loadNativeLibrary("liblouis");
+        } catch (NoSuchNativeLibraryException e) {
+          // Even if the library is not distributed within the jar file, it might be installed on the system.
+        }
+
         Objects.requireNonNull(printer, "The given printer for the LiblouisBrailleTextRasterizer was null!");
         try {
             mParser = AbstractBrailleTableParser.getParser(printer, "semantictable");
diff --git a/src/main/java/de/tudresden/inf/mci/brailleplot/util/NativeLibraryHelper.java b/src/main/java/de/tudresden/inf/mci/brailleplot/util/NativeLibraryHelper.java
new file mode 100644
index 00000000..cdc570ab
--- /dev/null
+++ b/src/main/java/de/tudresden/inf/mci/brailleplot/util/NativeLibraryHelper.java
@@ -0,0 +1,106 @@
+package de.tudresden.inf.mci.brailleplot.util;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+
+/**
+ * This class offers static methods for native library loading purposes.
+ * @author Georg Graßnick
+ * @version 2019.09.26
+ */
+public final class NativeLibraryHelper {
+
+    private static final Logger LOG = LoggerFactory.getLogger(NativeLibraryHelper.class);
+    private static final String LIB_PATH = calculateLibPath();
+
+    private NativeLibraryHelper() { }
+
+    /**
+     * Loads a library from the resources according to the current system architecture and operating system.
+     * @param libName The name of the library. Library prefixes ({@literal "}lib{@literal "}) are not treated separately.
+     *                The system specific file ending is added automatically and must not be included in the parameter.
+     * @return A File object representing the file of the library. The JNI is automatically set up to take of the library,
+     * so there is most likely no need for this return value.
+     * @throws NoSuchNativeLibraryException If the requested library could not be found or read.
+     */
+    public static synchronized File loadNativeLibrary(final String libName) throws NoSuchNativeLibraryException {
+        File libFile;
+        try {
+            String libPath = "native/" + libName + "/" + LIB_PATH + "/" + libName + dynamicLibFileEnding();
+            libFile = GeneralResource.getOrExportResourceFile(libPath).getAbsoluteFile();
+            registerNewSystemLibPath(libFile.getParent());
+        } catch (Exception e) {
+            throw new NoSuchNativeLibraryException("Could not provide native library from java resources", e);
+        }
+        LOG.debug("Found and exported native library \"" + libFile + "\" for requested library \"" + libName + "\"");
+        return libFile;
+    }
+
+    /**
+     * Adds a path in the file system to the search path of the JNI.
+     * @param path The path to add.
+     */
+    private static synchronized void registerNewSystemLibPath(final String path) {
+        // TODO Check if path is already included and abort, if so
+        String currentLibPath = System.getProperty("jna.library.path");
+        String newLibPath = null;
+        if (currentLibPath == null) {
+            newLibPath = path;
+        } else {
+            newLibPath = currentLibPath + File.pathSeparator + path;
+        }
+        LOG.debug("Setting JNI library path property to \"" + newLibPath + "\"");
+        System.setProperty("jna.library.path", newLibPath);
+    }
+
+    private static String calculateLibPath() {
+        return getArch() + "/" + getOs();
+    }
+
+    private static String getArch() {
+        String arch = System.getProperty("os.arch");
+        // reference: https://stackoverflow.com/a/36926327
+        switch (arch) {
+            case "x86":
+            case "i386":
+            case "i486":
+            case "i586":
+            case "i686":
+                return "x86_32";
+            case "x86_64":
+            case "amd64":
+                return "x86_64";
+            default:
+                throw new RuntimeException("Operating System architecture \"" + arch + "\" is currently not supported");
+        }
+    }
+
+    private static String getOs() {
+        String name = System.getProperty("os.name");
+        String nameLow = name.toLowerCase();
+        if (nameLow.contains("linux")) {
+            return "linux";
+        } else if (nameLow.contains("mac")) {
+            return "osx";
+        } else if (nameLow.contains("win")) {
+            return "win32";
+        } else {
+            throw new RuntimeException("Operating System \"" + name + "\" is currently not supported");
+        }
+    }
+
+    private static String dynamicLibFileEnding() {
+        switch (getOs()) {
+            case "win32":
+                return ".dll";
+            case "linux":
+                return ".so";
+            case "osx":
+                return ".dynlib";
+            default:
+                throw new IllegalStateException("If this exception was thrown, something is wrong with your code");
+        }
+    }
+}
diff --git a/src/main/java/de/tudresden/inf/mci/brailleplot/util/NoSuchNativeLibraryException.java b/src/main/java/de/tudresden/inf/mci/brailleplot/util/NoSuchNativeLibraryException.java
new file mode 100644
index 00000000..2f7331e5
--- /dev/null
+++ b/src/main/java/de/tudresden/inf/mci/brailleplot/util/NoSuchNativeLibraryException.java
@@ -0,0 +1,23 @@
+package de.tudresden.inf.mci.brailleplot.util;
+
+/**
+ * Indicates, some error has occurred while trying to load a native library from the java resources.
+ * @author Georg Graßnick
+ * @version 2019.09.26
+ */
+public class NoSuchNativeLibraryException extends RuntimeException {
+
+        public NoSuchNativeLibraryException() { }
+
+        public NoSuchNativeLibraryException(final String message) {
+            super(message);
+        }
+
+        public NoSuchNativeLibraryException(final Throwable cause) {
+            super(cause);
+        }
+
+        public NoSuchNativeLibraryException(final String message, final Throwable cause) {
+            super(message, cause);
+        }
+}
-- 
GitLab