Saturday, March 2, 2013

Finding Properties in JARs with Groovy

In previous blog posts I have looked at Searching JAR Files with Groovy to find entries (such as .class files) contained in the JAR and Viewing a JAR's Manifest File with Groovy. In this post, I look at using Groovy to find a particular property in a properties file contained within a JAR. The script in this post searches JARs in a provided directory and its subdirectories for a properties file containing the specified property.

The following Groovy script leverages several advantages of Groovy to recursively search a specified directory and its subdirectories for JAR files containing properties files that contain the specified property. The script outputs matching JARs and their properties files entries that contain the specified property. The script also shows the value that each property is set to in each matched JAR/property file.

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

/**
 * findPropertiesInJars.groovy
 *
 * findPropertiesInJars.groovy -d <<root_directories>> -p <<properties_to_search_for>>
 *
 * Script that looks for provided properties (assumed to be in files with
 * .properties extension) in JAR files (assumed to have .jar extensions) in the
 * provided directory and all of its subdirectories.
 */

def cli = new CliBuilder(
   usage: 'findPropertiesInJars.groovy -d <root_directories> -p <property_names_to_search_for>',
   header: '\nAvailable options (use -h for help):\n',
   footer: '\nInformation provided via above options is used to generate printed string.\n')
import org.apache.commons.cli.Option
cli.with
{
   h(longOpt: 'help', 'Help', args: 0, required: false)
   d(longOpt: 'directories', 'Directories to be searched', args: Option.UNLIMITED_VALUES, valueSeparator: ',', required: true)
   p(longOpt: 'properties', 'Property names to search for in JARs', args: Option.UNLIMITED_VALUES, valueSeparator: ',', required: true)
}
def opt = cli.parse(args)
if (!opt) return
if (opt.h) cli.usage()

def directories = opt.ds
def propertiesToSearchFor = opt.ps

import java.util.zip.ZipFile
import java.util.zip.ZipException

def matches = new TreeMap<String, Set<String>>()
directories.each
{ directory ->
   def dir = new File(directory)
   propertiesToSearchFor.each
   { propertyToFind ->
      dir.eachFileRecurse
      { file->
         if (file.isFile() && file.name.endsWith("jar"))
         {
            try
            {
               zip = new ZipFile(file)
               entries = zip.entries()
               entries.each
               { entry->
                  def entryName = entry.name
                  if (entryName.contains(".properties"))
                  {
                     def fullEntryName = file.canonicalPath + "!/" + entryName
                     def properties = new Properties()
                     try
                     {
                        def url = new URL("jar:file:" + File.separator + fullEntryName)
                        def jarConnection = (JarURLConnection) url.openConnection()
                        properties.load(jarConnection.inputStream)
                     }
                     catch (Exception exception)
                     {
                        println "Unable to load properties from ${fullEntryName} - ${exception}"
                     }
                     if (properties.get(propertyToFind) != null)
                     {
                        def pathPlusMatch = "${file.canonicalPath}\n\t\t${entryName}\n\t\t${propertyToFind}=${properties.get(propertyToFind)}"
                        if (matches.get(propertyToFind))
                        {
                           matches.get(propertyToFind).add(pathPlusMatch)
                        }
                        else
                        {
                           def containingJars = new TreeSet<String>()
                           containingJars.add(pathPlusMatch)
                           matches.put(propertyToFind, containingJars)
                        }
                     }
                  }
               }
            }
            catch (ZipException zipEx)
            {
               println "Unable to open JAR file ${file.name}"
            }
         }
      }
   }
}

matches.each
{ propertyName, containingJarNames ->
   println "\nProperty '${propertyName}' Found:"
   containingJarNames.each
   { containingJarName ->
      println "\t${containingJarName}"
   }
}

When the above script is run against JARs, it lists JARs with properties files that have the named property and its assigned value. The screen snapshot shown next demonstrates running the script against the Apache Camel distribution on my machine to find all properties named "artifactId" (Maven) in those numerous JAR files.

The above script takes advantage of several Groovy features. For example, Groovy's ability to directly use Java APIs and libraries is evident throughout the script with use of classes such as ZipFile (for accessing JAR contents), Properties (for accessing contents of properties files), JarURLConnection (also for accessing properties files' content), TreeSet (for easy sorting), and Apache Commons CLI (built into Groovy for command line support). Groovy's closures and concise syntax lead to greater fluency and readability as well.

This script catches exceptions even though Groovy does not require any exception (whether checked or runtime) to be caught. The reason for this is that an uncaught exception would lead to the script terminating. By catching any encountered exception during opening each JAR file or loading from a property file, an exception in those cases will only prevent that particular JAR or property file from being loaded without stopping others from being processed.

This script makes a couple significant assumptions. The first assumption is that the JAR files to be searched have a .jar extension and that the contained properties files have .properties extensions. The script uses built-in CLI support's nice feature of a single command-line flag allowing multiple directories and/or multiple property names to be searched for by separating the multiple values with commas.

There are times I want to know where a particular property is specified within my application and this script makes it easy to find where that particular property is specified in the JARs on the application's classpath.

No comments: