Monday, July 22, 2013

Groovy Sql and Oracle 'Invalid column index' SQLException

There are some minor potential dangers associated with Groovy's def keyword. The Groovy style and language feature guidelines for Java developers provides some warnings about use of def. In this blog post, I demonstrate an advantage of being more explicit in typing when using Groovy SQL with an Oracle database to avoid a potential "Invalid column index" SQLException because I've run into this issue a few times.

The following Groovy script provides comments on Oracle database tables matching a provided search string. In this case, what the script does is not as important as look at the code that defines the SQL query string (lines 18-21).

searchDbComments.groovy (using def without String typing or as String)
#!/usr/bin/env groovy
// searchDbComments.groovy

this.class.classLoader.rootLoader.addURL(
   new URL("file:///C:/oraclexe/app/oracle/product/11.2.0/server/jdbc/lib/ojdbc6.jar"))

if (args.length < 1)
{
   println "USAGE: searchDbComments.groovy <searchString>"
   System.exit(-1)
}

def searchString = args[0].toUpperCase()

import groovy.sql.Sql
def sql = Sql.newInstance("jdbc:oracle:thin:@localhost:1521:xe", "hr", "hr",
                          "oracle.jdbc.pool.OracleDataSource")
def dbTableCommentsQry = """
SELECT table_name, table_type, comments
  FROM user_tab_comments
 WHERE UPPER(comments) LIKE '%${searchString}%'"""

sql.eachRow(dbTableCommentsQry)
{
   println "${it.table_name} (${it.table_type}): ${it.comments}"
}

When the above code is executed, the following error is generated:

WARNING: Failed to execute: 
SELECT table_name, table_type, comments
  FROM user_tab_comments
 WHERE UPPER(comments) LIKE '%?%' because: Invalid column index

Caught: java.sql.SQLException: Invalid column index

java.sql.SQLException: Invalid column index
    at oracle.jdbc.driver.OraclePreparedStatement.setStringInternal(OraclePreparedStatement.java:5303)
    at oracle.jdbc.driver.OraclePreparedStatement.setObjectCritical(OraclePreparedStatement.java:8323)
    at oracle.jdbc.driver.OraclePreparedStatement.setObjectInternal(OraclePreparedStatement.java:8259)
    at oracle.jdbc.driver.OraclePreparedStatement.setObjectInternal(OraclePreparedStatement.java:9012)
    at oracle.jdbc.driver.OraclePreparedStatement.setObject(OraclePreparedStatement.java:8993)
    at oracle.jdbc.driver.OraclePreparedStatementWrapper.setObject(OraclePreparedStatementWrapper.java:230)
    at searchDbComments.run(searchDbComments.groovy:23)

Addressing the "invalid column index" SQLException is easy. One solution is to change the "def" on lines 18-21 to an explicit "String" type. Another solution, shown in the next code listing, is to use Groovy's "as" coercion keyword to explicitly allow the "def" to be used and have the dbTableCommentsQry variable be typed as a String.

searchDbComments.groovy (using as String)
#!/usr/bin/env groovy
// searchDbComments.groovy

this.class.classLoader.rootLoader.addURL(
   new URL("file:///C:/oraclexe/app/oracle/product/11.2.0/server/jdbc/lib/ojdbc6.jar"))

if (args.length < 1)
{
   println "USAGE: searchDbComments.groovy <searchString>"
   System.exit(-1)
}

def searchString = args[0].toUpperCase()

import groovy.sql.Sql
def sql = Sql.newInstance("jdbc:oracle:thin:@localhost:1521:xe", "hr", "hr",
                          "oracle.jdbc.pool.OracleDataSource")
def dbTableCommentsQry = """
SELECT table_name, table_type, comments
  FROM user_tab_comments
 WHERE UPPER(comments) LIKE '%${searchString}%'""" as String

sql.eachRow(dbTableCommentsQry)
{
   println "${it.table_name} (${it.table_type}): ${it.comments}"
}

Using "def" only or no "def" with no type at all leads to the above error. Explicitly defining the String variable used in the query either via static typing or via use of "as" keyword allows the code to execute properly. One could use a static typing with "def", but that is thought to be redundant.

