/* * IzPack - Copyright 2001-2008 Julien Ponge, All Rights Reserved. * * http://izpack.org/ http://izpack.codehaus.org/ * * Copyright 2007 Dennis Reil * * 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.XPackFile; import com.izforge.izpack.io.FileSpanningOutputStream; import com.izforge.izpack.util.Debug; import com.izforge.izpack.util.FileUtil; import com.izforge.izpack.adaptator.IXMLElement; import java.io.*; import java.net.URL; import java.util.*; import java.util.zip.*; /** * The packager class. The packager is used by the compiler to put files into an installer, and * create the actual installer files. * <p/> * This is a packager, which packs everything into multi volumes. * * @author Dennis Reil, <Dennis.Reil@reddot.de> */ public class MultiVolumePackager extends PackagerBase { public static final String INSTALLER_PAK_NAME = "installer"; /** * Executable zipped output stream. First to open, last to close. */ private ZipOutputStream primaryJarStream; private IXMLElement configdata = null; /** * The constructor. * * @throws CompilerException */ public MultiVolumePackager() throws CompilerException { this("default"); } /** * Extended constructor. * * @param compr_format Compression format to be used for packs compression format (if supported) * @throws CompilerException */ public MultiVolumePackager(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 MultiVolumePackager(String compr_format, int compr_level) throws CompilerException { initPackCompressor(compr_format, compr_level); } /** * Create the installer, beginning with the specified jar. If the name specified does not end in * ".jar", it is appended. If secondary jars are created for packs (if the Info object added has * a webDirURL set), they are created in the same directory, named sequentially by inserting * ".pack#" (where '#' is the pack number) ".jar" suffix: e.g. "foo.pack1.jar". If any file * exists, it is overwritten. */ public void createInstaller(File primaryFile) throws Exception { // first analyze the configuration this.analyzeConfigurationInformation(); // 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(); // Pack File Data may be written to separate jars String packfile = baseFile.getParent() + File.separator + INSTALLER_PAK_NAME; writePacks(new File(packfile)); // 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(); primaryJarStream.close(); sendStop(); } /** * ******************************************************************************************** * Listener assistance * ******************************************************************************************** */ private void analyzeConfigurationInformation() { String classname = this.getClass().getName(); String sizeprop = classname + ".volumesize"; String freespaceprop = classname + ".firstvolumefreespace"; if (this.configdata == null) { // no configdata given, set default values this.variables.setProperty(sizeprop, Long.toString(FileSpanningOutputStream.DEFAULT_VOLUME_SIZE)); this.variables.setProperty(freespaceprop, Long.toString(FileSpanningOutputStream.DEFAULT_ADDITIONAL_FIRST_VOLUME_FREE_SPACE_SIZE)); } else { // configdata was set String volumesize = configdata.getAttribute("volumesize", Long.toString(FileSpanningOutputStream.DEFAULT_VOLUME_SIZE)); String freespace = configdata.getAttribute("firstvolumefreespace", Long.toString(FileSpanningOutputStream.DEFAULT_ADDITIONAL_FIRST_VOLUME_FREE_SPACE_SIZE)); this.variables.setProperty(sizeprop, volumesize); this.variables.setProperty(freespaceprop, freespace); } } /*********************************************************************************************** * 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 = MultiVolumePackager.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); // copy anything except the manifest.mf List<String> excludes = new ArrayList<String>(); excludes.add("META-INF.MANIFEST.MF"); copyZipWithoutExcludes(inJarStream, primaryJarStream, excludes); // ugly code to modify the manifest-file to set MultiVolumeInstaller as main class // reopen Stream is = MultiVolumePackager.class.getResourceAsStream("/" + SKELETON_SUBPATH); if (is == null) { File skeleton = new File(Compiler.IZPACK_HOME, SKELETON_SUBPATH); is = new FileInputStream(skeleton); } inJarStream = new ZipInputStream(is); boolean found = false; ZipEntry ze = null; String modifiedmanifest = null; while (((ze = inJarStream.getNextEntry()) != null) && !found) { if ("META-INF/MANIFEST.MF".equals(ze.getName())) { long size = ze.getSize(); byte[] buffer = new byte[4096]; int readbytes = 0; int totalreadbytes = 0; StringBuffer manifest = new StringBuffer(); while (((readbytes = inJarStream.read(buffer)) > 0) && (totalreadbytes < size)) { totalreadbytes += readbytes; String tmp = new String(buffer, 0, readbytes, "utf-8"); manifest.append(tmp); } StringReader stringreader = new StringReader(manifest.toString()); BufferedReader reader = new BufferedReader(stringreader); String line = null; StringBuffer modified = new StringBuffer(); while ((line = reader.readLine()) != null) { if (line.startsWith("Main-Class:")) { line = "Main-Class: com.izforge.izpack.installer.MultiVolumeInstaller"; } modified.append(line); modified.append("\r\n"); } reader.close(); modifiedmanifest = modified.toString(); /* System.out.println("Manifest:"); System.out.println(manifest.toString()); System.out.println("Modified Manifest:"); System.out.println(modified.toString()); */ break; } } primaryJarStream.putNextEntry(new ZipEntry("META-INF/MANIFEST.MF")); primaryJarStream.write(modifiedmanifest.getBytes()); primaryJarStream.closeEntry(); } /** * Write an arbitrary object to primary jar. */ protected void writeInstallerObject(String entryName, Object object) throws IOException { primaryJarStream.putNextEntry(new 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"); for (String s : installerResourceURLMap.keySet()) { String name = s; 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); 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"); for (Object[] includedJarURL : includedJarURLs) { Object[] current = includedJarURL; 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. */ private void writePacks(File primaryfile) throws Exception { final int num = packsList.size(); sendMsg("Writing " + num + " Pack" + (num > 1 ? "s" : "") + " into installer"); Debug.trace("Writing " + num + " Pack" + (num > 1 ? "s" : "") + " into installer"); // Map to remember pack number and bytes offsets of back references Map storedFiles = new HashMap(); // First write the serialized files and file metadata data for each pack // while counting bytes. String classname = this.getClass().getName(); String volumesize = this.getVariables().getProperty(classname + ".volumesize"); String extraspace = this.getVariables().getProperty(classname + ".firstvolumefreespace"); long volumesizel = FileSpanningOutputStream.DEFAULT_VOLUME_SIZE; long extraspacel = FileSpanningOutputStream.DEFAULT_ADDITIONAL_FIRST_VOLUME_FREE_SPACE_SIZE; if (volumesize != null) { volumesizel = Long.parseLong(volumesize); } if (extraspace != null) { extraspacel = Long.parseLong(extraspace); } Debug.trace("Volumesize: " + volumesizel); Debug.trace("Extra space on first volume: " + extraspacel); FileSpanningOutputStream fout = new FileSpanningOutputStream(primaryfile.getParent() + File.separator + primaryfile.getName() + ".pak", volumesizel); fout.setFirstvolumefreespacesize(extraspacel); int packNumber = 0; for (PackInfo aPacksList : packsList) { PackInfo packInfo = aPacksList; Pack pack = packInfo.getPack(); pack.nbytes = 0; sendMsg("Writing Pack " + packNumber + ": " + pack.name, PackagerListener.MSG_VERBOSE); Debug.trace("Writing Pack " + packNumber + ": " + pack.name); ZipEntry entry = new ZipEntry("packs/pack" + packNumber); // write the metadata as uncompressed object stream to primaryJarStream // first write a packs entry primaryJarStream.putNextEntry(entry); ObjectOutputStream objOut = new ObjectOutputStream(primaryJarStream); // We write the actual pack files objOut.writeInt(packInfo.getPackFiles().size()); Iterator iter = packInfo.getPackFiles().iterator(); for (Object o : packInfo.getPackFiles()) { boolean addFile = !pack.loose; PackFile packfile = (PackFile) o; XPackFile pf = new XPackFile(packfile); File file = packInfo.getFile(packfile); Debug.trace("Next file: " + file.getAbsolutePath()); // use a back reference if file was in previous pack, and in // same jar Object[] info = (Object[]) storedFiles.get(file); if (info != null && !packJarsSeparate) { Debug.trace("File already included in other pack"); pf.setPreviousPackFileRef((String) info[0], (Long) info[1]); addFile = false; } if (addFile && !pf.isDirectory()) { long pos = fout.getFilepointer(); pf.setArchivefileposition(pos); // write out the filepointer int volumecountbeforewrite = fout.getVolumeCount(); FileInputStream inStream = new FileInputStream(file); long bytesWritten = copyStream(inStream, fout); fout.flush(); long posafterwrite = fout.getFilepointer(); Debug.trace("File (" + pf.sourcePath + ") " + pos + " <-> " + posafterwrite); if (fout.getFilepointer() != (pos + bytesWritten)) { Debug.trace("file: " + file.getName()); Debug.trace("(Filepos/BytesWritten/ExpectedNewFilePos/NewFilePointer) (" + pos + "/" + bytesWritten + "/" + (pos + bytesWritten) + "/" + fout.getFilepointer() + ")"); Debug.trace("Volumecount (before/after) (" + volumecountbeforewrite + "/" + fout.getVolumeCount() + ")"); throw new IOException("Error new filepointer is illegal"); } if (bytesWritten != pf.length()) { throw new IOException( "File size mismatch when reading " + file); } inStream.close(); // keine backreferences mglich // storedFiles.put(file, new long[] { packNumber, pos}); } objOut.writeObject(pf); // base info objOut.flush(); // make sure it is written // 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(); packNumber++; } // write metadata for reading in volumes int volumes = fout.getVolumeCount(); Debug.trace("Written " + volumes + " volumes"); String volumename = primaryfile.getName() + ".pak"; fout.flush(); fout.close(); primaryJarStream.putNextEntry(new ZipEntry("volumes.info")); ObjectOutputStream out = new ObjectOutputStream(primaryJarStream); out.writeInt(volumes); out.writeUTF(volumename); out.flush(); primaryJarStream.closeEntry(); // Now that we know sizes, write pack metadata to primary jar. primaryJarStream.putNextEntry(new ZipEntry("packs.info")); out = new ObjectOutputStream(primaryJarStream); out.writeInt(packsList.size()); for (PackInfo aPacksList : packsList) { PackInfo pack = aPacksList; out.writeObject(pack.getPack()); } out.flush(); primaryJarStream.closeEntry(); } /*********************************************************************************************** * Stream utilites for creation of the installer. **********************************************************************************************/ /** * Return a stream for the next jar. */ private ZipOutputStream getJarOutputStream(String name) throws IOException { File file = new File(baseFile.getParentFile(), name); sendMsg("Building installer jar: " + file.getAbsolutePath()); Debug.trace("Building installer jar: " + file.getAbsolutePath()); ZipOutputStream jar = new ZipOutputStream(new FileOutputStream(file)); jar.setLevel(Deflater.BEST_COMPRESSION); // jar.setPreventClose(true); // Needed at using FilterOutputStreams which // calls close // of the slave at finalizing. return jar; } /** * 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? * * @see #copyStream(InputStream, OutputStream) */ private void copyZip(ZipInputStream zin, 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. ZipEntry newEntry = new 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); 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... } } } /** * Copies specified contents of one jar to another without the specified files * <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? * * @see #copyStream(InputStream, OutputStream) */ private void copyZipWithoutExcludes(ZipInputStream zin, ZipOutputStream out, List<String> excludes) 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 (excludes != null) { Iterator<String> i = excludes.iterator(); boolean skip = false; while (i.hasNext()) { // Make "excludes" self to support regex. String doExclude = i.next(); if (testName.matches(doExclude)) { skip = true; break; } } if (skip) { continue; } } if (currentSet.contains(currentName)) { continue; } try { // Create new entry for zip file. ZipEntry newEntry = new 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); 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... } } } /** * Copies all the data from the specified input stream to the specified output stream. * * @param in the input stream to read * @param out the output stream to write * @return the total number of bytes copied * @throws IOException if an I/O error occurs */ private long copyStream(InputStream in, OutputStream out) throws IOException { byte[] buffer = new byte[5120]; long bytesCopied = 0; int bytesInBuffer; while ((bytesInBuffer = in.read(buffer)) != -1) { out.write(buffer, 0, bytesInBuffer); bytesCopied += bytesInBuffer; } return bytesCopied; } /* (non-Javadoc) * @see com.izforge.izpack.compiler.IPackager#addConfigurationInformation(com.izforge.izpack.adaptator.IXMLElement) */ public void addConfigurationInformation(IXMLElement data) { this.configdata = data; } /* (non-Javadoc) * @see com.izforge.izpack.compiler.PackagerBase#writePacks() */ protected void writePacks() throws Exception { // TODO Auto-generated method stub } }