Wednesday, July 15, 2009

Java dependency walker tool

How many times you been stuck with a problem like this: You have numerous jars that cames with the external APIs you are using in your java project. When it is time to bundle the java application you do not know which jar files are the only ones that are absolutely needed for the application to run successfully. Most of the times to save time you bundle all the jars with the application with this unsettling feeling at the back of your mind that this is gonna be an inefficient application. Moreover, you bear the consequences of high upload times to the server of such a bloated application. In this situation haven't you felt if you had a tool that would tell you which are the absolutely necessary jar files to bundle?

Here it is: I've made this tool which is only one java class. I am also bundling the ant build file so that you do not have to spend too much time figuring out how to run this thing.

How to run this tool:
1. Pick one folder as a base folder for this tool. Make a folder structure src->com->gp. Copy the source code of the JavaDependencyWalker.java in a file with the same name in the gp folder.
2. Now, copy the code of build.xml into a file of the same name in the base folder.
3. If you already do not have an ant install please download it from here http://ant.apache.org and install it.
4. Edit the build.xml file and modify the three proerties mentioned at the top with the following guidelines:
  • scolon.separated.folders.ofjars = For the value of this property put a semi-colon separated absolute paths where the program can find all the jar files required for the class to run. For example, if the main class is a web service client that uses axis2 APIs then put the path to the lib folder of the axis2 install here. Also, do not forget to put paths to jar files of the custom classes you have built here. For example, you must put the path to the jar that contains the main class to run!
  • main.class = This is the main class to run, for which you are finding the exact jars requirements.
  • args.to.class = Here give all the arguments that will be required for the main class to run.
5. Run the ant command "ant run" in the base folder. If everything goes fine you must have a list of jar files at the end of the run which are the jar files you will need!

Below is the source code:
JavaDependencyWalker:
package com.gp;

import java.io.BufferedInputStream;
import java.io.DataInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;

import java.util.Enumeration;
import java.util.Hashtable;
import java.util.StringTokenizer;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;

public class JavaDependencyWalker {
private Hashtable m_jarsHash = new Hashtable();

public JavaDependencyWalker(String jarPaths) {
StringTokenizer stTok = new StringTokenizer(jarPaths, ";");
while (stTok.hasMoreTokens()) {
File f = new File(stTok.nextToken());
if (f.exists() && f.isDirectory()) {
addAllJars(f);
} else {
System.err.println(f.getName() +
" :: Cannot find this folder.");
}
}
}

private void addAllJars(File f) {
if (f.isDirectory()) {
File[] files = f.listFiles();
for (int i = 0; i < files.length; i++) {
addAllJars(files[i]);
}
} else {
if (f.getName().toLowerCase().endsWith(".jar") ||
f.getName().toLowerCase().endsWith(".zip")) {
try {
JarFile jf = new JarFile(f);
Enumeration entries = jf.entries();
while (entries.hasMoreElements()) {
String entryName =
((JarEntry)entries.nextElement()).getName();
if (entryName.endsWith(".class")) {
entryName =
entryName.substring(0, entryName.indexOf("."));
StringTokenizer stTok =
new StringTokenizer(entryName, "/");
String str = "";
while (stTok.hasMoreTokens()) {
str += stTok.nextToken() + ".";
}
str = str.substring(0, str.length() - 1);
m_jarsHash.put(str, f.getCanonicalPath());
} else {
// skip it
}
}
jf.close();
} catch (IOException e) {
System.err.println(f.getName() + " :: " + e.getMessage());
}
} else {
// skip this file
}
}
}

public String getJarFilepath(String classname) {
return (String)m_jarsHash.get(classname);
}

public static void usage() {
System.out.println("java -Ddeps=<semi-colon seperated paths where all jars/zips may be found> -DclassToRun=<fully qualified classname to run> JavaDependencyWalker <all arguments to give to the class to run>");
}

public static void main(String[] args) {
String dependencies = System.getProperty("deps");
String classname = System.getProperty("classToRun");
if (dependencies == null || classname == null) {
usage();
System.exit(1);
}

JavaDependencyWalker javaDependencyWalker =
new JavaDependencyWalker(dependencies);

try {
String cp = "";
boolean noFound = true;
do {
Runtime rt = Runtime.getRuntime();

String cmdArr[] = null;
if (cp.equals("")) {
cmdArr = new String[args.length+2];
cmdArr[0] = "java";
cmdArr[1] = classname;
for (int i=0; i < args.length; i++) {
cmdArr[i+2] = args[i];
}
// cmd =
//"java " + args[1] + " ";
}
else {
cmdArr = new String[args.length+4];
cmdArr[0] = "java";
cmdArr[1] = "-classpath";
cmdArr[2] = cp;
cmdArr[3] = classname;
for (int i=0; i < args.length; i++) {
cmdArr[i+4] = args[i];
}
}
System.out.println("Command:" + cmdArr);
Process proc = rt.exec(cmdArr);
DataInputStream in =
new DataInputStream(proc.getErrorStream());
String line;
noFound = false;
while ((line = in.readLine()) != null) {
//System.out.println(line);
if (line.indexOf("ClassNotFoundException") != -1) {
String classNF =
line.substring(line.lastIndexOf(":") + 1).trim();
String cnfPackage = classNF.replace('/', '.');
if (javaDependencyWalker.getJarFilepath(cnfPackage) ==
null)
throw new Exception("No jar found for class " +
cnfPackage);
System.out.println("cnf: " + classNF + " found in " +
javaDependencyWalker.getJarFilepath(classNF));
noFound = true;
cp += javaDependencyWalker.getJarFilepath(cnfPackage) + ";";
} else if (line.indexOf("NoClassDef") != -1) {
String classNF =
line.substring(line.lastIndexOf(":") + 1).trim();
String ncdfPackage = classNF.replace('/', '.');
if (javaDependencyWalker.getJarFilepath(ncdfPackage) ==
null)
throw new Exception("No jar found for class " +
classNF);
System.out.println("NCDF: " + classNF + " found in " +
javaDependencyWalker.getJarFilepath(ncdfPackage) +
" " + ncdfPackage);
noFound = true;
cp += javaDependencyWalker.getJarFilepath(ncdfPackage) + ";";
}
}
} while (noFound);
StringTokenizer st = new StringTokenizer(cp, ";");
while (st.hasMoreTokens()) {
System.out.println(st.nextToken());
}
} catch (Exception ex) {
ex.printStackTrace();
} finally {
}

}
}


ant build.xml:
<project name="JavaDependencyWalker" default="compile">
<property name="scolon.separated.folders.ofjars" value="c:/FUPv5/lib;c:/FUPv5/DeleteFile/classes;c:/FUPv5/WSClient/classes;c:/FUPv5/common/classes"/>
<property name="main.class" value="com.oracle.orion.fupv5.ws.client.SRWSClient"/>
<property name="args.to.class" value="-sr sdfds34 -user sldkfjd -password sdlfkjd -endpoint http://wd2088.us.oracle.com:7778/gateway/services/SID0003321 -type ddf -comm sdfd -stat Done"/>

<path id="classpath">
<pathelement location="classes"/>
</path>

<target name="compile">
<mkdir dir="classes"/>
<javac destdir="classes" classpathref="classpath" source="1.5" target="1.5" >
<src path="src"/>
</javac>
</target>

<target name="run" depends="compile">
<java classname="com.gp.JavaDependencyWalker" classpathref="classpath" fork="yes">
<jvmarg line="-Ddeps=${scolon.separated.folders.ofjars} -DclassToRun=${main.class}"/>
<arg line="${args.to.class}"/>
</java>
</target>
</project>