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.

No comments: