1/*
2 * ProGuard -- shrinking, optimization, obfuscation, and preverification
3 *             of Java bytecode.
4 *
5 * Copyright (c) 2002-2013 Eric Lafortune (eric@graphics.cornell.edu)
6 *
7 * This program is free software; you can redistribute it and/or modify it
8 * under the terms of the GNU General Public License as published by the Free
9 * Software Foundation; either version 2 of the License, or (at your option)
10 * any later version.
11 *
12 * This program is distributed in the hope that it will be useful, but WITHOUT
13 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
14 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
15 * more details.
16 *
17 * You should have received a copy of the GNU General Public License along
18 * with this program; if not, write to the Free Software Foundation, Inc.,
19 * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
20 */
21package proguard.io;
22
23import proguard.classfile.ClassConstants;
24
25import java.io.*;
26import java.util.*;
27import java.util.jar.*;
28import java.util.zip.*;
29
30
31/**
32 * This DataEntryWriter sends data entries to a given jar/zip file.
33 * The manifest and comment properties can optionally be set.
34 *
35 * @author Eric Lafortune
36 */
37public class JarWriter implements DataEntryWriter, Finisher
38{
39    private final DataEntryWriter dataEntryWriter;
40    private final Manifest        manifest;
41    private final String          comment;
42
43    private OutputStream    currentParentOutputStream;
44    private ZipOutputStream currentJarOutputStream;
45    private Finisher        currentFinisher;
46    private DataEntry       currentDataEntry;
47
48    // The names of the jar entries that are already in the jar.
49    private final Set jarEntryNames = new HashSet();
50
51
52    /**
53     * Creates a new JarWriter without manifest or comment.
54     */
55    public JarWriter(DataEntryWriter dataEntryWriter)
56    {
57        this(dataEntryWriter, null, null);
58    }
59
60
61    /**
62     * Creates a new JarWriter.
63     */
64    public JarWriter(DataEntryWriter dataEntryWriter,
65                     Manifest        manifest,
66                     String          comment)
67    {
68        this.dataEntryWriter = dataEntryWriter;
69        this.manifest        = manifest;
70        this.comment         = comment;
71    }
72
73
74    // Implementations for DataEntryWriter.
75
76    public boolean createDirectory(DataEntry dataEntry) throws IOException
77    {
78        //Make sure we can start with a new entry.
79        if (!prepareEntry(dataEntry))
80        {
81            return false;
82        }
83
84        // Close the previous ZIP entry, if any.
85        closeEntry();
86
87        // Get the directory entry name.
88        String name = dataEntry.getName() + ClassConstants.INTERNAL_PACKAGE_SEPARATOR;
89
90        // We have to check if the name is already used, because
91        // ZipOutputStream doesn't handle this case properly (it throws
92        // an exception which can be caught, but the ZipDataEntry is
93        // remembered anyway).
94        if (jarEntryNames.add(name))
95        {
96            // Create a new directory entry.
97            currentJarOutputStream.putNextEntry(new ZipEntry(name));
98            currentJarOutputStream.closeEntry();
99        }
100
101        // Clear the finisher.
102        currentFinisher  = null;
103        currentDataEntry = null;
104
105        return true;
106    }
107
108
109    public OutputStream getOutputStream(DataEntry dataEntry) throws IOException
110    {
111        return getOutputStream(dataEntry,  null);
112    }
113
114
115    public OutputStream getOutputStream(DataEntry dataEntry,
116                                        Finisher  finisher) throws IOException
117    {
118        //Make sure we can start with a new entry.
119        if (!prepareEntry(dataEntry))
120        {
121            return null;
122        }
123
124        // Do we need a new entry?
125        if (!dataEntry.equals(currentDataEntry))
126        {
127            // Close the previous ZIP entry, if any.
128            closeEntry();
129
130            // Get the entry name.
131            String name = dataEntry.getName();
132
133            // We have to check if the name is already used, because
134            // ZipOutputStream doesn't handle this case properly (it throws
135            // an exception which can be caught, but the ZipDataEntry is
136            // remembered anyway).
137            if (!jarEntryNames.add(name))
138            {
139                throw new IOException("Duplicate zip entry ["+dataEntry+"]");
140            }
141
142            // Create a new entry.
143            currentJarOutputStream.putNextEntry(new ZipEntry(name));
144
145            // Set up the finisher for the entry.
146            currentFinisher  = finisher;
147            currentDataEntry = dataEntry;
148        }
149
150        return currentJarOutputStream;
151    }
152
153
154    public void finish() throws IOException
155    {
156        // Finish the entire ZIP stream, if any.
157        if (currentJarOutputStream != null)
158        {
159            // Close the previous ZIP entry, if any.
160            closeEntry();
161
162            // Finish the entire ZIP stream.
163            currentJarOutputStream.finish();
164            currentJarOutputStream    = null;
165            currentParentOutputStream = null;
166            jarEntryNames.clear();
167        }
168    }
169
170
171    public void close() throws IOException
172    {
173        // Close the parent stream.
174        dataEntryWriter.close();
175    }
176
177
178    // Small utility methods.
179
180    /**
181     * Makes sure the current output stream is set up for the given entry.
182     */
183    private boolean prepareEntry(DataEntry dataEntry) throws IOException
184    {
185        // Get the parent stream, new or exisiting.
186        // This may finish our own jar output stream.
187        OutputStream parentOutputStream =
188            dataEntryWriter.getOutputStream(dataEntry.getParent(), this);
189
190        // Did we get a stream?
191        if (parentOutputStream == null)
192        {
193            return false;
194        }
195
196        // Do we need a new stream?
197        if (currentParentOutputStream == null)
198        {
199            currentParentOutputStream = parentOutputStream;
200
201            // Create a new jar stream, with a manifest, if set.
202            currentJarOutputStream = manifest != null ?
203                new JarOutputStream(parentOutputStream, manifest) :
204                new ZipOutputStream(parentOutputStream);
205
206            // Add a comment, if set.
207            if (comment != null)
208            {
209                currentJarOutputStream.setComment(comment);
210            }
211        }
212
213        return true;
214    }
215
216
217    /**
218     * Closes the previous ZIP entry, if any.
219     */
220    private void closeEntry() throws IOException
221    {
222        if (currentDataEntry != null)
223        {
224            // Let any finisher finish up first.
225            if (currentFinisher != null)
226            {
227                currentFinisher.finish();
228                currentFinisher = null;
229            }
230
231            currentJarOutputStream.closeEntry();
232            currentDataEntry = null;
233        }
234    }
235}
236