001/* 002 * $Id: Packager.java,v 1.40 2005/08/26 06:48:50 bartzkau Exp $ 003 * IzPack - Copyright 2001-2005 Julien Ponge, All Rights Reserved. 004 * 005 * http://www.izforge.com/izpack/ 006 * http://developer.berlios.de/projects/izpack/ 007 * 008 * Licensed under the Apache License, Version 2.0 (the "License"); 009 * you may not use this file except in compliance with the License. 010 * You may obtain a copy of the License at 011 * 012 * http://www.apache.org/licenses/LICENSE-2.0 013 * 014 * Unless required by applicable law or agreed to in writing, software 015 * distributed under the License is distributed on an "AS IS" BASIS, 016 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 017 * See the License for the specific language governing permissions and 018 * limitations under the License. 019 */ 020 021package com.izforge.izpack.compiler; 022 023import java.io.File; 024import java.io.FileInputStream; 025import java.io.IOException; 026import java.io.InputStream; 027import java.io.ObjectOutputStream; 028import java.io.OutputStream; 029import java.net.URL; 030import java.util.ArrayList; 031import java.util.HashMap; 032import java.util.HashSet; 033import java.util.Iterator; 034import java.util.List; 035import java.util.Map; 036import java.util.Properties; 037import java.util.Set; 038import java.util.zip.Deflater; 039import java.util.zip.ZipException; 040import java.util.zip.ZipInputStream; 041 042// The declarations for ZipOutputStreams will be done 043// as full qualified to clear at the use point that 044// we do not use the standard class else the extended 045// from apache. 046//import org.apache.tools.zip.ZipOutputStream; 047//import org.apache.tools.zip.ZipEntry; 048 049import com.izforge.izpack.CustomData; 050import com.izforge.izpack.GUIPrefs; 051import com.izforge.izpack.Info; 052import com.izforge.izpack.Pack; 053import com.izforge.izpack.PackFile; 054import com.izforge.izpack.Panel; 055import com.izforge.izpack.compressor.PackCompressor; 056import com.izforge.izpack.compressor.PackCompressorFactory; 057//import com.izforge.izpack.util.JarOutputStream; 058 059/** 060 * The packager class. The packager is used by the compiler to put files into an installer, and 061 * create the actual installer files. 062 * 063 * @author Julien Ponge 064 * @author Chadwick McHenry 065 */ 066public class Packager 067{ 068 069 /** Path to the skeleton installer. */ 070 public static final String SKELETON_SUBPATH = "lib/installer.jar"; 071 072 /** Base file name of all jar files. This has no ".jar" suffix. */ 073 private File baseFile = null; 074 075 /** Executable zipped output stream. First to open, last to close. 076 * Attention! This is our own JarOutputStream, not the java standard! */ 077 private com.izforge.izpack.util.JarOutputStream primaryJarStream; 078 079 /** Basic installer info. */ 080 private Info info = null; 081 082 /** Gui preferences of instatller. */ 083 private GUIPrefs guiPrefs = null; 084 085 /** The variables used in the project */ 086 private Properties variables = new Properties(); 087 088 /** The ordered panels informations. */ 089 private List panelList = new ArrayList(); 090 091 /** The ordered packs informations (as PackInfo objects). */ 092 private List packsList = new ArrayList(); 093 094 /** The ordered langpack ISO3 names. */ 095 private List langpackNameList = new ArrayList(); 096 097 /** The ordered custom actions informations. */ 098 private List customDataList = new ArrayList(); 099 100 /** The langpack URLs keyed by ISO3 name. */ 101 private Map installerResourceURLMap = new HashMap(); 102 103 /** Jar file URLs who's contents will be copied into the installer. */ 104 private Set includedJarURLs = new HashSet(); 105 106 /** Each pack is created in a separte jar if webDirURL is non-null. */ 107 private boolean packJarsSeparate = false; 108 109 /** The listeners. */ 110 private PackagerListener listener; 111 112 /** The compression format to be used for pack compression */ 113 private PackCompressor compressor; 114 115 /** Files which are always written into the container file */ 116 private HashMap alreadyWrittenFiles = new HashMap(); 117 /** The constructor. 118 * @throws CompilerException*/ 119 public Packager() throws CompilerException 120 { 121 this("default"); 122 } 123 124 /** 125 * Extended constructor. 126 * @param compr_format Compression format to be used for packs 127 * compression format (if supported) 128 * @throws CompilerException 129 */ 130 public Packager(String compr_format) throws CompilerException 131 { 132 this( compr_format, -1); 133 } 134 135 /** 136 * Extended constructor. 137 * @param compr_format Compression format to be used for packs 138 * @param compr_level Compression level to be used with the chosen 139 * compression format (if supported) 140 * @throws CompilerException 141 */ 142 public Packager(String compr_format, int compr_level) throws CompilerException 143 { 144 compressor = PackCompressorFactory.get( compr_format); 145 compressor.setCompressionLevel(compr_level); 146 } 147 148 /** 149 * Create the installer, beginning with the specified jar. If the name specified does not end in 150 * ".jar", it is appended. If secondary jars are created for packs (if the Info object added has 151 * a webDirURL set), they are created in the same directory, named sequentially by inserting 152 * ".pack#" (where '#' is the pack number) ".jar" suffix: e.g. "foo.pack1.jar". If any file 153 * exists, it is overwritten. 154 */ 155 public void createInstaller(File primaryFile) throws Exception 156 { 157 // preliminary work 158 String baseName = primaryFile.getName(); 159 if (baseName.endsWith(".jar")) 160 { 161 baseName = baseName.substring(0, baseName.length() - 4); 162 baseFile = new File(primaryFile.getParentFile(), baseName); 163 } 164 else 165 baseFile = primaryFile; 166 167 info.setInstallerBase(baseFile.getName()); 168 packJarsSeparate = (info.getWebDirURL() != null); 169 170 // primary (possibly only) jar. -1 indicates primary 171 primaryJarStream = getJarOutputStream(baseFile.getName() + ".jar"); 172 173 sendStart(); 174 175 // write the primary jar. MUST be first so manifest is not overwritten 176 // by 177 // an included jar 178 writeSkeletonInstaller(); 179 180 writeInstallerObject("info", info); 181 writeInstallerObject("vars", variables); 182 writeInstallerObject("GUIPrefs", guiPrefs); 183 writeInstallerObject("panelsOrder", panelList); 184 writeInstallerObject("customData", customDataList); 185 writeInstallerObject("langpacks.info", langpackNameList); 186 writeInstallerResources(); 187 writeIncludedJars(); 188 189 // Pack File Data may be written to separate jars 190 writePacks(); 191 192 // Finish up. closeAlways is a hack for pack compressions other than 193 // default. Some of it (e.g. BZip2) closes the slave of it also. 194 // But this should not be because the jar stream should be open 195 // for the next pack. Therefore an own JarOutputStream will be used 196 // which close method will be blocked. 197 primaryJarStream.closeAlways(); 198 199 sendStop(); 200 } 201 202 /*********************************************************************************************** 203 * Listener assistance 204 **********************************************************************************************/ 205 206 /** 207 * Get the PackagerListener. 208 * @return the current PackagerListener 209 */ 210 public PackagerListener getPackagerListener() 211 { 212 return listener; 213 } 214 /** 215 * Adds a listener. 216 * 217 * @param listener The listener. 218 */ 219 public void setPackagerListener(PackagerListener listener) 220 { 221 this.listener = listener; 222 } 223 224 /** 225 * Dispatches a message to the listeners. 226 * 227 * @param job The job description. 228 */ 229 private void sendMsg(String job) 230 { 231 sendMsg(job, PackagerListener.MSG_INFO); 232 } 233 234 /** 235 * Dispatches a message to the listeners at specified priority. 236 * 237 * @param job The job description. 238 * @param priority The message priority. 239 */ 240 private void sendMsg(String job, int priority) 241 { 242 if (listener != null) listener.packagerMsg(job, priority); 243 } 244 245 /** Dispatches a start event to the listeners. */ 246 private void sendStart() 247 { 248 if (listener != null) listener.packagerStart(); 249 } 250 251 /** Dispatches a stop event to the listeners. */ 252 private void sendStop() 253 { 254 if (listener != null) listener.packagerStop(); 255 } 256 257 /*********************************************************************************************** 258 * Public methods to add data to the Installer being packed 259 **********************************************************************************************/ 260 261 /** 262 * Sets the informations related to this installation. 263 * 264 * @param info The info section. 265 * @exception Exception Description of the Exception 266 */ 267 public void setInfo(Info info) throws Exception 268 { 269 sendMsg("Setting the installer information", PackagerListener.MSG_VERBOSE); 270 this.info = info; 271 if( ! getCompressor().useStandardCompression() && 272 getCompressor().getDecoderMapperName() != null ) 273 { 274 this.info.setPackDecoderClassName(getCompressor().getDecoderMapperName()); 275 } 276 } 277 278 /** 279 * Sets the GUI preferences. 280 * 281 * @param prefs The new gUIPrefs value 282 * @exception Exception Description of the Exception 283 */ 284 public void setGUIPrefs(GUIPrefs prefs) 285 { 286 sendMsg("Setting the GUI preferences", PackagerListener.MSG_VERBOSE); 287 guiPrefs = prefs; 288 } 289 290 /** 291 * Allows access to add, remove and update the variables for the project, which are maintained 292 * in the packager. 293 * 294 * @return map of variable names to values 295 */ 296 public Properties getVariables() 297 { 298 return variables; 299 } 300 301 /** 302 * Add a panel, where order is important. Only one copy of the class files neeed are inserted in 303 * the installer. 304 */ 305 public void addPanelJar(Panel panel, URL jarURL) 306 { 307 panelList.add(panel); // serialized to keep order/variables correct 308 addJarContent(jarURL); // each included once, no matter how many times 309 // added 310 } 311 312 /** 313 * Add a custom data like custom actions, where order is important. Only one copy of the class 314 * files neeed are inserted in the installer. 315 * 316 * @param ca custom action object 317 * @param url the URL to include once 318 */ 319 public void addCustomJar(CustomData ca, URL url) 320 { 321 customDataList.add(ca); // serialized to keep order/variables correct 322 addJarContent(url); // each included once, no matter how many times 323 // added 324 } 325 326 /** 327 * Adds a pack, order is mostly irrelevant. 328 * 329 * @param pack contains all the files and items that go with a pack 330 */ 331 public void addPack(PackInfo pack) 332 { 333 packsList.add(pack); 334 } 335 336 /** 337 * Gets the packages list 338 */ 339 public List getPacksList() 340 { 341 return packsList; 342 } 343 344 /** 345 * Adds a language pack. 346 * 347 * @param iso3 The ISO3 code. 348 * @param xmlURL The location of the xml local info 349 * @param flagURL The location of the flag image resource 350 * @exception Exception Description of the Exception 351 */ 352 public void addLangPack(String iso3, URL xmlURL, URL flagURL) 353 { 354 sendMsg("Adding langpack: " + iso3, PackagerListener.MSG_VERBOSE); 355 // put data & flag as entries in installer, and keep array of iso3's 356 // names 357 langpackNameList.add(iso3); 358 addResource("flag." + iso3, flagURL); 359 installerResourceURLMap.put("langpacks/" + iso3 + ".xml", xmlURL); 360 } 361 362 /** 363 * Adds a resource. 364 * 365 * @param resId The resource Id. 366 * @param url The location of the data 367 * @exception Exception Description of the Exception 368 */ 369 public void addResource(String resId, URL url) 370 { 371 sendMsg("Adding resource: " + resId, PackagerListener.MSG_VERBOSE); 372 installerResourceURLMap.put("res/" + resId, url); 373 } 374 375 /** 376 * Adds a native library. 377 * 378 * @param name The native library name. 379 * @param url The url to get the data from. 380 * @exception Exception Description of the Exception 381 */ 382 public void addNativeLibrary(String name, URL url) throws Exception 383 { 384 sendMsg("Adding native library: " + name, PackagerListener.MSG_VERBOSE); 385 installerResourceURLMap.put("native/" + name, url); 386 } 387 388 389 /** 390 * Adds a jar file content to the installer. Package structure is maintained. Need mechanism to 391 * copy over signed entry information. 392 * 393 * @param jarURL The url of the jar to add to the installer. We use a URL so the jar may be 394 * nested within another. 395 */ 396 public void addJarContent(URL jarURL) 397 { 398 addJarContent(jarURL, null); 399 } 400 /** 401 * Adds a jar file content to the installer. Package structure is maintained. Need mechanism to 402 * copy over signed entry information. 403 * 404 * @param jarURL The url of the jar to add to the installer. We use a URL so the jar may be 405 * nested within another. 406 */ 407 public void addJarContent(URL jarURL, List files) 408 { 409 Object [] cont = { jarURL, files }; 410 sendMsg("Adding content of jar: " + jarURL.getFile(), PackagerListener.MSG_VERBOSE); 411 includedJarURLs.add(cont); 412 } 413 414 /** 415 * Marks a native library to be added to the uninstaller. 416 * 417 * @param data the describing custom action data object 418 */ 419 public void addNativeUninstallerLibrary(CustomData data) 420 { 421 customDataList.add(data); // serialized to keep order/variables 422 // correct 423 424 } 425 426 /*********************************************************************************************** 427 * Private methods used when writing out the installer to jar files. 428 **********************************************************************************************/ 429 430 /** 431 * Write skeleton installer to primary jar. It is just an included jar, except that we copy the 432 * META-INF as well. 433 */ 434 private void writeSkeletonInstaller() throws IOException 435 { 436 sendMsg("Copying the skeleton installer", PackagerListener.MSG_VERBOSE); 437 438 InputStream is = Packager.class.getResourceAsStream("/" + SKELETON_SUBPATH); 439 if (is == null) 440 { 441 File skeleton = new File(Compiler.IZPACK_HOME, SKELETON_SUBPATH); 442 is = new FileInputStream(skeleton); 443 } 444 ZipInputStream inJarStream = new ZipInputStream(is); 445 copyZip(inJarStream, primaryJarStream); 446 } 447 448 /** 449 * Write an arbitrary object to primary jar. 450 */ 451 private void writeInstallerObject(String entryName, Object object) throws IOException 452 { 453 primaryJarStream.putNextEntry(new org.apache.tools.zip.ZipEntry(entryName)); 454 ObjectOutputStream out = new ObjectOutputStream(primaryJarStream); 455 out.writeObject(object); 456 out.flush(); 457 primaryJarStream.closeEntry(); 458 } 459 460 /** Write the data referenced by URL to primary jar. */ 461 private void writeInstallerResources() throws IOException 462 { 463 sendMsg("Copying " + installerResourceURLMap.size() + " files into installer"); 464 465 Iterator i = installerResourceURLMap.keySet().iterator(); 466 while (i.hasNext()) 467 { 468 String name = (String) i.next(); 469 InputStream in = ((URL) installerResourceURLMap.get(name)).openStream(); 470 primaryJarStream.putNextEntry(new org.apache.tools.zip.ZipEntry(name)); 471 copyStream(in, primaryJarStream); 472 primaryJarStream.closeEntry(); 473 in.close(); 474 } 475 } 476 477 /** Copy included jars to primary jar. */ 478 private void writeIncludedJars() throws IOException 479 { 480 sendMsg("Merging " + includedJarURLs.size() + " jars into installer"); 481 482 Iterator i = includedJarURLs.iterator(); 483 while (i.hasNext()) 484 { 485 Object [] current = (Object []) i.next(); 486 InputStream is = ((URL) current[0]).openStream(); 487 ZipInputStream inJarStream = new ZipInputStream(is); 488 copyZip(inJarStream, primaryJarStream, (List) current[1]); 489 } 490 } 491 492 /** 493 * Write Packs to primary jar or each to a separate jar. 494 */ 495 private void writePacks() throws Exception 496 { 497 final int num = packsList.size(); 498 sendMsg("Writing " + num + " Pack" + (num > 1 ? "s" : "") + " into installer"); 499 500 // Map to remember pack number and bytes offsets of back references 501 Map storedFiles = new HashMap(); 502 503 // First write the serialized files and file metadata data for each pack 504 // while counting bytes. 505 506 int packNumber = 0; 507 Iterator packIter = packsList.iterator(); 508 while (packIter.hasNext()) 509 { 510 PackInfo packInfo = (PackInfo) packIter.next(); 511 Pack pack = packInfo.getPack(); 512 pack.nbytes = 0; 513 514 // create a pack specific jar if required 515 com.izforge.izpack.util.JarOutputStream packStream = primaryJarStream; 516 if (packJarsSeparate) 517 { 518 // See installer.Unpacker#getPackAsStream for the counterpart 519 String name = baseFile.getName() + ".pack" + packNumber + ".jar"; 520 packStream = getJarOutputStream(name); 521 } 522 OutputStream comprStream = packStream; 523 524 sendMsg("Writing Pack " + packNumber + ": " + pack.name, PackagerListener.MSG_VERBOSE); 525 526 // Retrieve the correct output stream 527 org.apache.tools.zip.ZipEntry entry = 528 new org.apache.tools.zip.ZipEntry("packs/pack" + packNumber); 529 if( ! compressor.useStandardCompression()) 530 { 531 entry.setMethod(org.apache.tools.zip.ZipEntry.STORED); 532 entry.setComment(compressor.getCompressionFormatSymbols()[0]); 533 // We must set the entry before we get the compressed stream 534 // because some writes initialize data (e.g. bzip2). 535 packStream.putNextEntry(entry); 536 packStream.flush(); // flush before we start counting 537 comprStream = compressor.getOutputStream(packStream); 538 } 539 else 540 { 541 int level = compressor.getCompressionLevel(); 542 if( level >= 0 && level < 10 ) 543 packStream.setLevel(level); 544 packStream.putNextEntry(entry); 545 packStream.flush(); // flush before we start counting 546 } 547 548 ByteCountingOutputStream dos = new ByteCountingOutputStream(comprStream); 549 ObjectOutputStream objOut = new ObjectOutputStream(dos); 550 551 // We write the actual pack files 552 objOut.writeInt(packInfo.getPackFiles().size()); 553 554 Iterator iter = packInfo.getPackFiles().iterator(); 555 while (iter.hasNext()) 556 { 557 boolean addFile = !pack.loose; 558 PackFile pf = (PackFile) iter.next(); 559 File file = packInfo.getFile(pf); 560 561 // use a back reference if file was in previous pack, and in 562 // same jar 563 long[] info = (long[]) storedFiles.get(file); 564 if (info != null && !packJarsSeparate) 565 { 566 pf.setPreviousPackFileRef((int) info[0], info[1]); 567 addFile = false; 568 } 569 570 objOut.writeObject(pf); // base info 571 objOut.flush(); // make sure it is written 572 573 if (addFile && !pf.isDirectory()) 574 { 575 long pos = dos.getByteCount(); // get the position 576 577 FileInputStream inStream = new FileInputStream(file); 578 long bytesWritten = copyStream(inStream, objOut); 579 580 if (bytesWritten != pf.length()) 581 throw new IOException("File size mismatch when reading " + file); 582 583 inStream.close(); 584 storedFiles.put(file, new long[] { packNumber, pos}); 585 } 586 587 // even if not written, it counts towards pack size 588 pack.nbytes += pf.length(); 589 } 590 591 // Write out information about parsable files 592 objOut.writeInt(packInfo.getParsables().size()); 593 iter = packInfo.getParsables().iterator(); 594 while (iter.hasNext()) 595 objOut.writeObject(iter.next()); 596 597 // Write out information about executable files 598 objOut.writeInt(packInfo.getExecutables().size()); 599 iter = packInfo.getExecutables().iterator(); 600 while (iter.hasNext()) 601 objOut.writeObject(iter.next()); 602 603 // Write out information about updatecheck files 604 objOut.writeInt(packInfo.getUpdateChecks().size()); 605 iter = packInfo.getUpdateChecks().iterator(); 606 while (iter.hasNext()) 607 objOut.writeObject(iter.next()); 608 609 // Cleanup 610 objOut.flush(); 611 if( ! compressor.useStandardCompression()) 612 { 613 comprStream.close(); 614 } 615 616 packStream.closeEntry(); 617 618 // close pack specific jar if required 619 if (packJarsSeparate) packStream.closeAlways(); 620 621 packNumber++; 622 } 623 624 // Now that we know sizes, write pack metadata to primary jar. 625 primaryJarStream.putNextEntry(new org.apache.tools.zip.ZipEntry("packs.info")); 626 ObjectOutputStream out = new ObjectOutputStream(primaryJarStream); 627 out.writeInt(packsList.size()); 628 629 Iterator i = packsList.iterator(); 630 while (i.hasNext()) 631 { 632 PackInfo pack = (PackInfo) i.next(); 633 out.writeObject(pack.getPack()); 634 } 635 out.flush(); 636 primaryJarStream.closeEntry(); 637 } 638 639 /*********************************************************************************************** 640 * Stream utilites for creation of the installer. 641 **********************************************************************************************/ 642 643 /** Return a stream for the next jar. */ 644 private com.izforge.izpack.util.JarOutputStream getJarOutputStream(String name) throws IOException 645 { 646 File file = new File(baseFile.getParentFile(), name); 647 sendMsg("Building installer jar: " + file.getAbsolutePath()); 648 649 com.izforge.izpack.util.JarOutputStream jar = 650 new com.izforge.izpack.util.JarOutputStream(file); 651 jar.setLevel(Deflater.BEST_COMPRESSION); 652 jar.setPreventClose(true); // Needed at using FilterOutputStreams which calls close 653 // of the slave at finalizing. 654 655 return jar; 656 } 657 658 /** 659 * Copies contents of one jar to another. 660 * 661 * <p> 662 * TODO: it would be useful to be able to keep signature information from signed jar files, can 663 * we combine manifests and still have their content signed? 664 * 665 * @see #copyStream(InputStream, OutputStream) 666 */ 667 private void copyZip(ZipInputStream zin, org.apache.tools.zip.ZipOutputStream out) throws IOException 668 { 669 copyZip( zin, out, null ); 670 } 671 672 /** 673 * Copies specified contents of one jar to another. 674 * 675 * <p> 676 * TODO: it would be useful to be able to keep signature information from signed jar files, can 677 * we combine manifests and still have their content signed? 678 * 679 * @see #copyStream(InputStream, OutputStream) 680 */ 681 private void copyZip(ZipInputStream zin, org.apache.tools.zip.ZipOutputStream out, 682 List files) 683 throws IOException 684 { 685 java.util.zip.ZipEntry zentry; 686 if( ! alreadyWrittenFiles.containsKey( out )) 687 alreadyWrittenFiles.put(out, new HashSet()); 688 HashSet currentSet = (HashSet) alreadyWrittenFiles.get(out); 689 while ((zentry = zin.getNextEntry()) != null) 690 { 691 String currentName = zentry.getName(); 692 String testName = currentName.replace('/', '.'); 693 testName = testName.replace('\\', '.'); 694 if( files != null ) 695 { 696 Iterator i = files.iterator(); 697 boolean founded = false; 698 while( i.hasNext()) 699 { // Make "includes" self to support regex. 700 String doInclude = (String) i.next(); 701 if( testName.matches( doInclude ) ) 702 { 703 founded = true; 704 break; 705 } 706 } 707 if( ! founded ) 708 continue; 709 } 710 if( currentSet.contains(currentName)) 711 continue; 712 try 713 { 714 out.putNextEntry(new org.apache.tools.zip.ZipEntry(currentName)); 715 copyStream(zin, out); 716 out.closeEntry(); 717 zin.closeEntry(); 718 currentSet.add(currentName); 719 } 720 catch (ZipException x) 721 { 722 // This avoids any problem that can occur with duplicate 723 // directories. for instance all META-INF data in jars 724 // unfortunately this do not work with the apache ZipOutputStream... 725 } 726 } 727 } 728 729 /** 730 * Copies all the data from the specified input stream to the specified output stream. 731 * 732 * @param in the input stream to read 733 * @param out the output stream to write 734 * @return the total number of bytes copied 735 * @exception IOException if an I/O error occurs 736 */ 737 private long copyStream(InputStream in, OutputStream out) throws IOException 738 { 739 byte[] buffer = new byte[5120]; 740 long bytesCopied = 0; 741 int bytesInBuffer; 742 while ((bytesInBuffer = in.read(buffer)) != -1) 743 { 744 out.write(buffer, 0, bytesInBuffer); 745 bytesCopied += bytesInBuffer; 746 } 747 return bytesCopied; 748 } 749 /** 750 * Returns the current pack compressor 751 * @return Returns the current pack compressor. 752 */ 753 public PackCompressor getCompressor() 754 { 755 return compressor; 756 } 757}