| /* |
| * Licensed to the Apache Software Foundation (ASF) under one or more |
| * contributor license agreements. See the NOTICE file distributed with |
| * this work for additional information regarding copyright ownership. |
| * The ASF licenses this file to You under the Apache License, Version 2.0 |
| * (the "License"); you may not use this file except in compliance with |
| * the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| package org.apache.commons.compress.harmony.pack200; |
| |
| import java.io.BufferedOutputStream; |
| import java.io.IOException; |
| import java.io.OutputStream; |
| import java.util.ArrayList; |
| import java.util.List; |
| import java.util.jar.JarEntry; |
| import java.util.jar.JarFile; |
| import java.util.jar.JarInputStream; |
| import java.util.zip.GZIPOutputStream; |
| import java.util.zip.ZipEntry; |
| |
| /** |
| * 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 |
| * JarFile as input and an OutputStream. Options can be set, then {@code pack()} is called, to pack the Jar file into a pack200 archive. |
| */ |
| public class Archive { |
| |
| static class PackingFile { |
| |
| private final String name; |
| private byte[] contents; |
| private final long modtime; |
| private final boolean deflateHint; |
| private final boolean isDirectory; |
| |
| PackingFile(final byte[] bytes, final JarEntry jarEntry) { |
| name = jarEntry.getName(); |
| contents = bytes; |
| modtime = jarEntry.getTime(); |
| deflateHint = jarEntry.getMethod() == ZipEntry.DEFLATED; |
| isDirectory = jarEntry.isDirectory(); |
| } |
| |
| PackingFile(final String name, final byte[] contents, final long modtime) { |
| this.name = name; |
| this.contents = contents; |
| this.modtime = modtime; |
| deflateHint = false; |
| isDirectory = false; |
| } |
| |
| public byte[] getContents() { |
| return contents; |
| } |
| |
| public long getModtime() { |
| return modtime; |
| } |
| |
| public String getName() { |
| return name; |
| } |
| |
| public boolean isDefalteHint() { |
| return deflateHint; |
| } |
| |
| public boolean isDirectory() { |
| return isDirectory; |
| } |
| |
| public void setContents(final byte[] contents) { |
| this.contents = contents; |
| } |
| |
| @Override |
| public String toString() { |
| return name; |
| } |
| } |
| |
| static class SegmentUnit { |
| |
| private final List<Pack200ClassReader> classList; |
| |
| private final List<PackingFile> fileList; |
| |
| private int byteAmount; |
| |
| private int packedByteAmount; |
| |
| SegmentUnit(final List<Pack200ClassReader> classes, final List<PackingFile> files) { |
| classList = classes; |
| fileList = files; |
| byteAmount = 0; |
| // Calculate the amount of bytes in classes and files before packing |
| byteAmount += classList.stream().mapToInt(element -> element.b.length).sum(); |
| byteAmount += fileList.stream().mapToInt(element -> element.contents.length).sum(); |
| } |
| |
| public void addPackedByteAmount(final int amount) { |
| packedByteAmount += amount; |
| } |
| |
| public int classListSize() { |
| return classList.size(); |
| } |
| |
| public int fileListSize() { |
| return fileList.size(); |
| } |
| |
| public int getByteAmount() { |
| return byteAmount; |
| } |
| |
| public List<Pack200ClassReader> getClassList() { |
| return classList; |
| } |
| |
| public List<PackingFile> getFileList() { |
| return fileList; |
| } |
| |
| public int getPackedByteAmount() { |
| return packedByteAmount; |
| } |
| } |
| |
| private static final byte[] EMPTY_BYTE_ARRAY = {}; |
| |
| private final JarInputStream jarInputStream; |
| private final OutputStream outputStream; |
| private JarFile jarFile; |
| |
| private long currentSegmentSize; |
| |
| private final PackingOptions options; |
| |
| /** |
| * Creates an Archive with the given input file and a stream for the output |
| * |
| * @param jarFile - the input file |
| * @param outputStream TODO |
| * @param options - packing options (if null then defaults are used) |
| * @throws IOException If an I/O error occurs. |
| */ |
| public Archive(final JarFile jarFile, OutputStream outputStream, PackingOptions options) throws IOException { |
| if (options == null) { // use all defaults |
| options = new PackingOptions(); |
| } |
| this.options = options; |
| if (options.isGzip()) { |
| outputStream = new GZIPOutputStream(outputStream); |
| } |
| this.outputStream = new BufferedOutputStream(outputStream); |
| this.jarFile = jarFile; |
| jarInputStream = null; |
| PackingUtils.config(options); |
| } |
| |
| /** |
| * Creates an Archive with streams for the input and output. |
| * |
| * @param inputStream TODO |
| * @param outputStream TODO |
| * @param options packing options (if null then defaults are used) |
| * @throws IOException If an I/O error occurs. |
| */ |
| public Archive(final JarInputStream inputStream, OutputStream outputStream, PackingOptions options) throws IOException { |
| jarInputStream = inputStream; |
| if (options == null) { |
| // use all defaults |
| options = new PackingOptions(); |
| } |
| this.options = options; |
| if (options.isGzip()) { |
| outputStream = new GZIPOutputStream(outputStream); |
| } |
| this.outputStream = new BufferedOutputStream(outputStream); |
| PackingUtils.config(options); |
| } |
| |
| private boolean addJarEntry(final PackingFile packingFile, final List<Pack200ClassReader> javaClasses, final List<PackingFile> files) { |
| final long segmentLimit = options.getSegmentLimit(); |
| if (segmentLimit != -1 && segmentLimit != 0) { |
| // -1 is a special case where only one segment is created and |
| // 0 is a special case where one segment is created for each file |
| // except for files in "META-INF" |
| final long packedSize = estimateSize(packingFile); |
| if (packedSize + currentSegmentSize > segmentLimit && currentSegmentSize > 0) { |
| // don't add this JarEntry to the current segment |
| return false; |
| } |
| // do add this JarEntry |
| currentSegmentSize += packedSize; |
| } |
| |
| final String name = packingFile.getName(); |
| if (name.endsWith(".class") && !options.isPassFile(name)) { |
| final Pack200ClassReader classParser = new Pack200ClassReader(packingFile.contents); |
| classParser.setFileName(name); |
| javaClasses.add(classParser); |
| packingFile.contents = EMPTY_BYTE_ARRAY; |
| } |
| files.add(packingFile); |
| return true; |
| } |
| |
| private void doNormalPack() throws IOException, Pack200Exception { |
| PackingUtils.log("Start to perform a normal packing"); |
| List<PackingFile> packingFileList; |
| if (jarInputStream != null) { |
| packingFileList = PackingUtils.getPackingFileListFromJar(jarInputStream, options.isKeepFileOrder()); |
| } else { |
| packingFileList = PackingUtils.getPackingFileListFromJar(jarFile, options.isKeepFileOrder()); |
| } |
| |
| final List<SegmentUnit> segmentUnitList = splitIntoSegments(packingFileList); |
| int previousByteAmount = 0; |
| int packedByteAmount = 0; |
| |
| final int segmentSize = segmentUnitList.size(); |
| SegmentUnit segmentUnit; |
| for (int index = 0; index < segmentSize; index++) { |
| segmentUnit = segmentUnitList.get(index); |
| new Segment().pack(segmentUnit, outputStream, options); |
| previousByteAmount += segmentUnit.getByteAmount(); |
| packedByteAmount += segmentUnit.getPackedByteAmount(); |
| } |
| |
| PackingUtils.log("Total: Packed " + previousByteAmount + " input bytes of " + packingFileList.size() + " files into " + packedByteAmount + " bytes in " |
| + segmentSize + " segments"); |
| |
| outputStream.close(); |
| } |
| |
| private void doZeroEffortPack() throws IOException { |
| PackingUtils.log("Start to perform a zero-effort packing"); |
| if (jarInputStream != null) { |
| PackingUtils.copyThroughJar(jarInputStream, outputStream); |
| } else { |
| PackingUtils.copyThroughJar(jarFile, outputStream); |
| } |
| } |
| |
| private long estimateSize(final PackingFile packingFile) { |
| // The heuristic used here is for compatibility with the RI and should |
| // not be changed |
| final String name = packingFile.getName(); |
| if (name.startsWith("META-INF") || name.startsWith("/META-INF")) { |
| return 0; |
| } |
| long fileSize = packingFile.contents.length; |
| if (fileSize < 0) { |
| fileSize = 0; |
| } |
| return name.length() + fileSize + 5; |
| } |
| |
| /** |
| * Packs the archive. |
| * |
| * @throws Pack200Exception TODO |
| * @throws IOException If an I/O error occurs. |
| */ |
| public void pack() throws Pack200Exception, IOException { |
| if (0 == options.getEffort()) { |
| doZeroEffortPack(); |
| } else { |
| doNormalPack(); |
| } |
| } |
| |
| private List<SegmentUnit> splitIntoSegments(final List<PackingFile> packingFileList) { |
| final List<SegmentUnit> segmentUnitList = new ArrayList<>(); |
| List<Pack200ClassReader> classes = new ArrayList<>(); |
| List<PackingFile> files = new ArrayList<>(); |
| final long segmentLimit = options.getSegmentLimit(); |
| |
| final int size = packingFileList.size(); |
| PackingFile packingFile; |
| for (int index = 0; index < size; index++) { |
| packingFile = packingFileList.get(index); |
| if (!addJarEntry(packingFile, classes, files)) { |
| // not added because segment has reached maximum size |
| segmentUnitList.add(new SegmentUnit(classes, files)); |
| classes = new ArrayList<>(); |
| files = new ArrayList<>(); |
| currentSegmentSize = 0; |
| // add the jar to a new segment |
| addJarEntry(packingFile, classes, files); |
| // ignore the size of first entry for compatibility with RI |
| currentSegmentSize = 0; |
| } else if (segmentLimit == 0 && estimateSize(packingFile) > 0) { |
| // create a new segment for each class unless size is 0 |
| segmentUnitList.add(new SegmentUnit(classes, files)); |
| classes = new ArrayList<>(); |
| files = new ArrayList<>(); |
| } |
| } |
| // Change for Apache Commons Compress based on Apache Harmony. |
| // if (classes.size() > 0 && files.size() > 0) { |
| if (classes.size() > 0 || files.size() > 0) { |
| segmentUnitList.add(new SegmentUnit(classes, files)); |
| } |
| return segmentUnitList; |
| } |
| |
| } |