Modify archives, Part 1

Supplement Java's util.zip package to make it easy to write or modify existing archives

1 2 3 4 Page 2
Page 2 of 4

The second constructor argument is a Writer, to which the reports are sent. The t.exit() call at the end of main() causes the program to terminate with an exit status equal to the number of test failures; that number is a useful thing to know in a test script. It also prints the total error count if verbose mode is set.

The Tester class has several methods of interest. The check() method (of which there are several overloads) compares an expected value with an actual value and prints a message if they don't match:

t.check( "test_id", 0, f() );

The example above prints an error message if f() doesn't return 0.

I couldn't override a general version for methods that return objects, so an alternative is provided:

t.check( "test_id", f()==null, "f()==null" );

This version prints a message (including the string passed as the third argument) if the test specified in the second argument evaluates to false.

A third method:

t.println("Message");

simply prints the message if the tester is operating in verbose mode; otherwise, it prints nothing.

You can override the verbosity value passed to the constructor by calling the t.verbose(TESTER.ON) or t.verbose(TESTER.OFF) methods. Restore verbosity to the constructor-specified value with t.verbose(TESTER.RESTORE) To check if errors were found, use t.errors_were_found().

You'll note that I'm following good object-oriented design practice by not exposing the number of errors anywhere. There is no get_error_count() method. I can do this because the Tester object itself does everything that it needs to do with the error count when exit() is called.

We'll see lots of examples of the Tester class being used in subsequent listings.

Listing 4. /src/com/holub/tools/Tester.java
   1: package com.holub.tools;
   2: 
   3: import com.holub.tools.debug.Assert;
   4: import java.io.*;
   5: 
