001/*
002 * IzPack - Copyright 2001-2005 Julien Ponge, All Rights Reserved.
003 * 
004 * http://www.izforge.com/izpack/
005 * http://developer.berlios.de/projects/izpack/
006 * 
007 * Copyright 2002 Elmar Grom
008 * 
009 * Licensed under the Apache License, Version 2.0 (the "License");
010 * you may not use this file except in compliance with the License.
011 * You may obtain a copy of the License at
012 * 
013 *     http://www.apache.org/licenses/LICENSE-2.0
014 *     
015 * Unless required by applicable law or agreed to in writing, software
016 * distributed under the License is distributed on an "AS IS" BASIS,
017 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
018 * See the License for the specific language governing permissions and
019 * limitations under the License.
020 */
021
022package com.izforge.izpack.util;
023
024import java.io.File;
025import java.io.FileNotFoundException;
026import java.io.FileOutputStream;
027import java.io.InputStream;
028import java.io.OutputStream;
029import java.net.URL;
030import java.security.CodeSource;
031import java.security.ProtectionDomain;
032import java.text.CharacterIterator;
033import java.text.StringCharacterIterator;
034import java.util.Vector;
035
036/*---------------------------------------------------------------------------*/
037/**
038 * This class handles loading of native libraries. There must only be one instance of
039 * <code>Librarian</code> per Java runtime, therefore this class is implemented as a 'Singleton'.
040 * <br>
041 * <br>
042 * <code>Librarian</code> is capable of loading native libraries from a variety of different
043 * source locations. However, you should place your library files in the 'native' directory. The
044 * primary reason for supporting different source locations is to facilitate testing in a
045 * development environment, without the need to actually packing the application into a *.jar file.
046 * 
047 * @version 1.0 / 1/30/02
048 * @author Elmar Grom
049 */
050/*---------------------------------------------------------------------------*/
051public class Librarian implements CleanupClient
052{
053
054    // ------------------------------------------------------------------------
055    // Constant Definitions
056    // ------------------------------------------------------------------------
057
058    /** Used to identify jar URL protocols */
059    private static final String JAR_PROTOCOL = "jar";
060
061    /** Used to identify file URL protocols */
062    private static final String FILE_PROTOCOL = "file";
063
064    /**
065     * The key used to retrieve the location of temporary files form the system properties.
066     */
067    private static final String TEMP_LOCATION_KEY = "java.io.tmpdir";
068
069    /**
070     * The extension appended to the client name when searching for it as a resource. Since the
071     * client is an object, the extension should always be '.class'
072     */
073    private static final String CLIENT_EXTENSION = ".class";
074
075    /** The default directory for native library files. */
076    private static final String NATIVE = "native";
077
078    /** The block size used for reading and writing data, 4k. */
079    private static final int BLOCK_SIZE = 4096;
080
081    // ------------------------------------------------------------------------
082    // Variable Declarations
083    // ------------------------------------------------------------------------
084
085    /**
086     * The reference to the single instance of <code>Librarian</code>. Used in static methods in
087     * place of <code>this</code>.
088     */
089    private static Librarian me = null;
090
091    /**
092     * A list that is used to track all libraries that have been loaded. This list is used to ensure
093     * that each library is loaded only once.
094     */
095    private Vector trackList = new Vector();
096
097    /**
098     * A list of references to clients that use libraries that were extracted from a *.jar file.
099     * This is needed because the clients need to be called for freeing their libraries.
100     */
101    private Vector clients = new Vector();
102
103    /**
104     * A list of library names as they appear in the temporary directory. This is needed to free
105     * each library through the client. The index of each name corresponds to the index of the
106     * respective client in the <code>clients</code> list.
107     */
108    private Vector libraryNames = new Vector();
109
110    /**
111     * A list of fully qualified library names. This is needed to delete the temporary library files
112     * after use. The index of each name corresponds to the index of the respective client in the
113     * <code>clients</code> list.
114     */
115    private Vector temporaryFileNames = new Vector();
116
117    /** The extension to use for native libraries. */
118    private String extension = "";
119
120    /** The directory that is used to hold all native libraries. */
121    private String nativeDirectory = NATIVE;
122
123    /*--------------------------------------------------------------------------*/
124    /**
125     * This class is implemented as a 'Singleton'. Therefore the constructor is private to prevent
126     * instantiation of this class. Use <code>getInstance()</code> to obtain an instance for use.
127     * <br>
128     * <br>
129     * For more information about the 'Singleton' pattern I highly recommend the book Design
130     * Patterns by Gamma, Helm, Johnson and Vlissides ISBN 0-201-63361-2.
131     */
132    /*--------------------------------------------------------------------------*/
133    private Librarian()
134    {
135        Housekeeper.getInstance().registerForCleanup(this);
136        extension = '.' + TargetFactory.getInstance().getNativeLibraryExtension();
137    }
138
139    /*--------------------------------------------------------------------------*/
140    /**
141     * Returns an instance of <code>Librarian</code> to use.
142     * 
143     * @return an instance of <code>Librarian</code>.
144     */
145    /*--------------------------------------------------------------------------*/
146    public static Librarian getInstance()
147    {
148        if (me == null)
149        {
150            me = new Librarian();
151        }
152
153        return (me);
154    }
155
156    /*--------------------------------------------------------------------------*/
157    /**
158     * Loads the requested library. If the library is already loaded, this method returns
159     * immediately, without an attempt to load the library again. <br>
160     * <br>
161     * <b>Invocation Example:</b> This assumes that the call is made from the class that links with
162     * the library. If this is not the case, <code>this</code> must be replaced by the reference
163     * of the class that links with the library. <br>
164     * <br>
165     * <code>
166     * Librarian.getInstance ().loadLibrary ("MyLibrary", this);
167     * </code> <br>
168     * <br>
169     * Loading of a native library file works as follows:<br>
170     * <ul>
171     * <li>If the library is already loaded there is nothing to do.
172     * <li>An attempt is made to load the library by its name. If there is no system path set to
173     * the library, this attempt will fail.
174     * <li>If the client is located on the local file system, an attempt is made to load the
175     * library from the local files system as well.
176     * <li>If the library is located inside a *.jar file, it is extracted to 'java.io.tmpdir' and
177     * an attempt is made to load it from there.
178     * </ul>
179     * <br>
180     * <br>
181     * Loading from the local file system and from the *.jar file is attempted for the following
182     * potential locations of the library in this order:<br>
183     * <ol>
184     * <li>The same directory where the client is located
185     * <li>The native library directory
186     * </ol>
187     * 
188     * @param name the name of the library. A file extension and path are not needed, in fact if
189     * supplied, both is stripped off. A specific extension is appended.
190     * @param client the object that made the load request
191     * 
192     * @see #setNativeDirectory
193     * 
194     * @exception Exception if all attempts to load the library fail.
195     */
196    /*--------------------------------------------------------------------------*/
197    public synchronized void loadLibrary(String name, NativeLibraryClient client) throws Exception
198    {
199        String libraryName = strip(name);
200        String tempFileName = "";
201
202        // ----------------------------------------------------
203        // Return if the library is already loaded
204        // ----------------------------------------------------
205        if (loaded(libraryName)) { return; }
206
207        // ----------------------------------------------------
208        // First try a straight load
209        // ----------------------------------------------------
210        try
211        {
212            System.loadLibrary(libraryName);
213            return;
214        }
215        catch (UnsatisfiedLinkError exception)
216        {}
217        catch (SecurityException exception)
218        {}
219
220        // ----------------------------------------------------
221        // Next, try to get the protocol for loading the resource.
222        // ----------------------------------------------------
223        Class clientClass = client.getClass();
224        String resourceName = clientClass.getName();
225        int nameStart = resourceName.lastIndexOf('.') + 1;
226        resourceName = resourceName.substring(nameStart, resourceName.length()) + CLIENT_EXTENSION;
227        URL url = clientClass.getResource(resourceName);
228        if (url == null) { throw (new Exception("can't identify load protocol for " + libraryName
229                + extension)); }
230        String protocol = url.getProtocol();
231
232        // ----------------------------------------------------
233        // If it's a local file, load it from the current location
234        // ----------------------------------------------------
235        if (protocol.equalsIgnoreCase(FILE_PROTOCOL))
236        {
237            try
238            {
239                System.load(getClientPath(name, url));
240            }
241            catch (Throwable exception)
242            {
243                try
244                {
245                    System.load(getNativePath(name, client));
246                }
247                catch (Throwable exception2)
248                {
249                    throw (new Exception("error loading library"));
250                }
251            }
252        }
253
254        // ----------------------------------------------------
255        // If it is in a *.jar file, extract it to 'java.io.tmpdir'
256        // ----------------------------------------------------
257
258        else if (protocol.equalsIgnoreCase(JAR_PROTOCOL))
259        {
260            tempFileName = getTempFileName(libraryName);
261            try
262            {
263                extractFromJar(libraryName, tempFileName, client);
264
265                clients.add(client);
266                temporaryFileNames.add(tempFileName);
267                libraryNames.add(tempFileName.substring((tempFileName
268                        .lastIndexOf(File.separatorChar) + 1), tempFileName.length()));
269
270                // --------------------------------------------------
271                // Try loading the temporary file from 'java.io.tmpdir'.
272                // --------------------------------------------------
273                System.load(tempFileName);
274            }
275            catch (Throwable exception)
276            {
277                throw (new Exception("error loading library\n" + exception.toString()));
278            }
279        }
280    }
281
282    /*--------------------------------------------------------------------------*/
283    /**
284     * Verifies if the library has already been loaded and keeps track of all libraries that are
285     * verified.
286     * 
287     * @param name name of the library to verify
288     * 
289     * @return <code>true</code> if the library had already been loaded, otherwise
290     * <code>false</code>.
291     */
292    /*--------------------------------------------------------------------------*/
293    private boolean loaded(String name)
294    {
295        if (trackList.contains(name))
296        {
297            return (true);
298        }
299        else
300        {
301            trackList.add(name);
302            return (false);
303        }
304    }
305
306    /*--------------------------------------------------------------------------*/
307    /**
308     * Strips the extension of the library name, if it has one.
309     * 
310     * @param name the name of the library
311     * 
312     * @return the name without an extension
313     */
314    /*--------------------------------------------------------------------------*/
315    private String strip(String name)
316    {
317        int extensionStart = name.lastIndexOf('.');
318        int nameStart = name.lastIndexOf('/');
319        if (nameStart < 0)
320        {
321            nameStart = name.lastIndexOf('\\');
322        }
323        nameStart++;
324
325        String shortName;
326
327        if (extensionStart > 0)
328        {
329            shortName = name.substring(nameStart, extensionStart);
330        }
331        else
332        {
333            shortName = name.substring(nameStart, name.length());
334        }
335
336        return (shortName);
337    }
338
339    /*--------------------------------------------------------------------------*/
340    /**
341     * Makes an attempt to extract the named library from the jar file and to store it on the local
342     * file system for temporary use. If the attempt is successful, the fully qualified file name of
343     * the library on the local file system is returned.
344     * 
345     * @param name the simple name of the library
346     * @param destination the fully qualified name of the destination file.
347     * @param client the class that made the load request.
348     * 
349     * @exception Exception if the library can not be extracted from the *.jar file.
350     * @exception FileNotFoundException if the *.jar file does not exist. The way things operate
351     * here, this should actually never happen.
352     */
353    /*--------------------------------------------------------------------------*/
354    private void extractFromJar(String name, String destination, NativeLibraryClient client)
355            throws Exception
356    {
357        int bytesRead = 0;
358        OutputStream output = null;
359
360        // ----------------------------------------------------
361        // open an input stream for the library file
362        // ----------------------------------------------------
363        InputStream input = openInputStream(name, client);
364
365        // ----------------------------------------------------
366        // open an output stream for the temporary file
367        // ----------------------------------------------------
368        try
369        {
370            output = new FileOutputStream(destination);
371        }
372        catch (FileNotFoundException exception)
373        {
374            input.close();
375            throw (new Exception("can't create destination file"));
376        }
377        catch (SecurityException exception)
378        {
379            input.close();
380            throw (new Exception("creation of destination file denied"));
381        }
382        catch (Throwable exception)
383        {
384            input.close();
385            throw (new Exception("unknown problem creating destination file\n"
386                    + exception.toString()));
387        }
388
389        // ----------------------------------------------------
390        // pump the data
391        // ----------------------------------------------------
392        byte[] buffer = new byte[BLOCK_SIZE];
393        try
394        {
395            do
396            {
397                bytesRead = input.read(buffer);
398                if (bytesRead > 0)
399                {
400                    output.write(buffer, 0, bytesRead);
401                }
402            }
403            while (bytesRead > 0);
404        }
405        catch (Throwable exception)
406        {
407            throw (new Exception("error writing to destination file\n" + exception.toString()));
408        }
409
410        // ----------------------------------------------------
411        // flush the data and close both streams
412        // ----------------------------------------------------
413        finally
414        {
415            input.close();
416            output.flush();
417            output.close();
418        }
419    }
420
421    /*--------------------------------------------------------------------------*/
422    /**
423     * Returns the complete path (including file name) for the native library, assuming the native
424     * library is located in the same directory from which the client was loaded.
425     * 
426     * @param name the simple name of the library
427     * @param clientURL a URL that points to the client class
428     * 
429     * @return the path to the client
430     */
431    /*--------------------------------------------------------------------------*/
432    private String getClientPath(String name, URL clientURL)
433    {
434        String path = clientURL.getFile();
435
436        int nameStart = path.lastIndexOf('/') + 1;
437
438        path = path.substring(0, nameStart);
439        path = path + name + extension;
440        path = path.replace('/', File.separatorChar);
441        // Revise the URI-path to a file path; needed in uninstaller because it
442        // writes the jar contents into a sandbox; may be with blanks in the
443        // path.
444        path = revisePath(path);
445
446        return (path);
447    }
448
449    /*--------------------------------------------------------------------------*/
450    /**
451     * Returns the complete path (including file name) for the native library, assuming the native
452     * library is located in a directory where native libraries are ordinarily expected.
453     * 
454     * @param name the simple name of the library
455     * @param client the class that made the load request.
456     * 
457     * @return the path to the location of the native libraries.
458     */
459    /*--------------------------------------------------------------------------*/
460    private String getNativePath(String name, NativeLibraryClient client)
461    {
462        ProtectionDomain domain = client.getClass().getProtectionDomain();
463        CodeSource codeSource = domain.getCodeSource();
464        URL url = codeSource.getLocation();
465        String path = url.getPath();
466        path = path + nativeDirectory + '/' + name + extension;
467        path = path.replace('/', File.separatorChar);
468        // Revise the URI-path to a file path; needed in uninstaller because it
469        // writes the jar contents into a sandbox; may be with blanks in the
470        // path.
471        path = revisePath(path);
472
473        return (path);
474    }
475
476    /*--------------------------------------------------------------------------*/
477    /**
478     * Revises the given path to a file compatible path. In fact this method replaces URI-like
479     * entries with it chars (e.g. %20 with a space).
480     * 
481     * @param in path to be revised
482     * @return revised path
483     */
484    /*--------------------------------------------------------------------------*/
485    private String revisePath(String in)
486    {
487        // This was "stolen" from com.izforge.izpack.util.SelfModifier
488
489        StringBuffer sb = new StringBuffer();
490        CharacterIterator iter = new StringCharacterIterator(in);
491        for (char c = iter.first(); c != CharacterIterator.DONE; c = iter.next())
492        {
493            if (c == '%')
494            {
495                char c1 = iter.next();
496                if (c1 != CharacterIterator.DONE)
497                {
498                    int i1 = Character.digit(c1, 16);
499                    char c2 = iter.next();
500                    if (c2 != CharacterIterator.DONE)
501                    {
502                        int i2 = Character.digit(c2, 16);
503                        sb.append((char) ((i1 << 4) + i2));
504                    }
505                }
506            }
507            else
508            {
509                sb.append(c);
510            }
511        }
512        String path = sb.toString();
513        return path;
514    }
515
516    /*--------------------------------------------------------------------------*/
517    /**
518     * Opens an <code>InputStream</code> to the native library.
519     * 
520     * @param name the simple name of the library
521     * @param client the class that made the load request.
522     * 
523     * @return an <code>InputStream</code> from which the library can be read.
524     * 
525     * @exception Exception if the library can not be located.
526     */
527    /*--------------------------------------------------------------------------*/
528    private InputStream openInputStream(String name, NativeLibraryClient client) throws Exception
529    {
530        Class clientClass = client.getClass();
531        // ----------------------------------------------------
532        // try to open an input stream, assuming the library
533        // is located with the client
534        // ----------------------------------------------------
535        InputStream input = clientClass.getResourceAsStream(name + extension);
536
537        // ----------------------------------------------------
538        // if this is not successful, try to load from the
539        // location where all native libraries are supposed
540        // to be located.
541        // ----------------------------------------------------
542        if (input == null)
543        {
544            input = clientClass.getResourceAsStream('/' + nativeDirectory + '/' + name + extension);
545        }
546
547        // ----------------------------------------------------
548        // if this fails as well, throw an exception
549        // ----------------------------------------------------
550        if (input == null)
551        {
552            throw (new Exception("can't locate library"));
553        }
554        else
555        {
556            return (input);
557        }
558    }
559
560    /*--------------------------------------------------------------------------*/
561    /**
562     * Builds a temporary file name for the native library.
563     * 
564     * @param name the file name of the library
565     * 
566     * @return a fully qualified file name that can be used to store the file on the local file
567     * system.
568     */
569    /*--------------------------------------------------------------------------*/
570    /*
571     * $ @design
572     * 
573     * Avoid overwriting any existing files on the user's system. If by some remote chance a file by
574     * the same name should exist on the user's system, modify the temporary file name until a
575     * version is found that is unique on the system and thus won't interfere.
576     * --------------------------------------------------------------------------
577     */
578    private String getTempFileName(String name)
579    {
580        StringBuffer fileName = new StringBuffer();
581        String path = System.getProperty(TEMP_LOCATION_KEY);
582        if (path.charAt(path.length() - 1) == File.separatorChar)
583        {
584            path = path.substring(0, (path.length() - 1));
585        }
586        String modifier = "";
587        int counter = 0;
588        File file = null;
589
590        do
591        {
592            fileName.delete(0, fileName.length());
593            fileName.append(path);
594            fileName.append(File.separatorChar);
595            fileName.append(name);
596            fileName.append(modifier);
597            fileName.append(extension);
598
599            modifier = Integer.toString(counter);
600            counter++;
601
602            file = new File(fileName.toString());
603        }
604        while (file.exists());
605
606        return (fileName.toString());
607    }
608
609    /*--------------------------------------------------------------------------*/
610    /**
611     * Sets the directory where <code>Librarian</code> will search for native files. Directories
612     * are denoted relative to the root, where the root is the same location where the top level
613     * Java package directory is located (usually called <code>com</code>). The default directory
614     * is <code>native</code>.
615     * 
616     * @param directory the directory where native files are located.
617     */
618    /*--------------------------------------------------------------------------*/
619    public void setNativeDirectory(String directory)
620    {
621        if (directory == null)
622        {
623            nativeDirectory = "";
624        }
625        else
626        {
627            nativeDirectory = directory;
628        }
629    }
630
631    /*--------------------------------------------------------------------------*/
632    /**
633     * This method attempts to remove all native libraries that have been temporarily created from
634     * the system.
635     */
636    /*--------------------------------------------------------------------------*/
637    public void cleanUp()
638    {
639        for (int i = 0; i < clients.size(); i++)
640        {
641            // --------------------------------------------------
642            // free the library
643            // --------------------------------------------------
644            NativeLibraryClient client = (NativeLibraryClient) clients.elementAt(i);
645            String libraryName = (String) libraryNames.elementAt(i);
646
647            FreeThread free = new FreeThread(libraryName, client);
648            free.start();
649            try
650            {
651                // give the thread some time to get the library
652                // freed before attempting to delete it.
653                free.join(50);
654            }
655            catch (Throwable exception)
656            {} // nothing I can do
657
658            // --------------------------------------------------
659            // delete the library
660            // --------------------------------------------------
661            String tempFileName = (String) temporaryFileNames.elementAt(i);
662            try
663            {
664                File file = new File(tempFileName);
665                file.delete();
666            }
667            catch (Throwable exception)
668            {} // nothing I can do
669        }
670    }
671}
672/*---------------------------------------------------------------------------*/