7070 *
7171 * @author Kirill Vainer
7272 */
73- public final class NativeLibraryLoader {
74-
75- private static final Logger logger = Logger .getLogger (NativeLibraryLoader .class .getName ());
76- private static File extractionFolderOverride = null ;
77- private static File extractionFolder = null ;
78-
79- private static final HashMap <NativeLibrary .Key , NativeLibrary > nativeLibraryMap = new HashMap <>();
73+ public final class NativeLibraryLoader {
74+ /**
75+ * System property containing the filesystem directory where native libraries
76+ * should be extracted to or loaded from.
77+ */
78+ public static final String CUSTOM_EXTRACTION_FOLDER_PROPERTY = "com.jme3.NativeLibraryExtractionFolder" ;
79+
80+ /**
81+ * System property controlling whether native libraries should be extracted
82+ * from the classpath before loading. Defaults to {@code true}.
83+ */
84+ public static final String EXTRACT_NATIVE_LIBRARIES_PROPERTY = "com.jme3.ExtractNativeLibraries" ;
85+
86+ private static final Logger logger = Logger .getLogger (NativeLibraryLoader .class .getName ());
87+ private static File extractionFolderOverride = null ;
88+ private static File extractionFolder = null ;
89+ private static Boolean extractNativeLibrariesOverride = null ;
90+
91+ private static final HashMap <NativeLibrary .Key , NativeLibrary > nativeLibraryMap = new HashMap <>();
8092
8193 static {
8294 NativeLibraries .registerDefaultLibraries ();
@@ -174,18 +186,75 @@ public static boolean isUsingNativeBullet() {
174186 *
175187 * @param path Path where to extract native libraries.
176188 */
177- public static void setCustomExtractionFolder (String path ) {
178- extractionFolderOverride = new File (path ).getAbsoluteFile ();
179- }
189+ public static void setCustomExtractionFolder (String path ) {
190+ extractionFolderOverride = path == null ? null : new File (path ).getAbsoluteFile ();
191+ }
192+
193+ /**
194+ * Returns the configured custom extraction folder.
195+ *
196+ * @return the programmatic override if set, otherwise the
197+ * {@link #CUSTOM_EXTRACTION_FOLDER_PROPERTY} system property
198+ */
199+ public static File getCustomExtractionFolder () {
200+ if (extractionFolderOverride != null ) {
201+ return extractionFolderOverride ;
202+ }
203+
204+ String extractionFolderProperty = System .getProperty (CUSTOM_EXTRACTION_FOLDER_PROPERTY );
205+ if (extractionFolderProperty != null && !extractionFolderProperty .trim ().isEmpty ()) {
206+ return new File (extractionFolderProperty ).getAbsoluteFile ();
207+ }
208+
209+ return null ;
210+ }
211+
212+ /**
213+ * Specify whether native libraries should be extracted from the classpath
214+ * before loading. Set to {@code true} to preserve the default behavior.
215+ *
216+ * @param extractNativeLibraries true to extract classpath natives, false to
217+ * load existing files from the extraction folder
218+ */
219+ public static void setExtractNativeLibraries (boolean extractNativeLibraries ) {
220+ extractNativeLibrariesOverride = extractNativeLibraries ;
221+ }
222+
223+ /**
224+ * Clears the programmatic extraction flag override.
225+ */
226+ public static void clearExtractNativeLibrariesOverride () {
227+ extractNativeLibrariesOverride = null ;
228+ }
229+
230+ /**
231+ * Returns whether native libraries should be extracted before loading.
232+ *
233+ * @return the programmatic override if set, otherwise the
234+ * {@link #EXTRACT_NATIVE_LIBRARIES_PROPERTY} system property,
235+ * defaulting to true
236+ */
237+ public static boolean isExtractNativeLibraries () {
238+ if (extractNativeLibrariesOverride != null ) {
239+ return extractNativeLibrariesOverride ;
240+ }
241+
242+ String extractNativeLibrariesProperty = System .getProperty (EXTRACT_NATIVE_LIBRARIES_PROPERTY );
243+ return extractNativeLibrariesProperty == null
244+ || extractNativeLibrariesProperty .trim ().isEmpty ()
245+ || Boolean .parseBoolean (extractNativeLibrariesProperty );
246+ }
180247
181248 /**
182249 * Returns the folder where native libraries will be extracted.
183250 * This is automatically determined at run-time based on the
184- * following criteria:<br>
185- * <ul>
186- * <li>If a {@link #setCustomExtractionFolder(java.lang.String) custom
187- * extraction folder} has been specified, it is returned.
188- * <li>If the user can write to "java.io.tmpdir" folder, then it
251+ * following criteria:<br>
252+ * <ul>
253+ * <li>If a {@link #setCustomExtractionFolder(java.lang.String) custom
254+ * extraction folder} has been specified, it is returned.</li>
255+ * <li>If the {@link #CUSTOM_EXTRACTION_FOLDER_PROPERTY} system property
256+ * has been specified, it is returned.</li>
257+ * <li>If the user can write to "java.io.tmpdir" folder, then it
189258 * is used.</li>
190259 * <li>Otherwise, the {@link JmeSystem#getStorageFolder() storage folder}
191260 * is used, to prevent collisions, a special subfolder is used
@@ -196,10 +265,11 @@ public static void setCustomExtractionFolder(String path) {
196265 *
197266 * @return Path where natives will be extracted to.
198267 */
199- public static File getExtractionFolder () {
200- if (extractionFolderOverride != null ) {
201- return extractionFolderOverride ;
202- }
268+ public static File getExtractionFolder () {
269+ File customExtractionFolder = getCustomExtractionFolder ();
270+ if (customExtractionFolder != null ) {
271+ return customExtractionFolder ;
272+ }
203273 if (extractionFolder == null ) {
204274 File userTempDir = new File (System .getProperty ("java.io.tmpdir" ));
205275 if (!userTempDir .canWrite ()) {
@@ -480,79 +550,95 @@ public static String loadNativeLibrary(String name, boolean isRequired) {
480550 return null ;
481551 }
482552
483- URL url = Resources .getResource (pathInJar );
484-
485- if (url == null ) {
486- if (isRequired ) {
487- throw new UnsatisfiedLinkError (
488- "The required native library '" + library .getName () + "'"
489- + " was not found in the classpath via '" + pathInJar );
490- } else if (logger .isLoggable (Level .FINE )) {
491- logger .log (Level .FINE , "The optional native library ''{0}''" +
492- " was not found in the classpath via ''{1}''." ,
493- new Object []{library .getName (), pathInJar });
494- }
495- return null ;
496- }
497-
498- // The library has been found and is ready to be extracted.
499- // Determine what filename it should be extracted as.
500- String loadedAsFileName ;
501- if (library .getExtractedAsName () != null ) {
502- loadedAsFileName = library .getExtractedAsName ();
503- } else {
504- // Just use the original filename as it is in the JAR.
505- loadedAsFileName = Paths .get (pathInJar ).getFileName ().toString ();
506- }
507-
508- File extractionDirectory = getExtractionFolder ();
509- URLConnection conn ;
510-
511- try {
512- conn = url .openConnection ();
513- } catch (IOException ex ) {
514- // Maybe put more detail here? Not sure.
515- throw new UncheckedIOException ("Failed to open file: '" + url +
516- "'. Error: " + ex , ex );
517- }
518-
519- File targetFile = new File (extractionDirectory , loadedAsFileName );
520- try (InputStream in = conn .getInputStream ()) {
521- if (isExtractingRequired (conn , targetFile )) {
522- Files .copy (in , targetFile .toPath (), StandardCopyOption .REPLACE_EXISTING );
523-
524- // NOTE: On OSes that support "Date Created" property,
525- // this will cause the last modified date to be lower than
526- // date created which makes no sense
527- targetFile .setLastModified (conn .getLastModified ());
528-
529- if (logger .isLoggable (Level .FINE )) {
530- logger .log (Level .FINE , "Extracted native library from ''{0}'' into ''{1}''. " ,
531- new Object []{url , targetFile });
532- }
533- } else {
534- if (logger .isLoggable (Level .FINE )) {
535- logger .log (Level .FINE , "Not copying library {0}. Latest already extracted." ,
536- loadedAsFileName );
537- }
538- }
539-
540- library .getLoadFunction ().accept (targetFile .getAbsolutePath ());
541-
542- if (logger .isLoggable (Level .FINE )) {
543- logger .log (Level .FINE , "Loaded native library {0}." , library .getName ());
544- }
545-
546- return targetFile .getAbsolutePath ();
547- } catch (IOException ex ) {
548- /*if (ex.getMessage().contains("used by another process")) {
549- return;
550- }*/
551-
552- throw new UncheckedIOException ("Failed to extract native library to: "
553- + targetFile , ex );
554- }
555- }
553+ String loadedAsFileName = getLoadedAsFileName (library , pathInJar );
554+ File extractionDirectory = getExtractionFolder ();
555+ File targetFile = new File (extractionDirectory , loadedAsFileName );
556+
557+ if (isExtractNativeLibraries ()) {
558+ URL url = Resources .getResource (pathInJar );
559+
560+ if (url == null ) {
561+ if (isRequired ) {
562+ throw new UnsatisfiedLinkError (
563+ "The required native library '" + library .getName () + "'"
564+ + " was not found in the classpath via '" + pathInJar );
565+ } else if (logger .isLoggable (Level .FINE )) {
566+ logger .log (Level .FINE , "The optional native library ''{0}''" +
567+ " was not found in the classpath via ''{1}''." ,
568+ new Object []{library .getName (), pathInJar });
569+ }
570+ return null ;
571+ }
572+
573+ // The library has been found and is ready to be extracted.
574+ URLConnection conn ;
575+
576+ try {
577+ conn = url .openConnection ();
578+ } catch (IOException ex ) {
579+ // Maybe put more detail here? Not sure.
580+ throw new UncheckedIOException ("Failed to open file: '" + url +
581+ "'. Error: " + ex , ex );
582+ }
583+
584+ try (InputStream in = conn .getInputStream ()) {
585+ if (isExtractingRequired (conn , targetFile )) {
586+ Files .copy (in , targetFile .toPath (), StandardCopyOption .REPLACE_EXISTING );
587+
588+ // NOTE: On OSes that support "Date Created" property,
589+ // this will cause the last modified date to be lower than
590+ // date created which makes no sense
591+ targetFile .setLastModified (conn .getLastModified ());
592+
593+ if (logger .isLoggable (Level .FINE )) {
594+ logger .log (Level .FINE , "Extracted native library from ''{0}'' into ''{1}''. " ,
595+ new Object []{url , targetFile });
596+ }
597+ } else {
598+ if (logger .isLoggable (Level .FINE )) {
599+ logger .log (Level .FINE , "Not copying library {0}. Latest already extracted." ,
600+ loadedAsFileName );
601+ }
602+ }
603+ } catch (IOException ex ) {
604+ /*if (ex.getMessage().contains("used by another process")) {
605+ return;
606+ }*/
607+
608+ throw new UncheckedIOException ("Failed to extract native library to: "
609+ + targetFile , ex );
610+ }
611+ } else if (!targetFile .isFile ()) {
612+ if (isRequired ) {
613+ throw new UnsatisfiedLinkError (
614+ "The required native library '" + library .getName () + "'"
615+ + " was not found at '" + targetFile
616+ + "' and native library extraction is disabled" );
617+ } else if (logger .isLoggable (Level .FINE )) {
618+ logger .log (Level .FINE , "The optional native library ''{0}''" +
619+ " was not found at ''{1}'' and native library extraction is disabled." ,
620+ new Object []{library .getName (), targetFile });
621+ }
622+ return null ;
623+ }
624+
625+ library .getLoadFunction ().accept (targetFile .getAbsolutePath ());
626+
627+ if (logger .isLoggable (Level .FINE )) {
628+ logger .log (Level .FINE , "Loaded native library {0}." , library .getName ());
629+ }
630+
631+ return targetFile .getAbsolutePath ();
632+ }
633+
634+ private static String getLoadedAsFileName (NativeLibrary library , String pathInJar ) {
635+ if (library .getExtractedAsName () != null ) {
636+ return library .getExtractedAsName ();
637+ }
638+
639+ // Just use the original filename as it is in the JAR.
640+ return Paths .get (pathInJar ).getFileName ().toString ();
641+ }
556642
557643 /**
558644 * Checks if library extraction is required by comparing source and target
0 commit comments