by Jean Tessier
PATH
PATH
This application extracts information from compiled Java code and makes it available to users in a numbers of ways.
Many authors have stressed the importance of managing depencencies (Robert C. Martin, John Lakos). Managing dependencies means securing encapsulation and making sure that the code follows the architecture. It is also essential for the modularization of code and favors reuse. By extracting the information from the code itself, we can detect where we were sloppy, where we took shortcuts that we shouldn't have. Managing the dependencies can shorten the compilation time of the code.
This application also includes tools for computing metrics based on information extracted from compiled Java code. These measurements can be helpful to guide development and can point out areas where the code needs improvement.
Finally, this application includes a tool that computes the changes in the API of a system between successive versions. If you use third-party libraries, you can use it to verity that the parts you are dependending upon have not changed. If you provide libraries to others, you can use it to assertain the impact changes might have on your customers. You can even use it to monitor the evolution of your own software.
This project started because another project of mine became so complex that I couldn't track dependencies in my head anymore. I looked for a tool to help me and the only ones I could find were very expensive. So I resolved to build my own tool.
I haven't touched that other project since I started working on Dependency Finder.
At first, I thought of using the Reflection API, but it shows only the class's attributes and method signatures. If a method uses other classes as part of its processing, the Reflection API will not show that dependency.
Java compilers take source code and produce bytecode that they put in
.class
files. The format of these files is described in
The class
File Format.
I undertook to parse these files and extract the information I needed
directly from the compiled code.
Once I had access to the code structure, I got started on tools to report interesting information about it. I started with dependencies and quickly moved up to metrics and API differences. At the beginning, the output was only to standard output or a file. Quickly, I added XSL stylesheets so it would look a little better. Eventually, I added Swing application to keep the data in memory and play with it a while.
Dependency Finder comes as a ZIP file that includes everything you need to run it.
You need to have a Java Virtual Machine installed. You can get one for
free from Sun Microsytems. Make sure you define
the JAVA_HOME
environment variable.
For example, if you installed JDK 1.3 in C:\jdk1.3
:
SET JAVA_HOME=C:\jdk1.3
You can either add this line to your AUTOEXEC.BAT
file, or on WinNT
use Start | Settings | Control Panel | System and select the Environment
tab to create and edit variables.
Extract the ZIP file to some directory you like. The extraction will create a subdirectory named "DependencyFinder" that is the root of the package.
For example, if you extract it to C:\
, you will endup with a
directory called C:\DependencyFinder\
.
Create an environment variable named DEPENDENCYFINDER_HOME
and have it
point to the directory where you installed Dependency Finder.
For example, if you extracted the ZIP file in C:\
SET DEPENDENCYFINDER_HOME=C:\DependencyFinder
You can either enter this line in your AUTOEXEC.BAT
file, or on WinNT
use Start | Settings | Control Panel | System and select the Environment
tab to create and edit variables.
PATH
Add the "bin" subdirectory to your PATH
variable.
For example, if you extracted the ZIP file in C:\
SET PATH=C:\DependencyFinder\bin;%PATH%
This is only required on Win95/98.
Under Win95/98, you will need to reboot your computer for the changes to the environment to take effect.
If the VM runs out of memory, try setting the DEPENDENCYFINDER_OPTS
environment variable.
SET DEPENDENCYFINDER_OPTS=-Xmx128m
If you get the message "Out of environment space" while running the scripts
on a Windows machine, you will need to add the following line to your
C:\CONFIG.SYS
file and then reboot.
shell=command.com /e:8192 /p
This reserves more space for environment variables in the DOS shell. You can type:
C:\>command /?
for more details.
At this time, Dependency Finder does not have shell scripts to launch any of the tools the way it does under DOS.
You need to have a Java Virtual Machine installed. You can get one for
free from Linux or Solaris from Sun Microsytems.
Make sure you define the JAVA_HOME
environment variable.
Suppose you installed JDK 1.3 in /usr/java
.
For sh or bash:
JAVA_HOME=/usr/java export JAVA_HOME
For csh or tcsh:
setenv JAVA_HOME /usr/java
You can put this line in your .cshrc
file.
Extract the ZIP file to some directory you like. The extraction will create a subdirectory named "DependencyFinder" that is the root of the package.
$ unzip DependencyFinder.zip
For example, if you extract it to $HOME
, you will endup with a
directory called $HOME/DependencyFinder/
.
Create an environment variable named DEPENDENCYFINDER_HOME
and have it
point to the directory where you installed Dependency Finder.
If the VM runs out of memory, try modifying the DEPENDENCYFINDER_OPTS
environment variable to allocate more memory to the JVM.
Suppose you extracted the ZIP file in $HOME
.
For sh or bash:
DEPENDENCYFINDER_HOME=$HOME/DependencyFinder export DEPENDENCYFINDER_HOME DEPENDENCYFINDER_OPTS=-Xmx128m export DEPENDENCYFINDER_OPTS
For csh or tcsh:
setenv DEPENDENCYFINDER_HOME $HOME/DependencyFinder setenv DEPENDENCYFINDER_OPTS -Xmx128m
You can put these lines in your .cshrc
file.
PATH
Add the directory where you put the Unix scripts to your PATH
variable.
Suppose you extracted the ZIP file in $HOME
.
For sh or bash:
PATH=$DEPENDENCYFINDER_HOME\bin;$PATH export PATH
For csh or tcsh:
set path = ($DEPENDENCYFINDER_HOME\bin $path)
You can put this line in your .cshrc
file.
Dependency Finder also comes as a web application that you can easily deploy in just about any J2EE servlet container.
Use your favorite servlet container to deploy the WAR file. Most of them have tools for deploying WAR files. For some of them, it can be as simple as deploying the WAR file to a specific directory.
For instance, under Tomcat on Windows:
C:\>%CATALINA_HOME%\bin\shutdown C:\>copy DependencyFinder.war %CATALINA_HOME%\webapps C:\>%CATALINA_HOME%\bin\startup
web.xml
file
Next, you need to tell the wep application where to find the Java classes to
extract dependencies from. This is done via the web.xml
file in the
WEB-INF\
directory. Some application servers expose the content of this
file through some form of user interface. Under Tomcat, you have to edit
the file yourself using a text editor.
There are three application variable to adjust.
name
Used in the JSPs to tell the users what that are looking at. Put the name of the application that you are extracting from here, it will be shown in titles by the JSPs. source
Use this variable to tell Dependency Finder where to locate classes find which to extract a dependency graph. You can separate multiple locations by using commas. Each location can either be a JAR file, a ZIP file, a .class
file, or a directory that will be recursively searched for.class
files.mode
Optional. Tells Dependency Finder whether to minimize or maximize the resulting graph, or just leave it alone (raw, the default value). See below for the implications of minimizing or maximizing a graph.
Under some servlet containers, for example Tomcat, you will need to restart the servlet container for the changes to take effect.
C:\>%CATALINA_HOME%\bin\shutdown C:\>%CATALINA_HOME%\bin\startup
Or you can you the deployment tool in Tomcat Manager to install the web application.
Once you have installed Dependency Finder, you are ready to use all the tools now at your disposal. Almost all of them are Java applications that you launch using a batch file or shell script.
At the beginning, the toolset was just a group of batch files that offered a basic command-line interface (CLI). Later, I added a Swing graphical interface (GUI) that simply replicated the controls available through the CLI. Then I build a web application that mirrored the GUI. This might help explain why the the interface looks the way it does and why it may not always be very intuitive. Finally, I added a group of Ant tasks that mirror the major functions of the CLI.
We will start with the GUI-based tools, and then talk about the CLI-based tools and end with the Ant tasks.
CLI | Ant | GUI | Web |
---|---|---|---|
ClassDump
|
n/a | n/a | n/a |
ClassList
|
n/a | n/a | n/a |
ClassMetrics
|
<classmetrics>
|
n/a | n/a |
ClassReader
|
n/a | n/a | n/a |
ClosureToText
|
<xslt style="${dependencyfinder.home}/etc/ClosureToText.xsl">
|
n/a | n/a |
DependablesToHTML
|
<xslt style="${dependencyfinder.home}/etc/DependablesToHTML.xsl">
|
n/a | n/a |
DependablesToText
|
<xslt style="${dependencyfinder.home}/etc/DependablesToText.xsl">
|
n/a | n/a |
DependencyClosure
|
<dependencyclosure>
|
DependencyFinder >> File >> Closure
|
closure.jsp & advancedclosure.jsp
|
DependencyExtractor
|
<dependencyextractor>
|
DependencyFinder >> File >> Extract
|
extract.jsp
|
DependencyFinder
|
n/a | n/a | n/a |
DependencyGraphToHTML
|
<xslt style="${dependencyfinder.home}/etc/DependencyGraphToHTML.xsl">
|
n/a | n/a |
DependencyGraphToText
|
<xslt style="${dependencyfinder.home}/etc/DependencyGraphToText.xsl">
|
n/a | n/a |
DependencyMetrics
|
<dependencymetrics>
|
DependencyFinder >> File >> Metrics
|
n/a |
DependencyReporter
|
<dependencyreporter>
|
DependencyFinder >> File >> Dependency
|
query.jsp & advancedquery.jsp
|
DependentsToHTML
|
<xslt style="${dependencyfinder.home}/etc/DependentsToHTML.xsl">
|
n/a | n/a |
DependentsToText
|
<xslt style="${dependencyfinder.home}/etc/DependentsToText.xsl">
|
n/a | n/a |
DiffToHTML
|
<xslt style="${dependencyfinder.home}/etc/DiffToHTML.xsl">
|
n/a | n/a |
HideOutboundDependencyGraphToHTML
|
<xslt style="${dependencyfinder.home}/etc/HideOutboundDependencyGraphToHTML.xsl">
|
n/a | n/a |
HideOutboundDependencyGraphToText
|
<xslt style="${dependencyfinder.home}/etc/HideOutboundDependencyGraphToText.xsl">
|
n/a | n/a |
HideInboundDependencyGraphToHTML
|
<xslt style="${dependencyfinder.home}/etc/HideInboundDependencyGraphToHTML.xsl">
|
n/a | n/a |
HideInboundDependencyGraphToText
|
<xslt style="${dependencyfinder.home}/etc/HideInboundDependencyGraphToText.xsl">
|
n/a | n/a |
JarJarDiff
|
<jarjardiff>
|
n/a | n/a |
ListDiff
|
<listdiff>
|
n/a | n/a |
ListDiffToHTML
|
<xslt style="${dependencyfinder.home}/etc/ListDiffToHTML.xsl">
|
n/a | n/a |
ListDiffToText
|
<xslt style="${dependencyfinder.home}/etc/ListDiffToText.xsl">
|
n/a | n/a |
ListDocumentedElements
|
<javadoc> <doclet name="com.jeantessier.diff.ListDocumentedElements" path="${classesDir}"> <param name="-tag" value="level"/> <param name="-valid" value="published"/> <param name="-out" value="documented_elements.txt"/> </doclet> </javadoc> |
n/a | n/a |
ListInheritanceDiffToText
|
<xslt style="${dependencyfinder.home}/etc/ListInheritanceDiffToText.xsl">
|
n/a | n/a |
ListUnused
|
<xslt style="${dependencyfinder.home}/etc/ListUnused.xsl">
|
n/a | n/a |
OOMetrics
|
<oometrics>
|
OOMetricsGUI
|
n/a |
PublishedDiffToHTML
|
<xslt style="${dependencyfinder.home}/etc/PublishedDiffToHTML.xsl">
|
n/a | n/a |
XSLTProcess
|
<xslt>
|
n/a | n/a |
c2c
|
<dependencyreporter c2c="yes">
|
DependencyFinder >> File >> Dependency
|
query.jsp & advancedquery.jsp
|
c2p
|
<dependencyreporter c2p="yes">
|
DependencyFinder >> File >> Dependency
|
query.jsp & advancedquery.jsp
|
f2f
|
<dependencyreporter f2f="yes">
|
DependencyFinder >> File >> Dependency
|
query.jsp & advancedquery.jsp
|
p2p
|
<dependencyreporter p2p="yes">
|
DependencyFinder >> File >> Dependency
|
query.jsp & advancedquery.jsp
|
The main tool is DependencyFinder
. It extracts dependencies from
compiled code, maintains the dependency graph in memory, and provides
an interface for querying the graph and display the results.
The other tool computes metrics on a given codebase. It displays results in a table with sortable columns. Different tabs control the level of detail: package, class, or method.
All the tools support -help
command-line switch, which prints the list of
valid command-line switches for that particular tool. Many of the tools also
support some or all of the following four command-line switches:
-in
Many of the CLI tools are automated translations of XML data into either HTML or plain text. The scripts have hard-coded XSL stylesheets. You specify the XML input with this switch. -out filename
Writes the output of the command to filename. Most commands default to the standard output if you do not use -out
.-time
Prints the time it took to run the command. If you run commands against a large codebase or dependency graph, it can take a substantial amount of time before it returns. This information can help you plan lenghty operations in the future. -verbose [filename]
Writes summary processing information to filename. If you do not specify a file, the tool will write the information to the standard output stream.
You can run the functions of the CLI as part of your build script if you are using the Ant build tool from the Apache Foundation. There is a special help page on how to use Dependency Finder with Ant.
Dependency Finder uses specialized classes to parse .class
files. It also
includes some tools for looking at the contents of these files.
Use this tool to put the class in a .class
file in human-readable form.
With the -xml
switch, it dumps the complete structure to an XML file
that you can manipulate with your favorite XML editor.
This is a simple tally of how many classses, interfaces, methods, publics,
statics, finals, etc. there are in a given codebase. These tallies are
not as powerful as the metrics of OOMetrics
, but they can give you a
rough idea of the size and complexity of a piece of software.
Dependencies occur when one class uses the services of another class. For example, this can happen when a class inherits from another, has an attribute whose type is of another class, or when one of its methods calls a method on an object of another class.
Knowing about dependencies is useful for programmers when deciding the impact of a change. When you're about to rename a feature or class, it is useful to know all the places in the codebase where it is being used.
This is also useful for code reviewers and architects when assessing the coupling within an application or library.
Dependency Finder builds dependency graphs based on the information in class files.
So, a dependency is when the functioning of one element A
requires the
presence of another element B
. We say that A
depends on B
and we
write it:
A --> B
We say that A
has an outbound dependency while B
has an inbound
dependency. It is the same dependency, but whether it is inbound or
outbound is relative to how you look at it. We also say that A
is a
dependent and B
is dependable.
A dependency graph comprises nodes for software artifacts linked
together by two types of relationships. Artifacts are packages,
classes, and features. We use the term feature to designate
class attributes, constructors, and methods; we will be treating
them the same from here on. For the purpose of analyzing dependencies,
we do not distinguish between different types of features, whether they
are constructors or regular methods, and regardless of the feature's
characteristics, such as being marked as final
or static
.
Packages have classes, which themselves have features; this is called composition. Classes refer to each other and so do features; this is called a dependency.
So a feature node is linked to its class node through composition. The class node is also linked to its package node through composition. Each is linked to various other nodes of various type using dependency links.
Going through class files and collecting dependencies is called extracting. The application keeps dependencies in a graph. It begins with one node per Java package. To this it adds one node per Java class (including inner classes), and to those it adds one node per feature.
Dependency Finder can read all types of compiled Java: JAR files,
ZIP files, or class files. You can point it to a JAR, ZIP, or
.class
file. You can also point it to a directory and it will
explore that directory and all its subdirectories for files ending
in .class
. You can select more than one target, mix and match
JARs and ZIPs and class files and directories, and Dependency
Finder will aggregate all their contents together. It will sort
them all out.
The dependencies are gathered by the class
com.jeantessier.dependency.CodeDependencyCollector
. It traverses
the structure of a .class
file, as read by
com.jeantessier.classreader.Classfile
, and collects explicit
dependencies in the bytecode.
The dependency collector can detect three kinds of explicit dependencies, from which Dependency Finder can infer all implicit dependencies:
There are explicit dependencies, as present in the code itself. There are also implicit dependencies derived from these explicit ones. A dependency between two classes in two separate packages implies a dependency between the packages.
Dependency Finder analyzes compiled Java code, and as a result it is at the mercy of the information produced by the compiler. Some of the following limitations are the result of using Sun's compiler that comes with the JDK. But I will start with one limitation that applies regardless of the compiler.
If we look closely at this model, we see that recursive method calls and certain types of recursive structures can generate dependencies between a programming elemend (i.e., a class or a method) and itself. Or, if a method has a parameter or local variable whose type is the enclosing class itself, or if an attribute's type is its enclosing class, this introduce a dependency from the feature to the class of which it is a part of.
Dependency Finder does not keep track of dependencies on self. Dependency analysis is useful for two things:
You use compile-time dependency, or change impact analysis, to answer questions such as "If I change X, what else might I have to change too?" Dependencies on self are not all that useful here. If I am changing X, I don't need to be told that X will be impacted. Likewise, if I am making changes to features of a class, I don't need to be told that the class will be impacted; I am already editing it. I do, however, need to know what other features in this class might be impacted.
You use runtime dependency analysis to answer questions such as "For class X to run, what other classes do I need?" The class as a whole is the unit of loading. If a feature needs its class, the class is forcibly already loaded otherwise the JVM would never had gotten to the feature to begin with. Likewise, if a class needs itself, well it is already there.
So Dependency Finder does not keep track of dependencies on self. This includes implicit dependencies from a class or feature to its package, or from a package to itself.
It is often the case that Dependency Finder cannot track dependencies on
simple static final
constants.
In Java, you can define constants in classes and interfaces by marking them
as static
and final
. Subclasses or implementations cannot override the
constants because they are static. No one can change the value because it
is final. As a result, the compiler can use local optimizations to speed up
execution. When these constants are of primitive types or String
, the
compiler can inline the value directly in the client class, so that it makes
no reference at all to the class that owns the constant.
Under these circumstances, Dependency Finder cannot track dependencies on
simple constants that are referenced from outside their class. If the
constant are of some object type other than String
, the compiler cannot
perform the optimization and Dependency Finder will be able to track
dependencis on them.
Dependency Finder does not track the type of local variables in methods.
The compiler does not need to keep track of the type of local variables. It validates the assignment and then discards type information. The virtual machine will validate the bytecode and make sure that it was not tampered with. The compiler makes sure that an assignment to a local variable is type-safe. Any method call on the variable will be made either on an interface or a class that is compatible with the type of the local variable.
Most compilers have options to retain information about local variables,
such as their name and type. With javac
, you have to supply the -g
switch. Doing this helps symbolic debuggers give more useful context
information to their users.
Given all this, it barely matters if Dependency Finder tracks dependencies on the type of local variables very closely. Assignments will to objects whose type eventually reaches the exact type of the variable, and method calls will be made on that type's set of methods. Dependency Finder loses no information by ignoring possible local variables, just a little precision. And since the virtual machine does not use that information, it does not impact runtime dependencies anyway.
Select File | Extract
; this will popup a file dialog.
Select the files and/or directories to extract from and click Extract
.
You can repeat this command as often as needed; each time, the extracted
dependencies will be added to the current dependency graph in memory.
You can save the current dependency graph to an XML file by using
File | Save
. You can later reload the graph by using
File | Open
. Note that it is usually faster to extract the
information again from class files than to load it from an XML file.
You use DependencyExtractor
to extract dependencies. Simply string
out the files and directories on the command-line. All the dependencies
are merged into a single output.
E.g.,
C:\>DependencyExtractor classes %JAVA_HOME%\JRE\lib\rt.jar
You can specify an output file with the -out
switch:
C:\>DependencyExtractor %DEPENDENCYFINDER_HOME%\lib\DependencyFinder.jar -out df.txt
You can save the graph to XML with the -xml
switch:
C:\>DependencyExtractor %DEPENDENCYFINDER_HOME%\lib\DependencyFinder.jar -xml -out df.xml
Or you can use Java serialization to save the graph:
C:\>DependencyExtractor %DEPENDENCYFINDER_HOME%\lib\DependencyFinder.jar -serialize -out df.ser
Here is a simple, concrete example. We start with a trivial Java class in
the file test.java
and compiled into test.class
.
import java.util.*;
public class test { public static void main(String[] args) { Collection c = Collections.singleton(new Object()); } }
Here is the raw output dependency graph.
C:\>DependencyExtractor test.class
test --> java.lang.Object main(java.lang.String[]) --> java.lang.Object --> java.lang.Object.Object() --> java.lang.String --> java.util.Collections.singleton(java.lang.Object) --> java.util.Set test() --> java.lang.Object.Object() java.lang Object <-- test <-- test.main(java.lang.String[]) Object() <-- test.main(java.lang.String[]) <-- test.test() String <-- test.main(java.lang.String[]) java.util Collections singleton(java.lang.Object) <-- test.main(java.lang.String[]) Set <-- test.main(java.lang.String[])
We can save it to an XML document for use with the other tools.
C:\>DependencyFinder -xml -out test.xml test.class
And narrow down the graph to class-to-class dependencies.
C:\>c2c -scope-includes /test/ test.xml
test --> java.lang.Object --> java.lang.String --> java.util.Collections --> java.util.Set
c2c
is a shortcut for a set of switches to DependencyReporter
. All the
following commands are equivalent.
C:\>c2c -scope-includes /test/ test.xml C:\>c2c -class-scope-includes /test/ test.xml C:\>DependencyReporter -c2c -class-scope-includes /test/ test.xml C:\>DependencyReporter -class-scope -class-scope-includes /test/ -class-filter test.xml
For fun, I entered manually this graph description into Poseidon for UML.
We can narrow the graph further to class-to-package dependencies. Again, all these commands are equivalent.
C:\>c2p -scope-includes /test/ test.xml C:\>c2p -class-scope-includes /test/ test.xml C:\>DependencyReporter -c2p -scope-includes /test/ test.xml C:\>DependencyReporter -class-scope -class-scope-includes /test/ -package-filter test.xml
test --> java.lang --> java.util
Finally, a package-to-package dependency graph. All these commands are equivalent.
C:\>p2p test.xml C:\>DependencyReporter -p2p test.xml C:\>DependencyReporter -package-scope -package-filter test.xml
--> java.lang --> java.util java.lang <-- java.util <--
You use extract.jsp
to generate a dependency graph for the web application.
The graph will stay in memory for the lifetime of the process serving the
application, or until you extract it again with the JSP. This graph can
get pretty big, so make sure you allocate enough memory to the application
server.
If you just call extract.jsp
, it will print statistics on the current
graph, if any. These include the number of nodes in the graph, how long
it took to extract it, and a timestamp of when it was extracted.
If you click on the Launch button, or call extract.jsp?launch=Launch
,
it will start to extract a new dependency graph. The previous one will
remain in use for other callers until the extraction terminates successfully.
The JSP prints the name of classes as it extracts them. And at the end, it
prints how many classes it read and how it it took to analyze them.
You can use the URL "extract.jsp?launch=Launch
" to automatically
update the graph after automated compilation runs, such as during nightly
builds.
<target name="extract" depends="init"> <get src="${WEB_APP_URL}/extract.jsp?launch=Launch" dest="log.html"/> </target>
You use the com.jeantessier.dependencyfinder.ant.DependencyExtractor
task to
extract a graph as part of your build. You need to associate the class to a
tag using Ant's <taskdef>
task. You also need to tell Ant where to
find the Dependency Finder classes. There is a special
help page on how to do this.
<target name="extract" depends="init"> <dependencyextractor destfile="df.xml" xml="yes"> <path> <pathelement location="."/> </path> </dependencyextractor> <xslt style="${dependencyfinder.home}/etc/DependencyGraphToHTML.xsl" in="df.xml" out="df.html"/> </target>
It is often the case that an explicit dependency in the code can be implied from another explicit dependency in that code. For example, take the following code snippet:
class A extends B { public void a() { b(); } }
class B { public void b() {} }
We can analyze the dependencies. A
is a subclass of B
, so there is a
dependency A --> B
. And A.a()
calls b()
, which is defined
in B
, yielding the dependency A.a() --> B.b()
. Here is a
subset of the resulting dependency graph:
Clearly, the dependency A.a() --> B.b()
implies the dependency
A --> B
. We say that the latter is redundant; it does not
add anything to the overall connectivity of the graph. Another, very
frequent, example of this situation occurs with constructors.
class A extends B {} class B {}
Another variation occurs when a method either calls a method on an object
it received as a parameter or throws a new Exception
that is part of
its throws
clause.
class A { public void a(B b) { b.b(); } }
class B { public void b() {} }
We minimize the dependency graph when we remove redundant dependencies.
In the first example above, we would remove the dependency
A --> B
. Here is what would be the resulting graph:
We maximize the dependency graph when we expose all implicit dependencies
by making them explicit. Staying with our example,
A.a() --> B.b()
, maximizing it would introduce these
dependencies:
A.a() --> B
A --> B.b()
A --> B
Maximizing and minimizing a dependency graph deals with the trade off between space and time. Minimizing the graph saves on space, but it takes longer to traverse the graph to find if there is a dependency between two given nodes. Maximizing the graph saves on time, since all the dependencies are there for the picking, but the size of the graph goes up dramatically.
The illustrations below show how combinations of these are minimized or maximized. You can minimize a graph that has been maximized and obtain the same minimized graph as if you had minimized the original graph. Similarily, if you maximize a graph that has been minimized, you end up with the same graph as if you had minimized the original graph.
Here is a simple, concrete example. We start with a our trivial Java class from before.
import java.util.*;
public class test { public static void main(String[] args) { Collection c = Collections.singleton(new Object()); } }
Let's start with the obvious dependencies. test.main()
has a parameter
that's a String
array, a local variable of type Collection
, calls the
no-arg constructor for Object
, and calls a static method on Collections
.
That's four so far. Also, since test
extends Object
by default, that's
one more dependency. It also gets an implicit no-arg constructor that calls
Object
's no-arg constructor. We are now up to six dependencies.
The compiler does not keep track of local variables unless you call it with
the -g
switch. This means we are not guaranteed to find the dependency
test.main(java.lang.String[]) --> java.util.Collection
by simply
looking at compiled code. But we can examine the signature of singleton()
and since we have to call it, we'll need to get objects of these types.
The method takes a parameter of type Object
and returns a Set
. This
last one makes up for our loosing Collection
earlier since Set
extends
Collection
.
Total: seven dependencies.
The redundant dependencies are shown in bold. We can remove them by minimizing the graph.
Or, we can maximize the graph. The bold dependencies below show the original
dependencies. If the graph gets this complicated for something as trivial as
test.java
, imagine what your code will look like.
Here are the dimentions of various graphs.
Software | Artifacts | Dependencies | ||||||
Packages | Classes | Features | Minimized | Raw | Maximized | |||
test.java |
3 | 5 | 4 | 5 | (-29%) | 7 | 27 | (+286%) |
Jakarta-ORO | 9 | 101 | 835 | 2,301 | (-13%) | 2,652 | 4,641 | (+75%) |
Log4J | 44 | 542 | 3,331 | 10,880 | (-16%) | 12,961 | 27,215 | (+110%) |
Xerces | 51 | 880 | 11,218 | 38,194 | (-13%) | 43,893 | 80,472 | (+83%) |
Xalan | 58 | 855 | 10,763 | 35,317 | (-15%) | 41,594 | 81,745 | (+97%) |
Ant | 38 | 568 | 5,635 | 22,057 | (-16%) | 26,184 | 50,585 | (+93%) |
Ant w/ optional | 113 | 1,117 | 10,854 | 42,140 | (-16%) | 50,165 | 99,178 | (+98%) |
Dependency Finder | 27 | 364 | 3,587 | 13,098 | (-18%) | 15,897 | 28,923 | (+82%) |
Reporting means displaying a dependency graph. More often than not, the graph on display is a subset of a larger graph. A graph is composed of nodes and links. When we display a subset of a graph, we must choose which nodes and which links to include in the subset. We use Perl regular expressions to indicate what we are interested in.
So, reporting deals with selecting a subset of the dependency graph; that is, choosing a subset of the nodes making up the graph and a subset of the links to or from these nodes. The scope selects the nodes in the source graph that will be copied into the destination graph. We then filter the links (i.e., the dependencies) to or from nodes in the scope that will be copied into the destination graph. We must also copy the nodes at the other end of these link if they were not already in the destination graph.
We use the term scoping when talking about the selection process for nodes. The GUI and the JSPs have a section labeled "Select programming elements" to group the controls that govern scoping.
We use the term filtering when talking about the selection process for links. The GUI and the JSPs have a section labeled "Show dependencies" to group the controls that govern filtering.
Now both scoping and filtering have similar user interfaces. I will talk about the one for scoping here, but the filtering interface works the same way.
First, a set of checkboxes narrow the search to only packages, classes, features, or combinations thereof. In the CLI, you achieve the same effect through command-line switches.
C:\>DependencyReporter -package-scope \ -class-scope \ -feature-scope
C:\>DependencyReporter -all
C:\>DependencyReporter -f2f
Secondly, you specify Perl regular expressions in an including and an excluding text fields. A node must match the regular expressions in the including field and not match the ones in the excluding field. You can put multiple regular expressions in each field, separate them with commas and they will be or'ed together. On the command-line, just repeat the switches.
C:\>DependencyReporter -scope-includes // \ -scope-excludes /Test/
The selective graph copier uses Perl regular expressions to select scope nodes and filtered dependencies.
Here are links to pages explaining Perl regular expressions:
Special characters in regular expressions:
[A-Z]
Capitals \w
Alphanumerics [A-Za-z0-9_] \W
Non-alphanumerics [^A-Za-z0-9_] \w+
At least one alphanumeric \s
Space, including tab, carriage return, and line feed \s*
Zero or more white space \.
Matches '.', as opposed to any character
Sample rules:
//
Matches everything. /abc/
Matches anything with " abc
" in it. Case-sensitive./abc/i
Matches anything with " abc
" in it. Case-insensitive./^abc/
Matches anything starting with " abc
" in it./abc\(/
Matches anything with " abc(
" in it. Parentheses are special characters in Perl regular expressions and must be escaped with a backslash "\" character.
Examples:
/java/
Matches anything with " java
" in it./^java/
Matches anything that starts with " java
"./^com.jeantessier/
Matches anything that starts with " com.jeantessier
". Here, the '.' really matches any single character, which obviously includes the '.' character itself./Node.Accept\(/
Matches any Accept()
method of any class ending in "Node
"./Test/
Matches anything with " Test
" in it./(Node)\.\1\(/
Matches any constructor for Node
./(\w*Measurement)\.\1\(/
Matches any constructor for classes ending in Measurement
.
So far, we have seen an interface where the Perl regular expressions apply to all software artifacts, regardless of whether they are packages, classes, or features. Dependency Finder has a more elaborate interface, called the advanced view, which allows you to specify regular expressions that apply only to packages, or only to classes, or only to features.
The checkboxes to select packages, classes, and features are arrayed on the left of the user interface. The text fields next to each one are for regular expressions that will apply only to that type of programming element or software artifact. The top field in each column is from the simple view and applies to all artifacts.
We achieve the same effect in the CLI by using multiple switches. For
instance, -feature-scope-includes
is for the "including" field in the
"Select programming elements" group that applies only to features. It is
used in scoping feature nodes, if you use it along with -feature-scope
.
E.g.,
C:\>DependencyReporter -package-scope \ -class-scope \ -feature-scope
C:\>DependencyReporter -all
C:\>DependencyReporter -f2f
C:\>DependencyReporter -scope-includes //
C:\>DependencyReporter -scope-includes // \ -scope-excludes /Test/ \ -feature-scope-includes /someMethod/ \ -feature-scope-includes /anotherMethod/
Inbound dependencies show you who depends on a given programming element. They are very useful when assessing the impact of a change. Dependency Finder uses a textual notation to display them:
programming element <-- dependent 1 <-- dependent 2 ...
When there are a lot of dependencies, either because the graph is very big or an element is highly connected, it is useful to filter out the other dependencies so that inbound ones stand out and are easier to read.
In DependencyFinder
and the web application, there are three checkboxes
at the bottom of the control area. They control how the graph is rendered.
To see only inbound dependencies, check the one labeled "inbounds" and
uncheck the one labeled "outbounds". If a node has no inbound dependencies,
it will appear empty. You can remove empty nodes by unchecking the last
checkbox, the one labeled "empty nodes".
If you are using the command-line tools, what you have to work from is most
likely a graph rendered in XML, either from DependencyExtractor
or
DependencyReporter
. You can use XML stylesheets to convert the XML to
something more user-friendly. Dependency Finder provides two sample
stylesheets to show only nodes with inbound dependencies. You can apply them
with the following tools:
DependablesToText
DependablesToHTML
E.g.,
C:\>DependablesToText -in test.xml java.lang java.lang.Object <-- test <-- test.main(java.lang.String[]) java.lang.Object.Object() <-- test.main(java.lang.String[]) <-- test.test() java.lang.String <-- test.main(java.lang.String[]) java.util java.util.Collections java.util.Collections.singleton(java.lang.Object) <-- test.main(java.lang.String[]) java.util.Set <-- test.main(java.lang.String[])
The HTML version weaves cross-references between all the elements for easy navigation.
If you only want to remove outbound dependencies and keep empty nodes, use:
HideOutboundDependenciesToText
HideOutboundDependenciesToHTML
E.g.,
C:\>HideOutboundDependenciesToText -in test.xml
test test.main(java.lang.String[]) test.test() java.lang java.lang.Object <-- test <-- test.main(java.lang.String[]) java.lang.Object.Object() <-- test.main(java.lang.String[]) <-- test.test() java.lang.String <-- test.main(java.lang.String[]) java.util java.util.Collections java.util.Collections.singleton(java.lang.Object) <-- test.main(java.lang.String[]) java.util.Set <-- test.main(java.lang.String[])
Outbound dependencies show you who a given programming element depends upon. They are very useful when partitioning programming elements and figuring out what other classes a given class needs in order to run. Dependency Finder uses a textual notation to display them:
programming element --> dependable 1 --> dependable 2 ...
When there are a lot of dependencies, either because the graph is very big or an element is highly connected, it is useful to filter out the other dependencies so that outbound ones stand out and are easier to read.
In DependencyFinder
and the web application, there are three checkboxes
at the bottom of the control area. They control how the graph is rendered.
To see only outbound dependencies, check the one labeled "outbounds" and
uncheck the one labeled "inbounds". If a node has no outbound dependencies,
it will appear empty. You can remove empty nodes by unchecking the last
checkbox, the one labeled "empty nodes".
If you are using the command-line tools, what you have to work from is most
likely a graph rendered in XML, either from DependencyExtractor
or
DependencyReporter
. You can use XML stylesheets to convert the XML to
something more user-friendly. Dependency Finder provides two sample
stylesheets to show only outbound dependencies. You can apply them with the
following tools:
DependentsToText
DependentsToHTML
E.g.,
C:\>DependentsToText -in test.xml
test --> java.lang.Object test.main(java.lang.String[]) --> java.lang.Object --> java.lang.Object.Object() --> java.lang.String --> java.util.Collections.singleton(java.lang.Object) --> java.util.Set test.test() --> java.lang.Object.Object()
The HTML version weaves cross-references between all the elements for easy navigation.
If you only want to remove inbound dependencies and keep empty nodes, use:
HideInboundDependenciesToText
HideInboundDependenciesToHTML
E.g.,
C:\>HideInboundDependenciesToText -in test.xml
test --> java.lang.Object test.main(java.lang.String[]) --> java.lang.Object --> java.lang.Object.Object() --> java.lang.String --> java.util.Collections.singleton(java.lang.Object) --> java.util.Set test.test() --> java.lang.Object.Object() java.lang java.lang.Object java.lang.Object.Object() java.lang.String java.util java.util.Collections java.util.Collections.singleton(java.lang.Object) java.util.Set
You can combine the two approaches we just looked at and display the full graph. For the sake of brevity, when a node A depends on a node B and that node B also depends on node A, Dependency Finder writes it as:
A <-> B B <-> A
If you are using the command-line tools, what you have to work from is most
likely a graph rendered in XML, either from DependencyExtractor
or
DependencyReporter
. You can use XML stylesheets to convert the XML to
something more user-friendly. Dependency Finder provides two sample
stylesheets to show whole dependency graphs. You can apply them with the
following tools:
DependencyGraphToText
DependencyGraphToHTML
The HTML version weaves cross-references between all the elements for easy navigation.
E.g.,
C:\>DependencyGraphToText -in test.xml
test --> java.lang.Object test.main(java.lang.String[]) --> java.lang.Object --> java.lang.Object.Object() --> java.lang.String --> java.util.Collections.singleton(java.lang.Object) --> java.util.Set test.test() --> java.lang.Object.Object() java.lang java.lang.Object <-- test <-- test.main(java.lang.String[]) java.lang.Object.Object() <-- test.main(java.lang.String[]) <-- test.test() java.lang.String <-- test.main(java.lang.String[]) java.util java.util.Collections java.util.Collections.singleton(java.lang.Object) <-- test.main(java.lang.String[]) java.util.Set <-- test.main(java.lang.String[])
This table summarizes which CLI tool shows which information.
Tool outbounds inbounds empty DependencyGraphToHTML
DependencyGraphToText
X X X DependablesToHTML
DependablesToText
- X - DependentsToHTML
DependentsToText
X - - HideOutboundDependenciesToHTML
HideOutboundDependenciesToText
- X X HideInboundDependenciesToHTML
HideInboundDependenciesToText
X - X
Dependencies are not automatically transitive, it depends on the implementation details within the code. Suppose you have:
A --> B --> C
If you make a change in C
, you may or may not have to modify A
depending on how A
and B
are implemented.
A transitive closure is the set of nodes that can be reached by following downstream or upstream dependencies from a starting node. The nodes in this set might be impacted by a change, but they are not automatically. You get the upstream closure by following inbound dependencies. You get the downstream closure by following outbound dependencies. Each string of dependencies forms a path through the graph between two nodes. The number of dependencies that form the path give the path's length. The degree of separation between two nodes is the length of the shortest path between them.
Here is a further example:
A --> B --> C --> D --> E
C
is the starting point for the closure. The set of nodes whose
distance to the source is 0 is [C]
. The set of nodes whose
distance to the source is 1 is [B, D]
. The set of nodes whose
distance to the source is 2 is [A, E]
.
distance closure 0 [C]
1 [B, C, D]
no constraint [A, B, C, D, E]
upstream [A, B, C]
downstream [C, D, E]
Here is the general algorithm:
A transitive closure only keeps the shortest path between two nodes.
This is a simple tally of how many dependencies there are in a graph. It
can give you a rough idea of the complexity of a given codebase, but it is
not as powerful as the metrics computed by OOMetrics
.
Object-oriented software metrics help assess the quality of a piece of software. You can use them to verify that your architecture evolves in the right direction, from version to version. You can also use them to spot deviations from your quality guidelines, such as methods that have too many parameters or are too long.
Dependency Finder can use the information it extracts from class files to compute software quality metrics. Metrics measure certain aspects of software and provide a quantitative assessment of its quality.
Dependency Finder recognizes four levels of metrics:
Each metrics is composed of a number of measurements. What measurements make up a given metrics level is defined in a configuration file written in XML.
A set of metrics is a group of measurements pertaining to a given chunk of software. Dependency Finder recognizes four sizes of chunks:
- Project
- These metrics apply to the project as a whole.
- Group
- These metrics apply to groups of classes. A Java package is a de facto group, but you can specify arbitrary groups using Perl regular expressions.
- Class
- These metrics apply to a single class.
- Method
- These metrics apply to individual methods within a class.
It does not track metrics about class attributes because these are not really all that interesting. The only measurements worth something deal with dependencies, and I rolled those into class metrics.
The configuration file defines what measurements apply to which level of scope (package, group, class, or method). It also defines the rules for creating arbitrary groups. You can customize the content of your metrics report by using different configuration files.
Each measurement has a name and a class that has the semantics of the measurement. There are classes for counters, lists, simple mathematical expressions, etc. Each measurement can be expressed as a numeric value and the configuration file can defines the recommanded range for that measurement.
The current types of measurement are as follow:
AccumulatorMeasurement
- Aggregates lists in submetrics on the measurement's context. It uses Perl regular expressions to filter list elements.
CounterMeasurement
- A simple counter, it tallies the values that are put in it. If you try to add a non-number, it simply adds 1.
NameListMeasurement
- Accumulates a set of values. Its numerical value is the cardinality (i.e., size) of the set.
OOMetrics
uses it to keep track of dependencies.NbSubMetricsMeasurement
- Counts how many submetrics there are in the measurement's context. It can use selection criteria while counting. These are akin to "WHERE C > 10".
RatioMeasurement
- Divides one measurement (base) by another (divider). Both must be in the same context.
StatisticalMeasurement
- Computes the statistical properties of a given measurement across the submetrics of the measurement's context. Given a measurement name, it explores the tree of metrics rooted at the context and finds the numerical value of these named measurements in the tree. For these measurements, it computes:
- minimum value
- median value
- average value
- standard deviation
- maximum value
- sum
- number of data points
SumMeasurement
- Adds up numerical values. Use it with just one term to alias other measurements.
Whenever a measurement references another measurement, there is a chance
that that measurement is in fact a StatisticalMeasurement
standing in
for a collection of others at the levels below. The measurment making
the reference can specify explicitly which subvalue (i.e., minimum, median,
etc.) of the StatisticalMeasurement
it wants to use for its own
computation. Or, it can rely on the StatisticalMeasurement
's self
disposition setting. A StatisticalMeasurement
can specify explicitly
which of its subvalues it wants to use for its numerical value. By
default, this is the average value.
These measurements can potentially refer to other measurements and fall under this disposition feature:
NbSubMetricsMeasurement
RatioMeasurement
StatisticalMeasurement
SumMeasurement
Right now, measurement processing is fixed. In the future, you will be able to define your own types of measurements and plug them into the framework.
OOMetrics
and OOMetricsGUI
create one project-level metrics for the
whole project. This level has measurements that span the entire analyzed
codebase. For each .class
file, they create one class-level metrics for
that class, and one method-level metrics for each of its methods.
The tools add Class-level metrics to relevant group-level metrics. One such group, which the tools create automatically for each class, is named after the package that this class belongs to. You can define additional groups in the configuration file by providing a name for the group and one or more Perl regular expressions. If the class name matches any of the regular expressions, the class will be added to that group. This allows you to group together classes for a component or module that might span multiple Java packages.
E.g.,
<group-definitions> <group-definition> <name>Servlet API</name> <pattern>/^javax.servlet/</pattern> </group-definition> <group-definition> <name>Test</name> <pattern>/test/i</pattern> </group-definition> </group-definitions>
You use OOMetrics
and OOMetricsGUI
to extract .class
files and
compute metrics. The list of metrics is taken from a configuration file
written in XML. You set the file with the -configuration
switch to
either command. There is also a -default-configuration
switch that
kicks if you do not provide a -configuration
switch. This is how the
launching scripts can point to the standard configuration in
%DEPENDENCYFINDER_HOME%/etc/MetricsConfig.xml
even if you don't
specify a configuration on the command-line.
There are two configurations that come bundled with Dependency Finder.
%DEPENDENCYFINDER_HOME%/etc/MetricsConfig.xml
- It has a series of measurements and statistical compilations at various levels. It can assist you in evaluating the size and complexity of your software. You can use it to find large classes or methods, or to find pieces with a disproportionate number of dependencies.
%DEPENDENCYFINDER_HOME%/etc/MartinConfig.xml
- Computes
I
,A
, andD'
, as per Robert C. Martin's article.
You can start from these to define your own configurations where you adjust the output to the metrics you care about.
If you don't need a given measurement, put visible="no"
in its
<measurement>
tag. You could remove the tag altogether, but other
measurements might depend on it. They would compute faulty values it
you were to remove it, whereas marking it invisible will only remove
it from the final output.
You can add new measurements that are functions of ones that already exist.
AccumulatorMeasurement
- For merging and filtering name lists
RatioMeasurement
- For comparing values to one another
StatisticalMeasurement
- Get distribution information for another measurement
SumMeasurement
- Add measurments together or provide aliases to existing ones
Look at %DEPENDENCYFINDER_HOME%/etc/MartinConfig.xml
for an exemple
of how you can combine these types of measurements to compute complex
values.
You can also add your own group definitions to report on related
classes that might not all be in the same Java package. For example,
all classes with "Test" in the name, or all classes under a given
top-level package, such as javax.servlet
.
You can customize the valid range of measurements to the particular
conditions of your project. Each <measurement>
tag can
contain optional <lower-threshold>
and <upper-threshold>
tags. Reports can highlight measurement values that fall outside
of the valid range you specified.
specify valid range is none all values are valid lower threshold only all values greater than or equal to the threshold are valid upper threshold only all values lesser than or equal to the threshold are valid both lower and upper threshold only all values in the range between the lower and upper threshold, including them, are valid
Note that if all these ranges include the threshold themselves. If a value is equal to either the lower or the upper threshold, it is still considered valid. Also, if the upper threshold is less than the lower threshold, then no value will ever be considered valid. At this time, you cannot specify an exlusive range, where a measurement would be valid unless it fell within the designated range.
A future version of Dependency Finder will let you write custom measurement classes so you can compute your own metrics. Right now, the basic set is hard-coded and the configuration allows you mainly to customize the report.
Statistical measurements can print out as:
self_dispose [minimum median/average standard_deviation maximum sum (size_of_data_set)]
The first value is the numeric value of the measurement. Which one of the
values this is depends on the self disposition used in the measurement's
<init>
tag. By default, it is the average of the data set.
Select File | Extract
; this will popup a file dialog.
Select the files and/or directories to extract from and click Extract
.
You can repeat this command as often as needed; each time, the extracted
metrics will be added to the current set of metrics in memory.
You can limit the number of rows on display with the filtering mechanism at
the bottom of the user interface. You enter a Perl regular expression in
the text field and click the button marked Filter:
.
You can sort entries by the value of any measurement by clicking on the
header of each column. Each mouse click alternates between ascending
and descending sorting order. This also works for the name
column.
You use OOMetrics
to compute metrics and produce a report. Simply string
out the files and directories on the command-line. The content of the report
depends on the switches on the command-line.
E.g.,
C:\>OOMetrics -all -txt classes %JAVA_HOME%\JRE\lib\rt.jar
You must use at least one of the following switches that determine which groups of metrics are included in the report. You can combine them if you need to.
-project
- Include project-related metrics.
-groups
- Include group- and package-related metrics.
-classes
- Include class-related metrics.
-methods
- Include method-related metrics.
-all
- All of the above
You must also specify an output mode with one of these switches.
-csv
- Writes each section in its own CSV file, perfect for loading in Microsoft Excel and charting.
-txt
- Writes a single text report with all requested sections.
-xml
- Writes a single XML report with all requested sections.
You can specify an output file with the -out
switch. But in this case,
the switch only provides a prefix for the output files. OOMetrics
will
add the appropriate suffix, depending on the output mode. For text and
XML output, it writes only one file with the total report. For CSV output,
however, it writes one file per selected section; each filename starts
with the prefix you specify, followed by either "_project
",
"_groups
", "_classes
", or "_methods
" according to the section,
and ends with the ".csv
" suffix.
C:\>OOMetrics -all -txt %DEPENDENCYFINDER_HOME%\lib\DependencyFinder.jar -out df
You use the com.jeantessier.dependencyfinder.ant.OOMetrics
task to compute
metrics and produce a report. You need to associate the class to a tag using
Ant's <taskdef>
task. You also need to tell Ant where to find the
Dependency Finder classes. There is a special
help page on how to do this.
<oometrics allmetrics="yes" txt="yes" configuration="${dependencyfinder.home}/etc/MetricsConfig.xml" destprefix="metrics"> <path> <pathelement location="."/> </path> </oometrics>
If you are developing libraries or frameworks, that is, software that will
be used and/or extended by others, you need to know what changed from
version to version. Your clients depend on your published set of APIs for
their own development efforts. It becomes important, as you release new
revisions of your software, to keep track of which parts of the published
API changed, so that you can tell your clients how to upgrade their code.
The JarJarDiff
tool can compute differences automatically, so you can
quickly know what you might have broken in your customer's code.
Even if this is not your case, it is still a good practive to have a look at what changed across your software since the latest baseline. This allows you to keep track of changes to the classes and methods and verify that they are evolving in the right direction, whichever way you decide that is.
Typically, you compile Java source files to .class
files and javadocs.
You may even combine the .class
files into one or more JAR files. From
source code in .java
files, you can use javadoc
to generate HTML
documentation or you can use javac
/jar
to generate executable code in JAR
files.
![]() |
Source files that you write. |
![]() |
JDK tools and files they generate. |
![]() |
Dependency Finder tools and files they generate, based on your code. |
The tools in Dependency Finder work from the executable code, either in
JAR files or in individual .class
files; they do not work from either the
source code or the generated javadoc.
You use JarJarDiff
to extract API differences. The following command
computes the API differences between versions Old and New of
My Project. The differences are saved as an XML document in the file
diff.xml
.
C:\>JarJarDiff -name "My Project" \ -old-label Old \ -old old.jar \ -new-label New \ -new new.jar \ -out diff.xml
You use -old
switches to tell it where the old, or base version is
located. Each -old
switch can point to either a JAR file, a Zip file,
or a directory that will be searched for .class
files. You can
specify a label for the version with the -old-label
switch. If you
do not provide a label, the tool with create one by concatenating the
parameters to -old
switches.
You use -new
switches to tell it where the new, or modified version
is located. Each -new
switch can point to either a JAR file, a Zip
file, or a directory that will be searched for .class
files. You can
specify a label for the version with the -new-label
switch. If you
do not provide a label, the tool with create one by concatenating the
parameters to -new
switches.
By default, the raw results are written in XML to the standard output
stream (i.e., System.out
). You can write it to a file by using the
-out
switch.
You can also do the extraction from your build script if you use Ant.
You use the com.jeantessier.dependencyfinder.ant.JarJarDiff
task to compute
differences and produce a report. You need to associate the class to a tag
using Ant's <taskdef>
task. You also need to tell Ant where to find
the Dependency Finder classes. There is a special
help page on how to do this.
<jarjardiff destfile="xerces.xml" name="My Project" oldlabel="Old" newlabel="New"> <old> <pathelement location="old.jar"/> </old> <new> <pathelement location="new.jar"/> </new> </jarjardiff> <xslt style="${dependencyfinder.home}/etc/PublishedDiffToHTML.xsl" in="xerces.xml" out="xerces.html"/>
Dependency Finder includes XSL stylesheets for representing differences using HTML. The stylesheets include:
You can run them using the script of the same name and the -in
and
-out
switches. The scripts call XSLTProcess
and specify the XSL
stylesheet with the -xsl
switch. You can do the same, if you want,
with either our stylesheets or your own.
You use DiffToHTML
to convert all API differences to HTML. The
following command produces a full report from the output of the previous
JarJarDiff
command.
C:\>DiffToHTML -in diff.xml \ -out fullreport.html
You use PublishedDiffToHTML
to convert only public API differences
to HTML. The following command produces a full report from the output
of the previous JarJarDiff
command.
C:\>PublishedDiffToHTML -in diff.xml \ -out publishedAPIreport.html
ListDocumentedElements
is a Java doclet that can list all packages, classes,
interfaces, fields, constructors, and methods in a given set of Java source
files. You can run this tool on the "old version" source files to get the list
of everything in it (old list), and on the "new version" to get the list of
everything in the "new version" (new list). All you have left to do is compare
the two lists, either yourself or with the ListDiff
tool. This will show you
the names of all the additions and removals between the two versions.
You can also limit the reporting to specific programming elements that have
been earmarked using special javadoc tags. You get the list of earmarked
elements with ListDocumentedElements
. The following commands run this doclet
on the old as well as the new codebases, saving the results in individual text
files.
In this first case, the command will only list elements that you marked as
@level published
.
C:\>ListDocumentedElements -tag level \ -valid published \ -out old-list.txt \ old.java
C:\>ListDocumentedElements -tag level \ -valid published \ -out new-list.txt \ new.java
If you add Dependency Finder's JAR file to your CLASSPATH
, you can call
javadoc
directly instead:
C:\javadoc -doclet com.jeantessier.diff.ListDocumentedElements \ -tag level \ -valid published \ -out old-list.txt \ old.java
C:\javadoc -doclet com.jeantessier.diff.ListDocumentedElements \ -tag level \ -valid published \ -out new-list.txt \ new.java
Or, as an Ant task:
<target name="listdocumentedelements" depends="init"> <javadoc packagenames="${packageNames}"> <sourcepath path="src"/> <classpath refid="libs"/> <doclet name="com.jeantessier.diff.ListDocumentedElements" path="${classesDir}"> <param name="-tag" value="level"/> <param name="-invalid" value="developer"/> <param name="-out" value="df.txt"/> </doclet> </javadoc> </target>
In this second case, the command will only list elements that you did marked
not mark as @level developer
.
C:\>ListDocumentedElements -tag level \ -invalid published \ -out old-list.txt \ old.java
C:\>ListDocumentedElements -tag level \ -invalid developer \ -out new-list.txt \ new.java
You then feed the output from the doclet to JarJarDiff
. In the diagram
below, we show both .class
and JAR files being fed to JarJarDiff
, but you
only need to do either, not both. Dependency Finder tools can read JAR files
just as well as .class
files.
C:\>JarJarDiff -name "My Project" \ -old-label Old \ -old old.jar \ -old-documentation old-list.txt \ -new-label New \ -new new.jar \ -new-documentation new-list.txt \ -out diff.xml
The full list will include special sections that highlight changes in what is no more part of the documentation and what was newly added to the documentation.
C:\>DiffToHTML -in diff.xml \ -out fullreport.html
The list can also be fed to PublishedDiffToHTML
to further
limit the changes to the published public API.
C:\>PublishedDiffToHTML -in diff.xml \ -param validation-list new-list.txt \ -out publishedAPIreport.html
You use ListDiff
to list changes in what is considered part of the
published API. It only needs the lists generated by the doclet. No
source or bytecode required.
C:\>ListDiff -name "My Project" \ -old-label Old \ -old old-list.txt \ -new-label New \ -new new-list.txt \ -compress \ -out listdiff.xml
ListDiff
writes in XML and you can convert this XML
into HTML with ListDiffToHTML
, or to text with
ListDiffToText
.
C:\>ListDiffToHTML -in listdiff.xml \ -out docreport.html
ListDocumentationDiffToText
and ListDocumentationDiffToHTML
to extract similar
information from the output of JarJarDiff. The list
is written in HTML. But we recommend that you use
ListDiff
instead; it is faster and simpler.Look at the current bug list on SourceForge. There is also a help-oriented discussion forum.
You can also email me at jeantessier at users.sourceforge.net.