Search this blog ...

Tuesday, March 29, 2011

Tools to locate class file in JAR / CLASSPATH

There are a number of different approaches for obtaining the location (jar/directory) of a class at runtime.  The following approach work pretty well…

Class c = ...
System.out.println(c.getProtectionDomain().getCodeSource().getLocation().toString());

You could also try VM debug options at startup such as:

-XX:-TraceClassLoading
Trace loading of classes.

-XX:-TraceClassLoadingPreorder
Trace all classes loaded in order referenced (not loaded).

In terms of static/offline location of classes, most people will suggest jarbrowser, jarminator, or brute force by leveraging a combination of the unix find command, unzip –l, and grep.

I’ve created a derivative of the offline approach.  Essentially I have three scripts:

  1. jarclasspath is a shell script/java class combo used to formulate a valid and complete list of libraries and directories that are referenced from the specified source JAR by way of META-INF/MANIFEST.MF Class-Path directives. A classpath containing a single JAR file could ultimately expand to hundreds of libraries, should that top-level archive specify Class-Path directives, and in-turn the dependent libraries provide Class-Path directives and so-on.  See this post for more details regarding manifest classpath attributes.
  2. jarcheck is a simple perl script that checks for presence of the specified class file in the specified JAR file/or directory. Note that Class-Path manifest directives can reference JAR files and/or directories. This tool supports searching both.
  3. jarwhich is a demonstration shell script that shows how to leverage the jarclasspath and jarcheck scripts above.  This particular script will search the CLASSPATH (and dependent libraries/directories based on Class-Path directives) to locate the requested class file.

Testing…

Using my UCM flavoured WebLogic Server as the basis for the test, I sourced $DOMAIN_HOME/bin/setDomainEnv.sh to set CLASSPATH.

This resulted in a CLASSPATH containing just 16 entries, however, all is not as it seems! :

% echo $CLASSPATH
/u01/app/oracle/product/Middleware/oracle_common/modules/oracle.jdbc_11.1.1/ojdbc6dms.jar: ...
/u01/app/oracle/product/Middleware/wlserver_10.3/server/lib/weblogic.jar: ...
/u01/app/oracle/product/Middleware/modules/features/weblogic.server.modules_10.3.4.0.jar:
...

% echo $CLASSPATH | tr ":" "\n" | wc -l
16

The weblogic.server.modules_10.3.4.0.jar referenced above in the CLASSPATH however has a Class-Path manifest directive referencing some 180 additional dependent JAR files:

% echo $CLASSPATH | tr ":" "\n" | grep weblogic.server.modules 
/u01/app/oracle/product/Middleware/modules/features/weblogic.server.modules_10.3.4.0.jar

% unzip -l /u01/app/oracle/product/Middleware/modules/features/weblogic.server.modules_10.3.4.0.jar
Archive:  weblogic.server.modules_10.3.4.0.jar
  Length     Date   Time    Name
--------    ----   ----    ----
     8880  03-23-11 05:00   META-INF/MANIFEST.MF
--------                   -------
     8880                   1 file

% unzip -d /tmp /u01/app/oracle/product/Middleware/modules/features/weblogic.server.modules_10.3.4.0.jar META-INF/MANIFEST.MF
Archive:  weblogic.server.modules_10.3.4.0.jar
  inflating: /tmp/META-INF/MANIFEST.MF 

% cat /tmp/META-INF/MANIFEST.MF|more
Manifest-Version: 1.0
Implementation-Vendor: BEA Systems
Implementation-Title: Oracle WebLogic Server Module Dependencies 10.3
Thu Oct 28 06:03:12 PDT 2010
Implementation-Version: 10.3.4.0
Feature-Name: weblogic.server.modules
Source-Repository-Change-Id: 1374366
Class-Path: weblogic.server.modules.wlsve_10.3.4.0.jar ../com.bea.core
.antlr.runtime_2.7.7.jar ../com.bea.core.descriptor.j2ee_1.5.0.0.jar
../com.bea.core.descriptor.j2ee.binding_1.5.0.0.jar ../com.bea.core.d
escriptor.wl_1.3.3.0.jar ../com.bea.core.descriptor.wl.binding_1.3.3.
0.jar ../com.bea.core.datasource6_1.9.0.0.jar ../com.bea.core.datasou
rce6.binding_1.9.0.0.jar ../com.bea.core.beangen_1.7.0.0.jar ../com.b
ea.core.descriptor.settable.binding_1.7.0.0.jar ../com.bea.core.diagn
ostics.accessor_1.5.0.0.jar ../com.bea.core.diagnostics.accessor.bind
ing_1.5.0.0.jar ../com.bea.core.management.core_2.8.0.0.jar ../com.be
a.core.management.core.binding_2.8.0.0.jar ../com.bea.core.ejbgen_1.1
.0.2.jar ../org.apache.ant_1.7.1/lib/ant-all.jar ../com.bea.core.repa
ckaged.apache.bcel_5.2.1.0.jar ../com.bea.core.repackaged.jdt_3.5.2.0
.jar ../com.bea.core.apache.commons.collections_3.2.0.jar ../com.bea.
core.apache.commons.lang_2.1.0.jar ../com.bea.core.apache.commons.poo
l_1.3.0.jar ../com.bea.core.apache.commons.io_1.0.0.0_1-4.jar ../com.
bea.core.apache.commons.fileupload_1.0.0.0_1-2-1.jar ../com.bea.core.
apache.dom_1.0.0.0.jar ../com.bea.core.apache.logging_1.0.0.0.jar ../
org.apache.openjpa_1.2.0.0_1-1-1-SNAPSHOT.jar ../com.bea.core.xml.xml
beans_2.1.0.0_2-5-1.jar ../com.bea.core.logging_1.8.0.0.jar ../com.be
a.core.bea.opensaml_1.0.0.0_6-1-0-0.jar ../com.bea.core.bea.opensaml2
_1.0.0.0_6-1-0-0.jar ../com.bea.core.monitoring.harvester.api_2.3.0.0
.jar ../com.bea.core.monitoring.harvester.jmx_2.3.0.0.jar ../com.bea.
.....

