1 /* 
2  * Copyright 2005 Paul Hinds
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16package org.tp23.antinstaller.selfextract;
17
18import java.awt.HeadlessException;
19import java.io.BufferedOutputStream;
20import java.io.File;
21import java.io.FileFilter;
22import java.io.FileInputStream;
23import java.io.FileNotFoundException;
24import java.io.FileOutputStream;
25import java.io.IOException;
26import java.net.URL;
27import java.util.ArrayList;
28import java.util.jar.JarEntry;
29import java.util.jar.JarFile;
30import java.util.jar.JarInputStream;
31
32import javax.swing.JOptionPane;
33import javax.swing.UIManager;
34
35import org.tp23.antinstaller.InstallException;
36import org.tp23.antinstaller.renderer.swing.plaf.LookAndFeelFactory;
37import org.tp23.antinstaller.runtime.ExecInstall;
38import org.tp23.antinstaller.runtime.exe.FilterChain;
39import org.tp23.antinstaller.runtime.exe.FilterFactory;
40
41/**
42 *
43 * <p>Finds a file reference to the Jar that loads this class and then extracts that Jar
44 * to a temporary directory </p>
45 * <p> </p>
46 * @author Paul Hinds
47 * @version $Id: SelfExtractor.java,v 1.10 2007/01/28 08:44:40 teknopaul Exp $
48 */
49public class SelfExtractor {
50
51    public static final String CONFIG_RESOURCE = "/org/tp23/antinstaller/runtime/exe/selfextractor.fconfig";
52    
53    private File extractDir;
54    private File archiveFile;
55    private boolean overwrite = true;
56
57    private static int DEFAULT_BUFFER_SIZE = 1024;
58    private int BUFFER_SIZE = DEFAULT_BUFFER_SIZE;
59    private static boolean graphicsEnv = false;
60    private static String lookAndFeel = null;
61
62    /**
63     * returns the Jar that the reference object was loaded from.  If it was not
64     * loaded from a jar this methods behaviour is undefined
65     * @TODO define what happens
66     * @param reference
67     * @return A java.io.File reference to the Jar or null
68     */
69    public static File getEnclosingJar(Object reference) {
70        String thisClass = "/" + reference.getClass().getName().replace('.','/') + ".class";
71        URL jarUrl = reference.getClass().getResource(thisClass);
72        String stringForm = jarUrl.toString();
73        //String fileForm = jarUrl.getFile();
74
75        File file = null;
76        int endIdx = stringForm.indexOf("!/");
77        if(endIdx != -1){
78            String unescaped = null;
79            String fileNamePart = stringForm.substring("jar:file:".length(), endIdx);
80            file = new File(fileNamePart);
81            if ( ! file.exists()) {
82                // try to unescape encase the URL Handler has escaped the " " to %20
83                unescaped = unescape(fileNamePart);
84                file = new File(unescaped);
85            }
86            return file;
87        }
88        return null;
89        //throw new RuntimeException("Failed expanding Jar.");
90    }
91
92    /**
93     * Constructor for the SelfExtractor object.  Directly after constructing
94     * an instance the init() method should be called unless subclassing
95     */
96    public SelfExtractor() {
97    }
98
99    /**
00     * This has been moved from the default constructor to facilitate subclassing
01     * @return true if the lookAndFeel worked
02     */
03    public void init(){
04        System.out.println("Loading self extractor...");
05        archiveFile = getEnclosingJar(this);
06        makeTempDir();
07        try {
08            JarFile thisJar = new JarFile(archiveFile);
09            lookAndFeel = thisJar.getManifest().getMainAttributes().getValue("Look-And-Feel");
10            lookAndFeel = LookAndFeelFactory.getLafFromToken(lookAndFeel);
11            if(lookAndFeel != null) {
12                UIManager.setLookAndFeel(lookAndFeel);
13            }
14        }
15        catch (Throwable ex) {
16            // not concerned about Look and Feel
17        }
18    }
19    
20    /**
21     * Creates a new empty temporary directory for the file extraction
22     * @return
23     */
24    protected File makeTempDir(){
25        String tempDir = System.getProperty("java.io.tmpdir");
26        extractDir = new File(tempDir, "antinstall");
27        int idx = 0;
28        while (extractDir.exists()) {
29            extractDir = new File(tempDir, "antinstall" + (idx++));
30        }
31        extractDir.mkdirs();
32        extractDir.deleteOnExit();
33        return extractDir;
34    }
35
36    /**
37     *  Constructor for the SelfExtractor object that sets the buffersize in use.
38     *  The write buffer is the same size as the write buffer size because the read buffer reads
39     *  decompressed bytes
40     *  @param newBufferSize the size of the read buffer
41     */
42    public SelfExtractor(int newBufferSize) {
43        BUFFER_SIZE = newBufferSize;
44        archiveFile = getEnclosingJar(this);
45    }
46
47    /**
48     *  Sets the Directory into which the file will be extracted
49     *
50     *@param  newExtractDir  The new extract directory
51     */
52    public void setExtractDir(File newExtractDir) {
53        extractDir = newExtractDir;
54    }
55
56    /**
57     *  changes the archive to be extracted
58     *@param  newArchiveFile  The new archiveFile value
59     */
60    public void setArchiveFile(File newArchiveFile) {
61        archiveFile = newArchiveFile;
62    }
63
64    /**
65     *  Gets the Directory into which the files will be extracted that
66     *  is currently set in the ZipExtractor object
67     *@return    The extract directory value
68     */
69    public File getExtractDir() {
70        return extractDir;
71    }
72
73    /**
74     *  Gets the set in the ZipExtractor
75     *@return    The archiveFile value
76     */
77    public boolean isOverwrite() {
78        return overwrite;
79    }
80
81    /**
82     *  Gets the Directory into which the files will be extracted that
83     *  is currently set in the ZipExtractor object
84     *@return    The extract directory value
85     */
86    public void setOverwrite(boolean overwrite) {
87        this.overwrite = overwrite;
88    }
89
90    /**
91     *  Gets the set in the ZipExtractor
92     *@return    The archiveFile value
93     */
94    public File getArchiveFile() {
95        return archiveFile;
96    }
97
98    /**
99     *  Opens up the zip and gets a list of the files in it.  If the zip file
00     *  or the temp file have not been set NullPointerExceptions will get thrown
01     *@param  vebose  if true Prints out a list of the zips
02     *      contents on to the command line
03     *@return  an ArrayList of String objects that will
04     *         be as per the path in the zip
05     *@exception  FileNotFoundException  Description of Exception
06     *@exception  IOException            Description of Exception
07     */
08    public ArrayList getList(boolean vebose) throws FileNotFoundException, IOException {
09        JarInputStream zis = new JarInputStream(new FileInputStream(archiveFile));
10        JarEntry entry = null;
11        ArrayList result = new ArrayList();
12        while ( (entry = zis.getNextJarEntry()) != null) {
13            if (vebose) {
14                System.out.println(entry.getName());
15            }
16            result.add(entry.getName());
17        }
18        return result;
19    }
20
21    /**
22     * @return the number of files in the jar
23     * @throws FileNotFoundException
24     * @throws IOException
25     */
26    public int getFileCount() throws FileNotFoundException, IOException {
27        JarInputStream zis = new JarInputStream(new FileInputStream(archiveFile));
28        int count = 0;
29        while (  zis.getNextJarEntry() != null) {
30            count++;
31        }
32        return count;
33    }
34
35    /**
36     *  Opens up the zip and extracts the files to the temp dir.
37     *
38     *@param  vebose  if true Prints out a list of the zips contents on to System.out
39     *@return an ArrayList of java.io.File objects that
40     *      will be as per the path in the zip with the root being the temp dir
41     *@exception  FileNotFoundException
42     *@exception  IOException
43     */
44    public ArrayList extract(boolean vebose, boolean isX) throws FileNotFoundException, IOException {
45        int fileCount = getFileCount();
46        ProgressIndicator indicator = null;
47        if(isX){
48            try {
49                    indicator = new ProgressIndicator(fileCount);
50                    indicator.show();
51            }
52            catch ( Exception exc ) {
53                /*
54                 * Chances are, there are problems with the graphics environment
55                 * so trying falling back to text mode
56                 */
57                graphicsEnv = false;
58                isX = false;
59            }
60
61        }
62        JarInputStream zis = new JarInputStream(new FileInputStream(archiveFile));
63        JarEntry entry = null;
64        ArrayList result = new ArrayList();
65        while ( (entry = zis.getNextJarEntry()) != null) {
66            if (vebose) {
67                System.out.println("Extracting:" + entry.getName());
68            }
69            result.add(extract(zis, entry));
70            if (isX) {
71                indicator.tick();
72            }
73        }
74        if (isX) {
75            indicator.hide();
76        }
77        zis.close();
78        return result;
79    }
80
81
82
83    /**
84     *  Extract a single file from the stream. N.B. the stream must be in the correct
85     *  position for this to work
86     *@param  zis                        ZipInputStream open and ready
87     *@param  entry                      A valid entry read from the stream
88     *@return                            The inflated file generated in the temp dir
89     *@exception  FileNotFoundException
90     *@exception  IOException
91     */
92    private File extract(JarInputStream zis, JarEntry entry) throws FileNotFoundException, IOException {
93        createPath(entry.getName());
94        File fileToUse = new File(extractDir, entry.getName());
95        if (fileToUse.exists()) {
96            if (!overwrite) {
97                return fileToUse;
98            }
99        }
00        else {
01            fileToUse.createNewFile();
02        }
03        if (fileToUse.isDirectory()) {
04            return fileToUse;
05        }
06
07        BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(fileToUse), BUFFER_SIZE);
08        byte[] bytes = new byte[BUFFER_SIZE];
09        int len = 0;
10        while ( (len = zis.read(bytes)) >= 0) {
11            bos.write(bytes, 0, len);
12        }
13        bos.close();
14        zis.closeEntry();
15        return fileToUse;
16    }
17
18    /**
19     *  This adds all the necessary directories in the root of the zip path to the
20     *  temp dir.
21     *@param  entryName        The string name in the Zip file (virtual path)
22     *@exception  IOException  if the directories can not be made
23     */
24    private void createPath(String entryName) throws IOException {
25        int slashIdx = entryName.lastIndexOf('/');
26        if (slashIdx >= 0) {
27            // there is path info
28            String firstPath = entryName.substring(0, slashIdx);
29            File dir = new File(extractDir, firstPath);
30            if (!dir.exists()) {
31                dir.mkdirs();
32            }
33        }
34    }
35
36    /**
37     * Run method to use from the command line. This is fired via an entry in the 
38     * MANIFEST.MF in the Jar
39     *@param  args  The command line arguments
40     */
41    public static void main(String[] args) {
42        testX();
43        // FIXME move after parseArgs() and set graphicsEnv if text selected
44        // will need to test SelfExtractor and comment parseArgs() to ensure
45        // no side effects in the future.
46        SelfExtractor extractor = null;
47        try {
48            boolean verbose = false;
49            extractor = new SelfExtractor();
50            extractor.init();
51            extractor.extract(verbose, graphicsEnv);
52        }
53        catch (Exception e) {
54            e.printStackTrace();
55            String tempDir = "unknown";
56            if(extractor != null){
57                tempDir = extractor.getExtractDir().getAbsolutePath();
58            }
59            String warning = "Could not extract Jar file to directory:" + tempDir;
60            printXorTextWarning(warning);
61        }
62        
63        try {
64            FilterChain chain = FilterFactory.factory(CONFIG_RESOURCE);     
65            ExecInstall installExec = new ExecInstall(chain);
66            installExec.parseArgs(args, false);
67            installExec.setInstallRoot(extractor.getExtractDir());
68            // removes files on exit
69            installExec.setTempRoot(extractor.getExtractDir());
70
71            installExec.exec();
72        }
73        catch (InstallException e1) {
74            System.out.println("Cant load filter chain:/org/tp23/antinstaller/runtime/exe/selfextractor.fconfig");
75            e1.printStackTrace();
76        }
77    }
78
79    /**
80     * This tests for the existence of a  graphics environment and sets an
81     * internal flag so the test does not have to be repeated, it may be expensive.
82     * Prior to running this method the isGraphicsEnv() method will be invalid.
83     */
84    protected static void testX(){
85        try {
86            java.awt.GraphicsEnvironment.getLocalGraphicsEnvironment();
87            try {
88                boolean headless = java.awt.GraphicsEnvironment.isHeadless();
89                if(headless) {
90                    graphicsEnv = false;
91                    return;
92                }
93            } catch (Throwable e) {
94                // JDK 1.3 does not have the isHeadless() method but may still work in other situations
95            }
96            graphicsEnv = true;
97        }
98        catch (Throwable e) {
99            // thus graphicsEnv stays false;
00        }
01    }
02    
03    /**
04     * @see #testX()
05     * @return true if an X or windows environment is available
06     */
07    protected boolean isGraphicsEnv(){
08        return graphicsEnv;
09    }
10
11    protected static void printXorTextWarning(String warning){
12        if(graphicsEnv){
13            try {
14                JOptionPane.showMessageDialog(null, warning);
15            }
16            catch( HeadlessException headlessExc ) {
17                graphicsEnv = false;
18                System.out.println(warning);
19            }
20        }
21        else {
22            System.out.println(warning);
23        }
24    }
25
26    public static int deleteRecursive(File directory) {
27        int count = 0;
28        File[] files = directory.listFiles(new FileFilter() {
29            public boolean accept(File file) {
30                return!file.isDirectory();
31            }
32        });
33        for (int i = 0; i < files.length; i++) {
34            files[i].delete();
35            count++;
36        }
37        File[] dirs = directory.listFiles(new FileFilter() {
38            public boolean accept(File file) {
39                return file.isDirectory();
40            }
41        });
42        for (int i = 0; i < dirs.length; i++) {
43            count += deleteRecursive(dirs[i]);
44        }
45        directory.delete();
46        return count;
47    }
48
49    /**
50     * UN-URL encode string
51     * TODO should this not support UNICODE escapes
52     */
53    private static String unescape(final String s) {
54        StringBuffer sb = new StringBuffer(s.length());
55
56        for (int i = 0; i < s.length(); i++) {
57            char c = s.charAt(i);
58            switch (c) {
59                case '%': {
60                    try {
61                        sb.append( (char) Integer.parseInt(s.substring(i + 1, i + 3), 16));
62                        i += 2;
63                        break;
64                    }
65                    catch (NumberFormatException nfe) {
66                        throw new IllegalArgumentException();
67                    }
68                    catch (StringIndexOutOfBoundsException siob) {
69                        String end = s.substring(i);
70                        sb.append(end);
71                        if (end.length() == 2) i++;
72                    }
73                    break;
74                }
75                default: {
76                    sb.append(c);
77                    break;
78                }
79            }
80        }
81        return sb.toString();
82    }
83
84}
85