/*
* $Id$
* IzPack - Copyright 2001-2008 Julien Ponge, All Rights Reserved.
*
* http://izpack.org/
* http://izpack.codehaus.org/
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.izforge.izpack.compiler;
import com.izforge.izpack.Pack;
import com.izforge.izpack.PackFile;
import com.izforge.izpack.adaptator.IXMLElement;
import com.izforge.izpack.adaptator.IXMLWriter;
import com.izforge.izpack.adaptator.impl.XMLElementImpl;
import com.izforge.izpack.adaptator.impl.XMLWriter;
import com.izforge.izpack.util.FileUtil;
import java.io.*;
import java.net.URL;
import java.util.*;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.jar.Pack200;
import java.util.zip.Deflater;
import java.util.zip.ZipEntry;
import java.util.zip.ZipException;
import java.util.zip.ZipInputStream;
/**
* The packager class. The packager is used by the compiler to put files into an installer, and
* create the actual installer files.
*
* @author Julien Ponge
* @author Chadwick McHenry
*/
public class Packager extends PackagerBase
{
/**
* Executable zipped output stream. First to open, last to close.
* Attention! This is our own JarOutputStream, not the java standard!
*/
private com.izforge.izpack.util.JarOutputStream primaryJarStream;
/**
* The constructor.
*
* @throws CompilerException
*/
public Packager() throws CompilerException
{
this("default");
}
/**
* Extended constructor.
*
* @param compr_format Compression format to be used for packs
* compression format (if supported)
* @throws CompilerException
*/
public Packager(String compr_format) throws CompilerException
{
this(compr_format, -1);
}
/**
* Extended constructor.
*
* @param compr_format Compression format to be used for packs
* @param compr_level Compression level to be used with the chosen
* compression format (if supported)
* @throws CompilerException
*/
public Packager(String compr_format, int compr_level) throws CompilerException
{
initPackCompressor(compr_format, compr_level);
}
/* (non-Javadoc)
* @see com.izforge.izpack.compiler.IPackager#createInstaller(java.io.File)
*/
public void createInstaller(File primaryFile) throws Exception
{
// preliminary work
String baseName = primaryFile.getName();
if (baseName.endsWith(".jar"))
{
baseName = baseName.substring(0, baseName.length() - 4);
baseFile = new File(primaryFile.getParentFile(), baseName);
}
else
{
baseFile = primaryFile;
}
info.setInstallerBase(baseFile.getName());
packJarsSeparate = (info.getWebDirURL() != null);
// primary (possibly only) jar. -1 indicates primary
primaryJarStream = getJarOutputStream(baseFile.getName() + ".jar");
sendStart();
writeInstaller();
// Finish up. closeAlways is a hack for pack compressions other than
// default. Some of it (e.g. BZip2) closes the slave of it also.
// But this should not be because the jar stream should be open
// for the next pack. Therefore an own JarOutputStream will be used
// which close method will be blocked.
primaryJarStream.closeAlways();
sendStop();
}
/***********************************************************************************************
* Private methods used when writing out the installer to jar files.
**********************************************************************************************/
/**
* Write skeleton installer to primary jar. It is just an included jar, except that we copy the
* META-INF as well.
*/
protected void writeSkeletonInstaller() throws IOException
{
sendMsg("Copying the skeleton installer", PackagerListener.MSG_VERBOSE);
InputStream is = Packager.class.getResourceAsStream("/" + SKELETON_SUBPATH);
if (is == null)
{
File skeleton = new File(Compiler.IZPACK_HOME, SKELETON_SUBPATH);
is = new FileInputStream(skeleton);
}
ZipInputStream inJarStream = new ZipInputStream(is);
copyZip(inJarStream, primaryJarStream);
}
/**
* Write an arbitrary object to primary jar.
*/
protected void writeInstallerObject(String entryName, Object object) throws IOException
{
primaryJarStream.putNextEntry(new org.apache.tools.zip.ZipEntry(entryName));
ObjectOutputStream out = new ObjectOutputStream(primaryJarStream);
out.writeObject(object);
out.flush();
primaryJarStream.closeEntry();
}
/**
* Write the data referenced by URL to primary jar.
*/
protected void writeInstallerResources() throws IOException
{
sendMsg("Copying " + installerResourceURLMap.size() + " files into installer");
Iterator<String> i = installerResourceURLMap.keySet().iterator();
while (i.hasNext())
{
String name = i.next();
InputStream in = (installerResourceURLMap.get(name)).openStream();
org.apache.tools.zip.ZipEntry newEntry = new org.apache.tools.zip.ZipEntry(name);
long dateTime = FileUtil.getFileDateTime(installerResourceURLMap.get(name));
if (dateTime != -1)
{
newEntry.setTime(dateTime);
}
primaryJarStream.putNextEntry(newEntry);
PackagerHelper.copyStream(in, primaryJarStream);
primaryJarStream.closeEntry();
in.close();
}
}
/**
* Copy included jars to primary jar.
*/
protected void writeIncludedJars() throws IOException
{
sendMsg("Merging " + includedJarURLs.size() + " jars into installer");
Iterator<Object[]> i = includedJarURLs.iterator();
while (i.hasNext())
{
Object[] current = i.next();
InputStream is = ((URL) current[0]).openStream();
ZipInputStream inJarStream = new ZipInputStream(is);
copyZip(inJarStream, primaryJarStream, (List<String>) current[1]);
}
}
/**
* Write Packs to primary jar or each to a separate jar.
*/
protected void writePacks() throws Exception
{
final int num = packsList.size();
sendMsg("Writing " + num + " Pack" + (num > 1 ? "s" : "") + " into installer");
// Map to remember pack number and bytes offsets of back references
Map<File, Object[]> storedFiles = new HashMap<File, Object[]>();
// Pack200 files map
Map<Integer, File> pack200Map = new HashMap<Integer, File>();
int pack200Counter = 0;
// Force UTF-8 encoding in order to have proper ZipEntry names.
primaryJarStream.setEncoding("utf-8");
// First write the serialized files and file metadata data for each pack
// while counting bytes.
int packNumber = 0;
Iterator<PackInfo> packIter = packsList.iterator();
IXMLElement root = new XMLElementImpl("packs");
while (packIter.hasNext())
{
PackInfo packInfo = packIter.next();
Pack pack = packInfo.getPack();
pack.nbytes = 0;
if ((pack.id == null) || (pack.id.length() == 0))
{
pack.id = pack.name;
}
// create a pack specific jar if required
com.izforge.izpack.util.JarOutputStream packStream = primaryJarStream;
if (packJarsSeparate)
{
// See installer.Unpacker#getPackAsStream for the counterpart
String name = baseFile.getName() + ".pack-" + pack.id + ".jar";
packStream = getJarOutputStream(name);
}
OutputStream comprStream = packStream;
sendMsg("Writing Pack " + packNumber + ": " + pack.name, PackagerListener.MSG_VERBOSE);
// Retrieve the correct output stream
org.apache.tools.zip.ZipEntry entry = new org.apache.tools.zip.ZipEntry("packs/pack-" + pack.id);
if (!compressor.useStandardCompression())
{
entry.setMethod(ZipEntry.STORED);
entry.setComment(compressor.getCompressionFormatSymbols()[0]);
// We must set the entry before we get the compressed stream
// because some writes initialize data (e.g. bzip2).
packStream.putNextEntry(entry);
packStream.flush(); // flush before we start counting
comprStream = compressor.getOutputStream(packStream);
}
else
{
int level = compressor.getCompressionLevel();
if (level >= 0 && level < 10)
{
packStream.setLevel(level);
}
packStream.putNextEntry(entry);
packStream.flush(); // flush before we start counting
}
ByteCountingOutputStream dos = new ByteCountingOutputStream(comprStream);
ObjectOutputStream objOut = new ObjectOutputStream(dos);
// We write the actual pack files
objOut.writeInt(packInfo.getPackFiles().size());
Iterator iter = packInfo.getPackFiles().iterator();
while (iter.hasNext())
{
boolean addFile = !pack.loose;
boolean pack200 = false;
PackFile pf = (PackFile) iter.next();
File file = packInfo.getFile(pf);
if (file.getName().toLowerCase().endsWith(".jar") && info.isPack200Compression() && isNotSignedJar(file))
{
pf.setPack200Jar(true);
pack200 = true;
}
// use a back reference if file was in previous pack, and in
// same jar
Object[] info = storedFiles.get(file);
if (info != null && !packJarsSeparate)
{
pf.setPreviousPackFileRef((String) info[0], (Long) info[1]);
addFile = false;
}
objOut.writeObject(pf); // base info
if (addFile && !pf.isDirectory())
{
long pos = dos.getByteCount(); // get the position
if (pack200)
{
/*
* Warning!
*
* Pack200 archives must be stored in separated streams, as the Pack200 unpacker
* reads the entire stream...
*
* See http://java.sun.com/javase/6/docs/api/java/util/jar/Pack200.Unpacker.html
*/
pack200Map.put(pack200Counter, file);
objOut.writeInt(pack200Counter);
pack200Counter = pack200Counter + 1;
}
else
{
FileInputStream inStream = new FileInputStream(file);
long bytesWritten = PackagerHelper.copyStream(inStream, objOut);
inStream.close();
if (bytesWritten != pf.length())
{
throw new IOException("File size mismatch when reading " + file);
}
}
storedFiles.put(file, new Object[]{pack.id, pos});
}
// even if not written, it counts towards pack size
pack.nbytes += pf.length();
}
// Write out information about parsable files
objOut.writeInt(packInfo.getParsables().size());
iter = packInfo.getParsables().iterator();
while (iter.hasNext())
{
objOut.writeObject(iter.next());
}
// Write out information about executable files
objOut.writeInt(packInfo.getExecutables().size());
iter = packInfo.getExecutables().iterator();
while (iter.hasNext())
{
objOut.writeObject(iter.next());
}
// Write out information about updatecheck files
objOut.writeInt(packInfo.getUpdateChecks().size());
iter = packInfo.getUpdateChecks().iterator();
while (iter.hasNext())
{
objOut.writeObject(iter.next());
}
// Cleanup
objOut.flush();
if (!compressor.useStandardCompression())
{
comprStream.close();
}
packStream.closeEntry();
// close pack specific jar if required
if (packJarsSeparate)
{
packStream.closeAlways();
}
IXMLElement child = new XMLElementImpl("pack",root);
child.setAttribute("nbytes", Long.toString(pack.nbytes));
child.setAttribute("name", pack.name);
if (pack.id != null)
{
child.setAttribute("id", pack.id);
}
root.addChild(child);
packNumber++;
}
// Now that we know sizes, write pack metadata to primary jar.
primaryJarStream.putNextEntry(new org.apache.tools.zip.ZipEntry("packs.info"));
ObjectOutputStream out = new ObjectOutputStream(primaryJarStream);
out.writeInt(packsList.size());
Iterator<PackInfo> i = packsList.iterator();
while (i.hasNext())
{
PackInfo pack = i.next();
out.writeObject(pack.getPack());
}
out.flush();
primaryJarStream.closeEntry();
// Pack200 files
Pack200.Packer packer = createAgressivePack200Packer();
for (Integer key : pack200Map.keySet())
{
File file = pack200Map.get(key);
primaryJarStream.putNextEntry(new org.apache.tools.zip.ZipEntry("packs/pack200-" + key));
JarFile jar = new JarFile(file);
packer.pack(jar, primaryJarStream);
jar.close();
primaryJarStream.closeEntry();
}
}
private Pack200.Packer createAgressivePack200Packer()
{
Pack200.Packer packer = Pack200.newPacker();
Map<String, String> m = packer.properties();
m.put(Pack200.Packer.EFFORT, "9");
m.put(Pack200.Packer.SEGMENT_LIMIT, "-1");
m.put(Pack200.Packer.KEEP_FILE_ORDER, Pack200.Packer.FALSE);
m.put(Pack200.Packer.DEFLATE_HINT, Pack200.Packer.FALSE);
m.put(Pack200.Packer.MODIFICATION_TIME, Pack200.Packer.LATEST);
m.put(Pack200.Packer.CODE_ATTRIBUTE_PFX +"LineNumberTable", Pack200.Packer.STRIP);
m.put(Pack200.Packer.CODE_ATTRIBUTE_PFX + "LocalVariableTable", Pack200.Packer.STRIP);
m.put(Pack200.Packer.CODE_ATTRIBUTE_PFX + "SourceFile", Pack200.Packer.STRIP);
return packer;
}
private boolean isNotSignedJar(File file) throws IOException
{
JarFile jar = new JarFile(file);
Enumeration<JarEntry> entries = jar.entries();
while (entries.hasMoreElements())
{
JarEntry entry = entries.nextElement();
if (entry.getName().startsWith("META-INF") && entry.getName().endsWith(".SF"))
{
jar.close();
return false;
}
}
jar.close();
return true;
}
/***********************************************************************************************
* Stream utilites for creation of the installer.
**********************************************************************************************/
/**
* Return a stream for the next jar.
*/
private com.izforge.izpack.util.JarOutputStream getJarOutputStream(String name) throws IOException
{
File file = new File(baseFile.getParentFile(), name);
sendMsg("Building installer jar: " + file.getAbsolutePath());
com.izforge.izpack.util.JarOutputStream jar =
new com.izforge.izpack.util.JarOutputStream(file);
jar.setLevel(Deflater.BEST_COMPRESSION);
jar.setPreventClose(true); // Needed at using FilterOutputStreams which calls close
// of the slave at finalizing.
return jar;
}
/**
* Copies contents of one jar to another.
* <p/>
* <p/>
* TODO: it would be useful to be able to keep signature information from signed jar files, can
* we combine manifests and still have their content signed?
*/
private void copyZip(ZipInputStream zin, org.apache.tools.zip.ZipOutputStream out) throws IOException
{
copyZip(zin, out, null);
}
/**
* Copies specified contents of one jar to another.
* <p/>
* <p/>
* TODO: it would be useful to be able to keep signature information from signed jar files, can
* we combine manifests and still have their content signed?
*/
private void copyZip(ZipInputStream zin, org.apache.tools.zip.ZipOutputStream out,
List<String> files)
throws IOException
{
java.util.zip.ZipEntry zentry;
if (!alreadyWrittenFiles.containsKey(out))
{
alreadyWrittenFiles.put(out, new HashSet<String>());
}
HashSet<String> currentSet = alreadyWrittenFiles.get(out);
while ((zentry = zin.getNextEntry()) != null)
{
String currentName = zentry.getName();
String testName = currentName.replace('/', '.');
testName = testName.replace('\\', '.');
if (files != null)
{
Iterator<String> i = files.iterator();
boolean founded = false;
while (i.hasNext())
{ // Make "includes" self to support regex.
String doInclude = i.next();
if (testName.matches(doInclude))
{
founded = true;
break;
}
}
if (!founded)
{
continue;
}
}
if (currentSet.contains(currentName))
{
continue;
}
try
{
// Create new entry for zip file.
org.apache.tools.zip.ZipEntry newEntry = new org.apache.tools.zip.ZipEntry(currentName);
// Get input file date and time.
long fileTime = zentry.getTime();
// Make sure there is date and time set.
if (fileTime != -1)
{
newEntry.setTime(fileTime); // If found set it into output file.
}
out.putNextEntry(newEntry);
PackagerHelper.copyStream(zin, out);
out.closeEntry();
zin.closeEntry();
currentSet.add(currentName);
}
catch (ZipException x)
{
// This avoids any problem that can occur with duplicate
// directories. for instance all META-INF data in jars
// unfortunately this do not work with the apache ZipOutputStream...
}
}
}
/* (non-Javadoc)
* @see com.izforge.izpack.compiler.IPackager#addConfigurationInformation(com.izforge.izpack.adaptator.IXMLElement)
*/
public void addConfigurationInformation(IXMLElement data)
{
// TODO Auto-generated method stub
}
}