Modifying archives, Part 2: The Archive class

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

1 2 3 4 Page 3
Page 3 of 4
  88:   public Archive( String jar_file_path ) throws IOException
  89:     {   this( jar_file_path, true );
  90:     }
  91: 
         
/********************************
Clean up from a close, closing all handles and freeing memory.
*/
  92:   private final void deconstruct() throws IOException
  93:     {
  94:                                      // The archive is now unusable,
  95:         entries             = null; // so free up any internal
  96:         source              = null; // memory in case it's needed
  97:         destination         = null; // elsewhere and the Archive
  98:         source_file         = null; // itself isn't freed for some
  99:         destination_stream  = null; // reason.
 100:     }
 101: 
         
/********************************
Remove a file from the archive.
*/
 102:   public void remove( String internal_path )
 103:                                 throws IOException, InterruptedException
 104:     {
 105:         lock.acquire();
 106:         try
 107:         {   if( closed )
 108:                 throw new Closed();
 109: 
 110:             // When the archive is closed, all files in the "entries"
 111:             // list are copied from the original jar to the new
 112:             // one. By removing the file from the list, you will
 113:             // prevent that file from being copied to the new
 114:             // archive.
 115: 
 116:             archive_has_been_modified = true;
 117:             entries.remove( internal_path );
 118:         }
 119:         finally
 120:         {   lock.release();
 121:         }
 122:     }
 123: 
         
/********************************
Return an OutputStream that you can use to write to the indicated file. The current Archive object is locked until the returned stream is closed. Any existing file that is overwritten will no longer be accessible for reading via the current 'Archive' object. It's an error to call this method more than once with the same internal_path argument. Note that the returned output stream is not thread safe. Two threads cannot write to the same output stream simultaneously without some sort of explicit synchronization. I've done it this way because output streams are typically not shared between threads, and the overhead of synchronization would be nontrivial. The file is created within the archive if it doesn't already exist. @param internal_path The path (within the archive) to the desired file. @param appending true if you want the bytes written to the returned OutputStream to be appended to the original contents. @return A stream to receive the new data.
*/
 124:   public OutputStream output_stream_for( String internal_path,
 125:                                                    boolean appending )
 126:                                                     throws IOException
 127:     {   try
 128:         {
 129:             lock.acquire(); // Lock the archive. The lock is released
 130:                             // by write_accomplished when the stream
 131:                             // is closed.
 132:             if( closed )
 133:             {   lock.release();
 134:                 throw new Closed();
 135:             }
 136: 
 137:             archive_has_been_modified = true;
 138: 
 139:             // See if it's an existing file, and if so, remove
 140:             // it from the list of files that were in the original
 141:             // archive. Otherwise the new contents will be
 142:             // overwritten by the original when the Archive is closed.
 143: 
 144:             ZipEntry found = (ZipEntry)( entries.remove(internal_path));
 145: 
 146:             ZipEntry entry = new ZipEntry(internal_path);
 147: 
 148:             entry.setMethod( found != null 
 149:                                 ? found.getMethod() : compression ); 
 150: 
 151:             entry.setComment( entry.getMethod()==ZipEntry.DEFLATED
 152:                                 ? "compressed" : "uncompressed");
 153: 
 154:             OutputStream out = new Archive_OutputStream( entry );
 155: 
 156:             if( found != null && appending )
 157:                 copy_entry_to_stream( entry, out );
 158: 
 159:             return out;
 160:         }
 161:         catch(IOException e) // release on an exception toss, but
 162:         {   lock.release();  // not a finally.
 163:             throw e;
 164:         }
 165:         catch(InterruptedException e)
 166:         {   // fall through to null return.
 167:         }
 168:         return null;
 169:     }
 170: 
         
/** Convenience method. Original file is overwritten if it's there.
*/
 171:   public OutputStream output_stream_for( String internal_path )
 172:                                             throws IOException
 173:     {   return output_stream_for( internal_path, false );
 174:     }
 175: 
         