As you can see above, this short 16 entry CLASSPATH soon expands to something massive.  In fact, it expanded to 412 unique entries (primarily JAR files, but also directories)!

Let us run some tests:

% ~/bin/jarwhich oracle.jdbc.driver.OracleDriver
/u01/app/oracle/product/Middleware/oracle_common/modules/oracle.jdbc_11.1.1/ojdbc6dms.jar: oracle/jdbc/driver/OracleDriver.class
##Match Found.
/u01/app/oracle/product/Middleware/wlserver_10.3/server/lib/ojdbc6.jar: oracle/jdbc/driver/OracleDriver.class
##Match Found.


% ~/bin/jarwhich javax.mail.internet.MimeUtility
/u01/app/oracle/product/Middleware/modules/javax.mail_1.1.0.0_1-4-1.jar: javax/mail/internet/MimeUtility.class
##Match Found.

 

And finally, here are the scripts:

jarclasspath

#!/bin/sh
#
# Copyright (c) 2011, Matt Shannon.
#
#    NAME      jarclasspath
#

if [ -z "${JAVA_HOME}" ]; then
  printf "\n\nError: Ensure JAVA_HOME environment variable is set.\n"
  exit 1
fi

# Checks error
checkerror()
{
  RESULTCODE=$?
  if [ ${RESULTCODE} -ne 0 ];then
    exit 1
  fi
}

JAVARUNCMD="${JAVA_HOME}/bin/java"
JAVACMPCMD="${JAVA_HOME}/bin/javac"

## note - need to be careful with dollar signs and backslashes!

cat > /tmp/JarClasspath.java <<EOF
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.InputStream;
import java.io.IOException;

import java.util.List;
import java.util.ArrayList;

import java.util.jar.Attributes;
import java.util.jar.JarFile;
import java.util.jar.Manifest;


public class JarClasspath
{
  public static void main(String[] args)
  {
    if (args.length < 0)
    {
      System.err.println("Usage: arg0 = /path/to/jar");
      System.exit(-1);
    }

    String sourceJar = args[0];

    List<String> jars = new ArrayList<String>();

    try
    {
      File entry = new File(sourceJar);
      String entryPath = entry.getCanonicalPath();

      if (entry.isDirectory())
      {
        System.out.println(entryPath);
      }
      else if (entry.isFile())
      {
        if ((entryPath.toLowerCase().endsWith(".jar") ||
          entryPath.toLowerCase().endsWith(".zip")))
        {
          System.out.println(entryPath);
          jars.add(entryPath);
          printManifestClasspathEntries(jars, entry);
        }
      }
    }
    catch (Exception e)
    {
      e.printStackTrace();
      System.exit(-1);
    }
  }

  public static void printManifestClasspathEntries(
    List<String> jars,
    File sourceJar
  ) throws Exception
  {
    File[] entries = getManifestClasspathEntries(sourceJar);
    int length = (entries == null) ? 0 : entries.length;
    for (int i = 0 ; i < length; i++)
    {
      File entry = entries[i];
      if (entry == null)
      {
        continue;
      }

      String entryPath = entry.getCanonicalPath();

      if (entry.isDirectory())
      {
        System.out.println(entryPath);
      }
      else if (entry.isFile())
      {
        if ((entryPath.toLowerCase().endsWith(".jar") || 
          entryPath.toLowerCase().endsWith(".zip")))
        {
          if (! jars.contains(entryPath))
          {
            jars.add(entryPath);
            System.out.println(entryPath);
            printManifestClasspathEntries(jars, entry);
          }
        }
      }
    }
  }