/******************************************************
A simple class to help in testing. Various check() methods are passed a test id, an expected value, and an actual value. The test prints appropriate messages, and keeps track of the total error count for you. For example:
    class Test
    {    public static void main(String[] args);
         {
             // Create a tester that sends output to standard error, which
             // operates in verbose mode if there are any command-line
             // arguments.
    
             Tester t = new Tester( args.length < 0,
                                         com.holub.tools.Std.out() );
             //...
    
             t.check("test.1", 0,     foo());  // check that foo() returns 0.
             t.check("test.2", "abc", bar());  // check that bar() returns "abc".
             t.check("test.3", true , cow());  // check that cow() returns true
             t.check("test.4", true , dog()==null);  // check that dog() returns null
    
// Check arbitrary statement
             t.check("test.5", f()!=g(), "Expected f() to return same value as g()" );
    
             //...
             t.exit();
         }
     }
*/
   6: public class Tester
   7: {
   8:   private int               errors = 0;
   9:   private boolean           verbose;
  10:   private final PrintWriter log;
  11:   private final boolean     original_verbosity;
  12: 
/**
Create a tester that has the specified behavior and output stream: @param verbose Print messages even if test succeeds. (Normally, only failures are indicated.) @param log if not null, all output is sent here, otherwise output is sent to standard error.
*/
  13:   public Tester( boolean verbose, PrintWriter log )
  14:     {   this.verbose            = verbose;
  15:         this.original_verbosity = verbose;
  16:         this.log                = log;
  17:     }
  18: 
/******************************************************
Change the verbose mode, overriding the mode passed to the constructor. @param mode
Tester.ONMessages are reported.
Tester.OFFMessages aren't reported.
Tester.RESTOREUse Verbose mode specified in constructor.
*/
  19:   public void verbose( int mode )
  20:     {   switch( mode )
  21:         {
  22:         case ON:    verbose = true;                 break;
  23:         case OFF:   verbose = false;                break;
  24:         default:    verbose = original_verbosity;   break;
  25:         }
  26:     }
  27: 
  28:   public static final int ON      = 0;
  29:   public static final int OFF     = 1;
  30:   public static final int RESTORE = 2;
  31: 
/******************************************************
Check that and expected result of type String is equal to the actual result. @param test_id String that uniquely identifies this test. @param expected the expected result. @param actual the value returned from the function under test. @return true if the expected and actual parameters matched.
*/
  32:   public boolean check( String test_id, String expected, String actual)
  33:     {
  34:         Assert.is_true( log != null    , "Tester.check(): log is null"      );
  35:         Assert.is_true( test_id != null, "Tester.check(): test_id is null"  );
  36: 
  37:         boolean okay = expected.equals( actual );
  38: 
  39:         if( !okay )
  40:             ++errors;
  41: 
  42:         if( !okay || verbose )
  43:         {   log.println (  (okay ? "   okay " : "** FAIL ")
  44:                          + "("  + test_id + ")"
  45:                          + " expected: " + expected
  46:                          + " got: "      + actual
  47:                     );
  48:         }
  49:         return okay;
  50:     }
/******************************************************
Print the message if verbose mode is on.
*/
  51:   public void println( String message )
  52:     {   if( verbose )
  53:             log.println( "\t" + message );
  54:     }
  55: 
/******************************************************
For situations not covered by normal check() methods. If okay is false, ups the error count and prints the associated message string (assuming verbose is on). Otherwise does nothing.
*/
  56:   public void check( String test_id, boolean okay, String message )
  57:     {   Assert.is_true( message != null );
  58: 
  59:         if( !okay )
  60:             ++errors;
  61: 
  62:         if( !okay || verbose )
  63:         {
  64:             log.println (  (okay ? "   okay " : "** FAIL ")
  65:                          + "("  + test_id + ") "
  66:                          + message
  67:                     );
  68:         }
  69:     }
/******************************************************
Convenience method, compares a string against a StringBuffer.
*/
  70:   public boolean check( String test_id, String expected, StringBuffer actual)
  71:     {   return check( test_id, expected, actual.toString());
  72:     }
/******************************************************
Convenience method, compares two doubles.
*/
  73:   public boolean check( String test_id, double expected, double actual)
  74:     {   return check( test_id, "" + expected, "" + actual );
  75:     }
/******************************************************
Convenience method, compares two longs.
*/
  76:   public boolean check( String test_id, long expected, long actual)
  77:     {   return check( test_id, "" + expected, "" + actual );
  78:     }
/******************************************************
Convenience method, compares two booleans.
*/
  79:   public boolean check( String test_id, boolean expected, boolean actual)
  80:     {   return check( test_id, expected?"true":"false", actual?"true":"false" );
  81:     }
/******************************************************
Return true if any preceding check() call resulted in an error.
*/
  82:   public boolean errors_were_found()
  83:     {   return errors != 0;
  84:     }
/******************************************************
Exit the program, using the total error count as the exit status.
*/
  85:   public void exit()
  86:     {   if( verbose )
  87:             log.println( "\n" + errors + " errors detected" );
  88:         System.exit( errors );
  89:     }
  90: }

The FastBufferedOutputStream class

Let's move on to the first class we actually need for the archive-file class. The FastBufferedOutputStream (Listing 5) class solves two problems with Java's BufferedOutputStream, but otherwise works identically to a BufferedOutputStream.

First, recall that the BufferedOutputStream's write() methods are synchronized, even though it's rare that two threads will ever write to the same stream simultaneously without external synchronization; as a result, the fact that write() is synchronized can slow your program down measurably. I once found that something like 80 percent of the synchronization overhead associated with a program I wrote was caused by the unnecessary synchronization on the BufferedOutputStream's write() method. As seen in Listing 5, I solve the problem by simply not synchronizing the write() methods.

Second, FastBufferedOutputStream can export the buffered data from the stream if the stream has never been flushed to disk, a feature unsupported by BufferedOutputStream. I've provided two similar methods for this purpose, export_buffer_and_close() and export_buffer_and_close(OutputStream).

