001: /*
002: * Copyright 2005 Paul Hinds
003: *
004: * Licensed under the Apache License, Version 2.0 (the "License");
005: * you may not use this file except in compliance with the License.
006: * You may obtain a copy of the License at
007: *
008: * http://www.apache.org/licenses/LICENSE-2.0
009: *
010: * Unless required by applicable law or agreed to in writing, software
011: * distributed under the License is distributed on an "AS IS" BASIS,
012: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013: * See the License for the specific language governing permissions and
014: * limitations under the License.
015: */
016: package org.tp23.antinstaller.selfextract;
017:
018: import java.awt.HeadlessException;
019: import java.io.BufferedOutputStream;
020: import java.io.File;
021: import java.io.FileFilter;
022: import java.io.FileInputStream;
023: import java.io.FileNotFoundException;
024: import java.io.FileOutputStream;
025: import java.io.IOException;
026: import java.net.URL;
027: import java.util.ArrayList;
028: import java.util.jar.JarEntry;
029: import java.util.jar.JarFile;
030: import java.util.jar.JarInputStream;
031:
032: import javax.swing.JOptionPane;
033: import javax.swing.UIManager;
034:
035: import org.tp23.antinstaller.InstallException;
036: import org.tp23.antinstaller.renderer.swing.plaf.LookAndFeelFactory;
037: import org.tp23.antinstaller.runtime.ExecInstall;
038: import org.tp23.antinstaller.runtime.exe.FilterChain;
039: import org.tp23.antinstaller.runtime.exe.FilterFactory;
040:
041: /**
042: *
043: * <p>Finds a file reference to the Jar that loads this class and then extracts that Jar
044: * to a temporary directory </p>
045: * <p> </p>
046: * @author Paul Hinds
047: * @version $Id: SelfExtractor.java,v 1.10 2007/01/28 08:44:40 teknopaul Exp $
048: */
049: public class SelfExtractor {
050:
051: public static final String CONFIG_RESOURCE = "/org/tp23/antinstaller/runtime/exe/selfextractor.fconfig";
052:
053: private File extractDir;
054: private File archiveFile;
055: private boolean overwrite = true;
056:
057: private static int DEFAULT_BUFFER_SIZE = 1024;
058: private int BUFFER_SIZE = DEFAULT_BUFFER_SIZE;
059: private static boolean graphicsEnv = false;
060: private static String lookAndFeel = null;
061:
062: /**
063: * returns the Jar that the reference object was loaded from. If it was not
064: * loaded from a jar this methods behaviour is undefined
065: * @TODO define what happens
066: * @param reference
067: * @return A java.io.File reference to the Jar or null
068: */
069: public static File getEnclosingJar(Object reference) {
070: String this Class = "/"
071: + reference.getClass().getName().replace('.', '/')
072: + ".class";
073: URL jarUrl = reference.getClass().getResource(this Class);
074: String stringForm = jarUrl.toString();
075: //String fileForm = jarUrl.getFile();
076:
077: File file = null;
078: int endIdx = stringForm.indexOf("!/");
079: if (endIdx != -1) {
080: String unescaped = null;
081: String fileNamePart = stringForm.substring("jar:file:"
082: .length(), endIdx);
083: file = new File(fileNamePart);
084: if (!file.exists()) {
085: // try to unescape encase the URL Handler has escaped the " " to %20
086: unescaped = unescape(fileNamePart);
087: file = new File(unescaped);
088: }
089: return file;
090: }
091: return null;
092: //throw new RuntimeException("Failed expanding Jar.");
093: }
094:
095: /**
096: * Constructor for the SelfExtractor object. Directly after constructing
097: * an instance the init() method should be called unless subclassing
098: */
099: public SelfExtractor() {
100: }
101:
102: /**
103: * This has been moved from the default constructor to facilitate subclassing
104: * @return true if the lookAndFeel worked
105: */
106: public void init() {
107: System.out.println("Loading self extractor...");
108: archiveFile = getEnclosingJar(this );
109: makeTempDir();
110: try {
111: JarFile this Jar = new JarFile(archiveFile);
112: lookAndFeel = this Jar.getManifest().getMainAttributes()
113: .getValue("Look-And-Feel");
114: lookAndFeel = LookAndFeelFactory
115: .getLafFromToken(lookAndFeel);
116: if (lookAndFeel != null) {
117: UIManager.setLookAndFeel(lookAndFeel);
118: }
119: } catch (Throwable ex) {
120: // not concerned about Look and Feel
121: }
122: }
123:
124: /**
125: * Creates a new empty temporary directory for the file extraction
126: * @return
127: */
128: protected File makeTempDir() {
129: String tempDir = System.getProperty("java.io.tmpdir");
130: extractDir = new File(tempDir, "antinstall");
131: int idx = 0;
132: while (extractDir.exists()) {
133: extractDir = new File(tempDir, "antinstall" + (idx++));
134: }
135: extractDir.mkdirs();
136: extractDir.deleteOnExit();
137: return extractDir;
138: }
139:
140: /**
141: * Constructor for the SelfExtractor object that sets the buffersize in use.
142: * The write buffer is the same size as the write buffer size because the read buffer reads
143: * decompressed bytes
144: * @param newBufferSize the size of the read buffer
145: */
146: public SelfExtractor(int newBufferSize) {
147: BUFFER_SIZE = newBufferSize;
148: archiveFile = getEnclosingJar(this );
149: }
150:
151: /**
152: * Sets the Directory into which the file will be extracted
153: *
154: *@param newExtractDir The new extract directory
155: */
156: public void setExtractDir(File newExtractDir) {
157: extractDir = newExtractDir;
158: }
159:
160: /**
161: * changes the archive to be extracted
162: *@param newArchiveFile The new archiveFile value
163: */
164: public void setArchiveFile(File newArchiveFile) {
165: archiveFile = newArchiveFile;
166: }
167:
168: /**
169: * Gets the Directory into which the files will be extracted that
170: * is currently set in the ZipExtractor object
171: *@return The extract directory value
172: */
173: public File getExtractDir() {
174: return extractDir;
175: }
176:
177: /**
178: * Gets the set in the ZipExtractor
179: *@return The archiveFile value
180: */
181: public boolean isOverwrite() {
182: return overwrite;
183: }
184:
185: /**
186: * Gets the Directory into which the files will be extracted that
187: * is currently set in the ZipExtractor object
188: *@return The extract directory value
189: */
190: public void setOverwrite(boolean overwrite) {
191: this .overwrite = overwrite;
192: }
193:
194: /**
195: * Gets the set in the ZipExtractor
196: *@return The archiveFile value
197: */
198: public File getArchiveFile() {
199: return archiveFile;
200: }
201:
202: /**
203: * Opens up the zip and gets a list of the files in it. If the zip file
204: * or the temp file have not been set NullPointerExceptions will get thrown
205: *@param vebose if true Prints out a list of the zips
206: * contents on to the command line
207: *@return an ArrayList of String objects that will
208: * be as per the path in the zip
209: *@exception FileNotFoundException Description of Exception
210: *@exception IOException Description of Exception
211: */
212: public ArrayList getList(boolean vebose)
213: throws FileNotFoundException, IOException {
214: JarInputStream zis = new JarInputStream(new FileInputStream(
215: archiveFile));
216: JarEntry entry = null;
217: ArrayList result = new ArrayList();
218: while ((entry = zis.getNextJarEntry()) != null) {
219: if (vebose) {
220: System.out.println(entry.getName());
221: }
222: result.add(entry.getName());
223: }
224: return result;
225: }
226:
227: /**
228: * @return the number of files in the jar
229: * @throws FileNotFoundException
230: * @throws IOException
231: */
232: public int getFileCount() throws FileNotFoundException, IOException {
233: JarInputStream zis = new JarInputStream(new FileInputStream(
234: archiveFile));
235: int count = 0;
236: while (zis.getNextJarEntry() != null) {
237: count++;
238: }
239: return count;
240: }
241:
242: /**
243: * Opens up the zip and extracts the files to the temp dir.
244: *
245: *@param vebose if true Prints out a list of the zips contents on to System.out
246: *@return an ArrayList of java.io.File objects that
247: * will be as per the path in the zip with the root being the temp dir
248: *@exception FileNotFoundException
249: *@exception IOException
250: */
251: public ArrayList extract(boolean vebose, boolean isX)
252: throws FileNotFoundException, IOException {
253: int fileCount = getFileCount();
254: ProgressIndicator indicator = null;
255: if (isX) {
256: try {
257: indicator = new ProgressIndicator(fileCount);
258: indicator.show();
259: } catch (Exception exc) {
260: /*
261: * Chances are, there are problems with the graphics environment
262: * so trying falling back to text mode
263: */
264: graphicsEnv = false;
265: isX = false;
266: }
267:
268: }
269: JarInputStream zis = new JarInputStream(new FileInputStream(
270: archiveFile));
271: JarEntry entry = null;
272: ArrayList result = new ArrayList();
273: while ((entry = zis.getNextJarEntry()) != null) {
274: if (vebose) {
275: System.out.println("Extracting:" + entry.getName());
276: }
277: result.add(extract(zis, entry));
278: if (isX) {
279: indicator.tick();
280: }
281: }
282: if (isX) {
283: indicator.hide();
284: }
285: zis.close();
286: return result;
287: }
288:
289: /**
290: * Extract a single file from the stream. N.B. the stream must be in the correct
291: * position for this to work
292: *@param zis ZipInputStream open and ready
293: *@param entry A valid entry read from the stream
294: *@return The inflated file generated in the temp dir
295: *@exception FileNotFoundException
296: *@exception IOException
297: */
298: private File extract(JarInputStream zis, JarEntry entry)
299: throws FileNotFoundException, IOException {
300: createPath(entry.getName());
301: File fileToUse = new File(extractDir, entry.getName());
302: if (fileToUse.exists()) {
303: if (!overwrite) {
304: return fileToUse;
305: }
306: } else {
307: fileToUse.createNewFile();
308: }
309: if (fileToUse.isDirectory()) {
310: return fileToUse;
311: }
312:
313: BufferedOutputStream bos = new BufferedOutputStream(
314: new FileOutputStream(fileToUse), BUFFER_SIZE);
315: byte[] bytes = new byte[BUFFER_SIZE];
316: int len = 0;
317: while ((len = zis.read(bytes)) >= 0) {
318: bos.write(bytes, 0, len);
319: }
320: bos.close();
321: zis.closeEntry();
322: return fileToUse;
323: }
324:
325: /**
326: * This adds all the necessary directories in the root of the zip path to the
327: * temp dir.
328: *@param entryName The string name in the Zip file (virtual path)
329: *@exception IOException if the directories can not be made
330: */
331: private void createPath(String entryName) throws IOException {
332: int slashIdx = entryName.lastIndexOf('/');
333: if (slashIdx >= 0) {
334: // there is path info
335: String firstPath = entryName.substring(0, slashIdx);
336: File dir = new File(extractDir, firstPath);
337: if (!dir.exists()) {
338: dir.mkdirs();
339: }
340: }
341: }
342:
343: /**
344: * Run method to use from the command line. This is fired via an entry in the
345: * MANIFEST.MF in the Jar
346: *@param args The command line arguments
347: */
348: public static void main(String[] args) {
349: testX();
350: // FIXME move after parseArgs() and set graphicsEnv if text selected
351: // will need to test SelfExtractor and comment parseArgs() to ensure
352: // no side effects in the future.
353: SelfExtractor extractor = null;
354: try {
355: boolean verbose = false;
356: extractor = new SelfExtractor();
357: extractor.init();
358: extractor.extract(verbose, graphicsEnv);
359: } catch (Exception e) {
360: e.printStackTrace();
361: String tempDir = "unknown";
362: if (extractor != null) {
363: tempDir = extractor.getExtractDir().getAbsolutePath();
364: }
365: String warning = "Could not extract Jar file to directory:"
366: + tempDir;
367: printXorTextWarning(warning);
368: }
369:
370: try {
371: FilterChain chain = FilterFactory.factory(CONFIG_RESOURCE);
372: ExecInstall installExec = new ExecInstall(chain);
373: installExec.parseArgs(args, false);
374: installExec.setInstallRoot(extractor.getExtractDir());
375: // removes files on exit
376: installExec.setTempRoot(extractor.getExtractDir());
377:
378: installExec.exec();
379: } catch (InstallException e1) {
380: System.out
381: .println("Cant load filter chain:/org/tp23/antinstaller/runtime/exe/selfextractor.fconfig");
382: e1.printStackTrace();
383: }
384: }
385:
386: /**
387: * This tests for the existence of a graphics environment and sets an
388: * internal flag so the test does not have to be repeated, it may be expensive.
389: * Prior to running this method the isGraphicsEnv() method will be invalid.
390: */
391: protected static void testX() {
392: try {
393: java.awt.GraphicsEnvironment.getLocalGraphicsEnvironment();
394: try {
395: boolean headless = java.awt.GraphicsEnvironment
396: .isHeadless();
397: if (headless) {
398: graphicsEnv = false;
399: return;
400: }
401: } catch (Throwable e) {
402: // JDK 1.3 does not have the isHeadless() method but may still work in other situations
403: }
404: graphicsEnv = true;
405: } catch (Throwable e) {
406: // thus graphicsEnv stays false;
407: }
408: }
409:
410: /**
411: * @see #testX()
412: * @return true if an X or windows environment is available
413: */
414: protected boolean isGraphicsEnv() {
415: return graphicsEnv;
416: }
417:
418: protected static void printXorTextWarning(String warning) {
419: if (graphicsEnv) {
420: try {
421: JOptionPane.showMessageDialog(null, warning);
422: } catch (HeadlessException headlessExc) {
423: graphicsEnv = false;
424: System.out.println(warning);
425: }
426: } else {
427: System.out.println(warning);
428: }
429: }
430:
431: public static int deleteRecursive(File directory) {
432: int count = 0;
433: File[] files = directory.listFiles(new FileFilter() {
434: public boolean accept(File file) {
435: return !file.isDirectory();
436: }
437: });
438: for (int i = 0; i < files.length; i++) {
439: files[i].delete();
440: count++;
441: }
442: File[] dirs = directory.listFiles(new FileFilter() {
443: public boolean accept(File file) {
444: return file.isDirectory();
445: }
446: });
447: for (int i = 0; i < dirs.length; i++) {
448: count += deleteRecursive(dirs[i]);
449: }
450: directory.delete();
451: return count;
452: }
453:
454: /**
455: * UN-URL encode string
456: * TODO should this not support UNICODE escapes
457: */
458: private static String unescape(final String s) {
459: StringBuffer sb = new StringBuffer(s.length());
460:
461: for (int i = 0; i < s.length(); i++) {
462: char c = s.charAt(i);
463: switch (c) {
464: case '%': {
465: try {
466: sb.append((char) Integer.parseInt(s.substring(
467: i + 1, i + 3), 16));
468: i += 2;
469: break;
470: } catch (NumberFormatException nfe) {
471: throw new IllegalArgumentException();
472: } catch (StringIndexOutOfBoundsException siob) {
473: String end = s.substring(i);
474: sb.append(end);
475: if (end.length() == 2)
476: i++;
477: }
478: break;
479: }
480: default: {
481: sb.append(c);
482: break;
483: }
484: }
485: }
486: return sb.toString();
487: }
488:
489: }
|