001 /*******************************************************************************
002 * Copyright (C) 2009-2011 FuseSource Corp.
003 * Copyright (c) 2000, 2009 IBM Corporation and others.
004 *
005 * All rights reserved. This program and the accompanying materials
006 * are made available under the terms of the Eclipse Public License v1.0
007 * which accompanies this distribution, and is available at
008 * http://www.eclipse.org/legal/epl-v10.html
009 *******************************************************************************/
010 package org.fusesource.hawtjni.runtime;
011
012 import java.io.File;
013 import java.io.FileOutputStream;
014 import java.io.IOException;
015 import java.io.InputStream;
016 import java.net.MalformedURLException;
017 import java.net.URL;
018 import java.util.ArrayList;
019 import java.util.regex.Pattern;
020
021 /**
022 * Used to optionally extract and load a JNI library.
023 *
024 * It will search for the library in order at the following locations:
025 * <ol>
026 * <li> in the custom library path: If the "library.${name}.path" System property is set to a directory
027 * <ol>
028 * <li> "${name}-${version}" if the version can be determined.
029 * <li> "${name}"
030 * </ol>
031 * <li> system library path: This is where the JVM looks for JNI libraries by default.
032 * <ol>
033 * <li> "${name}-${version}" if the version can be determined.
034 * <li> "${name}"
035 * </ol>
036 * <li> classpath path: If the JNI library can be found on the classpath, it will get extracted
037 * and and then loaded. This way you can embed your JNI libraries into your packaged JAR files.
038 * They are looked up as resources in this order:
039 * <ol>
040 * <li> "META-INF/native/${platform}/${library}" : Store your library here if you want to embed more
041 * than one platform JNI library in the jar.
042 * <li> "META-INF/native/${library}": Store your library here if your JAR is only going to embedding one
043 * platform library.
044 * </ol>
045 * The file extraction is attempted until it succeeds in the following directories.
046 * <ol>
047 * <li> The directory pointed to by the "library.${name}.path" System property (if set)
048 * <li> a temporary directory (uses the "java.io.tmpdir" System property)
049 * </ol>
050 * </ol>
051 *
052 * where:
053 * <ul>
054 * <li>"${name}" is the name of library
055 * <li>"${version}" is the value of "library.${name}.version" System property if set.
056 * Otherwise it is set to the ImplementationVersion property of the JAR's Manifest</li>
057 * <li>"${os}" is your operating system, for example "osx", "linux", or "windows"</li>
058 * <li>"${bit-model}" is "64" if the JVM process is a 64 bit process, otherwise it's "32" if the
059 * JVM is a 32 bit process</li>
060 * <li>"${platform}" is "${os}${bit-model}", for example "linux32" or "osx64" </li>
061 * <li>"${library}": is the normal jni library name for the platform. For example "${name}.dll" on
062 * windows, "lib${name}.jnilib" on OS X, and "lib${name}.so" on linux</li>
063 * </ul>
064 *
065 * @author <a href="http://hiramchirino.com">Hiram Chirino</a>
066 */
067 public class Library {
068
069 static final String SLASH = System.getProperty("file.separator");
070
071 final private String name;
072 final private String version;
073 final private ClassLoader classLoader;
074 private boolean loaded;
075
076 public Library(String name) {
077 this(name, null, null);
078 }
079
080 public Library(String name, Class<?> clazz) {
081 this(name, version(clazz), clazz.getClassLoader());
082 }
083
084 public Library(String name, String version) {
085 this(name, version, null);
086 }
087
088 public Library(String name, String version, ClassLoader classLoader) {
089 if( name == null ) {
090 throw new IllegalArgumentException("name cannot be null");
091 }
092 this.name = name;
093 this.version = version;
094 this.classLoader= classLoader;
095 }
096
097 private static String version(Class<?> clazz) {
098 try {
099 return clazz.getPackage().getImplementationVersion();
100 } catch (Throwable e) {
101 }
102 return null;
103 }
104
105 public static String getOperatingSystem() {
106 String name = System.getProperty("os.name").toLowerCase().trim();
107 if( name.startsWith("linux") ) {
108 return "linux";
109 }
110 if( name.startsWith("mac os x") ) {
111 return "osx";
112 }
113 if( name.startsWith("win") ) {
114 return "windows";
115 }
116 return name.replaceAll("\\W+", "_");
117
118 }
119
120 public static String getPlatform() {
121 return getOperatingSystem()+getBitModel();
122 }
123
124 public static int getBitModel() {
125 String prop = System.getProperty("sun.arch.data.model");
126 if (prop == null) {
127 prop = System.getProperty("com.ibm.vm.bitmode");
128 }
129 if( prop!=null ) {
130 return Integer.parseInt(prop);
131 }
132 return -1; // we don't know..
133 }
134
135 /**
136 *
137 */
138 synchronized public void load() {
139 if( loaded ) {
140 return;
141 }
142 doLoad();
143 loaded = true;
144 }
145
146 private void doLoad() {
147 /* Perhaps a custom version is specified */
148 String version = System.getProperty("library."+name+".version");
149 if (version == null) {
150 version = this.version;
151 }
152 ArrayList<String> errors = new ArrayList<String>();
153
154 /* Try loading library from a custom library path */
155 String customPath = System.getProperty("library."+name+".path");
156 if (customPath != null) {
157 if( version!=null && load(errors, file(customPath, map(name + "-" + version))) )
158 return;
159 if( load(errors, file(customPath, map(name))) )
160 return;
161 }
162
163 /* Try loading library from java library path */
164 if( version!=null && load(errors, name + getBitModel() + "-" + version) )
165 return;
166 if( version!=null && load(errors, name + "-" + version) )
167 return;
168 if( load(errors, name ) )
169 return;
170
171
172 /* Try extracting the library from the jar */
173 if( classLoader!=null ) {
174 if( exractAndLoad(errors, version, customPath, getPlatformSpecifcResourcePath()) )
175 return;
176 if( exractAndLoad(errors, version, customPath, getOperatingSystemSpecifcResourcePath()) )
177 return;
178 // For the simpler case where only 1 platform lib is getting packed into the jar
179 if( exractAndLoad(errors, version, customPath, getResorucePath()) )
180 return;
181 }
182
183 /* Failed to find the library */
184 throw new UnsatisfiedLinkError("Could not load library. Reasons: " + errors.toString());
185 }
186
187 final public String getOperatingSystemSpecifcResourcePath() {
188 return getPlatformSpecifcResourcePath(getOperatingSystem());
189 }
190 final public String getPlatformSpecifcResourcePath() {
191 return getPlatformSpecifcResourcePath(getPlatform());
192 }
193 final public String getPlatformSpecifcResourcePath(String platform) {
194 return "META-INF/native/"+platform+"/"+map(name);
195 }
196
197 final public String getResorucePath() {
198 return "META-INF/native/"+map(name);
199 }
200
201 final public String getLibraryFileName() {
202 return map(name);
203 }
204
205
206 private boolean exractAndLoad(ArrayList<String> errors, String version, String customPath, String resourcePath) {
207 URL resource = classLoader.getResource(resourcePath);
208 if( resource !=null ) {
209
210 String libName = name + "-" + getBitModel();
211 if( version !=null) {
212 libName += "-" + version;
213 }
214
215 if( customPath!=null ) {
216 // Try to extract it to the custom path...
217 File target = file(customPath, map(libName));
218 if( extract(errors, resource, target) ) {
219 if( load(errors, target) ) {
220 return true;
221 }
222 }
223 }
224
225 // Fall back to extracting to the tmp dir
226 customPath = System.getProperty("java.io.tmpdir");
227 File target = file(customPath, map(libName));
228 if( extract(errors, resource, target) ) {
229 if( load(errors, target) ) {
230 return true;
231 }
232 }
233 }
234 return false;
235 }
236
237 private File file(String ...paths) {
238 File rc = null ;
239 for (String path : paths) {
240 if( rc == null ) {
241 rc = new File(path);
242 } else {
243 rc = new File(rc, path);
244 }
245 }
246 return rc;
247 }
248
249 private String map(String libName) {
250 /*
251 * libraries in the Macintosh use the extension .jnilib but the some
252 * VMs map to .dylib.
253 */
254 libName = System.mapLibraryName(libName);
255 String ext = ".dylib";
256 if (libName.endsWith(ext)) {
257 libName = libName.substring(0, libName.length() - ext.length()) + ".jnilib";
258 }
259 return libName;
260 }
261
262 private boolean extract(ArrayList<String> errors, URL source, File target) {
263 FileOutputStream os = null;
264 InputStream is = null;
265 boolean extracting = false;
266 try {
267 if (!target.exists() || isStale(source, target) ) {
268 is = source.openStream();
269 if (is != null) {
270 byte[] buffer = new byte[4096];
271 os = new FileOutputStream(target);
272 extracting = true;
273 int read;
274 while ((read = is.read(buffer)) != -1) {
275 os.write(buffer, 0, read);
276 }
277 os.close();
278 is.close();
279 chmod("755", target);
280 }
281 }
282 } catch (Throwable e) {
283 try {
284 if (os != null)
285 os.close();
286 } catch (IOException e1) {
287 }
288 try {
289 if (is != null)
290 is.close();
291 } catch (IOException e1) {
292 }
293 if (extracting && target.exists())
294 target.delete();
295 errors.add(e.getMessage());
296 return false;
297 }
298 return true;
299 }
300
301 private boolean isStale(URL source, File target) {
302
303 if( source.getProtocol().equals("jar") ) {
304 // unwrap the jar protocol...
305 try {
306 String parts[] = source.getFile().split(Pattern.quote("!"));
307 source = new URL(parts[0]);
308 } catch (MalformedURLException e) {
309 return false;
310 }
311 }
312
313 File sourceFile=null;
314 if( source.getProtocol().equals("file") ) {
315 sourceFile = new File(source.getFile());
316 }
317 if( sourceFile!=null && sourceFile.exists() ) {
318 if( sourceFile.lastModified() > target.lastModified() ) {
319 return true;
320 }
321 }
322 return false;
323 }
324
325 private void chmod(String permision, File path) {
326 if (getPlatform().startsWith("windows"))
327 return;
328 try {
329 Runtime.getRuntime().exec(new String[] { "chmod", permision, path.getCanonicalPath() }).waitFor();
330 } catch (Throwable e) {
331 }
332 }
333
334 private boolean load(ArrayList<String> errors, File lib) {
335 try {
336 System.load(lib.getPath());
337 return true;
338 } catch (UnsatisfiedLinkError e) {
339 errors.add(e.getMessage());
340 }
341 return false;
342 }
343
344 private boolean load(ArrayList<String> errors, String lib) {
345 try {
346 System.loadLibrary(lib);
347 return true;
348 } catch (UnsatisfiedLinkError e) {
349 errors.add(e.getMessage());
350 }
351 return false;
352 }
353
354 }