// Copyright (c) 2011, the Dart project authors. All Rights Reserved.
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.jar.JarOutputStream;
import java.util.jar.Manifest;
/**
* The JarProcessor utility can create and manipulate jar files and osgi bundles. In particular, it
* can:
* <p>
* <ul>
* <li><b>-empty</b> Given an existing osgi bundle, create an identical but empty osgi bundle. This
* new bundle has to dependencies on other bundles, and is good for pruning the bundle dependency
* graph.
* <li><b>-rename</b> Given an osgi bundle, copy and rename it. This changes the jar name and the id
* in the manifest.
* <li><b>-process</b> Given an osgi bundle and a file with class loading information, create a copy
* of the jar file with the entries sorted in the order in which they'll be loaded. This converts
* the random seeks in the jar file at load time into a sequential read. This decreases startup time
* for the application.
* <li><b>-processDir</b> Similar to the -process command, this command takes as input a directory
* and processes every jar file in that directory which is listed in the class loading information
* file.
* </ul>
*/
public class JarProcessor {
private static byte[] buffer = new byte[10240];
public static void main(String[] args) throws IOException {
if (args.length == 0) {
usage();
}
String sourceJar = null;
String sourceDir = null;
String outDirectory = null;
String pluginId = null;
String classListFile = null;
boolean createEmpty = false;
boolean renamePlugin = false;
boolean processPlugin = false;
boolean processDir = false;
for (int i = 0; i < args.length; i++) {
if (args[i].equals("-out")) {
if (++i >= args.length) {
usage();
}
outDirectory = args[i];
}
if (args[i].equals("-empty")) {
createEmpty = true;
if (++i >= args.length) {
usage();
}
sourceJar = args[i];
}
if (args[i].equals("-rename")) {
renamePlugin = true;
if (++i >= args.length) {
usage();
}
sourceJar = args[i];
if (++i >= args.length) {
usage();
}
pluginId = args[i];
}
if (args[i].equals("-process")) {
processPlugin = true;
if (++i >= args.length) {
usage();
}
sourceJar = args[i];
if (++i >= args.length) {
usage();
}
classListFile = args[i];
}
if (args[i].equals("-processDir")) {
processDir = true;
if (++i >= args.length) {
usage();
}
sourceDir = args[i];
if (++i >= args.length) {
usage();
}
classListFile = args[i];
}
}
if (createEmpty) {
createEmptyJar(sourceJar, outDirectory);
} else if (renamePlugin) {
renamePlugin(sourceJar, pluginId, outDirectory);
} else if (processPlugin) {
processPlugin(sourceJar, classListFile, outDirectory);
} else if (processDir) {
processDir(sourceDir, classListFile, outDirectory);
} else {
usage();
}
}
protected static void usage() {
System.out.println("usage: java -jar JarProcessor.jar -empty <jar> -out <dir>");
System.out.println("usage: java -jar JarProcessor.jar -rename <jar> <new_id> -out <dir>");
System.out.println("usage: java -jar JarProcessor.jar -process <jar> <classesFile> -out <dir>");
System.out.println("usage: java -jar JarProcessor.jar -processDir <dir> <classesFile> -out <dir>");
System.exit(0);
}
/**
* Create an empty copy of the given osgi bundle.
*
* @param sourceJarPath
* @param outDirectory
* @throws IOException
*/
static void createEmptyJar(String sourceJarPath, String outDirectory) throws IOException {
File sourceFile = new File(sourceJarPath);
JarFile sourceJar = new JarFile(sourceFile);
File tempFile = File.createTempFile(sourceFile.getName(), "jar");
createEmptyPlugin(sourceJar, tempFile);
moveOrReplace(sourceFile, tempFile, outDirectory);
}
/**
* Create copies of the plugins in the given directory with the jar entries sorted into load
* order.
*
* @param sourceDir
* @param classListFile
* @param outDirectory
* @throws IOException
*/
static void processDir(String sourceDir, String classListFile, String outDirectory)
throws IOException {
File dir = new File(sourceDir);
Map<String, List<String>> jarToClassMap = createJarToClassMap(new File(classListFile));
for (File file : dir.listFiles()) {
if (file.isFile() && file.getName().endsWith(".jar")) {
JarFile sourceJar = new JarFile(file);
File tempFile = File.createTempFile(file.getName(), "jar");
if (processPlugin(sourceJar, tempFile, jarToClassMap)) {
moveOrReplace(file, tempFile, outDirectory);
}
}
}
}
/**
* Create a copy of the given osgi plugin with the jar entries sorted into load order.
*
* @param sourceJarPath
* @param classListFile
* @param outDirectory
* @throws IOException
*/
static void processPlugin(String sourceJarPath, String classListFile, String outDirectory)
throws IOException {
File sourceFile = new File(sourceJarPath);
JarFile sourceJar = new JarFile(sourceFile);
File tempFile = File.createTempFile(sourceFile.getName(), "jar");
Map<String, List<String>> jarToClassMap = createJarToClassMap(new File(classListFile));
if (processPlugin(sourceJar, tempFile, jarToClassMap)) {
moveOrReplace(sourceFile, tempFile, outDirectory);
}
}
/**
* Create a renamed copy of the given osgi plugin.
*
* @param sourceJarPath
* @param newPluginId
* @param outDirectory
* @throws IOException
*/
static void renamePlugin(String sourceJarPath, String newPluginId, String outDirectory)
throws IOException {
File sourceFile = new File(sourceJarPath);
JarFile sourceJar = new JarFile(sourceFile);
File tempFile = File.createTempFile(sourceFile.getName(), "jar");
String newFileName = renamePlugin(sourceJar, tempFile, newPluginId);
moveOrReplace(sourceFile, tempFile, outDirectory, newFileName);
}
private static void copyManifestAttribute(String id, Manifest sourceManifest,
Manifest destManifest) {
destManifest.getMainAttributes().putValue(id, sourceManifest.getMainAttributes().getValue(id));
}
private static void copyStream(InputStream in, OutputStream out) throws IOException {
int count = in.read(buffer);
while (count != -1) {
out.write(buffer, 0, count);
count = in.read(buffer);
}
in.close();
}
private static void createEmptyPlugin(JarFile sourceJar, File tempFile) throws IOException {
Manifest oldManifest = sourceJar.getManifest();
Manifest manifest = new Manifest();
// Manifest-Version: 1.0
// Bundle-ActivationPolicy: lazy
// Bundle-Localization: plugin
// Bundle-ManifestVersion: 2
// Bundle-Name: %pluginName
// Bundle-RequiredExecutionEnvironment: JavaSE-1.6
// Bundle-SymbolicName: com.google.dash.tools.core;singleton:=true
// Bundle-Version: 0.1.0.qualifier
// Bundle-Vendor: %providerName
copyManifestAttribute("Manifest-Version", oldManifest, manifest);
copyManifestAttribute("Bundle-ActivationPolicy", oldManifest, manifest);
copyManifestAttribute("Bundle-ManifestVersion", oldManifest, manifest);
copyManifestAttribute("Bundle-Name", oldManifest, manifest);
copyManifestAttribute("Bundle-RequiredExecutionEnvironment", oldManifest, manifest);
copyManifestAttribute("Bundle-SymbolicName", oldManifest, manifest);
copyManifestAttribute("Bundle-Version", oldManifest, manifest);
copyManifestAttribute("Bundle-Vendor", oldManifest, manifest);
JarOutputStream outFile = new JarOutputStream(new FileOutputStream(tempFile), manifest);
outFile.finish();
outFile.close();
System.out.println("[created empty plugin " + sourceJar.getName() + "]");
}
//[Loaded org.eclipse.equinox.launcher.JNIBridge from file:/Users/devoncarew/build-dart/out/dash/plugins/org.eclipse.equinox.launcher_1.2.0.v20110502.jar]
//[Loaded org.eclipse.equinox.launcher.Main from file:/Users/devoncarew/build-dart/out/dash/plugins/org.eclipse.equinox.launcher_1.2.0.v20110502.jar]
//[Loaded org.eclipse.equinox.launcher.Main$EclipsePolicy from file:/Users/devoncarew/build-dart/out/dash/plugins/org.eclipse.equinox.launcher_1.2.0.v20110502.jar]
//[Loaded org.eclipse.equinox.launcher.Main$StartupClassLoader from file:/Users/devoncarew/build-dart/out/dash/plugins/org.eclipse.equinox.launcher_1.2.0.v20110502.jar]
//[Loaded org.eclipse.equinox.launcher.Main$SplashHandler from file:/Users/devoncarew/build-dart/out/dash/plugins/org.eclipse.equinox.launcher_1.2.0.v20110502.jar]
//[Loaded org.osgi.framework.BundleContext from file:/Users/devoncarew/build-dart/out/dash/plugins/org.eclipse.osgi_3.7.0.v20110613.jar]
//[Loaded org.eclipse.osgi.internal.profile.Profile from file:/Users/devoncarew/build-dart/out/dash/plugins/org.eclipse.osgi_3.7.0.v20110613.jar]
private static Map<String, List<String>> createJarToClassMap(File file) throws IOException {
Map<String, List<String>> jarMap = new HashMap<String, List<String>>();
BufferedReader in = new BufferedReader(new FileReader(file), 102400);
String line = in.readLine();
int classCount = 0;
while (line != null) {
if (!line.isEmpty()) {
if (line.startsWith("[Loaded ") && line.endsWith(".jar]")) {
line = line.substring(0, line.length() - 1);
String[] strs = line.split(" ");
// [Loaded
// org.eclipse.osgi.internal.profile.Profile
// from
// file:/Users/devoncarew/build-dart/out/dash/plugins/org.eclipse.osgi_3.7.0.v20110613.jar
String className = strs[1];
String pluginName = strs[3];
if (pluginName.indexOf('/') != -1) {
pluginName = pluginName.substring(pluginName.lastIndexOf('/') + 1);
}
if (pluginName.indexOf('_') != -1) {
pluginName = pluginName.substring(0, pluginName.indexOf('_'));
}
List<String> classList = jarMap.get(pluginName);
if (classList == null) {
classList = new ArrayList<String>();
jarMap.put(pluginName, classList);
}
classCount++;
classList.add(className);
}
}
line = in.readLine();
}
in.close();
System.out.println(jarMap.keySet().size() + " jar files, " + classCount + " classes.");
return jarMap;
}
private static void moveOrReplace(File sourceJar, File tempFile, String outDirectory)
throws IOException {
moveOrReplace(sourceJar, tempFile, outDirectory, null);
}
private static void moveOrReplace(File sourceJar, File tempFile, String outDirectory,
String newFileName) throws IOException {
File outFile = sourceJar;
if (outDirectory != null) {
File dir = new File(outDirectory);
outFile = new File(dir, sourceJar.getName());
}
if (newFileName != null) {
outFile = new File(outFile.getParentFile(), newFileName);
}
byte[] buffer = new byte[10480];
InputStream in = new FileInputStream(tempFile);
OutputStream out = new FileOutputStream(outFile);
int count = in.read(buffer);
while (count != -1) {
out.write(buffer, 0, count);
count = in.read(buffer);
}
out.close();
in.close();
}
private static boolean processPlugin(JarFile sourceJar, File tempFile,
Map<String, List<String>> jarToClassMap) throws IOException {
String bundleId = sourceJar.getName();
if (bundleId.indexOf('/') != -1) {
bundleId = bundleId.substring(bundleId.lastIndexOf('/') + 1);
}
if (bundleId.indexOf('_') != -1) {
bundleId = bundleId.substring(0, bundleId.indexOf('_'));
}
List<String> classes = jarToClassMap.get(bundleId);
if (classes == null) {
return false;
}
Manifest manifest = sourceJar.getManifest();
// Remove the SHA digest info for signed jar files.
Manifest newManifest = new Manifest();
for (Object key : manifest.getMainAttributes().keySet()) {
newManifest.getMainAttributes().put(key, manifest.getMainAttributes().get(key));
}
JarOutputStream outFile = new JarOutputStream(new FileOutputStream(tempFile), newManifest);
Set<String> writtenEntries = new HashSet<String>();
int reorderCount = 0;
int entryCount = 0;
for (String className : classes) {
JarEntry entry = sourceJar.getJarEntry(className.replace('.', '/') + ".class");
if (entry != null) {
InputStream in = sourceJar.getInputStream(entry);
JarEntry newEntry = new JarEntry(entry);
// Storing the entries does not improve our launch times.
//newEntry.setMethod(ZipEntry.STORED);
//newEntry.setCompressedSize(entry.getSize());
outFile.putNextEntry(newEntry);
copyStream(in, outFile);
outFile.closeEntry();
writtenEntries.add(entry.getName());
reorderCount++;
}
}
Enumeration<JarEntry> entries = sourceJar.entries();
while (entries.hasMoreElements()) {
JarEntry entry = entries.nextElement();
String entryName = entry.getName();
if (!entryName.startsWith("META-INF/")) {
entryCount++;
if (!writtenEntries.contains(entryName)) {
InputStream in = sourceJar.getInputStream(entry);
outFile.putNextEntry(entry);
copyStream(in, outFile);
outFile.closeEntry();
writtenEntries.add(entry.getName());
}
}
}
outFile.finish();
outFile.close();
System.out.println("[" + sourceJar.getName() + " rewritten, " + reorderCount + " of "
+ entryCount + " entries re-ordered]");
return true;
}
private static String renamePlugin(JarFile sourceJar, File tempFile, String newPluginId)
throws IOException {
String bundleVersion = sourceJar.getName();
if (bundleVersion.indexOf('/') != -1) {
bundleVersion = bundleVersion.substring(bundleVersion.lastIndexOf('/') + 1);
}
if (bundleVersion.indexOf('_') != -1) {
bundleVersion = bundleVersion.substring(bundleVersion.lastIndexOf('_'));
}
Manifest manifest = sourceJar.getManifest();
// Remove the SHA digest info for signed jar files.
Manifest newManifest = new Manifest();
for (Object key : manifest.getMainAttributes().keySet()) {
newManifest.getMainAttributes().put(key, manifest.getMainAttributes().get(key));
}
// rename plugin -
String oldSymName = manifest.getMainAttributes().getValue("Bundle-SymbolicName");
if (oldSymName != null) {
String newSymName;
if (oldSymName.indexOf(';') != -1) {
newSymName = newPluginId + oldSymName.substring(oldSymName.indexOf(';'));
} else {
newSymName = newPluginId;
}
newManifest.getMainAttributes().putValue("Bundle-SymbolicName", newSymName);
}
JarOutputStream outFile = new JarOutputStream(new FileOutputStream(tempFile), newManifest);
Enumeration<JarEntry> entries = sourceJar.entries();
while (entries.hasMoreElements()) {
JarEntry entry = entries.nextElement();
String entryName = entry.getName();
if (!entryName.startsWith("META-INF/")) {
InputStream in = sourceJar.getInputStream(entry);
outFile.putNextEntry(entry);
copyStream(in, outFile);
outFile.closeEntry();
}
}
outFile.finish();
outFile.close();
System.out.println("[renamed " + sourceJar.getName() + " to " + newPluginId + "]");
return newPluginId + bundleVersion;
}
}