Modifying archives, Part 2: The Archive class

The Archive class allows you to write or modify stored archive files

1 2 3 4 Page 4
Page 4 of 4
 370:   private class Archive_OutputStream extends OutputStream
 371:     {
 372:       private final   ZipEntry     entry;
 373:       private final   CRC32        crc  = new CRC32();
 374: 
 375:       private         OutputStream        out;
 376:       private         DelayedOutputStream stream;
 377: 
 378:       public Archive_OutputStream( ZipEntry entry ) throws IOException
 379:         {   this.entry  = entry;
 380: 
 381:             destination.setMethod( entry.getMethod()  );
 382: 
 383:             if( entry.getMethod() == ZipEntry.DEFLATED )
 384:             {   destination.putNextEntry( entry );
 385:                 out = destination;
 386:             }
 387:             else
 388:             {   stream = new DelayedOutputStream("Archive", ".tmp");
 389:                 out    = new FastBufferedOutputStream( stream );
 390:             }
 391: 
 392:             D.ebug("\t\tOpened " + entry.getComment() + " stream" );
 393:         }
 394: 
 395:       public void flush(){ /* meaningless in context */ }
 396: 
 397:       public void write(int the_byte) throws IOException
 398:         {   
 399:             // The other variants of write are inherited from
 400:             // OutputStream, and will call the current version.
 401: 
 402:             crc.update( the_byte );
 403:             out.write ( the_byte );
 404:         }
 405: 
 406:       public void close() throws IOException
 407:         {   
 408:             if( entry.getMethod() == ZipEntry.DEFLATED )
 409:             {
 410:                 D.ebug("\t\tClosing compressed stream. crc="
 411:                                     + entry.getCrc()
 412:                                     + " size="
 413:                                     + entry.getSize() );
 414:             }
 415:             else
 416:             {
 417:                 FastBufferedOutputStream buffer_stream =
 418:                                         (FastBufferedOutputStream)out;
 419: 
 420:                 entry.setCrc( crc.getValue() );
 421:                 entry.setSize( buffer_stream.bytes_written() );
 422: 
 423:                 destination.putNextEntry( entry );
 424: 
 425:                 D.ebug("\t\tClosing stored stream. crc="
 426:                                         + entry.getCrc()
 427:                                         + " size="
 428:                                         + entry.getSize() );
 429: 
 430: 
 431:                 // Transfer data from the buffer to the zip file
 432: 
 433:                 if( buffer_stream.export_buffer_and_close(destination) )
 434:                     D.ebug("\t\t\tGot data from internal buffer");
 435:                 else
 436:                 {   
 437:                     D.ebug("\t\tCopying from temporary file");
 438: 
 439:                     // If we get here, then the data couldn't be
 440:                     // transferred from the internal buffer to the
 441:                     // destination archive because the file was
 442:                     // large enough that the whole file wasn't
 443:                     // contained in the in-memory buffer.
 444: 
 445:                     InputStream in
 446:                         = new FileInputStream(stream.temporary_file());
 447: 
 448:                     byte[]      buffer = new byte[1024];
 449:                     int         got    = 0;
 450:                     while( (got = in.read(buffer)) > 0 )
 451:                         destination.write( buffer, 0, got );
 452:                     in.close();
 453:                     stream.delete_temporary();
 454:                 }
 455:             }
 456: 
 457:             destination.closeEntry();
 458:             write_accomplished();     
 459:         }
 460:     }
 461: 
         
