You can not select more than 25 topics Topics must start with a chinese character,a letter or number, can include dashes ('-') and can be up to 35 characters long.

Tar.java 24 kB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697
  1. /*
  2. * The Apache Software License, Version 1.1
  3. *
  4. * Copyright (c) 2000-2003 The Apache Software Foundation. All rights
  5. * reserved.
  6. *
  7. * Redistribution and use in source and binary forms, with or without
  8. * modification, are permitted provided that the following conditions
  9. * are met:
  10. *
  11. * 1. Redistributions of source code must retain the above copyright
  12. * notice, this list of conditions and the following disclaimer.
  13. *
  14. * 2. Redistributions in binary form must reproduce the above copyright
  15. * notice, this list of conditions and the following disclaimer in
  16. * the documentation and/or other materials provided with the
  17. * distribution.
  18. *
  19. * 3. The end-user documentation included with the redistribution, if
  20. * any, must include the following acknowlegement:
  21. * "This product includes software developed by the
  22. * Apache Software Foundation (http://www.apache.org/)."
  23. * Alternately, this acknowlegement may appear in the software itself,
  24. * if and wherever such third-party acknowlegements normally appear.
  25. *
  26. * 4. The names "The Jakarta Project", "Ant", and "Apache Software
  27. * Foundation" must not be used to endorse or promote products derived
  28. * from this software without prior written permission. For written
  29. * permission, please contact apache@apache.org.
  30. *
  31. * 5. Products derived from this software may not be called "Apache"
  32. * nor may "Apache" appear in their names without prior written
  33. * permission of the Apache Group.
  34. *
  35. * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
  36. * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
  37. * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
  38. * DISCLAIMED. IN NO EVENT SHALL THE APACHE SOFTWARE FOUNDATION OR
  39. * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
  40. * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
  41. * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
  42. * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
  43. * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
  44. * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
  45. * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
  46. * SUCH DAMAGE.
  47. * ====================================================================
  48. *
  49. * This software consists of voluntary contributions made by many
  50. * individuals on behalf of the Apache Software Foundation. For more
  51. * information on the Apache Software Foundation, please see
  52. * <http://www.apache.org/>.
  53. */
  54. package org.apache.tools.ant.taskdefs;
  55. import java.io.BufferedOutputStream;
  56. import java.io.File;
  57. import java.io.FileInputStream;
  58. import java.io.FileOutputStream;
  59. import java.io.IOException;
  60. import java.io.OutputStream;
  61. import java.util.Enumeration;
  62. import java.util.Vector;
  63. import java.util.zip.GZIPOutputStream;
  64. import org.apache.tools.ant.BuildException;
  65. import org.apache.tools.ant.DirectoryScanner;
  66. import org.apache.tools.ant.Project;
  67. import org.apache.tools.ant.types.EnumeratedAttribute;
  68. import org.apache.tools.ant.types.FileSet;
  69. import org.apache.tools.ant.util.MergingMapper;
  70. import org.apache.tools.ant.util.SourceFileScanner;
  71. import org.apache.tools.bzip2.CBZip2OutputStream;
  72. import org.apache.tools.tar.TarConstants;
  73. import org.apache.tools.tar.TarEntry;
  74. import org.apache.tools.tar.TarOutputStream;
  75. import org.apache.tools.zip.UnixStat;
  76. /**
  77. * Creates a tar archive.
  78. *
  79. * @author Stefano Mazzocchi
  80. * <a href="mailto:stefano@apache.org">stefano@apache.org</a>
  81. * @author <a href="mailto:stefan.bodewig@epost.de">Stefan Bodewig</a>
  82. * @author Magesh Umasankar
  83. *
  84. * @since Ant 1.1
  85. *
  86. * @ant.task category="packaging"
  87. */
  88. public class Tar extends MatchingTask {
  89. /**
  90. * @deprecated Tar.WARN is deprecated and is replaced with
  91. * Tar.TarLongFileMode.WARN
  92. */
  93. public static final String WARN = "warn";
  94. /**
  95. * @deprecated Tar.FAIL is deprecated and is replaced with
  96. * Tar.TarLongFileMode.FAIL
  97. */
  98. public static final String FAIL = "fail";
  99. /**
  100. * @deprecated Tar.TRUNCATE is deprecated and is replaced with
  101. * Tar.TarLongFileMode.TRUNCATE
  102. */
  103. public static final String TRUNCATE = "truncate";
  104. /**
  105. * @deprecated Tar.GNU is deprecated and is replaced with
  106. * Tar.TarLongFileMode.GNU
  107. */
  108. public static final String GNU = "gnu";
  109. /**
  110. * @deprecated Tar.OMIT is deprecated and is replaced with
  111. * Tar.TarLongFileMode.OMIT
  112. */
  113. public static final String OMIT = "omit";
  114. File tarFile;
  115. File baseDir;
  116. private TarLongFileMode longFileMode = new TarLongFileMode();
  117. Vector filesets = new Vector();
  118. Vector fileSetFiles = new Vector();
  119. /**
  120. * Indicates whether the user has been warned about long files already.
  121. */
  122. private boolean longWarningGiven = false;
  123. private TarCompressionMethod compression = new TarCompressionMethod();
  124. /**
  125. * Add a new fileset with the option to specify permissions
  126. */
  127. public TarFileSet createTarFileSet() {
  128. TarFileSet fileset = new TarFileSet();
  129. filesets.addElement(fileset);
  130. return fileset;
  131. }
  132. /**
  133. * Set is the name/location of where to create the tar file.
  134. * @deprecated for consistency with other tasks, please use setDestFile()
  135. */
  136. public void setTarfile(File tarFile) {
  137. this.tarFile = tarFile;
  138. }
  139. /**
  140. * Set is the name/location of where to create the tar file.
  141. * @since Ant 1.5
  142. * @param destFile The output of the tar
  143. */
  144. public void setDestFile(File destFile) {
  145. this.tarFile = destFile;
  146. }
  147. /**
  148. * This is the base directory to look in for things to tar.
  149. */
  150. public void setBasedir(File baseDir) {
  151. this.baseDir = baseDir;
  152. }
  153. /**
  154. * Set how to handle long files, those with a path&gt;100 chars.
  155. * Optional, default=warn.
  156. * <p>
  157. * Allowable values are
  158. * <ul>
  159. * <li> truncate - paths are truncated to the maximum length
  160. * <li> fail - paths greater than the maximim cause a build exception
  161. * <li> warn - paths greater than the maximum cause a warning and GNU is used
  162. * <li> gnu - GNU extensions are used for any paths greater than the maximum.
  163. * <li> omit - paths greater than the maximum are omitted from the archive
  164. * </ul>
  165. * @deprecated setLongFile(String) is deprecated and is replaced with
  166. * setLongFile(Tar.TarLongFileMode) to make Ant's Introspection
  167. * mechanism do the work and also to encapsulate operations on
  168. * the mode in its own class.
  169. */
  170. public void setLongfile(String mode) {
  171. log("DEPRECATED - The setLongfile(String) method has been deprecated."
  172. + " Use setLongfile(Tar.TarLongFileMode) instead.");
  173. this.longFileMode = new TarLongFileMode();
  174. longFileMode.setValue(mode);
  175. }
  176. /**
  177. * Set how to handle long files, those with a path&gt;100 chars.
  178. * Optional, default=warn.
  179. * <p>
  180. * Allowable values are
  181. * <ul>
  182. * <li> truncate - paths are truncated to the maximum length
  183. * <li> fail - paths greater than the maximim cause a build exception
  184. * <li> warn - paths greater than the maximum cause a warning and GNU is used
  185. * <li> gnu - GNU extensions are used for any paths greater than the maximum.
  186. * <li> omit - paths greater than the maximum are omitted from the archive
  187. * </ul>
  188. */
  189. public void setLongfile(TarLongFileMode mode) {
  190. this.longFileMode = mode;
  191. }
  192. /**
  193. * Set compression method.
  194. * Allowable values are
  195. * <ul>
  196. * <li> none - no compression
  197. * <li> gzip - Gzip compression
  198. * <li> bzip2 - Bzip2 compression
  199. * </ul>
  200. */
  201. public void setCompression(TarCompressionMethod mode) {
  202. this.compression = mode;
  203. }
  204. /**
  205. * do the business
  206. */
  207. public void execute() throws BuildException {
  208. if (tarFile == null) {
  209. throw new BuildException("tarfile attribute must be set!",
  210. getLocation());
  211. }
  212. if (tarFile.exists() && tarFile.isDirectory()) {
  213. throw new BuildException("tarfile is a directory!",
  214. getLocation());
  215. }
  216. if (tarFile.exists() && !tarFile.canWrite()) {
  217. throw new BuildException("Can not write to the specified tarfile!",
  218. getLocation());
  219. }
  220. Vector savedFileSets = (Vector) filesets.clone();
  221. try {
  222. if (baseDir != null) {
  223. if (!baseDir.exists()) {
  224. throw new BuildException("basedir does not exist!",
  225. getLocation());
  226. }
  227. // add the main fileset to the list of filesets to process.
  228. TarFileSet mainFileSet = new TarFileSet(fileset);
  229. mainFileSet.setDir(baseDir);
  230. filesets.addElement(mainFileSet);
  231. }
  232. if (filesets.size() == 0) {
  233. throw new BuildException("You must supply either a basedir "
  234. + "attribute or some nested filesets.",
  235. getLocation());
  236. }
  237. // check if tar is out of date with respect to each
  238. // fileset
  239. boolean upToDate = true;
  240. for (Enumeration e = filesets.elements(); e.hasMoreElements();) {
  241. TarFileSet fs = (TarFileSet) e.nextElement();
  242. String[] files = fs.getFiles(getProject());
  243. if (!archiveIsUpToDate(files, fs.getDir(getProject()))) {
  244. upToDate = false;
  245. }
  246. for (int i = 0; i < files.length; ++i) {
  247. if (tarFile.equals(new File(fs.getDir(getProject()),
  248. files[i]))) {
  249. throw new BuildException("A tar file cannot include "
  250. + "itself", getLocation());
  251. }
  252. }
  253. }
  254. if (upToDate) {
  255. log("Nothing to do: " + tarFile.getAbsolutePath()
  256. + " is up to date.", Project.MSG_INFO);
  257. return;
  258. }
  259. log("Building tar: " + tarFile.getAbsolutePath(), Project.MSG_INFO);
  260. TarOutputStream tOut = null;
  261. try {
  262. tOut = new TarOutputStream(
  263. compression.compress(
  264. new BufferedOutputStream(
  265. new FileOutputStream(tarFile))));
  266. tOut.setDebug(true);
  267. if (longFileMode.isTruncateMode()) {
  268. tOut.setLongFileMode(TarOutputStream.LONGFILE_TRUNCATE);
  269. } else if (longFileMode.isFailMode() ||
  270. longFileMode.isOmitMode()) {
  271. tOut.setLongFileMode(TarOutputStream.LONGFILE_ERROR);
  272. } else {
  273. // warn or GNU
  274. tOut.setLongFileMode(TarOutputStream.LONGFILE_GNU);
  275. }
  276. longWarningGiven = false;
  277. for (Enumeration e = filesets.elements();
  278. e.hasMoreElements();) {
  279. TarFileSet fs = (TarFileSet) e.nextElement();
  280. String[] files = fs.getFiles(getProject());
  281. if (files.length > 1 && fs.getFullpath().length() > 0) {
  282. throw new BuildException("fullpath attribute may only "
  283. + "be specified for "
  284. + "filesets that specify a "
  285. + "single file.");
  286. }
  287. for (int i = 0; i < files.length; i++) {
  288. File f = new File(fs.getDir(getProject()), files[i]);
  289. String name = files[i].replace(File.separatorChar, '/');
  290. tarFile(f, tOut, name, fs);
  291. }
  292. }
  293. } catch (IOException ioe) {
  294. String msg = "Problem creating TAR: " + ioe.getMessage();
  295. throw new BuildException(msg, ioe, getLocation());
  296. } finally {
  297. if (tOut != null) {
  298. try {
  299. // close up
  300. tOut.close();
  301. } catch (IOException e) {}
  302. }
  303. }
  304. } finally {
  305. filesets = savedFileSets;
  306. }
  307. }
  308. /**
  309. * tar a file
  310. */
  311. protected void tarFile(File file, TarOutputStream tOut, String vPath,
  312. TarFileSet tarFileSet)
  313. throws IOException {
  314. FileInputStream fIn = null;
  315. String fullpath = tarFileSet.getFullpath();
  316. if (fullpath.length() > 0) {
  317. vPath = fullpath;
  318. } else {
  319. // don't add "" to the archive
  320. if (vPath.length() <= 0) {
  321. return;
  322. }
  323. if (file.isDirectory() && !vPath.endsWith("/")) {
  324. vPath += "/";
  325. }
  326. String prefix = tarFileSet.getPrefix();
  327. // '/' is appended for compatibility with the zip task.
  328. if (prefix.length() > 0 && !prefix.endsWith("/")) {
  329. prefix = prefix + "/";
  330. }
  331. vPath = prefix + vPath;
  332. }
  333. if (vPath.startsWith("/") && !tarFileSet.getPreserveLeadingSlashes()) {
  334. int l = vPath.length();
  335. if (l <= 1) {
  336. // we would end up adding "" to the archive
  337. return;
  338. }
  339. vPath = vPath.substring(1, l);
  340. }
  341. try {
  342. if (vPath.length() >= TarConstants.NAMELEN) {
  343. if (longFileMode.isOmitMode()) {
  344. log("Omitting: " + vPath, Project.MSG_INFO);
  345. return;
  346. } else if (longFileMode.isWarnMode()) {
  347. log("Entry: " + vPath + " longer than " +
  348. TarConstants.NAMELEN + " characters.",
  349. Project.MSG_WARN);
  350. if (!longWarningGiven) {
  351. log("Resulting tar file can only be processed "
  352. + "successfully by GNU compatible tar commands",
  353. Project.MSG_WARN);
  354. longWarningGiven = true;
  355. }
  356. } else if (longFileMode.isFailMode()) {
  357. throw new BuildException(
  358. "Entry: " + vPath + " longer than " +
  359. TarConstants.NAMELEN + "characters.", getLocation());
  360. }
  361. }
  362. TarEntry te = new TarEntry(vPath);
  363. te.setModTime(file.lastModified());
  364. if (!file.isDirectory()) {
  365. te.setSize(file.length());
  366. te.setMode(tarFileSet.getMode());
  367. } else {
  368. te.setMode(tarFileSet.getDirMode());
  369. }
  370. te.setUserName(tarFileSet.getUserName());
  371. te.setGroupName(tarFileSet.getGroup());
  372. tOut.putNextEntry(te);
  373. if (!file.isDirectory()) {
  374. fIn = new FileInputStream(file);
  375. byte[] buffer = new byte[8 * 1024];
  376. int count = 0;
  377. do {
  378. tOut.write(buffer, 0, count);
  379. count = fIn.read(buffer, 0, buffer.length);
  380. } while (count != -1);
  381. }
  382. tOut.closeEntry();
  383. } finally {
  384. if (fIn != null) {
  385. fIn.close();
  386. }
  387. }
  388. }
  389. /**
  390. * @deprecated use the two-arg version instead.
  391. */
  392. protected boolean archiveIsUpToDate(String[] files) {
  393. return archiveIsUpToDate(files, baseDir);
  394. }
  395. /**
  396. * @since Ant 1.5.2
  397. */
  398. protected boolean archiveIsUpToDate(String[] files, File dir) {
  399. SourceFileScanner sfs = new SourceFileScanner(this);
  400. MergingMapper mm = new MergingMapper();
  401. mm.setTo(tarFile.getAbsolutePath());
  402. return sfs.restrict(files, baseDir, null, mm).length == 0;
  403. }
  404. /**
  405. * This is a FileSet with the option to specify permissions
  406. */
  407. public static class TarFileSet extends FileSet {
  408. private String[] files = null;
  409. private int fileMode = UnixStat.FILE_FLAG | UnixStat.DEFAULT_FILE_PERM;
  410. private int dirMode = UnixStat.DIR_FLAG | UnixStat.DEFAULT_DIR_PERM;
  411. private String userName = "";
  412. private String groupName = "";
  413. private String prefix = "";
  414. private String fullpath = "";
  415. private boolean preserveLeadingSlashes = false;
  416. public TarFileSet(FileSet fileset) {
  417. super(fileset);
  418. }
  419. public TarFileSet() {
  420. super();
  421. }
  422. /**
  423. * Get a list of files and directories specified in the fileset.
  424. * @return a list of file and directory names, relative to
  425. * the baseDir for the project.
  426. */
  427. public String[] getFiles(Project p) {
  428. if (files == null) {
  429. DirectoryScanner ds = getDirectoryScanner(p);
  430. String[] directories = ds.getIncludedDirectories();
  431. String[] filesPerSe = ds.getIncludedFiles();
  432. files = new String [directories.length + filesPerSe.length];
  433. System.arraycopy(directories, 0, files, 0, directories.length);
  434. System.arraycopy(filesPerSe, 0, files, directories.length,
  435. filesPerSe.length);
  436. }
  437. return files;
  438. }
  439. /**
  440. * A 3 digit octal string, specify the user, group and
  441. * other modes in the standard Unix fashion;
  442. * optional, default=0644
  443. */
  444. public void setMode(String octalString) {
  445. this.fileMode =
  446. UnixStat.FILE_FLAG | Integer.parseInt(octalString, 8);
  447. }
  448. public int getMode() {
  449. return fileMode;
  450. }
  451. /**
  452. * A 3 digit octal string, specify the user, group and
  453. * other modes in the standard Unix fashion;
  454. * optional, default=0755
  455. *
  456. * @since Ant 1.6
  457. */
  458. public void setDirMode(String octalString) {
  459. this.dirMode =
  460. UnixStat.DIR_FLAG | Integer.parseInt(octalString, 8);
  461. }
  462. /**
  463. * @since Ant 1.6
  464. */
  465. public int getDirMode() {
  466. return dirMode;
  467. }
  468. /**
  469. * The username for the tar entry
  470. * This is not the same as the UID, which is
  471. * not currently set by the task.
  472. */
  473. public void setUserName(String userName) {
  474. this.userName = userName;
  475. }
  476. public String getUserName() {
  477. return userName;
  478. }
  479. /**
  480. * The groupname for the tar entry; optional, default=""
  481. * This is not the same as the GID, which is
  482. * not currently set by the task.
  483. */
  484. public void setGroup(String groupName) {
  485. this.groupName = groupName;
  486. }
  487. public String getGroup() {
  488. return groupName;
  489. }
  490. /**
  491. * If the prefix attribute is set, all files in the fileset
  492. * are prefixed with that path in the archive.
  493. * optional.
  494. */
  495. public void setPrefix(String prefix) {
  496. this.prefix = prefix;
  497. }
  498. public String getPrefix() {
  499. return prefix;
  500. }
  501. /**
  502. * If the fullpath attribute is set, the file in the fileset
  503. * is written with that path in the archive. The prefix attribute,
  504. * if specified, is ignored. It is an error to have more than one file specified in
  505. * such a fileset.
  506. */
  507. public void setFullpath(String fullpath) {
  508. this.fullpath = fullpath;
  509. }
  510. public String getFullpath() {
  511. return fullpath;
  512. }
  513. /**
  514. * Flag to indicates whether leading `/'s should
  515. * be preserved in the file names.
  516. * Optional, default is <code>false</code>.
  517. */
  518. public void setPreserveLeadingSlashes(boolean b) {
  519. this.preserveLeadingSlashes = b;
  520. }
  521. public boolean getPreserveLeadingSlashes() {
  522. return preserveLeadingSlashes;
  523. }
  524. }
  525. /**
  526. * Set of options for long file handling in the task.
  527. *
  528. * @author Magesh Umasankar
  529. */
  530. public static class TarLongFileMode extends EnumeratedAttribute {
  531. // permissable values for longfile attribute
  532. public static final String WARN = "warn";
  533. public static final String FAIL = "fail";
  534. public static final String TRUNCATE = "truncate";
  535. public static final String GNU = "gnu";
  536. public static final String OMIT = "omit";
  537. private final String[] validModes = {WARN, FAIL, TRUNCATE, GNU, OMIT};
  538. public TarLongFileMode() {
  539. super();
  540. setValue(WARN);
  541. }
  542. public String[] getValues() {
  543. return validModes;
  544. }
  545. public boolean isTruncateMode() {
  546. return TRUNCATE.equalsIgnoreCase(getValue());
  547. }
  548. public boolean isWarnMode() {
  549. return WARN.equalsIgnoreCase(getValue());
  550. }
  551. public boolean isGnuMode() {
  552. return GNU.equalsIgnoreCase(getValue());
  553. }
  554. public boolean isFailMode() {
  555. return FAIL.equalsIgnoreCase(getValue());
  556. }
  557. public boolean isOmitMode() {
  558. return OMIT.equalsIgnoreCase(getValue());
  559. }
  560. }
  561. /**
  562. * Valid Modes for Compression attribute to Tar Task
  563. *
  564. */
  565. public static final class TarCompressionMethod extends EnumeratedAttribute {
  566. // permissable values for compression attribute
  567. /**
  568. * No compression
  569. */
  570. private static final String NONE = "none";
  571. /**
  572. * GZIP compression
  573. */
  574. private static final String GZIP = "gzip";
  575. /**
  576. * BZIP2 compression
  577. */
  578. private static final String BZIP2 = "bzip2";
  579. /**
  580. * Default constructor
  581. */
  582. public TarCompressionMethod() {
  583. super();
  584. setValue(NONE);
  585. }
  586. /**
  587. * Get valid enumeration values.
  588. * @return valid enumeration values
  589. */
  590. public String[] getValues() {
  591. return new String[] { NONE, GZIP, BZIP2 };
  592. }
  593. /**
  594. * This method wraps the output stream with the
  595. * corresponding compression method
  596. *
  597. * @param ostream output stream
  598. * @return output stream with on-the-fly compression
  599. * @exception IOException thrown if file is not writable
  600. */
  601. private OutputStream compress(final OutputStream ostream)
  602. throws IOException {
  603. final String value = getValue();
  604. if (GZIP.equals(value)) {
  605. return new GZIPOutputStream(ostream);
  606. } else {
  607. if (BZIP2.equals(value)) {
  608. ostream.write('B');
  609. ostream.write('Z');
  610. return new CBZip2OutputStream(ostream);
  611. }
  612. }
  613. return ostream;
  614. }
  615. }
  616. }