  public static File[] getManifestClasspathEntries(File file)
    throws IOException, FileNotFoundException
  {
    File[] results = null;

    InputStream is = null;
    try
    {
      is = new FileInputStream(file);

      JarFile jarfile = new JarFile(file);

      Manifest manifest = jarfile.getManifest();
      if (manifest != null)
      {
        Attributes attributes = manifest.getMainAttributes();
        if (attributes != null)
        {
          String cp = attributes.getValue(Attributes.Name.CLASS_PATH);
          if (cp != null)
          {
            String[] entries = cp.split("\\\\s");
            int length = (entries == null) ? 0 : entries.length;
            if (length > 0)
            {
              results = new File[length];
              for (int i = 0 ; i < length; i++)
              {
                if ((entries[i] == null) || (entries[i].trim().length() == 0))
                {
                  continue;
                }
                File f = new File(file.getParentFile(), entries[i]);
                results[i] = f;
              }
            }
          }
        }
      }
    }
    finally
    {
      if (is != null)
      {
        try
        {
          is.close();
        }
        catch (Exception ignore)
        {
          ;
        }
        is = null;
      }
    }
    return results;
  }
}
EOF

CP=/tmp

# Compile tool
${JAVACMPCMD} -classpath ${CP} -d "/tmp" /tmp/JarClasspath.java
checkerror

# Process arguments
for jar in "$@"; do
  ${JAVARUNCMD} -classpath ${CP} JarClasspath "${jar}"
  checkerror
done

jarcheck

#!/usr/bin/perl -w
#
# Checks if specified class is found in provided jar/directory
#
use Getopt::Std;

$0 =~ /([^\/]+)$/ ; # Pattern match; 1 or more chars at end of perl script file name (after the last / if present)
$SCRIPT = $1 ; # Contains the subpattern from the first set of parentheses in the last pattern matched

# Get the class argument.
getopts('s');

unless (((scalar @ARGV) == 2) &&
(defined ($class = $ARGV[0])) && ($class =~ /^[\w\.\/]+$/) &&
(defined ($jar = $ARGV[1])) && ($jar =~ /^[\w\.\/\-]+$/))
{
  die("Usage: $SCRIPT [-s] <class> <jar>\nwhere:",
      "\t<class> is the fully-qualified, dot or slash delimited name of a Java class.\n",
      "\t<jar> is the fully-qualified jar file or directory to search.\n",
      "\t-s tells jarcheck that the full class name is not provided\n");
}

# Get the partial name of the .class file for this class.
$classFile = $class ;
$classFile =~ s/\./\//g ;
$classFile .= ".class" ;

# If the jar is a file, Search it for the class file name.
if ((-f $jar) && (open(ARCHIVE, "unzip -l $jar|")))
{
  while(defined ($line = <ARCHIVE>))
  {
# escape any $ in the class file name
    $escaped = $classFile;
    $escaped =~ s/\$/\\\$/;

    # \b allows you to perform a "whole words only" search using a regular expression
if($line =~ /\b$escaped\b/)
    {
   print("$jar: ");
   if (defined($opt_s))
   {
     $line =~ /[_\w\/\$]+$classFile/;
     print($&);
   }
   else
   {
     print($classFile);
   }
   print("\n");

   exit(0);
}
  }
}
# If the jar is infact a directory, see if the .class file is under it.
elsif(-d $jar)
{
  if (-f "$jar/$classFile")
  {
print("$jar/$classFile\n");
exit(0);
  }
  elsif (defined($opt_s))
  {
$classFile =~ /[_\w]+\.class/;
$maxdepth = ($jar eq ".")? "-maxdepth 1":"";
$results = `find $jar $maxdepth | grep -w $&`;
if (!$?)
{
   print $results;
   exit(0);
}
  }
}

exit(1);

jarwhich

#!/bin/sh
#
# Copyright (c) 2011, Matt Shannon.
#
#    NAME      jarwhich
#

if [[ $# -ne 1 ]]; then
  printf "\nUsage: $0 <class>\n"
  printf "  where <class> is the fully-qualified, dot or slash delimited name of\n"
  printf "  the Java class to locate. e.g. oracle.jdbc.driver.OracleDriver\n"
  exit
fi

if [ -z "${CLASSPATH}" ]; then
  printf "\nError: Ensure CLASSPATH environment variable is set.\n"
  exit 1
fi

DIRNAMECMD=`which dirname`
SCRIPT_DIR="`${DIRNAMECMD} $0`/"
SCRIPT_DIR="`cd \"${SCRIPT_DIR}\" && pwd`"


jarCheck()
{
  "${SCRIPT_DIR}"/jarcheck "$1" "$2"
  return
}

file=/tmp/out$$
echo $CLASSPATH | tr ":" "\n" | sort -u | xargs "${SCRIPT_DIR}"/jarclasspath | sort -u > $file

for line in `cat $file`; do
  jarCheck $1 "${line}"
  if [[ $? -eq 0 ]]; then
    printf "##Match Found.\n"
    # exit 0
  fi
done

No comments:

Post a Comment