KickJava   Java API By Example, From Geeks To Geeks.

Java > Open Source Codes > de > schlichtherle > util > zip > BasicZipOutputStream


1 /*
2  * Copyright 2005-2006 Schlichtherle IT Services
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */

16
17 package de.schlichtherle.util.zip;
18
19 import de.schlichtherle.io.util.LEDataOutputStream;
20
21 import java.io.FilterOutputStream JavaDoc;
22 import java.io.IOException JavaDoc;
23 import java.io.OutputStream JavaDoc;
24 import java.io.UnsupportedEncodingException JavaDoc;
25 import java.util.Collections JavaDoc;
26 import java.util.Enumeration JavaDoc;
27 import java.util.Iterator JavaDoc;
28 import java.util.LinkedHashMap JavaDoc;
29 import java.util.Map JavaDoc;
30 import java.util.zip.CRC32 JavaDoc;
31 import java.util.zip.Deflater JavaDoc;
32 import java.util.zip.ZipException JavaDoc;
33
34 /**
35  * <em>This class is <b>not</b> intended for public use!</em>
36  * The methods in this class are unsynchronized and
37  * {@link #entries}/{@link #getEntry} enumerate/return {@link ZipEntry}
38  * instances which are originally used by this class rather than clones
39  * of them.
40  * The class {@link de.schlichtherle.io.archive.zip.Zip32OutputArchive}
41  * extends from this class in order to benefit from the slightly better
42  * performance.
43  *
44  * @see ZipOutputStream
45  * @author Christian Schlichtherle
46  * @version @version@
47  * @since TrueZIP 6.4
48  */