/********************************
Copies the contents of a file in the source archive to the indicated destination stream. This method is private because it doesn't do any locking. The output stream is not closed by this method.
*/
 176:   private void copy_entry_to_stream(ZipEntry entry, OutputStream out)
 177:                                                     throws IOException
 178:     {   InputStream in = source.getInputStream(entry);
 179:         try
 180:         {   byte[] buffer = new byte[1024];
 181: 
 182:             for(int got=0; (got = in.read(buffer,0,buffer.length)) >0 ;)
 183:             {   out.write(buffer, 0, got);
 184:             }
 185:         }
 186:         finally
 187:         {   in.close();
 188:         }
 189:     }
 190: 
         
/********************************
Called from the Archive_OutputStream's close() method. In the case of a compressed write, it just releases the lock. In the case of a "stored" write, it transfers from the ByteArrayOutputStream to the file, creating the necessary checksum.
*/
 191:   private void write_accomplished()
 192:     {   lock.release();
 193:     }
 194: 
         
/********************************
Return an InputStream that you can use to read from the indicated file. The current Archive object is locked until the returned stream is closed. Once a particular archived file is overwritten (by a call to output_stream_for), that file is no longer available for write, and an attempt to call input_stream_for() on that file will fail. @return a reference to an InputStream or null if no file that matches internal_path exists. @throw ZIPException if the requested file doesn't exist. @throw IOException if an I/O error occurs when the method tries to open the stream.
*/
 195:   public InputStream input_stream_for( String internal_path )
 196:                                         throws ZipException, IOException
 197:     {   
 198:         Assert.is_true( source != null, "source is null" );
 199: 
 200:         try
 201:         {   lock.acquire(); // Lock the archive. The lock is released
 202:                             // when the returned InputStream is closed.
 203:             if( closed )
 204:             {   lock.release();
 205:                 throw new Closed();
 206:             }
 207: 
 208:             ZipEntry current = (ZipEntry)entries.get( internal_path );
 209:             if( current == null )
 210:                 throw new ZipException(internal_path +" doesn't exist");
 211: 
 212:             InputStream in  = source.getInputStream(current);
 213:             return new Archive_InputStream( in );
 214:         }
 215:         catch( IOException e )  // ZipException extends IOException
 216:         {   lock.release();
 217:             throw e;
 218:         }
 219:         catch(InterruptedException e)
 220:         {   // fall through to null return.
 221:         }
 222:         return null;
 223:     }
 224: 
         
/********************************
Called from the Archive_InputStream's close() method.
*/
 225:   private void read_accomplished()
 226:     {   lock.release();
 227:     }
 228: 
         
/********************************
Close the current Archive object (rendering it unusable) and overwrite the original archive with the new contents. The original archive is not actually modified until close() is called. A call to this method blocks until any ongoing read or write operations complete (and the associated stream is closed). @throws ZipException Zip files must have more than at least one entry in them. A ZipException is thrown if the destination file is empty, either becuase you've removed everything or because you never put anything into it. The original archive will not have been changed if this exception is thrown.
*/
 229:   public void close()
 230:                 throws IOException, InterruptedException, ZipException
 231:     {   
 232:         // The main thing that close() does is copy any files that
 233:         // remain in the "entries" list from the original archive
 234:         // to the new one. The original compression mode of the
 235:         // file is preserved. Finally, the new archive is renamed
 236:         // to the original archive's name (thereby blasting the
 237:         // original out of existence.)
 238: 
 239:         lock.acquire();
 240:         try
 241:         {   if( !closed ) // Closing a closed archive is harmless
 242:             {
 243:                 if( source != null ) // there is a source archive
 244:                 {
 245:                     if( archive_has_been_modified )
 246:                       copy_remaining_files_from_source_to_destination();
 247:                     source.close();
 248:                 }
 249: 
 250:                 if( archive_has_been_modified )
 251:                 {   
 252:                     destination.close();
 253:                     if( !destination_stream.rename_temporary_to(
 254:                                                         source_file ) )
 255:                     {   D.ebug("***\t\tTemporary file not renamed!");
 256:                     }
 257:                 }
 258:                 else
 259:                 {   destination_stream.close();
 260:                     destination_stream.delete_temporary();
 261:                 }
 262: 
 263:                 closed = true;
 264:                 deconstruct();
 265:             }
 266:         }
 267:         catch( ZipException e )
 268:         {   // Thrown if the destination archive is empty.
 269:             // Clean up the temporary file, then rethrow
 270:             // the exception.
 271: 
 272:             destination_stream.close();
 273:             destination_stream.delete_temporary();
 274:             throw e;
 275:         }
 276:         finally
 277:         {   lock.release();
 278:         }
 279:     }
 280: 
         