There is nothing necessarily wrong about using "def," but one does need to be careful with its application. Guillaume Laforge has written that "def is fine in method bodies or for particular dynamic aspects, but for everything that is a 'contract' (method signatures, properties, etc), it's better to use explicit types."

Saturday, July 20, 2013

Escaping XML with Groovy 2.1

When posting source code to my blog, I often need to convert less than signs (<), and greater than signs (>) to their respective entity references so that they are not confused as HTML tags when the browser renders the output. I have often done this using quick search-and-replace syntax like %s/</\&lt;/g and %s/>/\&gt;/g with vim or Perl. However, Groovy 2.1 introduced a method to do this and in this post I demonstrate a Groovy script that makes use of that groovy.xml.XmlUtil.escapeXml(String) method.

escapeXml.groovy
#!/usr/bin/env groovy
/*
 * escapeXml.groovy
 *
 * Requires Groovy 2.1 or later.
 */
if (args.length < 1)
{
   println "USAGE: groovy escapeXml.groovy <xmlFileToBeProcessed>"
   System.exit(-1)
}
def inputFileName = args[0]
println "Processing ${inputFileName}..."
def inputFile = new File(inputFileName)
String outputFileName = inputFileName + ".escaped"
def outputFile = new File(outputFileName)
if (outputFile.createNewFile())
{
   outputFile.text = groovy.xml.XmlUtil.escapeXml(inputFile.text)
}
else
{
   println "Unable to create file ${outputFileName}"
}

The XmlUtil.escapeXml method is intended to, as its GroovyDoc states, "escape the following characters " ' & < > with their XML entities." Running source code through it helps to convert symbols to XML entity references that will be rendered properly by the browser. This is particularly helpful with Java code that uses generics, for example.

The Groovydoc states that the following transformations from symbols to corresponding entity references are supported:

SymbolEntity
Reference
"&quot;
'&apos;
&&amp;
<&lt;
>&gt;

One of the advantages of this approach is that I can escape all five of these special symbols in an entire String or file with a single command rather than one symbol at a time.

The Groovydoc for this XmlUtil.escapeXml method also states things that this method does not do:

  • "Does not escape control characters" [use XmlUtil.escapeControlCharacters(String) for this]
  • "Does not support DTDs or external entities"
  • "Does not treat surrogate pairs specially"
  • "Does not perform Unicode validation on its input"

My example above showed a Groovy script file that makes use of XmlUtil.escapeXml(String), but it can also be run inline on the command-line. This is done in DOS, for example, as shown here:

type escapeXml.groovy | groovy -e "println groovy.xml.XmlUtil.escapeXml(System.in.text)"

That command just shown will take the provided file (escapeXml.groovy itself in this case) and render output with the specific symbols replaced with entity references. It could be handled the same way in Linux/Unix with "cat" rather than "type." This is shown in the next screen snapshot.

This blog post has shown how XmlUtil.escapeXml(String) can be used within a script or on the command-line to escape certain commonly problematic XML characters to their entity references. Although not shown here, one could embed such code within a Java application as well.

Tuesday, July 16, 2013

Enhanced Groovy-based JAR/Manifest Diff Tool

This brief blog post provides another Groovy script that provides simple differencing of two JAR (or WAR or EAR) files and their MANIFEST.MF files. It represents a combination of the JAR comparison script I blogged on earlier, Rick's addition of Groovy's CliBuilder to allow some output data to be turned off, my MANIFEST.MF comparison script, and an ability to use a command-line flag to enable the script to output the additional Manifest comparison data.

jarDiff2.groovy
#!/usr/bin/env groovy

/**
 * jarDiff2.groovy
 *
 * jarDiff2.groovy -htsc <first_jar_file> <second_jar_file>
 *
 * Script that compares two JAR files, reporting basic characteristics of each
 * along with differences between the two JARs. This script is based on the
 * original jarrDiff.groovy script with enhancements provided by Rick and with
 * enhancements for seeing more detailed differences between two Manifest files.
 *
 * Note this script can be used on any files using the Java ARchive format,
 * including WAR and EAR files.
 */

if (args.length < 2)
{
   println "\nUSAGE: jarDiff2.groovy -htsc <first_jar_file> <second_jar_file>\n"
   System.exit(-1)
}

