1 // Copyright 2007 Google Inc. All Rights Reserved.
3 package com
.google
.appengine
.tools
.util
;
5 import java
.io
.BufferedOutputStream
;
6 import java
.io
.ByteArrayOutputStream
;
8 import java
.io
.FileOutputStream
;
9 import java
.io
.IOException
;
10 import java
.io
.InputStream
;
11 import java
.io
.OutputStream
;
13 import java
.util
.jar
.JarEntry
;
14 import java
.util
.jar
.JarOutputStream
;
15 import java
.util
.jar
.Manifest
;
16 import java
.util
.logging
.Logger
;
19 * A utility for building multiple jar files, each of size less than a specified maximum, from a
20 * sequence of {@link JarEntry JarEntries} and {@link InputStream InputStreams}.
22 * This is an abstract class. Concrete subclasses must implement {@link #getNextJarEntry} in order
23 * to specify the sequence of {@link JarEntry JarEntries} and {@link InputStream InputStreams}.
25 * Concrete subclasses must also implement {@link #getManifest} to specify a single manifest that
26 * should be included in all the jars, or {@code null} if no manifest should be included.
28 * Usage: Construct an abstract subclass and then invoke the method {@link #run()}.
31 abstract class JarMaker
{
32 protected static final String EXT
= ".jar";
34 private static final int READ_BUFFER_SIZE_BYTES
= 8 * 1024;
35 private static final int FILE_BUFFER_INITIAL_SIZE_BYTES
= 512 * 1024;
37 private static Logger logger
= Logger
.getLogger(JarSplitter
.class.getName());
39 private final String baseName
;
40 private final File outputDirectory
;
41 private final int maximumSize
;
42 private final Set
<String
> excludes
;
43 private final boolean closeEachInputStream
;
45 private int nextFileIndex
= 0;
46 private long currentSize
= 0L;
47 private JarOutputStream currentStream
;
48 private int outputDigits
;
53 * @param baseName The base name of the emitted jar files. The file name will also include a
54 * suffix containing a number of numerical digits equal to {@code outputDigits}
55 * @param outputDigits Number of digits to use for the names of the emitted jar files
56 * @param outputDirectory The directory into which to emit the jar files. This directory will be
57 * created if it does not exist.
58 * @param maximumSize The maximum size of a jar file that should be emitted, in bytes. Note that
59 * the actual size of the jar files may be less than this value due to compression. The
60 * compression ratio is not specified. (In practice it is frequently 50%).
61 * @param excludes A set file-name suffixes. If this is not {@code null} then {@link JarEntry
62 * JarEntries} whose path includes one of these suffixes will be excluded from the
63 * generated jar files.
64 * @param closeEachInputStream Determines which party owns the closing of the input streams. If
65 * this is {@code true} then this class will do the closing, otherwise the caller will do
68 public JarMaker(String baseName
,
73 boolean closeEachInputStream
) {
74 this.baseName
= baseName
;
75 this.outputDirectory
= outputDirectory
;
76 this.maximumSize
= maximumSize
;
77 this.outputDigits
= outputDigits
;
78 this.excludes
= excludes
;
79 this.closeEachInputStream
= closeEachInputStream
;
83 * A pair consisting of a {@link JarEntry} and an {@link InputStream}.
85 protected static class JarEntryData
{
86 public JarEntry jarEntry
;
87 public InputStream inputStream
;
89 public JarEntryData(JarEntry jarEntry
, InputStream inputStream
) {
90 this.jarEntry
= jarEntry
;
91 this.inputStream
= inputStream
;
96 * Returns the next {@link JarEntryData} specifying the next {@link JarEntry}
97 * and {@link InputStream} that should be used in constructing the emitted jar files.
98 * @return The next {@link JarEntryData} or {@code null} to indicate that there are no more.
100 * This method will be called multiple times during the execution of {@link #run()}. A concrete
101 * subclass must implement this method.
102 * @throws IOException If any problems occur.
104 protected abstract JarEntryData
getNextJarEntry() throws IOException
;
107 * Returns a single {@link Manifest} that will be included in each of the emitted jar files,
108 * or {@code null} to indicate that no manifest should be included. This method will be invoked
109 * once during the execution of {@link #run}.
111 protected abstract Manifest
getManifest();
114 * Generates one or more jar files based on the data provided in the constructor, a single call to
115 * {@link #getManifest()} and multiple calls to {@link #getNextJarEntry()}.
117 * @throws IOException
119 public void run() throws IOException
{
120 outputDirectory
.mkdirs();
122 Manifest manifest
= getManifest();
123 long manifestSize
= (manifest
!= null) ?
getManifestSize(manifest
) : 0;
125 byte[] readBuffer
= new byte[READ_BUFFER_SIZE_BYTES
];
126 ByteArrayOutputStream fileBuffer
= new ByteArrayOutputStream(FILE_BUFFER_INITIAL_SIZE_BYTES
);
127 JarEntryData entryData
;
130 beginNewOutputStream(manifest
, manifestSize
);
131 while ((entryData
= getNextJarEntry()) != null) {
132 JarEntry entry
= entryData
.jarEntry
;
133 InputStream inputStream
= entryData
.inputStream
;
134 String name
= entry
.getName();
136 if (shouldIncludeFile(name
)) {
138 readIntoBuffer(inputStream
, readBuffer
, fileBuffer
);
139 if (closeEachInputStream
) {
142 long size
= fileBuffer
.size();
143 if ((currentSize
+ size
) >= maximumSize
) {
144 beginNewOutputStream(manifest
, manifestSize
);
147 logger
.fine("Copying entry: " + name
+ " (" + size
+ " bytes)");
148 currentStream
.putNextEntry(entry
);
149 fileBuffer
.writeTo(currentStream
);
154 currentStream
.close();
158 protected boolean shouldIncludeFile(String fileName
) {
159 if (excludes
== null) {
162 for (String suffix
: excludes
) {
163 if (fileName
.endsWith(suffix
)) {
164 logger
.fine("Skipping file matching excluded suffix '" + suffix
+ "': " + fileName
);
172 private long getManifestSize(Manifest manifest
) throws IOException
{
173 ByteArrayOutputStream baos
= new ByteArrayOutputStream();
175 manifest
.write(baos
);
182 private JarOutputStream
newJarOutputStream(Manifest manifest
) throws IOException
{
183 if (manifest
== null) {
184 return new JarOutputStream(createOutFile(nextFileIndex
++));
186 return new JarOutputStream(createOutFile(nextFileIndex
++), manifest
);
190 * Close the current output stream if there is one, and open a new one with the next available
193 private void beginNewOutputStream(Manifest manifest
, long manifestSize
) throws IOException
{
194 if (currentStream
!= null) {
195 currentStream
.close();
197 currentStream
= newJarOutputStream(manifest
);
198 currentSize
= manifestSize
;
201 private void readIntoBuffer(InputStream inputStream
, byte[] readBuffer
, ByteArrayOutputStream out
)
204 while ((count
= inputStream
.read(readBuffer
)) != -1) {
205 out
.write(readBuffer
, 0, count
);
209 private OutputStream
createOutFile(int index
) throws IOException
{
210 String formatString
= "%s-%0" + outputDigits
+ "d%s";
211 String newName
= String
.format(formatString
, baseName
, index
, EXT
);
212 File newFile
= new File(outputDirectory
, newName
);
213 logger
.fine("Opening new file: " + newFile
);
214 return new BufferedOutputStream(new FileOutputStream(newFile
));