Both methods close the stream. If the buffer was never flushed to the disk, the first override returns a byte array that contains the buffered characters, while the second override flushes the characters to the OutputStream you specify as an argument rather than to the OutputStream that the FastBufferedOutputStream wraps. A call to close() (or flush()) flushes the buffer to the wrapped OutputStream, of course.

The raison d'être for these two export methods will be apparent in Part 2's Archive class. A file that's being stored (uncompressed) in the archive is copied to a file wrapped with a FastBufferedOutputStream. If the file is small enough, it's never flushed to disk, so I can get it directly from the stream without incurring any file-I/O overhead. If it's large enough to have been written to the disk, then I can fall back and read the file manually. For example, the following code takes data from the FastBufferedOutputStream called temporary_file and transfers it to the destination stream:

FastBufferedOutputStream temporary_file
        = new FastBufferedOutputStream( new FileOutputStream("file.tmp") );
OutputStream destination;
//...
if( (temporary_file.export_buffer_and_close(destination)) == null )
{   
    InputStream in     = new FileInputStream("file.tmp");
    byte[]      buffer = new byte[1024];
    int         got    = 0;
    while( (got = in.read(buffer)) > 0 )
        destination.write( buffer, 0, got );
    in.close();
}

In the current application, the FastBufferedOutputStream wraps a temporary file in which I'm storing the data that's supposed to go into the archive. The destination stream references the ZipOutputStream for the archive. The foregoing code transfers the data from the temporary file to the archive, but it does it efficiently (from memory rather than from the disk) if the dataset is too small to be flushed to the disk.

Listing 5. /src/com/holub/io/FastBufferedOutputStream.java
   1: package com.holub.io;
   2: import  java.io.*;
   3: import  com.holub.tools.Tester; // for testing
   4: import  com.holub.io.Std;       // for testing
   5: 
   6: import  com.holub.tools.D;      // for testing
   7: //import com.holub.tools.debug.D;// for testing
   8: 
/**
This version of BufferedOutputStream isn't thread safe, so is much faster than the standard BufferedOutputStream in situations where the stream is not shared between threads; Otherwise, it works identically to java.io.BufferedOutputStream.
*/
   9: public class FastBufferedOutputStream extends FilterOutputStream
  10: {   
  11:   private final int      size;        // buffer size
  12:   private       byte[]   buffer;
  13:   private       int      current       = 0;
  14:   private       boolean  flushed       = false;
  15:   private       int      bytes_written = 0;
  16: 
  17:   public static final int DEFAULT_SIZE = 2048;
  18: 
/** Create a FastBufferedOutputStream whose buffer is
FastBufferedOutputStream.DEFULT_SIZE in size.
*/
  19:   public FastBufferedOutputStream( OutputStream out )
  20:     {   this( out, DEFAULT_SIZE );
  21:     }
  22: 
/**
Create a FastBufferedOutputStream whose buffer is the indicated size.
*/
  23:   public FastBufferedOutputStream( OutputStream out, int size )
  24:     {   super( out );
  25:         this.size   = size;
  26:         buffer      = new byte[ size ];
  27:     }
  28: 
  29:     // Inherit write(byte[]);
  30: 
  31:   public void close() throws IOException
  32:     {   D.ebug("\t\tFastBufferedOutputStream closing");
  33: 
  34:         flush();
  35:         buffer = null;
  36:         current = 0;
  37:         super.close();
  38:     }
  39: 
  40:   public void flush() throws IOException
  41:     {   if( current > 0 )
  42:         {   D.ebug("\t\tFlushing");
  43:             out.write( buffer, 0, current );
  44:             out.flush( );
  45:             current = 0;
  46:             flushed = true;
  47:         }
  48:     }
  49: 
/**
Write a character on the stream. Flush the buffer first if the buffer is full. That is, if you have a 10-character buffer, the flush occurs just before writing the 11th character.
*/
1 2 3 4 Page 2
Page 2 of 4