001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.apache.commons.compress.harmony.pack200; 018 019import java.io.BufferedOutputStream; 020import java.io.IOException; 021import java.io.OutputStream; 022import java.util.ArrayList; 023import java.util.List; 024import java.util.jar.JarEntry; 025import java.util.jar.JarFile; 026import java.util.jar.JarInputStream; 027import java.util.zip.GZIPOutputStream; 028import java.util.zip.ZipEntry; 029 030/** 031 * Archive is the main entry point to pack200 and represents a packed archive. An archive is constructed with either a 032 * JarInputStream and an output stream or a JarFile as input and an OutputStream. Options can be set, then 033 * {@code pack()} is called, to pack the Jar file into a pack200 archive. 034 */ 035public class Archive { 036 037 private final JarInputStream jarInputStream; 038 private final OutputStream outputStream; 039 private JarFile jarFile; 040 private long currentSegmentSize; 041 private final PackingOptions options; 042 043 /** 044 * Creates an Archive with streams for the input and output. 045 * 046 * @param inputStream TODO 047 * @param outputStream TODO 048 * @param options - packing options (if null then defaults are used) 049 * @throws IOException If an I/O error occurs. 050 */ 051 public Archive(final JarInputStream inputStream, OutputStream outputStream, PackingOptions options) 052 throws IOException { 053 jarInputStream = inputStream; 054 if (options == null) { 055 // use all defaults 056 options = new PackingOptions(); 057 } 058 this.options = options; 059 if (options.isGzip()) { 060 outputStream = new GZIPOutputStream(outputStream); 061 } 062 this.outputStream = new BufferedOutputStream(outputStream); 063 PackingUtils.config(options); 064 } 065 066 /** 067 * Creates an Archive with the given input file and a stream for the output 068 * 069 * @param jarFile - the input file 070 * @param outputStream TODO 071 * @param options - packing options (if null then defaults are used) 072 * @throws IOException If an I/O error occurs. 073 */ 074 public Archive(final JarFile jarFile, OutputStream outputStream, PackingOptions options) throws IOException { 075 if (options == null) { // use all defaults 076 options = new PackingOptions(); 077 } 078 this.options = options; 079 if (options.isGzip()) { 080 outputStream = new GZIPOutputStream(outputStream); 081 } 082 this.outputStream = new BufferedOutputStream(outputStream); 083 this.jarFile = jarFile; 084 jarInputStream = null; 085 PackingUtils.config(options); 086 } 087 088 /** 089 * Pack the archive 090 * 091 * @throws Pack200Exception TODO 092 * @throws IOException If an I/O error occurs. 093 */ 094 public void pack() throws Pack200Exception, IOException { 095 if (0 == options.getEffort()) { 096 doZeroEffortPack(); 097 } else { 098 doNormalPack(); 099 } 100 } 101 102 private void doZeroEffortPack() throws IOException { 103 PackingUtils.log("Start to perform a zero-effort packing"); 104 if (jarInputStream != null) { 105 PackingUtils.copyThroughJar(jarInputStream, outputStream); 106 } else { 107 PackingUtils.copyThroughJar(jarFile, outputStream); 108 } 109 } 110 111 private void doNormalPack() throws IOException, Pack200Exception { 112 PackingUtils.log("Start to perform a normal packing"); 113 List<PackingFile> packingFileList; 114 if (jarInputStream != null) { 115 packingFileList = PackingUtils.getPackingFileListFromJar(jarInputStream, options.isKeepFileOrder()); 116 } else { 117 packingFileList = PackingUtils.getPackingFileListFromJar(jarFile, options.isKeepFileOrder()); 118 } 119 120 final List<SegmentUnit> segmentUnitList = splitIntoSegments(packingFileList); 121 int previousByteAmount = 0; 122 int packedByteAmount = 0; 123 124 final int segmentSize = segmentUnitList.size(); 125 SegmentUnit segmentUnit; 126 for (int index = 0; index < segmentSize; index++) { 127 segmentUnit = segmentUnitList.get(index); 128 new Segment().pack(segmentUnit, outputStream, options); 129 previousByteAmount += segmentUnit.getByteAmount(); 130 packedByteAmount += segmentUnit.getPackedByteAmount(); 131 } 132 133 PackingUtils.log("Total: Packed " + previousByteAmount + " input bytes of " + packingFileList.size() 134 + " files into " + packedByteAmount + " bytes in " + segmentSize + " segments"); 135 136 outputStream.close(); 137 } 138 139 private List<SegmentUnit> splitIntoSegments(final List<PackingFile> packingFileList) { 140 final List<SegmentUnit> segmentUnitList = new ArrayList<>(); 141 List<Pack200ClassReader> classes = new ArrayList<>(); 142 List<PackingFile> files = new ArrayList<>(); 143 final long segmentLimit = options.getSegmentLimit(); 144 145 final int size = packingFileList.size(); 146 PackingFile packingFile; 147 for (int index = 0; index < size; index++) { 148 packingFile = packingFileList.get(index); 149 if (!addJarEntry(packingFile, classes, files)) { 150 // not added because segment has reached maximum size 151 segmentUnitList.add(new SegmentUnit(classes, files)); 152 classes = new ArrayList<>(); 153 files = new ArrayList<>(); 154 currentSegmentSize = 0; 155 // add the jar to a new segment 156 addJarEntry(packingFile, classes, files); 157 // ignore the size of first entry for compatibility with RI 158 currentSegmentSize = 0; 159 } else if (segmentLimit == 0 && estimateSize(packingFile) > 0) { 160 // create a new segment for each class unless size is 0 161 segmentUnitList.add(new SegmentUnit(classes, files)); 162 classes = new ArrayList<>(); 163 files = new ArrayList<>(); 164 } 165 } 166 // Change for Apache Commons Compress based on Apache Harmony. 167 // if (classes.size() > 0 && files.size() > 0) { 168 if (classes.size() > 0 || files.size() > 0) { 169 segmentUnitList.add(new SegmentUnit(classes, files)); 170 } 171 return segmentUnitList; 172 } 173 174 private boolean addJarEntry(final PackingFile packingFile, final List<Pack200ClassReader> javaClasses, final List<PackingFile> files) { 175 final long segmentLimit = options.getSegmentLimit(); 176 if (segmentLimit != -1 && segmentLimit != 0) { 177 // -1 is a special case where only one segment is created and 178 // 0 is a special case where one segment is created for each file 179 // except for files in "META-INF" 180 final long packedSize = estimateSize(packingFile); 181 if (packedSize + currentSegmentSize > segmentLimit && currentSegmentSize > 0) { 182 // don't add this JarEntry to the current segment 183 return false; 184 } 185 // do add this JarEntry 186 currentSegmentSize += packedSize; 187 } 188 189 final String name = packingFile.getName(); 190 if (name.endsWith(".class") && !options.isPassFile(name)) { 191 final Pack200ClassReader classParser = new Pack200ClassReader(packingFile.contents); 192 classParser.setFileName(name); 193 javaClasses.add(classParser); 194 packingFile.contents = new byte[0]; 195 } 196 files.add(packingFile); 197 return true; 198 } 199 200 private long estimateSize(final PackingFile packingFile) { 201 // The heuristic used here is for compatibility with the RI and should 202 // not be changed 203 final String name = packingFile.getName(); 204 if (name.startsWith("META-INF") || name.startsWith("/META-INF")) { 205 return 0; 206 } 207 long fileSize = packingFile.contents.length; 208 if (fileSize < 0) { 209 fileSize = 0; 210 } 211 return name.length() + fileSize + 5; 212 } 213 214 static class SegmentUnit { 215 216 private final List<Pack200ClassReader> classList; 217 218 private final List<PackingFile> fileList; 219 220 private int byteAmount; 221 222 private int packedByteAmount; 223 224 public SegmentUnit(final List<Pack200ClassReader> classes, final List<PackingFile> files) { 225 classList = classes; 226 fileList = files; 227 byteAmount = 0; 228 // Calculate the amount of bytes in classes and files before packing 229 byteAmount += classList.stream().mapToInt(element -> element.b.length).sum(); 230 byteAmount += fileList.stream().mapToInt(element -> element.contents.length).sum(); 231 } 232 233 public List<Pack200ClassReader> getClassList() { 234 return classList; 235 } 236 237 public int classListSize() { 238 return classList.size(); 239 } 240 241 public int fileListSize() { 242 return fileList.size(); 243 } 244 245 public List<PackingFile> getFileList() { 246 return fileList; 247 } 248 249 public int getByteAmount() { 250 return byteAmount; 251 } 252 253 public int getPackedByteAmount() { 254 return packedByteAmount; 255 } 256 257 public void addPackedByteAmount(final int amount) { 258 packedByteAmount += amount; 259 } 260 } 261 262 static class PackingFile { 263 264 private final String name; 265 private byte[] contents; 266 private final long modtime; 267 private final boolean deflateHint; 268 private final boolean isDirectory; 269 270 public PackingFile(final String name, final byte[] contents, final long modtime) { 271 this.name = name; 272 this.contents = contents; 273 this.modtime = modtime; 274 deflateHint = false; 275 isDirectory = false; 276 } 277 278 public PackingFile(final byte[] bytes, final JarEntry jarEntry) { 279 name = jarEntry.getName(); 280 contents = bytes; 281 modtime = jarEntry.getTime(); 282 deflateHint = jarEntry.getMethod() == ZipEntry.DEFLATED; 283 isDirectory = jarEntry.isDirectory(); 284 } 285 286 public byte[] getContents() { 287 return contents; 288 } 289 290 public String getName() { 291 return name; 292 } 293 294 public long getModtime() { 295 return modtime; 296 } 297 298 public void setContents(final byte[] contents) { 299 this.contents = contents; 300 } 301 302 public boolean isDefalteHint() { 303 return deflateHint; 304 } 305 306 public boolean isDirectory() { 307 return isDirectory; 308 } 309 310 @Override 311 public String toString() { 312 return name; 313 } 314 } 315 316}