View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   https://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.commons.compress.harmony.pack200;
20  
21  import java.io.BufferedOutputStream;
22  import java.io.IOException;
23  import java.io.OutputStream;
24  import java.util.ArrayList;
25  import java.util.List;
26  import java.util.jar.JarEntry;
27  import java.util.jar.JarFile;
28  import java.util.jar.JarInputStream;
29  import java.util.zip.GZIPOutputStream;
30  import java.util.zip.ZipEntry;
31  
32  /**
33   * Main entry point to pack200 and represents a packed archive. An archive is constructed with either a {@link JarInputStream} and an output stream or a
34   * {@link JarFile} as input and an OutputStream. Options can be set, then {@link #pack()} is called, to pack the Jar file into a pack200 archive.
35   */
36  public class Archive {
37  
38      static final class PackingFile {
39  
40          private final String name;
41          private byte[] contents;
42          private final long modtime;
43          private final boolean deflateHint;
44          private final boolean isDirectory;
45  
46          PackingFile(final byte[] bytes, final JarEntry jarEntry) {
47              name = jarEntry.getName();
48              contents = bytes;
49              modtime = jarEntry.getTime();
50              deflateHint = jarEntry.getMethod() == ZipEntry.DEFLATED;
51              isDirectory = jarEntry.isDirectory();
52          }
53  
54          PackingFile(final String name, final byte[] contents, final long modtime) {
55              this.name = name;
56              this.contents = contents;
57              this.modtime = modtime;
58              deflateHint = false;
59              isDirectory = false;
60          }
61  
62          public byte[] getContents() {
63              return contents;
64          }
65  
66          public long getModtime() {
67              return modtime;
68          }
69  
70          public String getName() {
71              return name;
72          }
73  
74          public boolean isDefalteHint() {
75              return deflateHint;
76          }
77  
78          public boolean isDirectory() {
79              return isDirectory;
80          }
81  
82          public void setContents(final byte[] contents) {
83              this.contents = contents;
84          }
85  
86          @Override
87          public String toString() {
88              return name;
89          }
90      }
91  
92      static final class SegmentUnit {
93  
94          private final List<Pack200ClassReader> classList;
95  
96          private final List<PackingFile> fileList;
97  
98          private int byteAmount;
99  
100         private int packedByteAmount;
101 
102         SegmentUnit(final List<Pack200ClassReader> classes, final List<PackingFile> files) {
103             classList = classes;
104             fileList = files;
105             byteAmount = 0;
106             // Calculate the amount of bytes in classes and files before packing
107             byteAmount += classList.stream().mapToInt(element -> element.b.length).sum();
108             byteAmount += fileList.stream().mapToInt(element -> element.contents.length).sum();
109         }
110 
111         public void addPackedByteAmount(final int amount) {
112             packedByteAmount += amount;
113         }
114 
115         public int classListSize() {
116             return classList.size();
117         }
118 
119         public int fileListSize() {
120             return fileList.size();
121         }
122 
123         public int getByteAmount() {
124             return byteAmount;
125         }
126 
127         public List<Pack200ClassReader> getClassList() {
128             return classList;
129         }
130 
131         public List<PackingFile> getFileList() {
132             return fileList;
133         }
134 
135         public int getPackedByteAmount() {
136             return packedByteAmount;
137         }
138     }
139 
140     private static final byte[] EMPTY_BYTE_ARRAY = {};
141 
142     private final JarInputStream jarInputStream;
143     private final OutputStream outputStream;
144     private JarFile jarFile;
145 
146     private long currentSegmentSize;
147 
148     private final PackingOptions options;
149 
150     /**
151      * Creates an Archive with the given input file and a stream for the output.
152      *
153      * @param jarFile      the input file.
154      * @param outputStream target output stream for the compressed data.
155      * @param options      packing options (if null then defaults are used).
156      * @throws IOException If an I/O error occurs.
157      */
158     public Archive(final JarFile jarFile, OutputStream outputStream, PackingOptions options) throws IOException {
159         if (options == null) { // use all defaults
160             options = new PackingOptions();
161         }
162         this.options = options;
163         if (options.isGzip()) {
164             outputStream = new GZIPOutputStream(outputStream);
165         }
166         this.outputStream = new BufferedOutputStream(outputStream);
167         this.jarFile = jarFile;
168         this.jarInputStream = null;
169         PackingUtils.config(options);
170     }
171 
172     /**
173      * Creates an Archive with streams for the input and output.
174      *
175      * @param inputStream  input stream of uncompressed JAR data.
176      * @param outputStream target output stream for the compressed data.
177      * @param options      packing options (if null then defaults are used).
178      * @throws IOException If an I/O error occurs.
179      */
180     public Archive(final JarInputStream inputStream, OutputStream outputStream, PackingOptions options) throws IOException {
181         jarInputStream = inputStream;
182         if (options == null) {
183             // use all defaults
184             options = new PackingOptions();
185         }
186         this.options = options;
187         if (options.isGzip()) {
188             outputStream = new GZIPOutputStream(outputStream);
189         }
190         this.outputStream = new BufferedOutputStream(outputStream);
191         PackingUtils.config(options);
192     }
193 
194     private boolean addJarEntry(final PackingFile packingFile, final List<Pack200ClassReader> javaClasses, final List<PackingFile> files) {
195         final long segmentLimit = options.getSegmentLimit();
196         if (segmentLimit != -1 && segmentLimit != 0) {
197             // -1 is a special case where only one segment is created and
198             // 0 is a special case where one segment is created for each file
199             // except for files in "META-INF"
200             final long packedSize = estimateSize(packingFile);
201             if (packedSize + currentSegmentSize > segmentLimit && currentSegmentSize > 0) {
202                 // don't add this JarEntry to the current segment
203                 return false;
204             }
205             // do add this JarEntry
206             currentSegmentSize += packedSize;
207         }
208 
209         final String name = packingFile.getName();
210         if (name.endsWith(".class") && !options.isPassFile(name)) {
211             final Pack200ClassReader classParser = new Pack200ClassReader(packingFile.contents);
212             classParser.setFileName(name);
213             javaClasses.add(classParser);
214             packingFile.contents = EMPTY_BYTE_ARRAY;
215         }
216         files.add(packingFile);
217         return true;
218     }
219 
220     private void doNormalPack() throws IOException, Pack200Exception {
221         PackingUtils.log("Start to perform a normal packing");
222         final List<PackingFile> packingFileList;
223         if (jarInputStream != null) {
224             packingFileList = PackingUtils.getPackingFileListFromJar(jarInputStream, options.isKeepFileOrder());
225         } else {
226             packingFileList = PackingUtils.getPackingFileListFromJar(jarFile, options.isKeepFileOrder());
227         }
228 
229         final List<SegmentUnit> segmentUnitList = splitIntoSegments(packingFileList);
230         int previousByteAmount = 0;
231         int packedByteAmount = 0;
232 
233         final int segmentSize = segmentUnitList.size();
234         SegmentUnit segmentUnit;
235         for (int index = 0; index < segmentSize; index++) {
236             segmentUnit = segmentUnitList.get(index);
237             new Segment().pack(segmentUnit, outputStream, options);
238             previousByteAmount += segmentUnit.getByteAmount();
239             packedByteAmount += segmentUnit.getPackedByteAmount();
240         }
241 
242         PackingUtils.log("Total: Packed " + previousByteAmount + " input bytes of " + packingFileList.size() + " files into " + packedByteAmount + " bytes in "
243                 + segmentSize + " segments");
244 
245         outputStream.close();
246     }
247 
248     private void doZeroEffortPack() throws IOException {
249         PackingUtils.log("Start to perform a zero-effort packing");
250         if (jarInputStream != null) {
251             PackingUtils.copyThroughJar(jarInputStream, outputStream);
252         } else {
253             PackingUtils.copyThroughJar(jarFile, outputStream);
254         }
255     }
256 
257     private long estimateSize(final PackingFile packingFile) {
258         // The heuristic used here is for compatibility with the RI and should
259         // not be changed
260         final String name = packingFile.getName();
261         if (name.startsWith("META-INF") || name.startsWith("/META-INF")) {
262             return 0;
263         }
264         long fileSize = packingFile.contents.length;
265         if (fileSize < 0) {
266             fileSize = 0;
267         }
268         return name.length() + fileSize + 5;
269     }
270 
271     /**
272      * Packs the archive.
273      *
274      * @throws Pack200Exception If a Pack200 semantic error occurs.
275      * @throws IOException      If an I/O error occurs.
276      */
277     public void pack() throws Pack200Exception, IOException {
278         if (0 == options.getEffort()) {
279             doZeroEffortPack();
280         } else {
281             doNormalPack();
282         }
283     }
284 
285     private List<SegmentUnit> splitIntoSegments(final List<PackingFile> packingFileList) {
286         final List<SegmentUnit> segmentUnitList = new ArrayList<>();
287         List<Pack200ClassReader> classes = new ArrayList<>();
288         List<PackingFile> files = new ArrayList<>();
289         final long segmentLimit = options.getSegmentLimit();
290 
291         final int size = packingFileList.size();
292         PackingFile packingFile;
293         for (int index = 0; index < size; index++) {
294             packingFile = packingFileList.get(index);
295             if (!addJarEntry(packingFile, classes, files)) {
296                 // not added because segment has reached maximum size
297                 segmentUnitList.add(new SegmentUnit(classes, files));
298                 classes = new ArrayList<>();
299                 files = new ArrayList<>();
300                 currentSegmentSize = 0;
301                 // add the jar to a new segment
302                 addJarEntry(packingFile, classes, files);
303                 // ignore the size of first entry for compatibility with RI
304                 currentSegmentSize = 0;
305             } else if (segmentLimit == 0 && estimateSize(packingFile) > 0) {
306                 // create a new segment for each class unless size is 0
307                 segmentUnitList.add(new SegmentUnit(classes, files));
308                 classes = new ArrayList<>();
309                 files = new ArrayList<>();
310             }
311         }
312         // Change for Apache Commons Compress based on Apache Harmony.
313         // if (classes.size() > 0 && files.size() > 0) {
314         if (classes.size() > 0 || files.size() > 0) {
315             segmentUnitList.add(new SegmentUnit(classes, files));
316         }
317         return segmentUnitList;
318     }
319 
320 }