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