TOTAL_WIDTH = 180
COLUMN_WIDTH = TOTAL_WIDTH / 2 - 3
ROW_SEPARATOR = "-".multiply(TOTAL_WIDTH)

import java.util.jar.Attributes
import java.util.jar.JarFile

// Set up the CLI options
//
def cli = new CliBuilder( usage: 'jarDiff.groovy -h -tsc ')
cli.with
{
   h longOpt: 'help', 'usage information'
   t longOpt: 'ignoreTime', args: 0, required: false, type: Boolean, 'Ignore time differences'
   s longOpt: 'ignoreSize', args: 0, required: false, type: Boolean, 'Ignore size differences'
   c longOpt: 'ignoreCrc', args: 0, required: false, type: Boolean, 'Ignore CRC differences'
   m longOpt: 'displayManifestDetails', args: 0, required: false, type: Boolean, 'Display Manifest differences details'
}

def opt = cli.parse(args)
if (!opt) return
if (opt.h)
{
   cli.usage()
   System.exit(-1)
}

def ignoreTime = opt.t
def ignoreSize = opt.s
def ignoreCrc = opt.c
def displayManifestDiffDetails = opt.m

if (opt.arguments().size < 2)
{
   println "Two JAR files required\n"
   cli.usage()
   System.exit(-1)
}

def file1Name = opt.arguments()[0]
def jar1File = new JarFile(file1Name)
def jar1 = extractJarContents(jar1File)
def file2Name = opt.arguments()[1]
def jar2File = new JarFile(file2Name)
def jar2 = extractJarContents(jar2File)

def entriesInJar1ButNotInJar2 = jar1.keySet() - jar2.keySet()
def entriesInJar2ButNotInJar1 = jar2.keySet() - jar1.keySet()

println ROW_SEPARATOR
println "| ${file1Name.center(COLUMN_WIDTH)} |${file2Name.center(COLUMN_WIDTH)} |"
print "| ${(Integer.toString(jar1File.size()) + " bytes").center(COLUMN_WIDTH)} |"
println "${(Integer.toString(jar2File.size()) + " bytes").center(COLUMN_WIDTH)} |"
println ROW_SEPARATOR

if (jar1File.manifest != jar2File.manifest)
{
   if (displayManifestDiffDetails)
   {
      displayDetailedManifestFilesDifferences(jar1File.manifest.mainAttributes, jar2File.manifest.mainAttributes)
   }
   else
   {
      def manifestPreStr = "# Manifest Entries: "
      def manifest1Str = manifestPreStr + Integer.toString(jar1File.manifest.mainAttributes.size())
      print "| ${manifest1Str.center(COLUMN_WIDTH)} |"
      def manifest2Str = manifestPreStr + Integer.toString(jar2File.manifest.mainAttributes.size())
      println "${manifest2Str.center(COLUMN_WIDTH)} |"
      println ROW_SEPARATOR
   }
}

entriesInJar1ButNotInJar2.each
{ entry1 ->
   print "| ${entry1.center(COLUMN_WIDTH)} |"
   println "${" ".center(entry1.size() > COLUMN_WIDTH ? 2 * COLUMN_WIDTH - entry1.size() : COLUMN_WIDTH)} |"
   println ROW_SEPARATOR
}
entriesInJar2ButNotInJar1.each
{ entry2 ->
   print "| ${" ".center(entry2.size() > COLUMN_WIDTH ? 2 * COLUMN_WIDTH - entry2.size() : COLUMN_WIDTH)}"
   println "| ${entry2.center(COLUMN_WIDTH)} |"
   println ROW_SEPARATOR
}

