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