Version 1.7.4
[gae.git] / java / src / main / com / google / appengine / tools / util / JarMaker.java
blob29e85be6cd4e980b60bf717415729e0fd0653434
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;
7 import java.io.File;
8 import java.io.FileOutputStream;
9 import java.io.IOException;
10 import java.io.InputStream;
11 import java.io.OutputStream;
12 import java.util.Set;
13 import java.util.jar.JarEntry;
14 import java.util.jar.JarOutputStream;
15 import java.util.jar.Manifest;
16 import java.util.logging.Logger;
18 /**
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}.
21 * <p>
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}.
24 * <p>
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.
27 * <p>
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;
50 /**
51 * Constructor
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
66 * it.
68 public JarMaker(String baseName,
69 int outputDigits,
70 File outputDirectory,
71 int maximumSize,
72 Set<String> excludes,
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;
82 /**
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;
95 /**
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.
99 * <p>
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;
129 try {
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)) {
137 fileBuffer.reset();
138 readIntoBuffer(inputStream, readBuffer, fileBuffer);
139 if (closeEachInputStream) {
140 inputStream.close();
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);
150 currentSize += size;
153 } finally {
154 currentStream.close();
158 protected boolean shouldIncludeFile(String fileName) {
159 if (excludes == null) {
160 return true;
162 for (String suffix : excludes) {
163 if (fileName.endsWith(suffix)) {
164 logger.fine("Skipping file matching excluded suffix '" + suffix + "': " + fileName);
165 return false;
169 return true;
172 private long getManifestSize(Manifest manifest) throws IOException{
173 ByteArrayOutputStream baos = new ByteArrayOutputStream();
174 try {
175 manifest.write(baos);
176 return baos.size();
177 } finally {
178 baos.close();
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
191 * index number.
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)
202 throws IOException {
203 int count;
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));