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    }