View Javadoc
1   /*
2    *  Licensed to the Apache Software Foundation (ASF) under one or more
3    *  contributor license agreements.  See the NOTICE file distributed with
4    *  this work for additional information regarding copyright ownership.
5    *  The ASF licenses this file to You under the Apache License, Version 2.0
6    *  (the "License"); you may not use this file except in compliance with
7    *  the License.  You may obtain a copy of the License at
8    *
9    *     http://www.apache.org/licenses/LICENSE-2.0
10   *
11   *  Unless required by applicable law or agreed to in writing, software
12   *  distributed under the License is distributed on an "AS IS" BASIS,
13   *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   *  See the License for the specific language governing permissions and
15   *  limitations under the License.
16   */
17  package org.apache.commons.compress.harmony.pack200;
18  
19  import java.io.BufferedOutputStream;
20  import java.io.IOException;
21  import java.io.OutputStream;
22  import java.util.ArrayList;
23  import java.util.List;
24  import java.util.jar.JarEntry;
25  import java.util.jar.JarFile;
26  import java.util.jar.JarInputStream;
27  import java.util.zip.GZIPOutputStream;
28  import java.util.zip.ZipEntry;
29  
30  /**
31   * 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
32   * JarFile as input and an OutputStream. Options can be set, then {@code pack()} is called, to pack the Jar file into a pack200 archive.
33   */
34  public class Archive {
35  
36      static class PackingFile {
37  
38          private final String name;
39          private byte[] contents;
40          private final long modtime;
41          private final boolean deflateHint;
42          private final boolean isDirectory;
43  
44          PackingFile(final byte[] bytes, final JarEntry jarEntry) {
45              name = jarEntry.getName();
46              contents = bytes;
47              modtime = jarEntry.getTime();
48              deflateHint = jarEntry.getMethod() == ZipEntry.DEFLATED;
49              isDirectory = jarEntry.isDirectory();
50          }
51  
52          PackingFile(final String name, final byte[] contents, final long modtime) {
53              this.name = name;
54              this.contents = contents;
55              this.modtime = modtime;
56              deflateHint = false;
57              isDirectory = false;
58          }
59  
60          public byte[] getContents() {
61              return contents;
62          }
63  
64          public long getModtime() {
65              return modtime;
66          }
67  
68          public String getName() {
69              return name;
70          }
71  
72          public boolean isDefalteHint() {
73              return deflateHint;
74          }
75  
76          public boolean isDirectory() {
77              return isDirectory;
78          }
79  
80          public void setContents(final byte[] contents) {
81              this.contents = contents;
82          }
83  
84          @Override
85          public String toString() {
86              return name;
87          }
88      }
89  
90      static class SegmentUnit {
91  
92          private final List<Pack200ClassReader> classList;
93  
94          private final List<PackingFile> fileList;
95  
96          private int byteAmount;
97  
98          private int packedByteAmount;
99  
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 }