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
  1. #!/usr/bin/env groovy  
  2.   
  3. /** 
  4.  * jarDiff2.groovy 
  5.  * 
  6.  * jarDiff2.groovy -htsc <first_jar_file> <second_jar_file> 
  7.  * 
  8.  * Script that compares two JAR files, reporting basic characteristics of each 
  9.  * along with differences between the two JARs. This script is based on the 
  10.  * original jarrDiff.groovy script with enhancements provided by Rick and with 
  11.  * enhancements for seeing more detailed differences between two Manifest files. 
  12.  * 
  13.  * Note this script can be used on any files using the Java ARchive format, 
  14.  * including WAR and EAR files. 
  15.  */  
  16.   
  17. if (args.length < 2)  
  18. {  
  19.    println "\nUSAGE: jarDiff2.groovy -htsc <first_jar_file> <second_jar_file>\n"  
  20.    System.exit(-1)  
  21. }  
  22.   
  23. TOTAL_WIDTH = 180  
  24. COLUMN_WIDTH = TOTAL_WIDTH / 2 - 3  
  25. ROW_SEPARATOR = "-".multiply(TOTAL_WIDTH)  
  26.   
  27. import java.util.jar.Attributes  
  28. import java.util.jar.JarFile  
  29.   
  30. // Set up the CLI options  
  31. //  
  32. def cli = new CliBuilder( usage: 'jarDiff.groovy -h -tsc ')  
  33. cli.with  
  34. {  
  35.    h longOpt: 'help''usage information'  
  36.    t longOpt: 'ignoreTime', args: 0, required: false, type: Boolean, 'Ignore time differences'  
  37.    s longOpt: 'ignoreSize', args: 0, required: false, type: Boolean, 'Ignore size differences'  
  38.    c longOpt: 'ignoreCrc', args: 0, required: false, type: Boolean, 'Ignore CRC differences'  
  39.    m longOpt: 'displayManifestDetails', args: 0, required: false, type: Boolean, 'Display Manifest differences details'  
  40. }  
  41.   
  42. def opt = cli.parse(args)  
  43. if (!opt) return  
  44. if (opt.h)  
  45. {  
  46.    cli.usage()  
  47.    System.exit(-1)  
  48. }  
  49.   
  50. def ignoreTime = opt.t  
  51. def ignoreSize = opt.s  
  52. def ignoreCrc = opt.c  
  53. def displayManifestDiffDetails = opt.m  
  54.   
  55. if (opt.arguments().size < 2)  
  56. {  
  57.    println "Two JAR files required\n"  
  58.    cli.usage()  
  59.    System.exit(-1)  
  60. }  
  61.   
  62. def file1Name = opt.arguments()[0]  
  63. def jar1File = new JarFile(file1Name)  
  64. def jar1 = extractJarContents(jar1File)  
  65. def file2Name = opt.arguments()[1]  
  66. def jar2File = new JarFile(file2Name)  
  67. def jar2 = extractJarContents(jar2File)  
  68.   
  69. def entriesInJar1ButNotInJar2 = jar1.keySet() - jar2.keySet()  
  70. def entriesInJar2ButNotInJar1 = jar2.keySet() - jar1.keySet()  
  71.   
  72. println ROW_SEPARATOR  
  73. println "| ${file1Name.center(COLUMN_WIDTH)} |${file2Name.center(COLUMN_WIDTH)} |"  
  74. print "| ${(Integer.toString(jar1File.size()) + " bytes").center(COLUMN_WIDTH)} |"  
  75. println "${(Integer.toString(jar2File.size()) + " bytes").center(COLUMN_WIDTH)} |"  
  76. println ROW_SEPARATOR  
  77.   
  78. if (jar1File.manifest != jar2File.manifest)  
  79. {  
  80.    if (displayManifestDiffDetails)  
  81.    {  
  82.       displayDetailedManifestFilesDifferences(jar1File.manifest.mainAttributes, jar2File.manifest.mainAttributes)  
  83.    }  
  84.    else  
  85.    {  
  86.       def manifestPreStr = "# Manifest Entries: "  
  87.       def manifest1Str = manifestPreStr + Integer.toString(jar1File.manifest.mainAttributes.size())  
  88.       print "| ${manifest1Str.center(COLUMN_WIDTH)} |"  
  89.       def manifest2Str = manifestPreStr + Integer.toString(jar2File.manifest.mainAttributes.size())  
  90.       println "${manifest2Str.center(COLUMN_WIDTH)} |"  
  91.       println ROW_SEPARATOR  
  92.    }  
  93. }  
  94.   
  95. entriesInJar1ButNotInJar2.each  
  96. { entry1 ->  
  97.    print "| ${entry1.center(COLUMN_WIDTH)} |"  
  98.    println "${" ".center(entry1.size() > COLUMN_WIDTH ? 2 * COLUMN_WIDTH - entry1.size() : COLUMN_WIDTH)} |"  
  99.    println ROW_SEPARATOR  
  100. }  
  101. entriesInJar2ButNotInJar1.each  
  102. { entry2 ->  
  103.    print "| ${" ".center(entry2.size() > COLUMN_WIDTH ? 2 * COLUMN_WIDTH - entry2.size() : COLUMN_WIDTH)}"  
  104.    println "| ${entry2.center(COLUMN_WIDTH)} |"  
  105.    println ROW_SEPARATOR  
  106. }  
  107.   
  108. jar1.each   
  109. { key, value ->  
  110.    if (!entriesInJar1ButNotInJar2.contains(key))  
  111.    {  
  112.       def jar2Entry = jar2.get(key)  
  113.       if (value != jar2Entry)  
  114.       {  
  115.          println "| ${key.center(COLUMN_WIDTH)} |${jar2Entry.name.center(COLUMN_WIDTH)} |"  
  116.          if (value.crc != jar2Entry.crc)  
  117.          {  
  118.             def crc1Str = "CRC: ${value.crc}"  
  119.             def crc2Str = "CRC: ${jar2Entry.crc}"  
  120.             print "| ${crc1Str.center(COLUMN_WIDTH)} |"  
  121.             println "${crc2Str.center(COLUMN_WIDTH)} |"  
  122.          }  
  123.          if (value.size != jar2Entry.size)  
  124.          {  
  125.             def size1Str = "${value.size} bytes"  
  126.             def size2Str = "${jar2Entry.size} bytes"  
  127.             print "| ${size1Str.center(COLUMN_WIDTH)} |"  
  128.             println "${size2Str.center(COLUMN_WIDTH)} |"  
  129.          }  
  130.          if (value.time != jar2Entry.time)  
  131.          {  
  132.             boolean crcDiff = (!ignoreCrc && value.crc != jar2Entry.crc)  
  133.             boolean sizeDiff = (!ignoreSize && value.size != jar2Entry.size)  
  134.             boolean timeDiff = (!ignoreTime && value.time != jar2Entry.time)  
  135.   
  136.             if(crcDiff || sizeDiff || timeDiff)  
  137.             {  
  138.                println "| ${key.center(COLUMN_WIDTH)} |${jar2Entry.name.center(COLUMN_WIDTH)} |"  
  139.                if (crcDiff)  
  140.                {  
  141.                   def crc1Str = "CRC: ${value.crc}"  
  142.                   def crc2Str = "CRC: ${jar2Entry.crc}"  
  143.                   print "| ${crc1Str.center(COLUMN_WIDTH)} |"  
  144.                   println "${crc2Str.center(COLUMN_WIDTH)} |"  
  145.                }  
  146.                if (sizeDiff)  
  147.                {  
  148.                   def size1Str = "${value.size} bytes"  
  149.                   def size2Str = "${jar2Entry.size} bytes"  
  150.                   print "| ${size1Str.center(COLUMN_WIDTH)} |"  
  151.                   println "${size2Str.center(COLUMN_WIDTH)} |"  
  152.                }  
  153.                if (timeDiff)  
  154.                {  
  155.                   def time1Str = "${new Date(value.time)}"  
  156.                   def time2Str = "${new Date(jar2Entry.time)}"  
  157.                   print "| ${time1Str.center(COLUMN_WIDTH)} |"  
  158.                   println "${time2Str.center(COLUMN_WIDTH)} |"  
  159.                }  
  160.                println ROW_SEPARATOR  
  161.             }  
  162.          }  
  163.       }  
  164.    }  
  165. }  
  166.   
  167.   
  168.   
  169. /** 
  170.  * Provide mapping of JAR entry names to characteristics about that JAR entry 
  171.  * for the JAR indicated by the provided JAR file name. 
  172.  * 
  173.  * @param jarFile JAR file from which to extract contents. 
  174.  * @return JAR entries and thir characteristics. 
  175.  */  
  176. def TreeMap<String, JarCharacteristics> extractJarContents(JarFile jarFile)  
  177. {  
  178.    def jarContents = new TreeMap<String, JarCharacteristics>()  
  179.    entries = jarFile.entries()  
  180.    entries.each  
  181.    { entry->  
  182.       jarContents.put(entry.name, new JarCharacteristics(entry.name, entry.crc, entry.size, entry.time));  
  183.    }  
  184.    return jarContents  
  185. }  
  186.   
  187.   
  188. /** 
  189.  * Add more detailed Manifest differences to output report. 
  190.  * 
  191.  * @param manifest1Attrs Main attributes of first JAR file's Manifest 
  192.  * @param manifest2Attrs Main attributes of second JAR file's Manifest. 
  193.  */  
  194. def displayDetailedManifestFilesDifferences(  
  195.    Attributes manifest1Attrs, Attributes manifest2Attrs)  
  196. {  
  197.    def attrsIn1ButNot2 = manifest1Attrs.keySet() - manifest2Attrs.keySet()  
  198.    def attrsIn2ButNot1 = manifest2Attrs.keySet() - manifest1Attrs.keySet()  
  199.    attrsIn1ButNot2.each  
  200.    {  
  201.       def attr1onlyStr = "${it}=${manifest1Attrs.get(it)}"   
  202.       print "| ${attr1onlyStr.center(COLUMN_WIDTH)} |"  
  203.       println "${" ".center(attr1onlyStr.size() > COLUMN_WIDTH ? 2 * COLUMN_WIDTH - attr1onlyStr.size() : COLUMN_WIDTH)} |"  
  204.    }  
  205.    println ROW_SEPARATOR  
  206.    attrsIn2ButNot1.each  
  207.    {  
  208.       def attr2onlyStr = "${it}=${manifest2Attrs.get(it)}"  
  209.       print "| ${" ".center(attr2onlyStr.size() > COLUMN_WIDTH ? 2 * COLUMN_WIDTH - attr2onlyStr.size() : COLUMN_WIDTH)}|"  
  210.       println " ${attr2onlyStr.center(COLUMN_WIDTH)} |"  
  211.    }  
  212.    println ROW_SEPARATOR  
  213.    manifest1Attrs.each  
  214.    {  
  215.       def key = it.key  
  216.       if (it.value != manifest2Attrs.get(key) && !attrsIn1ButNot2.contains(it.key))  
  217.       {  
  218.          def attr1Str = "${key}=${manifest1Attrs.get(key)}"  
  219.          print "| ${attr1Str.center(COLUMN_WIDTH)}"  
  220.          def attr2Str = "${key}=${manifest2Attrs.get(key)}"  
  221.          println "| ${attr2Str.center(COLUMN_WIDTH)} |"  
  222.       }  
  223.    }  
  224.    println ROW_SEPARATOR  
  225. }  

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
  1. @groovy.transform.Canonical  
  2. class JarCharacteristics  
  3. {  
  4.    String name  
  5.    long crc  
  6.    long size  
  7.    long time  
  8. }  

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: