001package com.pi4j.util;
002
003/*
004 * #%L
005 * **********************************************************************
006 * ORGANIZATION  :  Pi4J
007 * PROJECT       :  Pi4J :: Java Library (Core)
008 * FILENAME      :  NativeLibraryLoader.java
009 *
010 * This file is part of the Pi4J project. More information about
011 * this project can be found here:  https://www.pi4j.com/
012 * **********************************************************************
013 * %%
014 * Copyright (C) 2012 - 2021 Pi4J
015 * %%
016 * This program is free software: you can redistribute it and/or modify
017 * it under the terms of the GNU Lesser General Public License as
018 * published by the Free Software Foundation, either version 3 of the
019 * License, or (at your option) any later version.
020 *
021 * This program is distributed in the hope that it will be useful,
022 * but WITHOUT ANY WARRANTY; without even the implied warranty of
023 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
024 * GNU General Lesser Public License for more details.
025 *
026 * You should have received a copy of the GNU General Lesser Public
027 * License along with this program.  If not, see
028 * <http://www.gnu.org/licenses/lgpl-3.0.html>.
029 * #L%
030 */
031
032import com.pi4j.platform.Platform;
033
034import java.io.File;
035import java.io.FileNotFoundException;
036import java.io.IOException;
037import java.io.InputStream;
038import java.net.URISyntaxException;
039import java.nio.file.Files;
040import java.nio.file.Path;
041import java.nio.file.Paths;
042import java.nio.file.StandardCopyOption;
043import java.util.Set;
044import java.util.TreeSet;
045import java.util.logging.ConsoleHandler;
046import java.util.logging.FileHandler;
047import java.util.logging.Level;
048import java.util.logging.Logger;
049
050public class NativeLibraryLoader {
051
052        private static final Set<String> loadedLibraries = new TreeSet<>();
053        private static final Logger logger = Logger.getLogger(NativeLibraryLoader.class.getName());
054        private static boolean initialized;
055
056        // private constructor
057        private NativeLibraryLoader() {
058                // forbid object construction
059        }
060
061        public static synchronized void load(String fileName, String libName) {
062                // check for debug property; if found enable all logging levels
063                if (!initialized) {
064                        initialized = true;
065                        if (System.getProperty("pi4j.debug") != null) {
066                                logger.setLevel(Level.ALL);
067                                try {
068                                        // create an appending file handler
069                                        FileHandler fileHandler = new FileHandler("pi4j.log");
070                                        fileHandler.setLevel(Level.ALL);
071                                        ConsoleHandler consoleHandler = new ConsoleHandler();
072                                        consoleHandler.setLevel(Level.ALL);
073
074                                        // add to the desired loggers
075                                        logger.addHandler(fileHandler);
076                                        logger.addHandler(consoleHandler);
077                                } catch (IOException e) {
078                                        System.err.println("Unable to setup logging to debug. No logging will be done. Error: ");
079                                        e.printStackTrace();
080                                }
081                        }
082                }
083
084                // first, make sure that this library has not already been previously loaded
085                if (loadedLibraries.contains(fileName)) {
086                        logger.fine("Library [" + fileName + "] has already been loaded; no need to load again.");
087                        return;
088                }
089
090                // cache loaded library
091                loadedLibraries.add(fileName);
092
093                // determine if there is an overriding library path defined for native libraries
094                String libPath = System.getProperty("pi4j.library.path");
095                if(StringUtil.isNotNullOrEmpty(libPath, true)) {
096
097                        // if the overriding library path is set to "system", then attempt to use the system resolved library paths
098                        if (libPath.equalsIgnoreCase("system")) {
099                                logger.fine("Attempting to load library using {pi4j.library.path} system resolved library name: [" + libName + "]");
100                                try {
101                                        // load library from JVM system library path; based on library name
102                                        System.loadLibrary(libName);
103                                } catch (Exception ex) {
104                                        //throw this error
105                                        throw new UnsatisfiedLinkError("Pi4J was unable load the native library [" +
106                                                        libName + "] from the system defined library path.  The system property 'pi4j.library.path' is defined as [" +
107                                                        libPath + "]. You can alternatively define the 'pi4j.library.path' " +
108                                                        "system property to override this behavior and specify an absolute library path." +
109                                                        "; UNDERLYING EXCEPTION: [" + ex.getClass().getName() + "]=" + ex.getMessage());
110                                }
111                        }
112
113                        // if the overriding library path is set to "local", then attempt to use the JAR local path to resolve library
114                        else if (libPath.equalsIgnoreCase("local")) {
115                                // get local directory path of JAR file
116                                try {
117                                        libPath = NativeLibraryLoader.class.getProtectionDomain().getCodeSource().getLocation().toURI().getPath();
118                                } catch (URISyntaxException e) {
119                                        logger.severe(e.getMessage());
120                                        libPath = ".";
121                                }
122                                // build path based on lib directory and lib filename
123                                String path = Paths.get(libPath, fileName).toString();
124                                logger.fine("Attempting to load library using {pi4j.library.path} defined path: [" + path + "]");
125                                try {
126                                        // load library from local path of this JAR file
127                                        System.load(path);
128                                } catch (Exception ex) {
129                                        //throw this error
130                                        throw new UnsatisfiedLinkError("Pi4J was unable load the native library [" +
131                                                        libName + "] from the user defined library path.  The system property 'pi4j.library.path' is defined as [" +
132                                                        libPath + "]. Please make sure the defined the 'pi4j.library.path' " +
133                                                        "system property contains the correct absolute library path." +
134                                                        "; UNDERLYING EXCEPTION: [" + ex.getClass().getName() + "]=" + ex.getMessage());
135                                }
136                        }
137
138                        // if the overriding library path is set to something else, then attempt to use the defined path to resolve library
139                        else {
140                                // build path based on lib directory and lib filename
141                                String path = Paths.get(libPath, fileName).toString();
142                                logger.fine("Attempting to load library using {pi4j.library.path} defined path: [" + path + "]");
143                                try {
144                                        // load library from user defined absolute path provided via pi4j.library.path}
145                                        System.load(path);
146                                } catch (Exception ex) {
147                                        //throw this error
148                                        throw new UnsatisfiedLinkError("Pi4J was unable load the native library [" +
149                                                        libName + "] from the user defined library path.  The system property 'pi4j.library.path' is defined as [" +
150                                                        libPath + "]. Please make sure the defined the 'pi4j.library.path' " +
151                                                        "system property contains the correct absolute library path." +
152                                                        "; UNDERLYING EXCEPTION: [" + ex.getClass().getName() + "]=" + ex.getMessage());
153                                }
154                        }
155                }
156                // if there is no overriding library path defined, then attempt to load native library from embedded resource
157        else {
158                        //
159                        // path = /lib/{platform}/{linking:static|dynamic}/{filename}
160                        //
161                        String platform = System.getProperty("pi4j.platform", Platform.RASPBERRYPI.getId());
162
163                        // NOTE: As of 2018-04-23, Pi4J no longer includes
164                        //       a statically linked wiringPi lib for the Raspberry Pi platform.  The
165                        //       default linking for the Raspberry Pi platform should always be "dynamic"
166                        String linking = System.getProperty("pi4j.linking",
167                                        platform.equalsIgnoreCase(Platform.RASPBERRYPI.getId()) ? "dynamic" : "static");
168
169                        String path = "/lib/" + platform + "/" + linking + "/" + fileName;
170                        logger.fine("Attempting to load [" + fileName + "] using path: [" + path + "]");
171                        try {
172                                loadLibraryFromClasspath(path);
173                                logger.fine("Library [" + fileName + "] loaded successfully using embedded resource file: [" + path + "]");
174                        } catch (Exception | UnsatisfiedLinkError e) {
175                                logger.log(Level.SEVERE, "Unable to load [" + fileName + "] using path: [" + path + "]", e);
176                                // either way, we did what we could, no need to remove now the library from the loaded libraries
177                                // since we run inside one VM and nothing could possibly change, so there is no point in
178                                // trying out this logic again
179                        }
180                }
181        }
182
183        /**
184         * Loads library from classpath
185         *
186         * The file from classpath is copied into system temporary directory and then loaded. The temporary file is
187     * deleted after exiting. Method uses String as filename because the pathname is
188         * "abstract", not system-dependent.
189         *
190         * @param path
191         *            The file path in classpath as an absolute path, e.g. /package/File.ext (could be inside jar)
192         * @throws IOException
193         *             If temporary file creation or read/write operation fails
194         * @throws IllegalArgumentException
195         *             If source file (param path) does not exist
196         * @throws IllegalArgumentException
197         *             If the path is not absolute or if the filename is shorter than three characters (restriction
198     *             of {@see File#createTempFile(java.lang.String, java.lang.String)}).
199         */
200        public static void loadLibraryFromClasspath(String path) throws IOException {
201                Path inputPath = Paths.get(path);
202
203                if (!inputPath.isAbsolute()) {
204                        throw new IllegalArgumentException("The path has to be absolute, but found: " + inputPath);
205                }
206
207                String fileNameFull = inputPath.getFileName().toString();
208                int dotIndex = fileNameFull.indexOf('.');
209                if (dotIndex < 0 || dotIndex >= fileNameFull.length() - 1) {
210                        throw new IllegalArgumentException("The path has to end with a file name and extension, but found: " + fileNameFull);
211                }
212
213                String fileName = fileNameFull.substring(0, dotIndex);
214                String extension = fileNameFull.substring(dotIndex);
215
216                Path target = Files.createTempFile(fileName, extension);
217                File targetFile = target.toFile();
218                targetFile.deleteOnExit();
219
220                try (InputStream source = NativeLibraryLoader.class.getResourceAsStream(inputPath.toString())) {
221                        if (source == null) {
222                                throw new FileNotFoundException("File " + inputPath + " was not found in classpath.");
223                        }
224                        Files.copy(source, target, StandardCopyOption.REPLACE_EXISTING);
225                }
226                // Finally, load the library
227                System.load(target.toAbsolutePath().toString());
228        }
229}