/********************************
Copies any files from the source archive that have not been modified to the destination archive (and removes them from the entries list as they are copied).
*/
 281:   private void copy_remaining_files_from_source_to_destination()
 282:                                                     throws IOException
 283:     {   for(Iterator i = entries.values().iterator(); i.hasNext() ;)
 284:         {   ZipEntry current = (ZipEntry)i.next();
 285:             i.remove();
 286: 
 287:             ZipEntry entry = new ZipEntry(current.getName());
 288: 
 289:             entry.setMethod ( current.getMethod()   );
 290:             entry.setSize   ( current.getSize()     );
 291:             entry.setCrc    ( current.getCrc()      );
 292:             entry.setComment( current.getComment()  );
 293: 
 294:             D.ebug( "\t\tTransferring "+current.getName()+" to output");
 295: 
 296:             destination.putNextEntry( current );
 297:             copy_entry_to_stream( current, destination );
 298:         }
 299:     }
 300: 
         
/********************************
Close the archive, abandoning any changes that you've made. It's important to call this method (as compared to simply abandoning the Archive object) if you want to discard changes; otherwise, temporary files will be left on your disk. Reverting a closed archive is considered harmless, so is not flagged as an error.
*/
 301:   public void revert() throws IOException, InterruptedException
 302:     {   
 303:         // All that this method does is close, and then destroy, the
 304:         // temporary file that contains the partially assembled
 305:         // new archive. It also puts the Archive object into the
 306:         // "closed" state so that no further modifications are
 307:         // possible.
 308: 
 309:         lock.acquire();
 310:         try
 311:         {   if( closed )
 312:                 return;
 313: 
 314:             source.close();
 315: 
 316:             if( archive_has_been_modified )
 317:                 destination.close();
 318:             else
 319:                 destination_stream.close();
 320: 
 321:             destination_stream.delete_temporary();
 322:             closed = true;
 323:             deconstruct();
 324:         }
 325:         finally
 326:         {   lock.release();
 327:         }
 328:     }
 329: 
         
/********************************
A gang-of-four Decorator that wraps InputStream in such a way that the Archive object that created it is notified when the stream is closed.
*/
 330:   private class Archive_InputStream extends InputStream
 331:   {   private final InputStream wrapped;
 332: 
 333:       public Archive_InputStream( InputStream wrapped )
 334:         {   this.wrapped = wrapped;
 335:         }
 336: 
 337:       public int available()   throws IOException 
 338:         {   return wrapped.available();
 339:         }
 340: 
 341:       public void reset()  throws IOException
 342:         {   wrapped.reset();
 343:         }
 344: 
 345:       public long skip(long n)throws IOException
 346:         {   return wrapped.skip(n);
 347:         }
 348: 
 349:       public int read() throws IOException
 350:         {   return wrapped.read();
 351:         }
 352: 
 353:       public int read(byte[] b)throws IOException
 354:         {   return wrapped.read(b);
 355:         }
 356: 
 357:       public void mark( int limit ) {       wrapped.mark(limit);    }
 358:       public boolean markSupported(){return wrapped.markSupported();}
 359: 
 360:       public int read(byte[] b,int o,int l) throws IOException
 361:         {   return wrapped.read(b,o,l);
 362:         }
 363: 
 364:       public void close() throws IOException
 365:         {   wrapped.close();
 366:             read_accomplished();
 367:         }
 368:     }
 369: 
         
/********************************
The Archive_OutputStream is a real class, not a Decorator. The main problem is that storing a file mandates computing a checksum before writing the bytes. Though it's tempting to just save the file to a ByteArrayOutputStream and then get the bytes, the in-memory footprint can be too large. The current implementation transfers the bytes to a temporary file, then transfers them from a temporary file to the archive. The bytes are buffered internally, so for small files (under 2K in size), the temporary file is actually never created.
*/
Related:
1 2 3 4 Page 3
Page 3 of 4