Summary
This tip presents an easy way to make self-extracting Java Archives (JARs). A self-extracting archive can extract the contained files to user-selected directories on any Java platform. Your users are only required to have a Java runtime system. ( 1,200 words; November 16, 2001)
or many years, Phil Katz's archive creation, zip, has been one of the most popular file archiving formats. Sun has adopted the zip format as the basis for the Java Archive (JAR). Sun has extended the zip format's use with various conventions so that you can pack Java classes into an archive file. With the addition of the JAR manifest file, the Java runtime can easily locate and directly execute the main class of the Java application contained in the jar file.
Some
zip
utility tools can create self-extracting archives for various platforms, such as MS Windows. The utility tool combines a regular zip archive with an extractor program to generate a new executable (exe) file. Receivers of the exe file only need to run it to extract the original zip archive's contents. The executable runs the extractor program to extract the archived files into a user-specified output directory.You can convert a base zip or jar file into an executable jar file on any Java platform. Whereas the self-extracting zip can only create platform-specific executables, the self-extracting jar file can be distributed to and run on any platform supporting Java.
Creating the self-extracting jar file is straightforward. You just need a special JAR manifest file, a Java-based extraction program, the zip or jar file containing the base content files, and any Java SDK's
jar
utility application.The manifest file
To make executable JARs, you first need a manifest file calledMANIFEST.MF
in theMETA-INF
directory. The manifest file may contain a number of possible entries; however, for our purposes here, we just need to specify the name of the Java class that contains the Java-based extractor program'smain()
method:
Main-Class: ZipSelfExtractor
We've added a manifest file named
jarmanifest
to this tip's example code. For more information about the manifest file, see the Jar File Specification.The extractor
You can make the extractor program using various approaches. The approach we present here is simple and straightforward. First, the extraction program figures out the self-extracting jar file's name. With that name in hand, the extractor utilizes the standard, built-in Java zip/jar libraries to extract the content files from the archive. You can find the full source code forZipSelfExtractor
inZipSelfExtractor.java
.Getting the jar filename in the extractor program can be tricky. Although the jar file's name appears on the command line, that name is not passed to the class's
main()
method. Therefore, in the extractor program, we use the following code to extract the information from the URL that points to the extractor:
private String getJarFileName ()
{
myClassName = this.getClass().getName() + ".class";
URL urlJar =
this.getClass().getClassLoader().getSystemResource(myClassName);
String urlStr = urlJar.toString();
int from = "jar:file:".length();
int to = urlStr.indexOf("!/");
return urlStr.substring(from, to);
}
Notice that in the
getSystemResource()
method we passmyClassName
instead ofZipSelfExtractor.class
. That lets us change the extractor program name without changing that part of the code. We setmyClassName
by looking up the current class's name.Next, we extract the jar file's name. First, we ask for a URL to the class file containing the currently running class (which is the extractor program). Once we have the URL, we can snip out the jar file's name. By definition, the URL of the JAR extractor program follows the basic format:
jar:
, which shows that the executable runs from inside a jar file
- The jar file's URL, such as
file:/C:/temp/test.jar
, followed by the!
character
- The internal path name of the file within the JAR, such as
/ZipSelfExtractor.class
In the extractor program's case, the URL might look like:
jar:file:/home/johnm/test/zipper.jar!/ZipSelfExtractor.class
Now that we have the jar file's name, we can perform the extraction. The guts of the extraction program rely on the built-in, Java zip/jar file manipulation libraries to uncompress the content files contained in the archive. See Resources for more on the zip/jar file manipulation libraries.
For ease of use, the extractor is a graphical Java application. The application uses the
JFileChooser
class to let users specify the destination directory to which they want the files extracted. AProgressMonitor
shows the progress of the extraction process. If a file might overwrite an already existing file, the user is asked whether or not to overwrite the existing file. At the conclusion, a standard dialog box presents extraction statistics.Finally, the extractor program checks that it does not extract the files that make the jar file self-extracting -- the manifest file and the extractor's
.class
file; the program should just extract the original JAR contents. Those two files are artifacts of the self-extracting jar file and not part of the original, base content files.Packing the jar file
Now that we have the manifest file and the extractor program, we can build the self-extracting jar file. We can manually use the JDK'sjar
utility to make a self-extracting jar file. For example, assuming you have a zip file calledmyzip.zip
, you can perform the following steps to make a self-extracting file from it:
cd
to the directory containingmyzip.zip
- Download
zipper.jar
- Extract the files into the current directory. We've made it a self-extracting JAR:
java -jar zipper.jar
- Copy the
zipper.class
file toZipSelfExtractor.class
- Rename
myzip.zip
asmyzip.jar
- Update
myzip.jar
with thejarmanifest
andZipSelfExtractor.class
files:
jar uvfm myzip.jar jarmanifest ZipSelfExtractor.class
Now
myzip.jar
is self-extracting on all platforms containing Java Runtime Environment (JRE) 1.2 or later. To execute the self-extracting jar file, run:java -jar myzip.jar
Note that some platforms may have bindings already set up such that you can execute the jar file just by clicking on the
myzip.jar
file icon, which will run the command line equivalent.Exercise for the reader
The currentZipSelfExtract
does not integrate well if you make a self-extracting JAR out of an existing jar file containing a manifest file. Add intelligence to the self-extractor and the creation instructions so you can deal with existing jar files that contain manifest files.Free your hand from the JAR
A self-extracting jar file is a good mechanism for cross-platform file distribution. Self-extracting JARs are easy to create, and the minimal user requirement of a JRE 1.2 or later installation is a reasonable tradeoff for gaining cross-platform support.Instead of manually creating the self-extracting jar file, check out ZipAnywhere. ZipAnywhere is a full-featured
zip
/jar
utility tool written in 100% pure Java. It is a free GUI-based tool a la WinZip and can create self-extracting jar files with the click of a button.Reference Source Code:
/* ZipSelfExtractor.java */
/* Author: Z.S. Jin
Updates: John D. Mitchell */
import java.io.*;
import java.net.*;
import javax.swing.*;
import java.util.zip.*;
import java.util.*;
import java.text.*;
public class ZipSelfExtractor extends JFrame
{
private String myClassName;
static String MANIFEST = "META-INF/MANIFEST.MF";
public static void main(String[] args)
{
ZipSelfExtractor zse = new ZipSelfExtractor();
String jarFileName = zse.getJarFileName();
zse.extract(jarFileName);
System.exit(0);
}
ZipSelfExtractor()
{
}
private String getJarFileName()
{
myClassName = this.getClass().getName() + ".class";
URL urlJar = this.getClass().getClassLoader().getSystemResource(myClassName);
String urlStr = urlJar.toString();
int from = "jar:file:".length();
int to = urlStr.indexOf("!/");
return urlStr.substring(from, to);
}
public void extract(String zipfile)
{
File currentArchive = new File(zipfile);
JFileChooser fc = new JFileChooser();
fc.setCurrentDirectory(new File("."));
fc.setDialogType(JFileChooser.OPEN_DIALOG);
fc.setDialogTitle("Select destination directory for extracting " +
currentArchive.getName());
fc.setMultiSelectionEnabled(false);
fc.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);
if (fc.showDialog(ZipSelfExtractor.this, "Select")
!= JFileChooser.APPROVE_OPTION)
{
return; //only when user select valid dir, it can return approve_option
}
File outputDir = fc.getSelectedFile();
byte[] buf = new byte[1024];
SimpleDateFormat formatter = new SimpleDateFormat ("MM/dd/yyyy hh:mma",Locale.getDefault());
ProgressMonitor pm = null;
boolean overwrite = false;
ZipFile zf = null;
FileOutputStream out = null;
InputStream in = null;
try
{
zf = new ZipFile(currentArchive);
int size = zf.size();
int extracted = 0;
pm = new ProgressMonitor(getParent(), "Extracting files...", "starting", 0, size-4);
pm.setMillisToDecideToPopup(0);
pm.setMillisToPopup(0);
Enumeration entries = zf.entries();
for (int i=0; i<size; i++)
{
ZipEntry entry = (ZipEntry) entries.nextElement();
if(entry.isDirectory())
continue;
String pathname = entry.getName();
if(myClassName.equals(pathname) || MANIFEST.equals(pathname.toUpperCase()))
continue;
extracted ++;
pm.setProgress(i);
pm.setNote(pathname);
if(pm.isCanceled())
return;
in = zf.getInputStream(entry);
File outFile = new File(outputDir, pathname);
Date archiveTime = new Date(entry.getTime());
if(overwrite==false)
{
if(outFile.exists())
{
Object[] options = {"Yes", "Yes To All", "No"};
Date existTime = new Date(outFile.lastModified());
Long archiveLen = new Long(entry.getSize());
String msg = "File name conflict: "
+ "There is already a file with "
+ "that name on the disk!/n"
+ "/nFile name: " + outFile.getName()
+ "/nExisting file: "
+ formatter.format(existTime) + ", "
+ outFile.length() + "Bytes"
+ "/nFile in archive:"
+ formatter.format(archiveTime) + ", "
+ archiveLen + "Bytes"
+"/n/nWould you like to overwrite the file?";
int result = JOptionPane.showOptionDialog(ZipSelfExtractor.this,
msg, "Warning", JOptionPane.DEFAULT_OPTION,
JOptionPane.WARNING_MESSAGE, null, options,options[0]);
if(result == 2) // No
{
continue;
}
else if( result == 1) //YesToAll
{
overwrite = true;
}
}
}
File parent = new File(outFile.getParent());
if (parent != null && !parent.exists())
{
parent.mkdirs();
}
out = new FileOutputStream(outFile);
while (true)
{
int nRead = in.read(buf, 0, buf.length);
if (nRead <= 0)
break;
out.write(buf, 0, nRead);
}
out.close();
outFile.setLastModified(archiveTime.getTime());
}
pm.close();
zf.close();
getToolkit().beep();
JOptionPane.showMessageDialog
(ZipSelfExtractor.this,
"Extracted " + extracted +
" file" + ((extracted > 1) ? "s": "") +
" from the/n" +
zipfile + "/narchive into the/n" +
outputDir.getPath() +
"/ndirectory.",
"Zip Self Extractor",
JOptionPane.INFORMATION_MESSAGE);
}
catch (Exception e)
{
System.out.println(e);
if(zf!=null) { try { zf.close(); } catch(IOException ioe) {;} }
if(out!=null) { try {out.close();} catch(IOException ioe) {;} }
if(in!=null) { try { in.close(); } catch(IOException ioe) {;} }
}
}
}