001: /*
002: * IzPack - Copyright 2001-2008 Julien Ponge, All Rights Reserved.
003: *
004: * http://izpack.org/ http://izpack.codehaus.org/
005: *
006: * Copyright 2007 Dennis Reil
007: *
008: * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
009: * in compliance with the License. You may obtain a copy of the License at
010: *
011: * http://www.apache.org/licenses/LICENSE-2.0
012: *
013: * Unless required by applicable law or agreed to in writing, software distributed under the License
014: * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
015: * or implied. See the License for the specific language governing permissions and limitations under
016: * the License.
017: */
018: package com.izforge.izpack.compiler;
019:
020: import java.io.BufferedReader;
021: import java.io.File;
022: import java.io.FileInputStream;
023: import java.io.FileOutputStream;
024: import java.io.IOException;
025: import java.io.InputStream;
026: import java.io.ObjectOutputStream;
027: import java.io.OutputStream;
028: import java.io.StringReader;
029: import java.net.URL;
030: import java.util.ArrayList;
031: import java.util.HashMap;
032: import java.util.HashSet;
033: import java.util.Iterator;
034: import java.util.List;
035: import java.util.Map;
036: import java.util.zip.Deflater;
037: import java.util.zip.ZipEntry;
038: import java.util.zip.ZipException;
039: import java.util.zip.ZipInputStream;
040: import java.util.zip.ZipOutputStream;
041:
042: import net.n3.nanoxml.XMLElement;
043:
044: import com.izforge.izpack.Pack;
045: import com.izforge.izpack.PackFile;
046: import com.izforge.izpack.XPackFile;
047: import com.izforge.izpack.io.FileSpanningOutputStream;
048: import com.izforge.izpack.util.Debug;
049: import com.izforge.izpack.util.FileUtil;
050:
051: /**
052: * The packager class. The packager is used by the compiler to put files into an installer, and
053: * create the actual installer files.
054: *
055: * This is a packager, which packs everything into multi volumes.
056: *
057: * @author Dennis Reil, <Dennis.Reil@reddot.de>
058: */
059: public class MultiVolumePackager extends PackagerBase {
060:
061: public static final String INSTALLER_PAK_NAME = "installer";
062:
063: /** Executable zipped output stream. First to open, last to close. */
064: private ZipOutputStream primaryJarStream;
065:
066: private XMLElement configdata = null;
067:
068: /**
069: * The constructor.
070: *
071: * @throws CompilerException
072: */
073: public MultiVolumePackager() throws CompilerException {
074: this ("default");
075: }
076:
077: /**
078: * Extended constructor.
079: *
080: * @param compr_format Compression format to be used for packs compression format (if supported)
081: * @throws CompilerException
082: */
083: public MultiVolumePackager(String compr_format)
084: throws CompilerException {
085: this (compr_format, -1);
086: }
087:
088: /**
089: * Extended constructor.
090: *
091: * @param compr_format Compression format to be used for packs
092: * @param compr_level Compression level to be used with the chosen compression format (if
093: * supported)
094: * @throws CompilerException
095: */
096: public MultiVolumePackager(String compr_format, int compr_level)
097: throws CompilerException {
098: initPackCompressor(compr_format, compr_level);
099: }
100:
101: /**
102: * Create the installer, beginning with the specified jar. If the name specified does not end in
103: * ".jar", it is appended. If secondary jars are created for packs (if the Info object added has
104: * a webDirURL set), they are created in the same directory, named sequentially by inserting
105: * ".pack#" (where '#' is the pack number) ".jar" suffix: e.g. "foo.pack1.jar". If any file
106: * exists, it is overwritten.
107: */
108: public void createInstaller(File primaryFile) throws Exception {
109: // first analyze the configuration
110: this .analyzeConfigurationInformation();
111:
112: // preliminary work
113: String baseName = primaryFile.getName();
114: if (baseName.endsWith(".jar")) {
115: baseName = baseName.substring(0, baseName.length() - 4);
116: baseFile = new File(primaryFile.getParentFile(), baseName);
117: } else
118: baseFile = primaryFile;
119:
120: info.setInstallerBase(baseFile.getName());
121: packJarsSeparate = (info.getWebDirURL() != null);
122:
123: // primary (possibly only) jar. -1 indicates primary
124: primaryJarStream = getJarOutputStream(baseFile.getName()
125: + ".jar");
126:
127: sendStart();
128:
129: writeInstaller();
130:
131: // Pack File Data may be written to separate jars
132: String packfile = baseFile.getParent() + File.separator
133: + INSTALLER_PAK_NAME;
134: writePacks(new File(packfile));
135:
136: // Finish up. closeAlways is a hack for pack compressions other than
137: // default. Some of it (e.g. BZip2) closes the slave of it also.
138: // But this should not be because the jar stream should be open
139: // for the next pack. Therefore an own JarOutputStream will be used
140: // which close method will be blocked.
141: // primaryJarStream.closeAlways();
142: primaryJarStream.close();
143:
144: sendStop();
145: }
146:
147: /***********************************************************************************************
148: * Listener assistance
149: **********************************************************************************************/
150:
151: private void analyzeConfigurationInformation() {
152: String classname = this .getClass().getName();
153: String sizeprop = classname + ".volumesize";
154: String freespaceprop = classname + ".firstvolumefreespace";
155:
156: if (this .configdata == null) {
157: // no configdata given, set default values
158: this .variables
159: .setProperty(
160: sizeprop,
161: Long
162: .toString(FileSpanningOutputStream.DEFAULT_VOLUME_SIZE));
163: this .variables
164: .setProperty(
165: freespaceprop,
166: Long
167: .toString(FileSpanningOutputStream.DEFAULT_ADDITIONAL_FIRST_VOLUME_FREE_SPACE_SIZE));
168: } else {
169: // configdata was set
170: String volumesize = configdata
171: .getAttribute(
172: "volumesize",
173: Long
174: .toString(FileSpanningOutputStream.DEFAULT_VOLUME_SIZE));
175: String freespace = configdata
176: .getAttribute(
177: "firstvolumefreespace",
178: Long
179: .toString(FileSpanningOutputStream.DEFAULT_ADDITIONAL_FIRST_VOLUME_FREE_SPACE_SIZE));
180: this .variables.setProperty(sizeprop, volumesize);
181: this .variables.setProperty(freespaceprop, freespace);
182: }
183: }
184:
185: /***********************************************************************************************
186: * Private methods used when writing out the installer to jar files.
187: **********************************************************************************************/
188:
189: /**
190: * Write skeleton installer to primary jar. It is just an included jar, except that we copy the
191: * META-INF as well.
192: */
193: protected void writeSkeletonInstaller() throws IOException {
194: sendMsg("Copying the skeleton installer",
195: PackagerListener.MSG_VERBOSE);
196:
197: InputStream is = MultiVolumePackager.class
198: .getResourceAsStream("/" + SKELETON_SUBPATH);
199: if (is == null) {
200: File skeleton = new File(Compiler.IZPACK_HOME,
201: SKELETON_SUBPATH);
202: is = new FileInputStream(skeleton);
203: }
204: ZipInputStream inJarStream = new ZipInputStream(is);
205:
206: // copy anything except the manifest.mf
207: List<String> excludes = new ArrayList<String>();
208: excludes.add("META-INF.MANIFEST.MF");
209: copyZipWithoutExcludes(inJarStream, primaryJarStream, excludes);
210:
211: // ugly code to modify the manifest-file to set MultiVolumeInstaller as main class
212: // reopen Stream
213: is = MultiVolumePackager.class.getResourceAsStream("/"
214: + SKELETON_SUBPATH);
215: if (is == null) {
216: File skeleton = new File(Compiler.IZPACK_HOME,
217: SKELETON_SUBPATH);
218: is = new FileInputStream(skeleton);
219: }
220: inJarStream = new ZipInputStream(is);
221: boolean found = false;
222: ZipEntry ze = null;
223: String modifiedmanifest = null;
224: while (((ze = inJarStream.getNextEntry()) != null) && !found) {
225: if ("META-INF/MANIFEST.MF".equals(ze.getName())) {
226: long size = ze.getSize();
227: byte[] buffer = new byte[4096];
228: int readbytes = 0;
229: int totalreadbytes = 0;
230: StringBuffer manifest = new StringBuffer();
231: while (((readbytes = inJarStream.read(buffer)) > 0)
232: && (totalreadbytes < size)) {
233: totalreadbytes += readbytes;
234: String tmp = new String(buffer, 0, readbytes,
235: "utf-8");
236: manifest.append(tmp);
237: }
238:
239: StringReader stringreader = new StringReader(manifest
240: .toString());
241: BufferedReader reader = new BufferedReader(stringreader);
242: String line = null;
243: StringBuffer modified = new StringBuffer();
244: while ((line = reader.readLine()) != null) {
245: if (line.startsWith("Main-Class:")) {
246: line = "Main-Class: com.izforge.izpack.installer.MultiVolumeInstaller";
247: }
248: modified.append(line);
249: modified.append("\r\n");
250: }
251: reader.close();
252: modifiedmanifest = modified.toString();
253: /*
254: System.out.println("Manifest:");
255: System.out.println(manifest.toString());
256: System.out.println("Modified Manifest:");
257: System.out.println(modified.toString());
258: */
259: break;
260: }
261: }
262:
263: primaryJarStream.putNextEntry(new ZipEntry(
264: "META-INF/MANIFEST.MF"));
265: primaryJarStream.write(modifiedmanifest.getBytes());
266: primaryJarStream.closeEntry();
267: }
268:
269: /**
270: * Write an arbitrary object to primary jar.
271: */
272: protected void writeInstallerObject(String entryName, Object object)
273: throws IOException {
274: primaryJarStream.putNextEntry(new ZipEntry(entryName));
275: ObjectOutputStream out = new ObjectOutputStream(
276: primaryJarStream);
277: out.writeObject(object);
278: out.flush();
279: primaryJarStream.closeEntry();
280: }
281:
282: /** Write the data referenced by URL to primary jar. */
283: protected void writeInstallerResources() throws IOException {
284: sendMsg("Copying " + installerResourceURLMap.size()
285: + " files into installer");
286:
287: for (String s : installerResourceURLMap.keySet()) {
288: String name = s;
289: InputStream in = (installerResourceURLMap.get(name))
290: .openStream();
291:
292: org.apache.tools.zip.ZipEntry newEntry = new org.apache.tools.zip.ZipEntry(
293: name);
294: long dateTime = FileUtil
295: .getFileDateTime(installerResourceURLMap.get(name));
296: if (dateTime != -1) {
297: newEntry.setTime(dateTime);
298: }
299: primaryJarStream.putNextEntry(newEntry);
300:
301: copyStream(in, primaryJarStream);
302: primaryJarStream.closeEntry();
303: in.close();
304: }
305: }
306:
307: /** Copy included jars to primary jar. */
308: protected void writeIncludedJars() throws IOException {
309: sendMsg("Merging " + includedJarURLs.size()
310: + " jars into installer");
311:
312: for (Object[] includedJarURL : includedJarURLs) {
313: Object[] current = includedJarURL;
314: InputStream is = ((URL) current[0]).openStream();
315: ZipInputStream inJarStream = new ZipInputStream(is);
316: copyZip(inJarStream, primaryJarStream,
317: (List<String>) current[1]);
318: }
319: }
320:
321: /**
322: * Write Packs to primary jar or each to a separate jar.
323: */
324: private void writePacks(File primaryfile) throws Exception {
325:
326: final int num = packsList.size();
327: sendMsg("Writing " + num + " Pack" + (num > 1 ? "s" : "")
328: + " into installer");
329: Debug.trace("Writing " + num + " Pack" + (num > 1 ? "s" : "")
330: + " into installer");
331: // Map to remember pack number and bytes offsets of back references
332: Map storedFiles = new HashMap();
333:
334: // First write the serialized files and file metadata data for each pack
335: // while counting bytes.
336:
337: String classname = this .getClass().getName();
338: String volumesize = this .getVariables().getProperty(
339: classname + ".volumesize");
340: String extraspace = this .getVariables().getProperty(
341: classname + ".firstvolumefreespace");
342:
343: long volumesizel = FileSpanningOutputStream.DEFAULT_VOLUME_SIZE;
344: long extraspacel = FileSpanningOutputStream.DEFAULT_ADDITIONAL_FIRST_VOLUME_FREE_SPACE_SIZE;
345:
346: if (volumesize != null) {
347: volumesizel = Long.parseLong(volumesize);
348: }
349: if (extraspace != null) {
350: extraspacel = Long.parseLong(extraspace);
351: }
352: Debug.trace("Volumesize: " + volumesizel);
353: Debug.trace("Extra space on first volume: " + extraspacel);
354: FileSpanningOutputStream fout = new FileSpanningOutputStream(
355: primaryfile.getParent() + File.separator
356: + primaryfile.getName() + ".pak", volumesizel);
357: fout.setFirstvolumefreespacesize(extraspacel);
358:
359: int packNumber = 0;
360: for (PackInfo aPacksList : packsList) {
361: PackInfo packInfo = aPacksList;
362: Pack pack = packInfo.getPack();
363: pack.nbytes = 0;
364:
365: sendMsg("Writing Pack " + packNumber + ": " + pack.name,
366: PackagerListener.MSG_VERBOSE);
367: Debug
368: .trace("Writing Pack " + packNumber + ": "
369: + pack.name);
370: ZipEntry entry = new ZipEntry("packs/pack" + packNumber);
371: // write the metadata as uncompressed object stream to primaryJarStream
372: // first write a packs entry
373:
374: primaryJarStream.putNextEntry(entry);
375: ObjectOutputStream objOut = new ObjectOutputStream(
376: primaryJarStream);
377:
378: // We write the actual pack files
379: objOut.writeInt(packInfo.getPackFiles().size());
380:
381: Iterator iter = packInfo.getPackFiles().iterator();
382: for (Object o : packInfo.getPackFiles()) {
383: boolean addFile = !pack.loose;
384: XPackFile pf = new XPackFile((PackFile) o);
385: File file = packInfo.getFile(pf.getPackfile());
386: Debug.trace("Next file: " + file.getAbsolutePath());
387: // use a back reference if file was in previous pack, and in
388: // same jar
389: Object[] info = (Object[]) storedFiles.get(file);
390: if (info != null && !packJarsSeparate) {
391: Debug.trace("File already included in other pack");
392: pf.setPreviousPackFileRef((String) info[0],
393: (Long) info[1]);
394: addFile = false;
395: }
396:
397: if (addFile && !pf.isDirectory()) {
398: long pos = fout.getFilepointer();
399:
400: pf.setArchivefileposition(pos);
401:
402: // write out the filepointer
403: int volumecountbeforewrite = fout.getVolumeCount();
404:
405: FileInputStream inStream = new FileInputStream(file);
406: long bytesWritten = copyStream(inStream, fout);
407: fout.flush();
408:
409: long posafterwrite = fout.getFilepointer();
410: Debug.trace("File (" + pf.sourcePath + ") " + pos
411: + " <-> " + posafterwrite);
412:
413: if (fout.getFilepointer() != (pos + bytesWritten)) {
414: Debug.trace("file: " + file.getName());
415: Debug
416: .trace("(Filepos/BytesWritten/ExpectedNewFilePos/NewFilePointer) ("
417: + pos
418: + "/"
419: + bytesWritten
420: + "/"
421: + (pos + bytesWritten)
422: + "/"
423: + fout.getFilepointer()
424: + ")");
425: Debug.trace("Volumecount (before/after) ("
426: + volumecountbeforewrite + "/"
427: + fout.getVolumeCount() + ")");
428: throw new IOException(
429: "Error new filepointer is illegal");
430: }
431:
432: if (bytesWritten != pf.length()) {
433: throw new IOException(
434: "File size mismatch when reading "
435: + file);
436: }
437: inStream.close();
438: // keine backreferences mglich
439: // storedFiles.put(file, new long[] { packNumber, pos});
440: }
441:
442: objOut.writeObject(pf); // base info
443: objOut.flush(); // make sure it is written
444: // even if not written, it counts towards pack size
445: pack.nbytes += pf.length();
446: }
447: // Write out information about parsable files
448: objOut.writeInt(packInfo.getParsables().size());
449: iter = packInfo.getParsables().iterator();
450: while (iter.hasNext()) {
451: objOut.writeObject(aPacksList);
452: }
453:
454: // Write out information about executable files
455: objOut.writeInt(packInfo.getExecutables().size());
456: iter = packInfo.getExecutables().iterator();
457: while (iter.hasNext()) {
458: objOut.writeObject(aPacksList);
459: }
460:
461: // Write out information about updatecheck files
462: objOut.writeInt(packInfo.getUpdateChecks().size());
463: iter = packInfo.getUpdateChecks().iterator();
464: while (iter.hasNext()) {
465: objOut.writeObject(aPacksList);
466: }
467:
468: // Cleanup
469: objOut.flush();
470: packNumber++;
471: }
472:
473: // write metadata for reading in volumes
474: int volumes = fout.getVolumeCount();
475: Debug.trace("Written " + volumes + " volumes");
476: String volumename = primaryfile.getName() + ".pak";
477:
478: fout.flush();
479: fout.close();
480:
481: primaryJarStream.putNextEntry(new ZipEntry("volumes.info"));
482: ObjectOutputStream out = new ObjectOutputStream(
483: primaryJarStream);
484: out.writeInt(volumes);
485: out.writeUTF(volumename);
486: out.flush();
487: primaryJarStream.closeEntry();
488:
489: // Now that we know sizes, write pack metadata to primary jar.
490: primaryJarStream.putNextEntry(new ZipEntry("packs.info"));
491: out = new ObjectOutputStream(primaryJarStream);
492: out.writeInt(packsList.size());
493:
494: for (PackInfo aPacksList : packsList) {
495: PackInfo pack = aPacksList;
496: out.writeObject(pack.getPack());
497: }
498: out.flush();
499: primaryJarStream.closeEntry();
500: }
501:
502: /***********************************************************************************************
503: * Stream utilites for creation of the installer.
504: **********************************************************************************************/
505:
506: /** Return a stream for the next jar. */
507: private ZipOutputStream getJarOutputStream(String name)
508: throws IOException {
509: File file = new File(baseFile.getParentFile(), name);
510: sendMsg("Building installer jar: " + file.getAbsolutePath());
511: Debug
512: .trace("Building installer jar: "
513: + file.getAbsolutePath());
514: ZipOutputStream jar = new ZipOutputStream(new FileOutputStream(
515: file));
516: jar.setLevel(Deflater.BEST_COMPRESSION);
517: // jar.setPreventClose(true); // Needed at using FilterOutputStreams which
518: // calls close
519: // of the slave at finalizing.
520:
521: return jar;
522: }
523:
524: /**
525: * Copies specified contents of one jar to another.
526: *
527: * <p>
528: * TODO: it would be useful to be able to keep signature information from signed jar files, can
529: * we combine manifests and still have their content signed?
530: *
531: * @see #copyStream(InputStream, OutputStream)
532: */
533: private void copyZip(ZipInputStream zin, ZipOutputStream out,
534: List<String> files) throws IOException {
535: java.util.zip.ZipEntry zentry;
536: if (!alreadyWrittenFiles.containsKey(out))
537: alreadyWrittenFiles.put(out, new HashSet<String>());
538: HashSet<String> currentSet = alreadyWrittenFiles.get(out);
539: while ((zentry = zin.getNextEntry()) != null) {
540: String currentName = zentry.getName();
541: String testName = currentName.replace('/', '.');
542: testName = testName.replace('\\', '.');
543: if (files != null) {
544: Iterator<String> i = files.iterator();
545: boolean founded = false;
546: while (i.hasNext()) { // Make "includes" self to support regex.
547: String doInclude = i.next();
548: if (testName.matches(doInclude)) {
549: founded = true;
550: break;
551: }
552: }
553: if (!founded)
554: continue;
555: }
556: if (currentSet.contains(currentName))
557: continue;
558: try {
559: // Create new entry for zip file.
560: ZipEntry newEntry = new ZipEntry(currentName);
561: // Get input file date and time.
562: long fileTime = zentry.getTime();
563: // Make sure there is date and time set.
564: if (fileTime != -1)
565: newEntry.setTime(fileTime); // If found set it into output file.
566: out.putNextEntry(newEntry);
567:
568: copyStream(zin, out);
569: out.closeEntry();
570: zin.closeEntry();
571: currentSet.add(currentName);
572: } catch (ZipException x) {
573: // This avoids any problem that can occur with duplicate
574: // directories. for instance all META-INF data in jars
575: // unfortunately this do not work with the apache ZipOutputStream...
576: }
577: }
578: }
579:
580: /**
581: * Copies specified contents of one jar to another without the specified files
582: *
583: * <p>
584: * TODO: it would be useful to be able to keep signature information from signed jar files, can
585: * we combine manifests and still have their content signed?
586: *
587: * @see #copyStream(InputStream, OutputStream)
588: */
589: private void copyZipWithoutExcludes(ZipInputStream zin,
590: ZipOutputStream out, List<String> excludes)
591: throws IOException {
592: java.util.zip.ZipEntry zentry;
593: if (!alreadyWrittenFiles.containsKey(out))
594: alreadyWrittenFiles.put(out, new HashSet<String>());
595: HashSet<String> currentSet = alreadyWrittenFiles.get(out);
596: while ((zentry = zin.getNextEntry()) != null) {
597: String currentName = zentry.getName();
598: String testName = currentName.replace('/', '.');
599: testName = testName.replace('\\', '.');
600: if (excludes != null) {
601: Iterator<String> i = excludes.iterator();
602: boolean skip = false;
603: while (i.hasNext()) {
604: // Make "excludes" self to support regex.
605: String doExclude = i.next();
606: if (testName.matches(doExclude)) {
607: skip = true;
608: break;
609: }
610: }
611: if (skip) {
612: continue;
613: }
614: }
615: if (currentSet.contains(currentName))
616: continue;
617: try {
618: // Create new entry for zip file.
619: ZipEntry newEntry = new ZipEntry(currentName);
620: // Get input file date and time.
621: long fileTime = zentry.getTime();
622: // Make sure there is date and time set.
623: if (fileTime != -1)
624: newEntry.setTime(fileTime); // If found set it into output file.
625: out.putNextEntry(newEntry);
626:
627: copyStream(zin, out);
628: out.closeEntry();
629: zin.closeEntry();
630: currentSet.add(currentName);
631: } catch (ZipException x) {
632: // This avoids any problem that can occur with duplicate
633: // directories. for instance all META-INF data in jars
634: // unfortunately this do not work with the apache ZipOutputStream...
635: }
636: }
637: }
638:
639: /**
640: * Copies all the data from the specified input stream to the specified output stream.
641: *
642: * @param in the input stream to read
643: * @param out the output stream to write
644: * @return the total number of bytes copied
645: * @exception IOException if an I/O error occurs
646: */
647: private long copyStream(InputStream in, OutputStream out)
648: throws IOException {
649: byte[] buffer = new byte[5120];
650: long bytesCopied = 0;
651: int bytesInBuffer;
652: while ((bytesInBuffer = in.read(buffer)) != -1) {
653: out.write(buffer, 0, bytesInBuffer);
654: bytesCopied += bytesInBuffer;
655: }
656: return bytesCopied;
657: }
658:
659: /* (non-Javadoc)
660: * @see com.izforge.izpack.compiler.IPackager#addConfigurationInformation(net.n3.nanoxml.XMLElement)
661: */
662: public void addConfigurationInformation(XMLElement data) {
663: this .configdata = data;
664: }
665:
666: /* (non-Javadoc)
667: * @see com.izforge.izpack.compiler.PackagerBase#writePacks()
668: */
669: protected void writePacks() throws Exception {
670: // TODO Auto-generated method stub
671:
672: }
673:
674: }
|