/********************************
A Unit-test class. Run the unit test with
java com.holub.tools.Archive\$Test
Omit the backslash if you're running a Windows shell.
Include an (arbitrary) command-line argument if you want
verbose output, otherwise output is generated (on standard
output) only if a test fails. The exit status is the number
of failed tests -- 0 if none.
*/
 462:   static public class Test
 463:   {   private static final String STORED_ZIP      ="A.stored.zip"    ;
 464:       private static final String COMPRESSED_ZIP  ="A.compressed.zip";
 465:       private static final String FILE_1          ="root.txt"        ;
 466:       private static final String FILE_2          ="/subdir/file.txt";
 467: 
 468:       private static final boolean overwrite = false;
 469:       private static final boolean append    = true;
 470: 
 471:       static public void main(String[] args)
 472:         {
 473:             Tester  t = new Tester(args.length > 0, Std.out());
 474:             try
 475:             {
 476:                 // Remove previous test files to make sure that
 477:                 // everything works as expected.
 478: 
 479:                 new File( STORED_ZIP     ).delete();
 480:                 new File( COMPRESSED_ZIP ).delete();
 481: 
 482:                 // Test to see if we can do simple reads and writes.
 483: 
 484:                 Archive stored  = new Archive( 
 485:                                 STORED_ZIP, Archive.UNCOMPRESSED  );
 486: 
 487:                 Archive compressed  = new Archive(
 488:                                 COMPRESSED_ZIP, Archive.COMPRESSED  );
 489: 
 490:                 OutputStream out = 
 491:                         stored.output_stream_for( FILE_1, overwrite);
 492:                 out.write('a');
 493:                 out.write('b');
 494:                 out.close();
 495: 
 496:                 out = stored.output_stream_for( FILE_2, overwrite);
 497:                 out.write('c');
 498:                 out.close();
 499: 
 500: 
 501:                 out = compressed.output_stream_for( FILE_1, overwrite);
 502:                 out.write('d');
 503:                 out.close();
 504: 
 505:                 out = compressed.output_stream_for( FILE_2, overwrite);
 506:                 out.write('e');
 507:                 out.close();
 508: 
 509:                 stored.close();
 510:                 compressed.close();
 511:                 stored      = new Archive(
 512:                                     STORED_ZIP, Archive.UNCOMPRESSED);
 513:                 compressed  = new Archive(
 514:                                     COMPRESSED_ZIP,Archive.COMPRESSED );
 515: 
 516:                 InputStream in = stored.input_stream_for( FILE_1 );
 517:                 t.check( "Archive.1.0", 'a', in.read() );
 518:                 t.check( "Archive.1.1", 'b', in.read() );
 519:                 t.check( "Archive.1.2", -1,  in.read() );
 520:                 in.close();
 521: 
 522:                 in = stored.input_stream_for( FILE_2 );
 523:                 t.check( "Archive.2.0", 'c', in.read() );
 524:                 t.check( "Archive.2.1", -1,  in.read() );
 525:                 in.close();
 526: 
 527:                 in = compressed.input_stream_for( FILE_1 );
 528:                 t.check( "Archive.3.0", 'd', in.read() );
 529:                 t.check( "Archive.3.1", -1,  in.read() );
 530:                 in.close();
 531: 
 532:                 in = compressed.input_stream_for( FILE_2 );
 533:                 t.check( "Archive.4.0", 'e', in.read() );
 534:                 t.check( "Archive.4.1", -1,  in.read() );
 535:                 in.close();
 536: 
 537:                 stored.close();
 538:                 compressed.close();
 539: 
 540:                 // Test to see if we can do append to existing files
 541:                 
 542:                 stored      = new Archive(
 543:                                     STORED_ZIP, Archive.UNCOMPRESSED);
 544:                 compressed  = new Archive(
 545:                                     COMPRESSED_ZIP, Archive.COMPRESSED);
 546: 
 547:                 out = stored.output_stream_for( FILE_1, append);
 548:                 out.write('B');
 549:                 out.close();
 550: 
 551:                 out = stored.output_stream_for( FILE_2, append);
 552:                 out.write('C');
 553:                 out.close();
 554: 
 555: 
 556:                 out = compressed.output_stream_for( FILE_1, append);
 557:                 out.write('D');
 558:                 out.close();
 559: 
 560:                 out = compressed.output_stream_for( FILE_2, append);
 561:                 out.write('E');
 562:                 out.close();
 563: 
 564:                 stored.close();
 565:                 compressed.close();
 566: 
 567:                 stored      = new Archive(
 568:                                     STORED_ZIP, Archive.UNCOMPRESSED);
 569:                 compressed  = new Archive(
 570:                                     COMPRESSED_ZIP, Archive.COMPRESSED);
 571: 
 572:                 in = stored.input_stream_for( FILE_1 );
 573:                 t.check( "Archive.5.0", 'a', in.read() );
 574:                 t.check( "Archive.5.1", 'b', in.read() );
 575:                 t.check( "Archive.5.2", 'B', in.read() );
 576:                 t.check( "Archive.5.3", -1,  in.read() );
 577:                 in.close();
 578: 
 579:                 in = stored.input_stream_for( FILE_2 );
 580:                 t.check( "Archive.6.0", 'c', in.read() );
 581:                 t.check( "Archive.6.1", 'C', in.read() );
 582:                 t.check( "Archive.6.2", -1,  in.read() );
 583:                 in.close();
 584: 
 585:                 in = compressed.input_stream_for( FILE_1 );
 586:                 t.check( "Archive.7.0", 'd', in.read() );
 587:                 t.check( "Archive.7.1", 'D', in.read() );
 588:                 t.check( "Archive.7.2", -1,  in.read() );
 589:                 in.close();
 590: 
 591:                 in = compressed.input_stream_for( FILE_2 );
 592:                 t.check( "Archive.8.0", 'e', in.read() );
 593:                 t.check( "Archive.8.1", 'E', in.read() );
 594:                 t.check( "Archive.8.2", -1,  in.read() );
 595:                 in.close();
 596: 
 597:                 stored.close();
 598:                 compressed.close();
 599: 
 600:                 // Test to see if we can modify an existing file. Also,
 601:                 // use a large file this time to see if it's really
 602:                 // compressing correctly.
 603: 
 604:                 t.println("Checking large read/write "
 605:                                         +"[takes a few seconds]");
 606: 
 607:                 stored      = new Archive(
 608:                                     STORED_ZIP, Archive.UNCOMPRESSED  );
 609:                 compressed  = new Archive(
 610:                                     COMPRESSED_ZIP);
 611: 
 612:                 OutputStream out2;
 613: 
 614:                 out  = stored.output_stream_for( FILE_1 );
 615:                 out2 = compressed.output_stream_for( FILE_1 );
 616: 
 617:                 in = new FileInputStream("Archive.java");
 618:                 byte[] buffer = new byte[1024];
 619:                 for( int got=0; (got = in.read(buffer)) > 0 ;)
 620:                 {   out.write(buffer,0,got);
 621:                     out2.write(buffer,0,got);
 622:                 }
 623:                 out.close();
 624:                 out2.close();
 625: 
 626:                 stored.close();
 627:                 compressed.close();
 628: 
 629:                 stored      = new Archive(
 630:                                     STORED_ZIP, Archive.UNCOMPRESSED );
 631:                 compressed  = new Archive(
 632:                                     COMPRESSED_ZIP );
 633: 
 634:                 in              = stored.input_stream_for( FILE_1 );
 635:                 InputStream in2 = compressed.input_stream_for( FILE_1 );
 636:                 InputStream sample= new FileInputStream("Archive.java");
 637:                 int         c;
 638: 
 639:                 t.verbose(Tester.OFF);
 640:                 while( (c = sample.read()) >= 0 )
 641:                 {   t.check( "Archive.9.0", c, in.read()  );
 642:                     t.check( "Archive.9.0", c, in2.read() );
 643:                 }
 644:                 in.close();
 645:                 in2.close();
 646:                 sample.close();
 647:                 t.verbose(Tester.RESTORE);
 648:                 t.check( "Archive.9.1", t.errors_were_found()==false,
 649:                                                     "Overwrite-test" );
 650: 
 651:                 stored.close();
 652:                 compressed.close();
 653: 
 654:                 // Test file removal. First remove one file
 655: 
 656:                 stored = new Archive( STORED_ZIP );
 657:                 stored.remove( FILE_1 );
 658:                 stored.close();
 659: 
 660:                 stored = new Archive( STORED_ZIP );
 661:                 try
 662:                 {
 663:                     stored.input_stream_for( FILE_1 ); // should fail
 664:                     t.check( "Archive.10.0.a", false,
 665:                                             "Removal (failure a)" );
 666:                 }
 667:                 catch( ZipException e )
 668:                 {   if( e.getMessage().indexOf(FILE_1) >= 0 )
 669:                         t.check( "Archive.10.0", true, "Removal" );
 670:                     else
 671:                         t.check( "Archive.10.0", false,
 672:                                             "Removal (failure b)" );
 673:                 }
 674: 
 675:                 // Now remove second file, should cause an exception on
 676:                 // the close since an empty archive isn't permitted.
 677: 
 678:                 stored = new Archive( STORED_ZIP );
 679:                 stored.remove( FILE_2 );
 680:                 try
 681:                 {   stored.close();
 682:                     t.check( "Archive.11.0", false, "Remove all files");
 683:                 }
 684:                 catch( ZipException e )
 685:                 {   t.check( "Archive.11.0", true, "Remove all files" );
 686:                 }
 687: 
 688:                 // Finally, test the revert method.
 689: 
 690:                 compressed = new Archive( STORED_ZIP );
 691:                 out = compressed.output_stream_for( "foo" );
 692:                 out.write('x');
 693:                 out.close();
 694:                 compressed.revert();
 695: 
 696:                 stored = new Archive( STORED_ZIP );
 697:                 try
 698:                 {
 699:                     in = stored.input_stream_for("foo"); // should fail
 700:                     t.check( "Archive.12.0.a", false,
 701:                                             "Revert (failure a)" );
 702:                 }
 703:                 catch( ZipException e )
 704:                 {   
 705:                     if( e.getMessage().indexOf("foo") >= 0 )
 706:                         t.check( "Archive.12.0", true, "Revert" );
 707:                     else
 708:                         t.check( "Archive.12.0", false,
 709:                                             "Revert (failure b)" );
 710:                 }
 711:             }
 712:             catch( Exception e )
 713:             {   t.check( "FastBufferedOutputStream.Abort", false,
 714:                                 "Terminated by Exception toss" );
 715:                 e.printStackTrace();
 716:             }
 717:             finally{ t.exit(); }
 718:         }
 719:     }
 720: }
         

