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    }