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