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