jar1.each 
{ key, value ->
   if (!entriesInJar1ButNotInJar2.contains(key))
   {
      def jar2Entry = jar2.get(key)
      if (value != jar2Entry)
      {
         println "| ${key.center(COLUMN_WIDTH)} |${jar2Entry.name.center(COLUMN_WIDTH)} |"
         if (value.crc != jar2Entry.crc)
         {
            def crc1Str = "CRC: ${value.crc}"
            def crc2Str = "CRC: ${jar2Entry.crc}"
            print "| ${crc1Str.center(COLUMN_WIDTH)} |"
            println "${crc2Str.center(COLUMN_WIDTH)} |"
         }
         if (value.size != jar2Entry.size)
         {
            def size1Str = "${value.size} bytes"
            def size2Str = "${jar2Entry.size} bytes"
            print "| ${size1Str.center(COLUMN_WIDTH)} |"
            println "${size2Str.center(COLUMN_WIDTH)} |"
         }
         if (value.time != jar2Entry.time)
         {
            boolean crcDiff = (!ignoreCrc && value.crc != jar2Entry.crc)
            boolean sizeDiff = (!ignoreSize && value.size != jar2Entry.size)
            boolean timeDiff = (!ignoreTime && value.time != jar2Entry.time)

            if(crcDiff || sizeDiff || timeDiff)
            {
               println "| ${key.center(COLUMN_WIDTH)} |${jar2Entry.name.center(COLUMN_WIDTH)} |"
               if (crcDiff)
               {
                  def crc1Str = "CRC: ${value.crc}"
                  def crc2Str = "CRC: ${jar2Entry.crc}"
                  print "| ${crc1Str.center(COLUMN_WIDTH)} |"
                  println "${crc2Str.center(COLUMN_WIDTH)} |"
               }
               if (sizeDiff)
               {
                  def size1Str = "${value.size} bytes"
                  def size2Str = "${jar2Entry.size} bytes"
                  print "| ${size1Str.center(COLUMN_WIDTH)} |"
                  println "${size2Str.center(COLUMN_WIDTH)} |"
               }
               if (timeDiff)
               {
                  def time1Str = "${new Date(value.time)}"
                  def time2Str = "${new Date(jar2Entry.time)}"
                  print "| ${time1Str.center(COLUMN_WIDTH)} |"
                  println "${time2Str.center(COLUMN_WIDTH)} |"
               }
               println ROW_SEPARATOR
            }
         }
      }
   }
}



/**
 * Provide mapping of JAR entry names to characteristics about that JAR entry
 * for the JAR indicated by the provided JAR file name.
 *
 * @param jarFile JAR file from which to extract contents.
 * @return JAR entries and thir characteristics.
 */
def TreeMap<String, JarCharacteristics> extractJarContents(JarFile jarFile)
{
   def jarContents = new TreeMap<String, JarCharacteristics>()
   entries = jarFile.entries()
   entries.each
   { entry->
      jarContents.put(entry.name, new JarCharacteristics(entry.name, entry.crc, entry.size, entry.time));
   }
   return jarContents
}


/**
 * Add more detailed Manifest differences to output report.
 *
 * @param manifest1Attrs Main attributes of first JAR file's Manifest
 * @param manifest2Attrs Main attributes of second JAR file's Manifest.
 */
def displayDetailedManifestFilesDifferences(
   Attributes manifest1Attrs, Attributes manifest2Attrs)
{
   def attrsIn1ButNot2 = manifest1Attrs.keySet() - manifest2Attrs.keySet()
   def attrsIn2ButNot1 = manifest2Attrs.keySet() - manifest1Attrs.keySet()
   attrsIn1ButNot2.each
   {
      def attr1onlyStr = "${it}=${manifest1Attrs.get(it)}" 
      print "| ${attr1onlyStr.center(COLUMN_WIDTH)} |"
      println "${" ".center(attr1onlyStr.size() > COLUMN_WIDTH ? 2 * COLUMN_WIDTH - attr1onlyStr.size() : COLUMN_WIDTH)} |"
   }
   println ROW_SEPARATOR
   attrsIn2ButNot1.each
   {
      def attr2onlyStr = "${it}=${manifest2Attrs.get(it)}"
      print "| ${" ".center(attr2onlyStr.size() > COLUMN_WIDTH ? 2 * COLUMN_WIDTH - attr2onlyStr.size() : COLUMN_WIDTH)}|"
      println " ${attr2onlyStr.center(COLUMN_WIDTH)} |"
   }
   println ROW_SEPARATOR
   manifest1Attrs.each
   {
      def key = it.key
      if (it.value != manifest2Attrs.get(key) && !attrsIn1ButNot2.contains(it.key))
      {
         def attr1Str = "${key}=${manifest1Attrs.get(key)}"
         print "| ${attr1Str.center(COLUMN_WIDTH)}"
         def attr2Str = "${key}=${manifest2Attrs.get(key)}"
         println "| ${attr2Str.center(COLUMN_WIDTH)} |"
      }
   }
   println ROW_SEPARATOR
}

