    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}