001    /*******************************************************************************
002     * Copyright (c) 2009 Progress Software, Inc.
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 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 String getPlatform() {
121            return getOperatingSystem()+getBitModel();
122        }
123        
124        protected 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 + "-" + version) ) 
165                return;        
166            if( load(errors, name ) )
167                return;
168            
169            
170            /* Try extracting the library from the jar */
171            if( classLoader!=null ) {
172                if( exractAndLoad(errors, version, customPath, getPlatformSpecifcResourcePath()) ) 
173                    return;
174                if( exractAndLoad(errors, version, customPath, getOperatingSystemSpecifcResourcePath()) ) 
175                    return;
176                // For the simpler case where only 1 platform lib is getting packed into the jar
177                if( exractAndLoad(errors, version, customPath, getResorucePath()) )
178                    return;
179            }
180    
181            /* Failed to find the library */
182            throw new UnsatisfiedLinkError("Could not load library. Reasons: " + errors.toString()); 
183        }
184    
185        final public String getOperatingSystemSpecifcResourcePath() {
186            return getPlatformSpecifcResourcePath(getOperatingSystem());
187        }
188        final public String getPlatformSpecifcResourcePath() {
189            return getPlatformSpecifcResourcePath(getPlatform());
190        }
191        final public String getPlatformSpecifcResourcePath(String platform) {
192            return "META-INF/native/"+platform+"/"+map(name);
193        }
194    
195        final public String getResorucePath() {
196            return "META-INF/native/"+map(name);
197        }
198    
199        final public String getLibraryFileName() {
200            return map(name);
201        }
202    
203        
204        private boolean exractAndLoad(ArrayList<String> errors, String version, String customPath, String resourcePath) {
205            URL resource = classLoader.getResource(resourcePath);
206            if( resource !=null ) {
207                
208                String libName = name;
209                if( version !=null) {
210                    libName += "-" + version;
211                }
212                
213                if( customPath!=null ) {
214                    // Try to extract it to the custom path...
215                    File target = file(customPath, map(libName));
216                    if( extract(errors, resource, target) ) {
217                        if( load(errors, target) ) {
218                            return true;
219                        }
220                    }
221                }
222                
223                // Fall back to extracting to the tmp dir
224                customPath = System.getProperty("java.io.tmpdir");
225                File target = file(customPath, map(libName));
226                if( extract(errors, resource, target) ) {
227                    if( load(errors, target) ) {
228                        return true;
229                    }
230                }
231            }
232            return false;
233        }
234    
235        private File file(String ...paths) {
236            File rc = null ;
237            for (String path : paths) {
238                if( rc == null ) {
239                    rc = new File(path);
240                } else {
241                    rc = new File(rc, path);
242                }
243            }
244            return rc;
245        }
246        
247        private String map(String libName) {
248            /*
249             * libraries in the Macintosh use the extension .jnilib but the some
250             * VMs map to .dylib.
251             */
252            libName = System.mapLibraryName(libName);
253            String ext = ".dylib"; 
254            if (libName.endsWith(ext)) {
255                libName = libName.substring(0, libName.length() - ext.length()) + ".jnilib"; 
256            }
257            return libName;
258        }
259    
260        private boolean extract(ArrayList<String> errors, URL source, File target) {
261            FileOutputStream os = null;
262            InputStream is = null;
263            boolean extracting = false;
264            try {
265                if (!target.exists() || isStale(source, target) ) {
266                    is = source.openStream();
267                    if (is != null) {
268                        byte[] buffer = new byte[4096];
269                        os = new FileOutputStream(target);
270                        extracting = true;
271                        int read;
272                        while ((read = is.read(buffer)) != -1) {
273                            os.write(buffer, 0, read);
274                        }
275                        os.close();
276                        is.close();
277                        chmod("755", target);
278                    }
279                }
280            } catch (Throwable e) {
281                try {
282                    if (os != null)
283                        os.close();
284                } catch (IOException e1) {
285                }
286                try {
287                    if (is != null)
288                        is.close();
289                } catch (IOException e1) {
290                }
291                if (extracting && target.exists())
292                    target.delete();
293                errors.add(e.getMessage());
294                return false;
295            }
296            return true;
297        }
298    
299        private boolean isStale(URL source, File target) {
300            
301            if( source.getProtocol().equals("jar") ) {
302                // unwrap the jar protocol...
303                try {
304                    String parts[] = source.getFile().split(Pattern.quote("!"));
305                    source = new URL(parts[0]);
306                } catch (MalformedURLException e) {
307                    return false;
308                }
309            }
310            
311            File sourceFile=null;
312            if( source.getProtocol().equals("file") ) {
313                sourceFile = new File(source.getFile());
314            }
315            if( sourceFile!=null && sourceFile.exists() ) {
316                if( sourceFile.lastModified() > target.lastModified() ) {
317                    return true;
318                }
319            }
320            return false;
321        }
322    
323        private void chmod(String permision, File path) {
324            if (getPlatform().startsWith("windows"))
325                return; 
326            try {
327                Runtime.getRuntime().exec(new String[] { "chmod", permision, path.getCanonicalPath() }).waitFor(); 
328            } catch (Throwable e) {
329            }
330        }
331    
332        private boolean load(ArrayList<String> errors, File lib) {
333            try {
334                System.load(lib.getPath());
335                return true;
336            } catch (UnsatisfiedLinkError e) {
337                errors.add(e.getMessage());
338            }
339            return false;
340        }
341        
342        private boolean load(ArrayList<String> errors, String lib) {
343            try {
344                System.loadLibrary(lib);
345                return true;
346            } catch (UnsatisfiedLinkError e) {
347                errors.add(e.getMessage());
348            }
349            return false;
350        }
351    
352    }