Like the first version of the script, this script relies on the very simple JarCharacteristics.groovy class. With Groovy 1.8 or later, this class is simple (earlier versions of Groovy need some additional code implemented because they don't have @Canonical):

JarCharacteristics.groovy
@groovy.transform.Canonical
class JarCharacteristics
{
   String name
   long crc
   long size
   long time
}

The version of the JAR differencing script shown in this post uses command-line arguments to specify when not to display differences in JAR entries due to CRC, size, or modification date. An additional flag also more detailed manifest files differences to be displayed when the flag is specified. The default is to show regular JAR entries differences based on CRC, size, and modification date, but not show the detailed differences in Manifest files. The flags can be used to disable differencing on CRC, size, or modification date or to enable more verbose Manifest files differences output.

Wednesday, July 10, 2013

Comparing Manifest Files with Groovy

My last post discussed comparing the high-level contents of two JAR files using a Groovy script and a 2011 post covered displaying a particular JAR file's manifest file. This post illustrates the combination of those two ideas with an example of comparing the Manifest files of two different JAR files.

As illustrated in the just-referenced two blog posts, it is easy to manipulate the contents of a JAR file with Groovy. While this manipulation is all available in the standard JDK and is thus available to Java applications, Groovy is better suited for scripting needs like this. Groovy also provides some convenient syntax shortcuts. I like the fact that the Groovy script feels like a script rather than like an object-oriented production piece of software.

Here is the code for comparing the Manifest.MF files of two provided JAR files:

manifestDiff.groovy
#!/usr/bin/env groovy

/**
 * manifestDiff.groovy
 *
 * manifestDiff.groovy <first_jar_file> <second_jar_file>
 *
 * Script that compares the MANIFEST.MF files of two JAR files.
 */

if (args.length < 2)
{
   println "\nUSAGE: manifestDiff.groovy <first_jar_file> <second_jar_file>\n"
   System.exit(-1)
}

TOTAL_WIDTH = 180
COLUMN_WIDTH = TOTAL_WIDTH / 2 - 3
ROW_SEPARATOR = "-".multiply(TOTAL_WIDTH)

import java.util.jar.JarFile

def file1Name = args[0]
def jar1File = new JarFile(file1Name)
def num1Attrs = jar1File.manifest.mainAttributes.size()
def file2Name = args[1]
def jar2File = new JarFile(file2Name)
def num2Attrs = jar2File.manifest.mainAttributes.size()

println ROW_SEPARATOR
println "| ${file1Name.center(COLUMN_WIDTH)}| ${file2Name.center(COLUMN_WIDTH)} |"
print "| ${(Integer.toString(num1Attrs) + (num1Attrs != 1 ? " attributes" : " attribute")).center(COLUMN_WIDTH)}|"
println " ${(Integer.toString(num2Attrs) + (num2Attrs != 1 ? " atttributes" : " attribute")).center(COLUMN_WIDTH)} |"
println ROW_SEPARATOR

if (jar1File.manifest != jar2File.manifest)
{
   def manifest1Attrs = jar1File.manifest.mainAttributes
   def manifest2Attrs = jar2File.manifest.mainAttributes
   def attrsIn1ButNot2 = manifest1Attrs.keySet() - manifest2Attrs.keySet()
   def attrsIn2ButNot1 = manifest2Attrs.keySet() - manifest1Attrs.keySet()
   attrsIn1ButNot2.each
   {
      def attr1onlyStr = "${it}=${manifest1Attrs.get(it)}" 
      print "| ${attr1onlyStr.center(COLUMN_WIDTH)}| "
      println "${" ".center(attr1onlyStr.size() > COLUMN_WIDTH ? 2 * COLUMN_WIDTH - attr1onlyStr.size() : COLUMN_WIDTH)} |"
   }
   println ROW_SEPARATOR
   attrsIn2ButNot1.each
   {
      def attr2onlyStr = "${it}=${manifest2Attrs.get(it)}"
      print "| ${" ".center(attr2onlyStr.size() > COLUMN_WIDTH ? 2 * COLUMN_WIDTH - attr2onlyStr.size() : COLUMN_WIDTH)}|"
      println " ${attr2onlyStr.center(COLUMN_WIDTH)} |"
   }
   println ROW_SEPARATOR
   manifest1Attrs.each
   {
      def key = it.key
      if (it.value != manifest2Attrs.get(key) && !attrsIn1ButNot2.contains(it.key))
      {
         def attr1Str = "${key}=${manifest1Attrs.get(key)}"
         print "| ${attr1Str.center(COLUMN_WIDTH)}"
         def attr2Str = "${key}=${manifest2Attrs.get(key)}"
         println "| ${attr2Str.center(COLUMN_WIDTH)} |"
      }
   }
   println ROW_SEPARATOR
}
else
{
   println "Manifests deemed identical."
}

The script shown here makes it easy to quickly see the differences between contents of Manifest files of two JARs. Common characteristics of containing JAR's name and number of attributes in each Manifest file are shown in the script. Other manifest attributes are only displayed in the output if the attribute is unique to one Manifest file or if an attribute with the same name in each Manifest file has a different value for that attribute.

The above script could be combined with the jarDiff.groovy script I blogged on previously to see how two Manifest files differ when that script identifies that there are differences.

This is another example of why Groovy is such a useful scripting language in Java development environments.

Wednesday, July 3, 2013

Comparing JARs with Groovy

It can sometimes be useful to compare the contents of two JARs. In this blog post, I demonstrate a Groovy script that acts like a simple "diff" tool for comparing two JAR files.

The Groovy script shown here, jarDiff.groovy, can undoubtedly be improved upon, but does perform what I wanted it to. The script compares two provided JARs in the following ways:

  • Shows path, name, and size of both JARs regardless of whether they are identical or different.
  • Shows entries in each JAR that do not exist in the other JAR
  • Shows entries that are in common (by name) in each JAR but have different attributes (CRC, size, or modification date)

The above characteristics of the script's output mean that, for identical JARs, only the path/file name of each JAR and the size of each JAR are displayed. For different JARs, those same attributes will be displayed along with entries that exist in only one JAR and not the other and entries common between the two JARs with differing CRC, size, or modification date. An important distinction to make regarding this script is that it mostly is useful for comparing metadata in two JARs and does not provide differencing at the level of methods/APIs (as would be provided by a tool such as javap) or at the source code level (would require a decompiler). This script identifies that differences exist and these other tools can then be used to investigate the deeper details of the differences.

#!/usr/bin/env groovy

/**
 * jarDiff.groovy
 *
 * jarDiff.groovy <first_jar_file> <second_jar_file>
 *
 * Script that compares two JAR files, reporting basic characteristics of each
 * along with differences between the two JARs.
 */

if (args.length < 2)
{
   println "\nUSAGE: jarDiff.groovy <first_jar_file> <second_jar_file>\n"
   System.exit(-1)
}

TOTAL_WIDTH = 180
COLUMN_WIDTH = TOTAL_WIDTH / 2 - 3
ROW_SEPARATOR = "-".multiply(TOTAL_WIDTH)

import java.util.jar.JarFile

def file1Name = args[0]
def jar1File = new JarFile(file1Name)
def jar1 = extractJarContents(jar1File)
def file2Name = args[1]
def jar2File = new JarFile(file2Name)
def jar2 = extractJarContents(jar2File)

def entriesInJar1ButNotInJar2 = jar1.keySet() - jar2.keySet()
def entriesInJar2ButNotInJar1 = jar2.keySet() - jar1.keySet()

println ROW_SEPARATOR
println "| ${file1Name.center(COLUMN_WIDTH)} |${file2Name.center(COLUMN_WIDTH)} |"
print "| ${(Integer.toString(jar1File.size()) + " bytes").center(COLUMN_WIDTH)} |"
println "${(Integer.toString(jar2File.size()) + " bytes").center(COLUMN_WIDTH)} |"
println ROW_SEPARATOR

if (jar1File.manifest != jar2File.manifest)
{
   def manifestPreStr = "# Manifest Entries: "
   def manifest1Str = manifestPreStr + Integer.toString(jar1File.manifest.mainAttributes.size())
   print "| ${manifest1Str.center(COLUMN_WIDTH)} |"
   def manifest2Str = manifestPreStr + Integer.toString(jar2File.manifest.mainAttributes.size())
   println "${manifest2Str.center(COLUMN_WIDTH)} |"
   println ROW_SEPARATOR
}

entriesInJar1ButNotInJar2.each
{ entry1 ->
   print "| ${entry1.center(COLUMN_WIDTH)} |"
   println "${" ".center(entry1.size() > COLUMN_WIDTH ? 2 * COLUMN_WIDTH - entry1.size() : COLUMN_WIDTH)} |"
   println ROW_SEPARATOR
}
entriesInJar2ButNotInJar1.each
{ entry2 ->
   print "| ${" ".center(entry2.size() > COLUMN_WIDTH ? 2 * COLUMN_WIDTH - entry2.size() : COLUMN_WIDTH)}"
   println "| ${entry2.center(COLUMN_WIDTH)} |"
   println ROW_SEPARATOR
}

jar1.each 
{ key, value ->
   if (!entriesInJar1ButNotInJar2.contains(key))
   {
      def jar2Entry = jar2.get(key)
      if (value != jar2Entry)
      {
         println "| ${key.center(COLUMN_WIDTH)} |${jar2Entry.name.center(COLUMN_WIDTH)} |"
         if (value.crc != jar2Entry.crc)
         {
            def crc1Str = "CRC: ${value.crc}"
            def crc2Str = "CRC: ${jar2Entry.crc}"
            print "| ${crc1Str.center(COLUMN_WIDTH)} |"
            println "${crc2Str.center(COLUMN_WIDTH)} |"
         }
         if (value.size != jar2Entry.size)
         {
            def size1Str = "${value.size} bytes"
            def size2Str = "${jar2Entry.size} bytes"
            print "| ${size1Str.center(COLUMN_WIDTH)} |"
            println "${size2Str.center(COLUMN_WIDTH)} |"
         }
         if (value.time != jar2Entry.time)
         {
            def time1Str = "${new Date(value.time)}"
            def time2Str = "${new Date(jar2Entry.time)}"
            print "| ${time1Str.center(COLUMN_WIDTH)} |"
            println "${time2Str.center(COLUMN_WIDTH)} |"
         }
         println ROW_SEPARATOR
      }
   }
}

/**
 * Provide mapping of JAR entry names to characteristics about that JAR entry
 * for the JAR indicated by the provided JAR file name.
 *
 * @param jarFile JAR file from which to extract contents.
 * @return JAR entries and thir characteristics.
 */
def TreeMap<String, JarCharacteristics> extractJarContents(JarFile jarFile)
{
   def jarContents = new TreeMap<String, JarCharacteristics>()
   entries = jarFile.entries()
   entries.each
   { entry->
      jarContents.put(entry.name, new JarCharacteristics(entry.name, entry.crc, entry.size, entry.time));
   }
   return jarContents
}

UPDATE: The above script references a class called JarCharacteristics. This class is a simple data holder made really easy in Groovy thanks to Groovy's implicit property support and the utility of the @Canonical annotation.

JarCharacteristics.groovy
@groovy.transform.Canonical
class JarCharacteristics
{
   String name
   long crc
   long size
   long time
}

I did not need to write get/set methods as Groovy provides them out-of-the-box and the use of @Canonical means that I get equals(Object) and hashCode() overridden implementations "for free" along with implicit constructor support (as well as toString() which the script does not make use of).

Like all Groovy scripts, the above could be written in Java, but Groovy is better suited to script writing than Java. The above Groovy script makes use of Groovy features that I have covered in previous blog posts such as Scripted Reports with Groovy (for formatting output of differences) and Searching JAR Files with Groovy (for perusing and reading JAR files).

There are several potential enhancements for this script. These include having the script show differences in MANIFEST.MF files beyond the differences detected in all files in the JARs by comparing the contents of one manifest file to another. Other enhancements might use comparison of the methods defined on the classes/interfaces/enums contained in the JARs via use of reflection. For now, however, I am content to use javap or javac -Xprint to see the method changes once the above script identifies differences in a particular class, enum, or interface.

Being able to quickly identify differences between two JARs can be beneficial in a variety of circumstances such as comparing versions of one's own generated JARs for changes or for comparing JARs of provided libraries and frameworks that are not named in such a way to make their differences obvious. The Groovy script demonstrated in this post identifies high-level differences between two JARs and at the same time shows off some nice Groovy features.