49 public class BasicZipOutputStream
50         extends FilterOutputStream JavaDoc
51         implements ZipConstants {
52
53     /**
54      * The file comment.
55      */

56     private String JavaDoc comment = "";
57
58     /**
59      * Default compression method for next entry.
60      */

61     private short method = ZipEntry.DEFLATED;
62
63     /**
64      * The list of ZIP entries started to be written so far.
65      * Maps entry names to zip entries [{@link String} -> {@link ZipEntry}].
66      */

67     private final Map JavaDoc entries = new LinkedHashMap JavaDoc();
68
69     /**
70      * Start of entry data.
71      */

72     private long dataStart;
73
74     /**
75      * Start of central directory.
76      */

77     private long cdOffset;
78
79     /**
80      * Length of central directory.
81      */

82     private long cdLength;
83
84     /**
85      * The encoding to use for entry names and comments.
86      */

87     private final String JavaDoc encoding;
88
89     private boolean finished;
90
91     private boolean closed;
92
93     /**
94      * Current entry.
95      */

96     private ZipEntry entry;
97     
98     /**
99      * Whether or not we need to deflate the current entry.
100      * This can be used together with the DEFLATED method to write already
101      * compressed entry data into the ZIP file.
102      */

103     private boolean deflate;// = true;
104

105     /**
106      * CRC instance to avoid parsing DEFLATED data twice.
107      */

108     private final CRC32 JavaDoc crc = new CRC32 JavaDoc();
109
110     /**
111      * This Deflater object is used for deflated output.
112      * This is actually an instance of the class {@link ZipDeflater}.
113      */

114     private final Deflater JavaDoc def = new ZipDeflater();
115
116     /**
117      * This buffer holds deflated data for output.
118      */

119     private final byte[] dbuf = new byte[FLATER_BUF_LENGTH];
120
121     private final byte[] sbuf = new byte[1];
122     
123     /**
124      * Creates a new ZIP output stream decorating the given output stream,
125      * using the {@link #DEFAULT_ENCODING}.
126      *
127      * @throws NullPointerException If <code>out</code> is <code>null</code>.
128      */

129     public BasicZipOutputStream(
130             final OutputStream out)
131     throws NullPointerException JavaDoc {
132         super(toLEDataOutputStream(out));
133
134         // Check parameters (fail fast).
135
if (out == null)
136             throw new NullPointerException JavaDoc("out");
137
138         this.encoding = DEFAULT_ENCODING;
139     }
140
141     /**
142      * Creates a new ZIP output stream decorating the given output stream.
143      *
144      * @throws NullPointerException If <code>out</code> or <code>encoding</code> is
145      * <code>null</code>.
146      * @throws UnsupportedEncodingException If encoding is not supported by
147      * this JVM.
148      */

149     public BasicZipOutputStream(
150             final OutputStream out,
151             final String JavaDoc encoding)
152     throws NullPointerException JavaDoc,
153             UnsupportedEncodingException JavaDoc {
154         super(toLEDataOutputStream(out));
155
156         // Check parameters (fail fast).
157
if (out == null)
158             throw new NullPointerException JavaDoc("out");
159         if (encoding == null)
160             throw new NullPointerException JavaDoc("encoding");
161         "".getBytes(encoding); // may throw UnsupportedEncodingException!
162

163         this.encoding = encoding;
164     }
165
166     private static LEDataOutputStream toLEDataOutputStream(OutputStream out) {
167         return out instanceof LEDataOutputStream
168                 ? (LEDataOutputStream) out
169                 : new LEDataOutputStream(out);
170     }
171
172     /**
173      * Returns the encoding to use for filenames and the file comment.
174      */

175     public String JavaDoc getEncoding() {
176         return encoding;
177     }
178
179     /**
180      * Returns the number of ZIP entries written so far.
181      */

182     public int size() {
183         return entries.size();
184     }
185
186     /**
187      * Returns an enumeration of the ZIP entries written so far.
188      * Note that the enumerated entries are shared with this class.
189      * It is illegal to put more entries into this ZIP output stream
190      * concurrently or modify the state of the enumerated entries.
191      */

192     public Enumeration JavaDoc entries() {
193         return Collections.enumeration(entries.values());
194     }
195
196     /**
197      * Returns the {@link ZipEntry} for the given name or
198      * <code>null</code> if no entry with that name exists.
199      * Note that the returned entry is shared with this class.
200      * It is illegal to change its state!
201      *
202      * @param name Name of the ZIP entry.
203      */

204     public ZipEntry getEntry(String JavaDoc name) {
205         return (ZipEntry) entries.get(name);
206     }
207
208     /**
209      * Set the file comment.
210      */

211     public void setComment(String JavaDoc comment) {
212         this.comment = comment;
213     }
214
215     public String JavaDoc getComment() {
216         return comment;
217     }
218     
219     /**
220      * Sets the compression level for subsequent entries.
221      */

222     public void setLevel(int level) {
223     def.setLevel(level);
224     }
225
226     /**
227      * Returns the compression level currently used.
228      */

229     public int getLevel() {
230         return ((ZipDeflater) def).getLevel();
231     }
232
233     /**
234      * Sets the default compression method for subsequent entries.
235      *
236      * <p>Default is DEFLATED.</p>
237      */

238     public void setMethod(short method) {
239     if (method != STORED && method != DEFLATED)
240         throw new IllegalArgumentException JavaDoc("Invalid compression method!");
241         this.method = method;
242     }
243
244     public short getMethod() {
245         return method;
246     }
247
248     /**
249      * Returns the total number of (compressed) bytes this stream has written
250      * to the underlying stream.
251      */

252     public long length() {
253         return ((LEDataOutputStream) out).size();
254     }
255
256     /**
257      * Returns <code>true</code> if and only if this
258      * <code>ZipOutputStream</code> is currently writing a ZIP entry.
259      */

260     public boolean busy() {
261         return entry != null;
262     }
263
264     /**
265      * Equivalent to
266      * {@link #putNextEntry(ZipEntry, boolean) putNextEntry(ze, true)}.
267      */

268     public final void putNextEntry(final ZipEntry ze)
269     throws IOException JavaDoc {
270         putNextEntry(ze, true);
271     }
272     
273     /**
274      * Starts writing the next ZIP entry to the underlying stream.
275      * Note that if two or more entries with the same name are written
276      * consecutively to this stream, the last entry written will shadow
277      * all other entries, i.e. all of them are written to the ZIP compatible
278      * file (and hence require space), but only the last will be accessible
279      * from the central directory.
280      * This is unlike the genuine {@link java.util.zip.ZipOutputStream
281      * java.util.zip.ZipOutputStream} which would throw a {@link ZipException}
282      * in this method when the second entry with the same name is to be written.
283      *
284      * @param ze The ZIP entry to write.
285      * @param deflate Whether or not the entry data should be deflated.
286      * Use this to directly write already deflated data only!
287      *
288      * @throws ZipException If and only if writing the entry is impossible
289      * because the resulting file would not comply to the ZIP file
290      * format specification.
291      * @throws IOException On any I/O related issue.
292      */

293     public void putNextEntry(final ZipEntry ze, final boolean deflate)
294     throws IOException JavaDoc {
295         closeEntry();
296
297         final String JavaDoc name = ze.getName();
298         /*if (entries.get(name) != null)
299             throw new ZipException(name + ": Duplicate entry!");*/

300
301         if (ze.getMethod() == -1) // not specified
302
ze.setMethod(getMethod());
303         if (ze.getTime() == -1) // not specified
304
ze.setTime(System.currentTimeMillis());
305         
306         // check sum of name, extra and comment size
307
int size = name.getBytes(encoding).length;
308         final byte[] extra = ze.getExtra();
309         if (extra != null)
310             size += extra.length;
311         final String JavaDoc comment = ze.getComment();
312         if (comment != null)
313             size += comment.getBytes(encoding).length;
314         if (size > 0xFFFF)
315             throw new ZipException JavaDoc(
316                     "Sum of entry name, extra fields and comment too long (max " + 0xFFFF + "): " + size);
317         
318         switch (ze.getMethod()) {
319             case ZipEntry.STORED:
320                 {
321                     final String JavaDoc s = " is required for STORED method!";
322                     if (ze.getCrc() == -1)
323                         throw new ZipException JavaDoc("CRC checksum" + s);
324                     if (ze.getSize() == -1)
325                         throw new ZipException JavaDoc("Uncompressed size" + s);
326                     ze.setCompressedSize(ze.getSize());
327                 }
328                 this.deflate = false;
329                 break;
330                 
331             case ZipEntry.DEFLATED:
332                 if (!deflate) {
333                     final String JavaDoc s = " is required for DEFLATED method when writing raw deflated data!";
334                     if (ze.getCrc() == -1)
335                         throw new ZipException JavaDoc("CRC checksum" + s);
336                     if (ze.getCompressedSize() == -1)
337                         throw new ZipException JavaDoc("Compressed size" + s);
338                     if (ze.getSize() == -1)
339                         throw new ZipException JavaDoc("Uncompressed size" + s);
340                 }
341                 this.deflate = deflate;
342                 break;
343                 
344             default:
345                 throw new ZipException JavaDoc(
346                         "Unsupported compression method: " + ze.getMethod());
347         }
348
349         finished = false;
350
351         // Write LFH BEFORE putting the entry in the map.
352
entry = ze;
353         writeLocalFileHeader();
354
355         // Store entry now so immediate subsequent call to getEntry(...)
356
// returns it.
357
final ZipEntry old = (ZipEntry) entries.put(name, ze);
358         assert old == null;
359     }
360
361     /**
362      * @throws IOException On any I/O related issue.
363      */

364     private void writeLocalFileHeader() throws IOException JavaDoc {
365         final ZipEntry entry = this.entry;
366         assert entry != null;
367
368         final LEDataOutputStream dos = (LEDataOutputStream) out;
369
370         entry.offset = dos.size();
371
372         dos.writeInt(LFH_SIG);
373
374         // version needed to extract
375
// general purpose bit flag
376
final boolean useDD = entry.getMethod() == DEFLATED;
377         if (useDD) {
378             // Version 2.0 required to extract.
379
dos.writeShort(20);
380             // Bit 3 set to signal that we use a data descriptor
381
dos.writeShort(8);
382         } else {
383             dos.writeShort(10);
384             dos.writeShort(0);
385         }
386
387         // Compression method.
388
dos.writeShort(entry.getMethod());
389
390         // Last modification time and date in DOS format.
391
dos.writeInt((int) entry.getDosTime());
392
393         // CRC32.
394
// Compressed length.
395
// Uncompressed length.
396
if (useDD) {
397             dos.writeInt(0);
398             dos.writeInt(0);
399             dos.writeInt(0);
400         } else {
401             dos.writeInt((int) entry.getCrc());
402             dos.writeInt((int) entry.getCompressedSize());
403             dos.writeInt((int) entry.getSize());
404         }
405
406         // file name length
407
final byte[] name = entry.getName().getBytes(encoding);
408         dos.writeShort(name.length);
409
410         // extra field length
411
byte[] extra = entry.getExtra();
412         if (extra == null)
413             extra = new byte[0];
414         dos.writeShort(extra.length);
415
416         // file name
417
dos.write(name);
418
419         // extra field
420
dos.write(extra);
421
422         dataStart = dos.size();
423     }
424     
425     /**
426      * @throws IOException On any I/O related issue.
427      */

428     public void write(int b) throws IOException JavaDoc {
429         byte[] buf = sbuf;
430         buf[0] = (byte) b;
431         write(buf, 0, 1);
432     }
433     
434     /**
435      * @throws IOException On any I/O related issue.
436      */

437     public void write(final byte[] b, final int off, final int len)
438     throws IOException JavaDoc {
439         if (entry != null) {
440             if (len <= 0)
441                 return;
442             if (deflate) {
443                 // Fast implementation.
444
assert !def.finished();
445                 def.setInput(b, off, len);
446                 while (!def.needsInput())
447                     deflate();
448                 crc.update(b, off, len);
449             } else {
450                 out.write(b, off, len);
451                 if (entry.getMethod() != DEFLATED)
452                     crc.update(b, off, len);
453             }
454         } else {
455             out.write(b, off, len);
456         }
457     }
458
459     /**
460      * @throws IOException If there is no current ZIP entry to write.
461      */

462     /*private final void ensureEntry() throws IOException {
463         if (!busy())
464             throw new IOException("There is no current ZIP entry to write!");
465     }*/

466
467     private final void deflate() throws IOException JavaDoc {
468         final int dlen = def.deflate(dbuf, 0, dbuf.length);
469         if (dlen > 0)
470             out.write(dbuf, 0, dlen);
471     }
472
473     /**
474      * Writes all necessary data for this entry to the underlying stream.
475      *
476      * @throws ZipException If and only if writing the entry is impossible
477      * because the resulting file would not comply to the ZIP file
478      * format specification.
479      * @throws IOException On any I/O related issue.
480      */

481     public void closeEntry() throws IOException JavaDoc {
482         if (entry == null)
483             return;
484
485         switch (entry.getMethod()) {
486             case ZipEntry.STORED:
487                 final long expectedCrc = crc.getValue();
488                 if (entry.getCrc() != expectedCrc) {
489                     throw new ZipException JavaDoc("Bad CRC checksum for entry "
490                                            + entry.getName() + ": "
491                                            + Long.toHexString(entry.getCrc())
492                                            + " instead of "
493                                            + Long.toHexString(expectedCrc));
494                 }
495                 final long written = ((LEDataOutputStream) out).size();
496                 if (entry.getSize() != written - dataStart) {
497                     throw new ZipException JavaDoc("Bad size for entry "
498                                            + entry.getName() + ": "
499                                            + entry.getSize()
500                                            + " instead of "
501                                            + (written - dataStart));
502                 }
503                 break;
504                 
505             case ZipEntry.DEFLATED:
506                 if (deflate) {
507                     assert !def.finished();
508                     def.finish();
509                     while (!def.finished())
510                         deflate();
511
512                     entry.setCrc(crc.getValue());
513                     entry.setCompressedSize(def.getTotalOut() & 0xFFFFFFFFl);
514                     entry.setSize(def.getTotalIn() & 0xFFFFFFFFl);
515
516                     def.reset();
517                 } else {
518                     // Note: There is no way to check whether the written
519
// data matches the crc, the compressed size and the
520
// uncompressed size!
521
}
522                 break;
523                 
524             default:
525                 throw new ZipException JavaDoc(
526                         "Unsupported compression method: " + entry.getMethod());
527         }
528
529         writeDataDescriptor();
530         flush();
531         crc.reset();
532         entry = null;
533     }
534
535     /**
536      * @throws IOException On any I/O related issue.
537      */

538     private void writeDataDescriptor() throws IOException JavaDoc {
539         final ZipEntry entry = this.entry;
540         assert entry != null;
541
542         if (entry.getMethod() == STORED)
543             return;
544
545         final LEDataOutputStream dos = (LEDataOutputStream) out;
546
547         dos.writeInt(DD_SIG);
548         dos.writeInt((int) entry.getCrc());
549         dos.writeInt((int) entry.getCompressedSize());
550         dos.writeInt((int) entry.getSize());
551     }
552
553     /**
554      * Closes the current entry and writes the central directory to the
555      * underlying output stream.
556      * <p>
557      * <b>Notes:</b>
558      * <ul>
559      * <li>The underlying stream is not closed.</li>
560      * <li>Unlike Sun's implementation in J2SE 1.4.2, you may continue to use
561      * this ZIP output stream with putNextEntry(...) and the like.
562      * When you finally close the stream, the central directory will
563      * contain <em>all</em> entries written.</li>
564      * </ul>
565      *
566      * @throws ZipException If and only if writing the entry is impossible
567      * because the resulting file would not comply to the ZIP file
568      * format specification.
569      * @throws IOException On any I/O related issue.
570      */

571     public void finish() throws IOException JavaDoc {
572         if (finished)
573             return;
574
575         // Order is important here!
576
finished = true;
577
578         closeEntry();
579         final LEDataOutputStream dos = (LEDataOutputStream) out;
580         cdOffset = dos.size();
581         final Iterator JavaDoc i = entries.values().iterator();
582         while (i.hasNext())
583             writeCentralFileHeader((ZipEntry) i.next());
584         cdLength = dos.size() - cdOffset;
585         writeEndOfCentralDirectory();
586     }
587
588     /**
589      * Writes the central file header entry
590      *
591      * @throws IOException On any I/O related issue.
592      */

593     private void writeCentralFileHeader(final ZipEntry ze) throws IOException JavaDoc {
594         assert ze != null;
595
596         final LEDataOutputStream dos = (LEDataOutputStream) out;
597
598         dos.writeInt(CFH_SIG);
599
600         // version made by
601
dos.writeShort((ze.getPlatform() << 8) | 20);
602
603         // version needed to extract
604
// general purpose bit flag
605
if (ze.getMethod() == DEFLATED) {
606             // requires version 2 as we are going to store length info
607
// in the data descriptor
608
dos.writeShort(20);
609
610             // bit3 set to signal, we use a data descriptor
611
dos.writeShort(8);
612         } else {
613             dos.writeShort(10);
614             dos.writeShort(0);
615         }
616
617         // compression method
618
dos.writeShort(ze.getMethod());
619
620         // last mod. time and date
621
dos.writeInt((int) ze.getDosTime());
622
623         // CRC
624
// compressed length
625
// uncompressed length
626
dos.writeInt((int) ze.getCrc());
627         dos.writeInt((int) ze.getCompressedSize());
628         dos.writeInt((int) ze.getSize());
629
630         // file name length
631
final byte[] name = ze.getName().getBytes(encoding);
632         dos.writeShort(name.length);
633
634         // extra field length
635
byte[] extra = ze.getExtra();
636         if (extra == null)
637             extra = new byte[0];
638         dos.writeShort(extra.length);
639
640         // file comment length
641
String JavaDoc comment = ze.getComment();
642         if (comment == null)
643             comment = "";
644         final byte[] data = comment.getBytes(encoding);
645         dos.writeShort(data.length);
646
647         // disk number start
648
dos.writeShort(0);
649
650         // internal file attributes
651
dos.writeShort(0);
652
653         // external file attributes
654
dos.writeInt(0);
655
656         // relative offset of local file header
657
dos.writeInt((int) ze.offset);
658
659         // file name
660
dos.write(name);
661
662         // extra field
663
dos.write(extra);
664
665         // file comment
666
dos.write(data);
667     }
668
669     /**
670      * Writes the &quot;End of central dir record&quot;
671      *
672      * @throws IOException On any I/O related issue.
673      */

674     private void writeEndOfCentralDirectory() throws IOException JavaDoc {
675         final LEDataOutputStream dos = (LEDataOutputStream) out;
676
677         dos.writeInt(EOCD_SIG);
678
679         // disk numbers
680
dos.writeShort(0);
681         dos.writeShort(0);
682
683         // number of entries
684
dos.writeShort(entries.size());
685         dos.writeShort(entries.size());
686
687         // length and location of CD
688
dos.writeInt((int) cdLength);
689         dos.writeInt((int) cdOffset);
690
691         // ZIP file comment
692
String JavaDoc comment = getComment();
693         if (comment == null)
694             comment = "";
695         byte[] data = comment.getBytes(encoding);
696         dos.writeShort(data.length);
697         dos.write(data);
698     }
699
700     /**
701      * Closes this output stream and releases any system resources
702      * associated with the stream.
703      * This closes the open output stream writing to this ZIP file,
704      * if any.
705      *
706      * @throws IOException On any I/O related issue.
707      */

708     public void close() throws IOException JavaDoc {
709         if (closed)
710             return;
711
712         // Order is important here!
713
closed = true;
714
715         try {
716             finish();
717         } finally {
718             entries.clear();
719             super.close();
720         }
721     }
722
723     /**
724      * A Deflater which can be asked for its current deflation level.
725      */

726     private static class ZipDeflater extends Deflater JavaDoc {
727         private int level = Deflater.DEFAULT_COMPRESSION;
728         
729         public ZipDeflater() {
730             super(Deflater.DEFAULT_COMPRESSION, true);
731         }
732         
733         public int getLevel() {
734             return level;
735         }
736         
737         public void setLevel(int level) {
738             super.setLevel(level);
739             this.level = level;
740         }
741     }
742 }
743
Popular Tags