1/*
2 * ProGuard -- shrinking, optimization, obfuscation, and preverification
3 *             of Java bytecode.
4 *
5 * Copyright (c) 2002-2014 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 * This DataEntryWriter sends data entries to a given jar/zip file.
32 * The manifest and comment properties can optionally be set.
33 *
34 * @author Eric Lafortune
35 */
36public class JarWriter implements DataEntryWriter, Finisher
37{
38    private final DataEntryWriter dataEntryWriter;
39    private final Manifest        manifest;
40    private final String          comment;
41
42    private OutputStream    currentParentOutputStream;
43    private ZipOutputStream currentJarOutputStream;
44    private Finisher        currentFinisher;
45    private DataEntry       currentDataEntry;
46
47    // The names of the jar entries that are already in the jar.
48    private final Set jarEntryNames = new HashSet();
49
50
51    /**
52     * Creates a new JarWriter without manifest or comment.
53     */
54    public JarWriter(DataEntryWriter dataEntryWriter)
55    {
56        this(dataEntryWriter, null, null);
57    }
58
59
60    /**
61     * Creates a new JarWriter.
62     */
63    public JarWriter(DataEntryWriter dataEntryWriter,
64                     Manifest        manifest,
65                     String          comment)
66    {
67        this.dataEntryWriter = dataEntryWriter;
68        this.manifest        = manifest;
69        this.comment         = comment;
70    }
71
72
73    // Implementations for DataEntryWriter.
74
75    public boolean createDirectory(DataEntry dataEntry) throws IOException
76    {
77        // Make sure we can start with a new entry.
78        if (!prepareEntry(dataEntry))
79        {
80            return false;
81        }
82
83        // Close the previous ZIP entry, if any.
84        closeEntry();
85
86        // Get the directory entry name.
87        String name = dataEntry.getName() + ClassConstants.PACKAGE_SEPARATOR;
88
89        // We have to check if the name is already used, because
90        // ZipOutputStream doesn't handle this case properly (it throws
91        // an exception which can be caught, but the ZipDataEntry is
92        // remembered anyway).
93        if (jarEntryNames.add(name))
94        {
95            // Create a new directory entry.
96            currentJarOutputStream.putNextEntry(new ZipEntry(name));
97            currentJarOutputStream.closeEntry();
98        }
99
100        // Clear the finisher.
101        currentFinisher  = null;
102        currentDataEntry = null;
103
104        return true;
105    }
106
107
108    public OutputStream getOutputStream(DataEntry dataEntry) throws IOException
109    {
110        return getOutputStream(dataEntry,  null);
111    }
112
113
114    public OutputStream getOutputStream(DataEntry dataEntry,
115                                        Finisher  finisher) throws IOException
116    {
117        //Make sure we can start with a new entry.
118        if (!prepareEntry(dataEntry))
119        {
120            return null;
121        }
122
123        // Do we need a new entry?
124        if (!dataEntry.equals(currentDataEntry))
125        {
126            // Close the previous ZIP entry, if any.
127            closeEntry();
128
129            // Get the entry name.
130            String name = dataEntry.getName();
131
132            // We have to check if the name is already used, because
133            // ZipOutputStream doesn't handle this case properly (it throws
134            // an exception which can be caught, but the ZipDataEntry is
135            // remembered anyway).
136            if (!jarEntryNames.add(name))
137            {
138                throw new IOException("Duplicate zip entry ["+dataEntry+"]");
139            }
140
141            // Create a new entry.
142            currentJarOutputStream.putNextEntry(new ZipEntry(name));
143
144            // Set up the finisher for the entry.
145            currentFinisher  = finisher;
146            currentDataEntry = dataEntry;
147        }
148
149        return currentJarOutputStream;
150    }
151
152
153    public void finish() throws IOException
154    {
155        // Finish the entire ZIP stream, if any.
156        if (currentJarOutputStream != null)
157        {
158            // Close the previous ZIP entry, if any.
159            closeEntry();
160
161            // Finish the entire ZIP stream.
162            currentJarOutputStream.finish();
163            currentJarOutputStream    = null;
164            currentParentOutputStream = null;
165            jarEntryNames.clear();
166        }
167    }
168
169
170    public void close() throws IOException
171    {
172        // Close the parent stream.
173        dataEntryWriter.close();
174    }
175
176
177    // Small utility methods.
178
179    /**
180     * Makes sure the current output stream is set up for the given entry.
181     */
182    private boolean prepareEntry(DataEntry dataEntry) throws IOException
183    {
184        // Get the parent stream, new or existing.
185        // This may finish our own jar output stream.
186        OutputStream parentOutputStream =
187            dataEntryWriter.getOutputStream(dataEntry.getParent(), this);
188
189        // Did we get a stream?
190        if (parentOutputStream == null)
191        {
192            return false;
193        }
194
195        // Do we need a new stream?
196        if (currentParentOutputStream == null)
197        {
198            currentParentOutputStream = parentOutputStream;
199
200            // Create a new jar stream, with a manifest, if set.
201            currentJarOutputStream = manifest != null ?
202                new JarOutputStream(parentOutputStream, manifest) :
203                new ZipOutputStream(parentOutputStream);
204
205            // Add a comment, if set.
206            if (comment != null)
207            {
208                currentJarOutputStream.setComment(comment);
209            }
210        }
211
212        return true;
213    }
214
215
216    /**
217     * Closes the previous ZIP entry, if any.
218     */
219    private void closeEntry() throws IOException
220    {
221        if (currentDataEntry != null)
222        {
223            // Let any finisher finish up first.
224            if (currentFinisher != null)
225            {
226                currentFinisher.finish();
227                currentFinisher = null;
228            }
229
230            currentJarOutputStream.closeEntry();
231            currentDataEntry = null;
232        }
233    }
234}
235