001 /*
002 * This file is part of muCommander, http://www.mucommander.com
003 * Copyright (C) 2002-2008 Maxence Bernard
004 *
005 * muCommander is free software; you can redistribute it and/or modify
006 * it under the terms of the GNU General Public License as published by
007 * the Free Software Foundation; either version 3 of the License, or
008 * (at your option) any later version.
009 *
010 * muCommander is distributed in the hope that it will be useful,
011 * but WITHOUT ANY WARRANTY; without even the implied warranty of
012 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
013 * GNU General Public License for more details.
014 *
015 * You should have received a copy of the GNU General Public License
016 * along with this program. If not, see <http://www.gnu.org/licenses/>.
017 */
018
019
020 package com.mucommander.file.archiver;
021
022 import com.mucommander.file.AbstractFile;
023 import com.mucommander.io.BufferedRandomOutputStream;
024 import com.mucommander.io.RandomAccessOutputStream;
025 import org.apache.tools.bzip2.CBZip2OutputStream;
026
027 import java.io.BufferedOutputStream;
028 import java.io.IOException;
029 import java.io.OutputStream;
030 import java.util.zip.GZIPOutputStream;
031
032
033 /**
034 * Archiver is an abstract class that represents a generic file archiver and abstracts the underlying
035 * compression method and specifics of the format.
036 *
037 * <p>Subclasses implement specific archive formats (Zip, Tar...) but cannot be instanciated directly.
038 * Instead, the <code>getArchiver</code> methods can be used to retrieve an Archiver
039 * instance for a specified archive format. A list of available archive formats can be dynamically retrieved
040 * using {@link #getFormats(boolean) getFormats}.
041 *
042 * <p>Archive formats fall into 2 categories:
043 * <ul>
044 * <li><i>Single entry formats:</i> Formats that can only store one entry without any directory structure, e.g. Gzip or Bzip2.
045 * <li><i>Many entries formats:</i> Formats that can store multiple entries along with a directory structure, e.g. Zip or Tar.
046 * </ul>
047 *
048 * @author Maxence Bernard
049 */
050 public abstract class Archiver {
051
052 /** Zip archive format (many entries format) */
053 public final static int ZIP_FORMAT = 0;
054 /** Gzip archive format (single entry format) */
055 public final static int GZ_FORMAT = 1;
056 /** Bzip2 archive format (single entry format) */
057 public final static int BZ2_FORMAT = 2;
058 /** Tar archive format without any compression (many entries format) */
059 public final static int TAR_FORMAT = 3;
060 /** Tar archive compressed with Gzip format (many entries format) */
061 public final static int TAR_GZ_FORMAT = 4;
062 /** Tar archive compressed with Bzip2 format (many entries format) */
063 public final static int TAR_BZ2_FORMAT = 5;
064
065 /** Boolean array describing for each format if it can store more than one entry */
066 private final static boolean SUPPORTS_MANY_ENTRIES[] = {
067 true,
068 false,
069 false,
070 true,
071 true,
072 true
073 };
074
075 /** Array of single entry formats: many entries formats are considered to be single entry formats as well */
076 private final static int SINGLE_ENTRY_FORMATS[] = {
077 ZIP_FORMAT,
078 GZ_FORMAT,
079 BZ2_FORMAT,
080 TAR_FORMAT,
081 TAR_GZ_FORMAT,
082 TAR_BZ2_FORMAT
083 };
084
085 /** Array of many entries formats */
086 private final static int MANY_ENTRIES_FORMATS[] = {
087 ZIP_FORMAT,
088 TAR_FORMAT,
089 TAR_GZ_FORMAT,
090 TAR_BZ2_FORMAT
091 };
092
093 /** Array of format names */
094 private final static String FORMAT_NAMES[] = {
095 "Zip",
096 "Gzip",
097 "Bzip2",
098 "Tar",
099 "Tar/Gzip",
100 "Tar/Bzip2"
101 };
102
103 /** Array of format extensions */
104 private final static String FORMAT_EXTENSIONS[] = {
105 "zip",
106 "gz",
107 "bz2",
108 "tar",
109 "tar.gz",
110 "tar.bz2"
111 };
112
113
114 /** The underlying stream this archiver is writing to */
115 protected OutputStream out;
116 /** Archive format of this Archiver */
117 protected int format;
118 /** Archive format's name of this Archiver */
119 protected String formatName;
120
121
122 /**
123 * Creates a new Archiver.
124 *
125 * @param out the OutputStream this Archiver will write to
126 */
127 Archiver(OutputStream out) {
128 this.out = out;
129 }
130
131 /**
132 * Returns the <code>OutputStream</code> this Archiver is writing to.
133 *
134 * @return the OutputStream this Archiver is writing to
135 */
136 public OutputStream getOutputStream() {
137 return out;
138 }
139
140
141 /**
142 * Returns the archiver format used by this Archiver. See format constants.
143 */
144 public int getFormat() {
145 return this.format;
146 }
147
148 /**
149 * Sets the archiver format used by this Archiver, for internal use only.
150 */
151 private void setFormat(int format) {
152 this.format = format;
153 }
154
155
156 /**
157 * Returns the name of the archive format used by this Archiver.
158 */
159 public String getFormatName() {
160 return FORMAT_NAMES[this.format];
161 }
162
163
164 /**
165 * Returns true if the format used by this Archiver can store more than one entry.
166 */
167 public boolean supportsManyFiles() {
168 return formatSupportsManyFiles(this.format);
169 }
170
171
172 /**
173 * Returns true if the format used by this Archiver can store an optional comment.
174 */
175 public boolean supportsComment() {
176 return formatSupportsComment(this.format);
177 }
178
179
180 /**
181 * Sets an optional comment in the archive, the {@link #supportsComment()} or
182 * {@link #formatSupportsComment(int)} must first be called to make sure
183 * the archive format supports comment, otherwise calling this method will have no effect.
184 *
185 * <p>Implementation note: Archiver implementations must override this method to handle comments
186 *
187 * @param comment the comment to be stored in the archive
188 */
189 public void setComment(String comment) {
190 // No-op
191 }
192
193
194 /**
195 * Normalizes the entry path, that is :
196 * <ul>
197 * <li>replace any \ character occurrence by / as this usually is the default separator for archive files
198 * <li>if the entry is a directory, add a trailing slash to the path if it doesn't have one already
199 * </ul>
200 */
201 protected String normalizePath(String entryPath, boolean isDirectory) {
202 // Replace any \ character by /
203 entryPath = entryPath.replace('\\', '/');
204
205 // If entry is a directory, make sure the path contains a trailing /
206 if(isDirectory && !entryPath.endsWith("/"))
207 entryPath += "/";
208
209 return entryPath;
210 }
211
212
213 ////////////////////
214 // Static methods //
215 ////////////////////
216
217 /**
218 * Returns an Archiver for the specified format and that uses the given {@link AbstractFile} to write entries to.
219 * <code>null</code> is returned if the specified format is not valid.
220 *
221 * <p>This method will first attempt to get a {@link RandomAccessOutputStream} if the given file is able to supply
222 * one, and if not, fall back to a regular <code>OutputStream</code>. Note that if the file exists, its contents
223 * will be overwritten. Write bufferring is used under the hood to improve performance.</p>
224 *
225 * @param file the AbstractFile which the returned Archiver will write entries to
226 * @param format an archive format
227 * @return an Archiver for the specified format and that uses the given {@link AbstractFile} to write entries to ;
228 * null if the specified format is not valid.
229 * @throws IOException if the file cannot be opened for write, or if an error occurred while intializing the archiver
230 */
231 public static Archiver getArchiver(AbstractFile file, int format) throws IOException {
232 OutputStream out = null;
233
234 if(file.hasRandomAccessOutputStream()) {
235 try {
236 // Important: if the file exists, it has to be overwritten as AbstractFile#getRandomAccessOutputStream()
237 // does NOT overwrite the file. This fixes bug #30.
238 if(file.exists())
239 file.delete();
240
241 out = new BufferedRandomOutputStream(file.getRandomAccessOutputStream());
242 }
243 catch(IOException e) {
244 // Fall back to a regular OutputStream
245 }
246 }
247
248 if(out==null)
249 out = new BufferedOutputStream(file.getOutputStream(false));
250
251 return getArchiver(out, format);
252 }
253
254
255 /**
256 * Returns an Archiver for the specified format and that uses the given <code>OutputStream</code> to write entries to.
257 * <code>null</code> is returned if the specified format is not valid. Whenever possible, a
258 * {@link RandomAccessOutputStream} should be supplied as some formats take advantage of having a random write access.
259 *
260 * @param out the OutputStream which the returned Archiver will write entries to
261 * @param format an archive format
262 * @return an Archiver for the specified format and that uses the given {@link AbstractFile} to write entries to ;
263 * null if the specified format is not valid.
264 * @throws IOException if an error occurred while intializing the archiver
265 */
266 public static Archiver getArchiver(OutputStream out, int format) throws IOException {
267 Archiver archiver;
268
269 switch(format) {
270 case ZIP_FORMAT:
271 archiver = new ZipArchiver(out);
272 break;
273 case GZ_FORMAT:
274 archiver = new SingleFileArchiver(new GZIPOutputStream(out));
275 break;
276 case BZ2_FORMAT:
277 archiver = new SingleFileArchiver(createBzip2OutputStream(out));
278 break;
279 case TAR_FORMAT:
280 archiver = new TarArchiver(out);
281 break;
282 case TAR_GZ_FORMAT:
283 archiver = new TarArchiver(new GZIPOutputStream(out));
284 break;
285 case TAR_BZ2_FORMAT:
286 archiver = new TarArchiver(createBzip2OutputStream(out));
287 break;
288
289 default:
290 return null;
291 }
292
293 archiver.setFormat(format);
294
295 return archiver;
296 }
297
298 /**
299 * Creates and returns a Bzip2 <code>OutputStream</code> using the given <code>OutputStream</code> as the underlying
300 * stream.
301 *
302 * @param out the underlying stream
303 * @return a Bzip2 OutputStream
304 * @throws IOException if an error occurred while initializing the Bzip2 OutputStream
305 */
306 protected static OutputStream createBzip2OutputStream(OutputStream out) throws IOException {
307 // Writes the 2 magic bytes 'BZ', as required by CBZip2OutputStream. A quote from CBZip2OutputStream's Javadoc:
308 // "Attention: The caller is resonsible to write the two BZip2 magic bytes "BZ" to the specified stream
309 // prior to calling this constructor."
310
311 out.write('B');
312 out.write('Z');
313
314 return new CBZip2OutputStream(out);
315 }
316
317
318 /**
319 * Returns an array of available archive formats, single entry formats or many entries formats
320 * depending on the value of the specified boolean parameter.
321 *
322 * @param manyEntries if true, a list of many entries formats (a subset of single entry formats) will be returned
323 */
324 public static int[] getFormats(boolean manyEntries) {
325 return manyEntries? MANY_ENTRIES_FORMATS : SINGLE_ENTRY_FORMATS;
326 }
327
328
329 /**
330 * Returns the name of the given archive format. The returned name can be used for display in a GUI.
331 *
332 * @param format an archive format
333 */
334 public static String getFormatName(int format) {
335 return FORMAT_NAMES[format];
336 }
337
338
339 /**
340 * Returns the default archive format extension. Note: some formats such as Tar/Gzip have several common
341 * extensions (e.g. tar.gz or tgz), the most common one will be returned.
342 *
343 * @param format an archive format
344 */
345 public static String getFormatExtension(int format) {
346 return FORMAT_EXTENSIONS[format];
347 }
348
349
350 /**
351 * Returns true if the specified archive format supports storage of more than one entry.
352 *
353 * @param format an archive format
354 */
355 public static boolean formatSupportsManyFiles(int format) {
356 return SUPPORTS_MANY_ENTRIES[format];
357 }
358
359
360 /**
361 * Returns true if the specified archive format can store an optional comment.
362 *
363 * @param format an archive format
364 */
365 public static boolean formatSupportsComment(int format) {
366 return format==ZIP_FORMAT;
367 }
368
369
370 //////////////////////
371 // Abstract methods //
372 //////////////////////
373
374 /**
375 * Creates a new entry in the archive with the given path. The specified file is used to determine
376 * whether the entry is a directory or a regular file, and to set the entry's size, permissions and date.
377 *
378 * <p>If the entry is a regular file (not a directory), an OutputStream which can be used to write the contents
379 * of the entry will be returned, <code>null</code> otherwise. The OutputStream <b>must not</b> be closed once
380 * it has been used (Archiver takes care of this), only the {@link #close() close} method has to be called when
381 * all entries have been created.
382 *
383 * <p>If this Archiver uses a single entry format, the specified path and file won't be used at all.
384 * Also in this case, this method must be invoked only once (single entry), it will throw an IOException
385 * if invoked more than once.
386 *
387 * @param entryPath the path to be used to create the entry in the archive.
388 * This parameter is simply ignored if the archive is a single entry format.
389 * @param file AbstractFile instance used to determine if the entry is a directory, and to set the entry's date.
390 * This parameter is simply ignored if the archive is a single entry format.
391 *
392 * @exception IOException if this Archiver failed to write the entry, or in the case of a single entry archiver, if
393 * this method was called more than once.
394 */
395 public abstract OutputStream createEntry(String entryPath, AbstractFile file) throws IOException;
396
397
398 /**
399 * Closes the underlying OuputStream and ressources used by this Archiver to write the archive. This method
400 * must be called when all entries have been added to the archive.
401 */
402 public abstract void close() throws IOException;
403 }