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