Conclusion

So that's the Archive class. Judging by the volume of complaints in Sun's bug database about how hard it is to modify archives, this class should be pretty useful to many people in its own right. As I mentioned in Part 1 of this series, the reason I wrote it was to support a caching class loader, which I'll present in a subsequent column. Next month, I'm going to briefly digress into a discussion of a threading-related problem that's come over my horizon only recently, and which can seriously affect the behavior of your code on multiple-processor machines. This is an important enough issue that I wanted to get it in front of everybody as soon as possible, so I hope you won't be put off by having to wait an extra month for that class loader.

Allen Holub has been working in the computer industry since 1979. He is widely published in magazines (Dr. Dobb's Journal, Programmers Journal, Byte, and MSJ. among others). He writes the Java Toolbox column for JavaWorld and also writes the OO-Design Process column for the IBM developerWorks Component Zone. Moreover, Allen moderates the Programming Theory & Practice discussion in ITworld.com's Java forum. Allen has eight books to his credit, the latest of which covers the traps and pitfalls of Java threading (Taming Java Threads [Berkeley: Apress, 2000; ISBN 1893115100]). He's been designing and building object-oriented software for longer than he cares to remember. After eight years as a C++ programmer, Allen abandoned C++ for Java in early 1996. He now looks at C++ as a bad dream, the memory of which is mercifully fading. He's been teaching programming (first C, then C++ and MFC, now OO-design and Java) both on his own and for the University of California at Berkeley Extension since 1982. Allen offers both public classes and in-house training in Java and object-oriented design topics. He also does object-oriented design consulting and contract Java programming. Get information, and contact Allen, via his Website at http://www.holub.com.

Learn more about this topic

Related:
1 2 3 4 Page 4
Page 4 of 4