Showing posts with label Groovy. Show all posts
Showing posts with label Groovy. Show all posts

Monday, April 9, 2018

Clearer Code with JDK 10 Local Variable Type Inference

One of the first fruits of Project Amber, Local-Variable Type Inference (JEP 286), has been delivered with JDK 10. JEP 286's "Summary" describes its purpose, "Enhance the Java Language to extend type inference to declarations of local variables with initializers." In conjunction with this release, Stuart Marks has published the March 2018 article "Style Guidelines for Local Variable Type Inference in Java."

In "Style Guidelines for Local Variable Type Inference in Java," Marks postulates four "Principles" that lead to seven "Guidelines" that help developers to apply var properly to "help improve good code, making it shorter and clearer without compromising understandability." The articulated guidelines attempt to strike a balance that brings benefits of less redundant code with the benefits of explicitly readable code. The article outlines cases where var should be used and where it shouldn't be used. In general var is best used when other naming conventions or other constructs in use provide significant detail about the local variable type that is only repeated with the explicit typing. On the other hand, there are cases where much information is lost if the explicit type is removed and in such cases, use of var is discouraged. Another typical case where use of local variable type inference might be preferred is when the explicit typing is difficult to read and is only used in an intermediate step. It may not be important to see the complex explicit type for that intermediate step.

I couldn't help but think about my earliest days working with Groovy while reading the document "Style Guidelines for Local Variable Type Inference in Java." Most of the literature I read sang the virtues of using Groovy's def keyword wherever possible to conserve keystrokes and make the Groovy code more concise. In an effort to appear to "speak Groovy" fluently, I embraced what I deemed to be "idiomatic Groovy." However, after several months of this, I realized that some uses of the def keyword significantly reduced the readability of my Groovy code when I returned to it months after writing it. I began to use personal guidelines similar to those espoused by Marks with Java's LVTI with my Groovy code.

I wasn't the only one who learned that mindless application of Groovy's handy def keyword could actually lead to less maintainable Groovy code. Benefits that can be gained from explicit typing in Groovy are discussed in the StackOverflow thread "Groovy: 'def' keyword vs concrete type." The book Programming Grails has a section called "'def' Considered Harmful" in its first chapter and Rob Hinds's post "Groovy: A Retrospective" states, "... typing is better - I still don't buy that typing def rather than the actual type is actually that much of a saving." The "official" Groovy Style Guide features a section "Optional typing advice" that talks about whether or not to use def. However, for the similar case to which Java LVTI applies (local variables), the advice is, "you're more free to decide when to type or not."

Like so many other Java APIs and language constructs, the best Java code will not be written to always use LVTI or to never use LVTI. Instead, as is so often the case in software development, the decision on whether to use LVTI or not to use LVTI depends on the context in which it would potentially be used. Reviewing the "Style Guidelines for Local Variable Type Inference in Java" document and trying out LVTI and reviewing others' code using LVTI are the best ways to build an intuitive sense for when to apply LVTI and when not to. The amber-dev mailing list thread starting with the message "LVTI Style Guide" provides significant more discussion surrounding this document on when to use and not use Java's Local Variable Type Inference. The blog post "Representing the Impractical and Impossible with JDK 10 'var'" demonstrates how to "declare variables with types that were erstwhile impractical or impossible to represent" using Local Variable Type Inference.

Saturday, March 25, 2017

Using Groovy to Quickly Analyze Terracotta HealthCheck Properties

One of the considerations when configuring Terracotta servers with tc-config.xml is the specification of health check properties between Terracotta servers (L2-L2), from clients to server (L1-L2), and from server to client (L2-L1). Terracotta checks the combination of these properties' configurations in high-availability scenarios to ensure that these combinations fall in certain ranges. This blog post demonstrates using Groovy to parse and analyze a given tc-config.xml file to determine whether Terracotta will provide a WARN-level message regarding these properties' configurations.

The "About HealthChecker" section of the Terracotta 4.3.2 BigMemory Max High-Availability Guide (PDF) describes the purpose of the HealthChecker: "HealthChecker is a connection monitor similar to TCP keep-alive. HealthChecker functions between Terracotta server instances (in High Availability environments), and between Terracotta sever instances and clients. Using HealthChecker, Terracotta nodes can determine if peer nodes are reachable, up, or in a GC operation. If a peer node is unreachable or down, a Terracotta node using HealthChecker can take corrective action."

The Terracotta 4.3.2 BigMemory Max High-Availability Guide includes a table under the section HealthChecker Properties that articulates the Terracotta properties that go into the calculations used to determine if warnings about misconfigured high availability should be logged. There are similarly named properties specified for each of the combinations (l2.healthcheck.l1.* properties for server-to-clients [L2L1], l2.healthcheck.l2.* for server-to-server [L2L2], and l1.healthcheck.l2.* for clients-to-server [L1L2]) and the properties significant to the high availability configuration checks (the * portion of the properties names just referenced) are ping.enabled, ping.idletime, ping.interval, ping.probes, socketConnect, socketConnectCount, and socketConnectTimeout. This post's associated Groovy script assumes that one has the ping.enabled and socketConnect properties for L2-L2, L1-L2, and L2-L1 all configured to true (which is the default for both properties for all L2L2, L1L2, L2L1 combinations).

The Terracotta class com.tc.l2.ha.HASettingsChecker detects two combinations of these properties that lead to WARN-level log messages starting with the phrase, "High Availability Not Configured Properly: ...". The two warning messages specifically state, "High Availability Not Configured Properly: L1L2HealthCheck should be less than L2-L2HealthCheck + ElectionTime + ClientReconnectWindow" and "High Availability Not Configured Properly: L1L2HealthCheck should be more than L2-L2HealthCheck + ElectionTime".

The Terracotta class HASettingsChecker implements the formula outlined in the "Calculating HealthChecker Maximum" section of the High Availability Guide in its method interNodeHealthCheckTime(int,int,int,int,int):

    pingIdleTime + ((socketConnectCount) * (pingInterval * pingProbes + socketConnectTimeout * pingInterval))

The following Groovy script parses an indicated tc-config.xml file and applies the same health check properties check to the relevant properties defined in that file's <tc-properties> section. The Groovy script shown here has no external dependencies other than a valid tc-config.xml file to be parsed and analyzed. The script would be shorter and require less future maintenance if it accessed the String constants defined in com.tc.properties.TCPropertiesConsts instead of defining its own hard-coded versions of these.

checkTCServerProperties.groovy

#!/usr/bin/env groovy

def cli = new CliBuilder(
   usage: 'checkTCServerProperties -f <pathToTcConfigXmlFile> [-v] [-h]',
   header: '\nAvailable options (use -h for help):\n',
   footer: '\nParses referenced tc-config.xml file and analyzes its health check parameters..\n')
import org.apache.commons.cli.Option
cli.with
{
   h(longOpt: 'help', 'Usage Information', required: false)
   f(longOpt: 'file', 'Path to tc-config.xml File', args: 1, required: true)
   v(longOpt: 'verbose', 'Specifies verbose output', args: 0, required: false)
}
def opt = cli.parse(args)

if (!opt) return
if (opt.h) cli.usage()

String tcConfigFileName = opt.f
boolean verbose = opt.v

println "Checking ${tcConfigFileName}'s properties..."
def tcConfigXml = new XmlSlurper().parse(tcConfigFileName)
TreeMap<String, String> properties = new TreeSet<>()
tcConfigXml."tc-properties".property.each
{ tcProperty ->
   String tcPropertyName = tcProperty.@name
   String tcPropertyValue = tcProperty.@value
   properties.put(tcPropertyName, tcPropertyValue)
}
if (verbose)
{
   properties.each
   { propertyName, propertyValue ->
      println "${propertyName}: ${propertyValue}"
   }
}

boolean isL2L1PingEnabled = extractBoolean(properties, "l2.healthcheck.l1.ping.enabled")
boolean isL2L2PingEnabled = extractBoolean(properties, "l2.healthcheck.l2.ping.enabled")
boolean isL1L2PingEnabled = extractBoolean(properties, "l1.healthcheck.l2.ping.enabled")
boolean isPingEnabled = isL2L1PingEnabled && isL2L2PingEnabled && isL1L2PingEnabled
println "Health Check Ping ${isPingEnabled ? 'IS' : 'is NOT'} enabled."
if (!isPingEnabled)
{
   System.exit(-1)
}

Long pingIdleTimeL2L1 = extractLong(properties, "l2.healthcheck.l1.ping.idletime")
Long pingIdleTimeL2L2 = extractLong(properties, "l2.healthcheck.l2.ping.idletime")
Long pingIdleTimeL1L2 = extractLong(properties, "l1.healthcheck.l2.ping.idletime")

Long pingIntervalL2L1 = extractLong(properties, "l2.healthcheck.l1.ping.interval")
Long pingIntervalL2L2 = extractLong(properties, "l2.healthcheck.l2.ping.interval")
Long pingIntervalL1L2 = extractLong(properties, "l1.healthcheck.l2.ping.interval")

Long pingProbesL2L1 = extractLong(properties, "l2.healthcheck.l1.ping.probes")
Long pingProbesL2L2 = extractLong(properties, "l2.healthcheck.l2.ping.probes")
Long pingProbesL1L2 = extractLong(properties, "l1.healthcheck.l2.ping.probes")

boolean socketConnectL2L1 = extractBoolean(properties, "l2.healthcheck.l1.socketConnect")
boolean socketConnectL2L2 = extractBoolean(properties, "l2.healthcheck.l2.socketConnect")
boolean socketConnectL1L2 = extractBoolean(properties, "l1.healthcheck.l2.socketConnect")

if (!socketConnectL2L1 || !socketConnectL2L2 || !socketConnectL1L2)
{
   println "Socket connect is disabled."
   System.exit(-2)
}

Long socketConnectTimeoutL2L1 = extractLong(properties, "l2.healthcheck.l1.socketConnectTimeout")
Long socketConnectTimeoutL2L2 = extractLong(properties, "l2.healthcheck.l2.socketConnectTimeout")
Long socketConnectTimeoutL1L2 = extractLong(properties, "l1.healthcheck.l2.socketConnectTimeout")

Long socketConnectCountL2L1 = extractLong(properties, "l2.healthcheck.l1.socketConnectCount")
Long socketConnectCountL2L2 = extractLong(properties, "l2.healthcheck.l2.socketConnectCount")
Long socketConnectCountL1L2 = extractLong(properties, "l1.healthcheck.l2.socketConnectCount")

Long maximumL2L1 = calculateMaximumTime(pingIdleTimeL2L1, pingIntervalL2L1, pingProbesL2L1, socketConnectCountL2L1, socketConnectTimeoutL2L1)
Long maximumL2L2 = calculateMaximumTime(pingIdleTimeL2L2, pingIntervalL2L2, pingProbesL2L2, socketConnectCountL2L2, socketConnectTimeoutL2L2)
Long maximumL1L2 = calculateMaximumTime(pingIdleTimeL1L2, pingIntervalL1L2, pingProbesL1L2, socketConnectCountL1L2, socketConnectTimeoutL1L2)

if (verbose)
{
   println "L2-L1 Maximum Time: ${maximumL2L1}"
   println "L2-L2 Maximum Time: ${maximumL2L2}"
   println "L1-L2 Maximum Time: ${maximumL1L2}"
}

long electionTime = 5000
long clientReconnectWindow = 120000

long maximumL2L2Election = maximumL2L2 + electionTime
long maximumL2L2ElectionReconnect = maximumL2L2Election + clientReconnectWindow

if (verbose)
{
   println "L2-L2 Maximum Time + ElectionTime: ${maximumL2L2Election}"
   println "L2-L2 Maximum Time + ElectionTime + Client Reconnect Window: ${maximumL2L2ElectionReconnect}"   
}

if (maximumL1L2 < maximumL2L2Election)
{
   print "WARNING: Will lead to 'High Availability Not Configured Properly: L1L2HealthCheck should be more than L2-L2HealthCheck + ElectionTime' "
   println "because ${maximumL1L2} < ${maximumL2L2Election}."
}
else if (maximumL1L2 > maximumL2L2ElectionReconnect)
{
   print "WARNING: Will lead to 'High Availability Not Configured Properly: L1L2HealthCheck should be less than L2-L2HealthCheck + ElectionTime + ClientReconnectWindow' "
   println "because ${maximumL1L2} > ${maximumL2L2ElectionReconnect}."
}

/**
 * Extract a Boolean value for the provided property name from the provided
 * properties.
 *
 * @return Boolean value associated with the provided property name.
 */
boolean extractBoolean(TreeMap<String, String> properties, String propertyName)
{
   return  properties != null && properties.containsKey(propertyName)
         ? Boolean.valueOf(properties.get(propertyName))
         : false
}

/**
 * Extract a Long value for the provided property name from the provided
 * properties.
 *
 * @return Long value associated with the provided property name.
 */
Long extractLong(TreeMap<String, String> properties, String propertyName)
{
   return  properties != null && properties.containsKey(propertyName)
         ? Long.valueOf(properties.get(propertyName))
         : 0
}

/**
 * Provides the maximum time as calculated using the following formula:
 *
 * Maximum Time =
 *      (ping.idletime) + socketConnectCount *
 *      [(ping.interval * ping.probes) + (socketConnectTimeout * ping.interval)]
 */
Long calculateMaximumTime(Long pingIdleTime, Long pingInterval, Long pingProbes,
   Long socketConnectCount, Long socketConnectTimeout)
{
   return pingIdleTime + socketConnectCount * pingInterval * (pingProbes + socketConnectTimeout)
}

This script will also be available on GitHub. At some point, I may address some of its weaknesses and limitations in that GitHub version. Specifically, as shown above, this script currently assumes the default values for "election time" and "client reconnect window", but these could be parsed from the tc-config.xml file.

The following screen snapshots demonstrate this script in action against various tc-config.xml files. The first image depicts the script's behavior when ping is not enabled. The second image depicts the script's behavior when socket checking is not enabled. The third and fourth images depict the two warnings one might encounter when properties for high availability configuration are not configured properly. The fifth image depicts a fully successful execution of the script that indicates a configuration of health check properties that are in the expected ranges.

Ping Not Enabled (not default)

Socket Not Enabled (not default)

HealthCheck Properties Warning #1

HealthCheck Properties Warning #2

HealthCheck Properties Enabled and Configured Properly

I have used a simple spreadsheet to perform these calculations and that works fairly well. However, the Groovy script discussed in this post allows for automatic parsing of a candidate tc-config.xml file rather than needing to copy and paste values into the spreadsheet. The Groovy script could be adapted to use Terracotta provided Java files as discussed earlier. There are also several other enhancements that could make the script more useful such as parsing the client reconnect window and election time from the tc-config.xml file rather than assuming the default values.

Friday, March 13, 2015

Excellent! Groovy Intends to Join Apache Software Foundation

In the post "Total Bummer: Pivotal Drops Groovy", I briefly wrote about Pivotal's decision to drop Groovy and hoped that Groovy would find a new home. I was pleased to read the announcement that the Groovy project intends to join the Apache Software Foundation. My experience is that some of the best maintained, best supported, and best documented open source projects are those with a corporate sponsor or those associated with the Apache Software Foundation. I have benefited tremendously from several Apache projects over the years including Ant, Struts, Apache HTTP Server, Apache Commons, Camel, Log4J, Lucene, Apache POI, Apache FOP, and Tomcat. The Apache Software Foundation also houses several other highly popular projects including Hadoop, HBase, Apache Cordova, MyFaces, and Solr.

Groovy already enjoys some associations with Apache projects. For example, Groovy bakes in Ant support and Commons CLI support (Groovy's CliBuilder). The Apache page listing projects grouped by programming languages includes a "Groovy" section that lists Apache Camel and Apache OFBiz.

According to Guillaume Laforge, there were discussions about Groovy's next home with several organizations including the Eclipse Foundation, the Software Freedom Conservancy, and the Apache Software Foundation. Matt Raible has provided follow-up to this post in a question-and-answer format with Laforge in the post Groovy Moving to Apache. Of particular interest to me is the expanding on the "gray areas" Laforge alluded to. These "gray areas" include differences and limitations associated with the Apache Software Foundation such as process, repository control, and potential corporate funding of an individual project.

One of the several advantages of using Apache projects is the liberal Apache 2 License. Groovy was already available under this license and obviously will continue to use that license as part of the Apache Software Foundation.

Like all projects introduced to the Apache Software Foundation, Groovy will begin in the Apache Incubator. Grails is not at this time slated for the Apache Software Foundation, though that could come in the future. Cédric Champeau briefly mentions Groovy and Apache in his post Who is Groovy?

Friday, February 13, 2015

Writing Groovy's groovy.util.slurpersupport.GPathResult (XmlSlurper) Content as XML

In a previous blog post, I described using XmlNodePrinter to present XML parsed with XmlParser in a nice format to standard output, as a Java String, and in a new file. Because XmlNodePrinter works with groovy.util.Node instances, it works well with XmlParser, but doesn't work so well with XmlSlurper because XmlSlurper deals with instances of groovy.util.slurpersupport.GPathResult rather than instances of groovy.util.Node. This post looks at how groovy.xml.XmlUtil can be used to present GPathResult objects that results from slurping XML to standard output, as a Java String, and to a new file.

This post's first code listing demonstrates slurping XML with XmlSlurper and writing the slurped GPathResult to standard output using XmlUtil's serialize(GPathResult, OutputStream) method and the System.out handle.

slurpAndPrintXml.groovy : Writing XML to Standard Output
#!/usr/bin/env groovy

// slurpAndPrintXml.groovy
//
// Use Groovy's XmlSlurper to "slurp" provided XML file and use XmlUtil to write
// XML content out to standard output.

if (args.length < 1)
{
   println "USAGE: groovy slurpAndPrint.xml <xmlFile>"
}

String xmlFileName = args[0]

xml = new XmlSlurper().parse(xmlFileName)

import groovy.xml.XmlUtil
XmlUtil xmlUtil = new XmlUtil()
xmlUtil.serialize(xml, System.out)

The next code listing demonstrates use of XmlUtil's serialize(GPathResult) method to serialize the GPathResult to a Java String.

slurpXmlToString.groovy : Writing XML to Java String
#!/usr/bin/env groovy

// slurpXmlToString.groovy
//
// Use Groovy's XmlSlurper to "slurp" provided XML file and use XmlUtil to
// write the XML content to a String.

if (args.length < 1)
{
   println "USAGE: groovy slurpAndPrint.xml <xmlFile>"
}

String xmlFileName = args[0]

xml = new XmlSlurper().parse(xmlFileName)

import groovy.xml.XmlUtil
XmlUtil xmlUtil = new XmlUtil()
String xmlString = xmlUtil.serialize(xml)
println "String:\n${xmlString}"

The third code listing demonstrates use of XmlUtil's serialize(GPathResult, Writer) method to write the GPathResult representing the slurped XML to a file via a FileWriter instance.

slurpAndSaveXml.groovy : Writing XML to File
#!/usr/bin/env groovy

// slurpAndSaveXml.groovy
//
// Uses Groovy's XmlSlurper to "slurp" XML and then uses Groovy's XmlUtil
// to write slurped XML back out to a file with the provided name. The
// first argument this script expects is the path/name of the XML file to be
// slurped and the second argument expected by this script is the path/name of
// the file to which the XML should be saved/written.

if (args.length < 2)
{
   println "USAGE: groovy slurpAndSaveXml.groovy <xmlFile> <outputFile>"
}

String xmlFileName = args[0]
String outputFileName = args[1]
xml = new XmlSlurper().parse(xmlFileName)

import groovy.xml.XmlUtil
XmlUtil xmlUtil = new XmlUtil()
xmlUtil.serialize(xml, new FileWriter(new File(outputFileName)))

The examples in this post have demonstrated writing/serializing XML slurped into GPathResult objects through the use of XmlUtil methods. The XmlUtil class also provides methods for serializing the groovy.util.Node instances that XmlParser provides. The three methods accepting an instance of Node are similar to the three methods above for instances of GPathResult. Similarly, other methods of XmlUtil provide similar support for XML represented as instances of org.w3c.dom.Element, Java String, and groovy.lang.Writable.

The XmlNodePrinter class covered in my previous blog post can be used to serialize XmlParser's parsed XML represented as a Node. The XmlUtil class also can be used to serialize XmlParser's parsed XML represented as a Node but offers the additional advantage of being able to serialize XmlSlurper's slurped XML represented as a GPathResult.

Monday, February 9, 2015

Writing Groovy's groovy.util.Node (XmlParser) Content as XML

Groovy's XmlParser makes it easy to parse an XML file, XML input stream, or XML string using one its overloaded parse methods (or parseText in the case of the String). The XML content parsed with any of these methods is made available as a groovy.util.Node instance. This blog post describes how to make the return trip and write the content of the Node back to XML in alternative formats such as a File or a String.

Groovy's MarkupBuilder provides a convenient approach for generating XML from Groovy code. For example, I demonstrated writing XML based on SQL query results in the post GroovySql and MarkupBuilder: SQL-to-XML. However, when one wishes to write/serialize XML from a Groovy Node, an easy and appropriate approach is to use XmlNodePrinter as demonstrated in Updating XML with XmlParser.

The next code listing, parseAndPrintXml.groovy demonstrates use of XmlParser to parse XML from a provided file and use of XmlNodePrinter to write that Node parsed from the file to standard output as XML.

parseAndPrintXml.groovy : Writing XML to Standard Output
#!/usr/bin/env groovy

// parseAndPrintXml.groovy
//
// Uses Groovy's XmlParser to parse provided XML file and uses Groovy's
// XmlNodePrinter to print the contents of the Node parsed from the XML with
// XmlParser to standard output.

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

XmlParser xmlParser = new XmlParser()
Node xml = xmlParser.parse(new File(args[0]))
XmlNodePrinter nodePrinter = new XmlNodePrinter(preserveWhitespace:true)
nodePrinter.print(xml)

Putting aside the comments and code for checking command line arguments, there are really 4 lines (lines 15-18) in the above code listing of significance to this discussion. These four lines demonstrate instantiating an XmlParser (line 15), using the instance of XmlParser to "parse" a File instance based on a provided argument file name (line 16), instantiating an XmlNodePrinter (line 17), and using that XmlNodePrinter instance to "print" the parsed XML to standard output (line 18).

Although writing XML to standard output can be useful for a user to review or to redirect output to another script or tool, there are times when it is more useful to have access to the parsed XML as a String. The next code listing is just a bit more involved than the last one and demonstrates use of XmlNodePrinter to write the parsed XML contained in an Node instance as a Java String.

parseXmlToString.groovy : Writing XML to Java String
#!/usr/bin/env groovy

// parseXmlToString.groovy
//
// Uses Groovy's XmlParser to parse provided XML file and uses Groovy's
// XmlNodePrinter to write the contents of the Node parsed from the XML with
// XmlParser into a Java String.

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

XmlParser xmlParser = new XmlParser()
Node xml = xmlParser.parse(new File(args[0]))
StringWriter stringWriter = new StringWriter()
XmlNodePrinter nodePrinter = new XmlNodePrinter(new PrintWriter(stringWriter))
nodePrinter.setPreserveWhitespace(true)
nodePrinter.print(xml)
String xmlString = stringWriter.toString()
println "XML as String:\n${xmlString}"

As the just-shown code listing demonstrates, one can instantiate an instance of XmlNodePrinter that writes to a PrintWriter that was instantiated with a StringWriter. This StringWriter ultimately makes the XML available as a Java String.

Writing XML from a groovy.util.Node to a File is very similar to writing it to a String with a FileWriter used instead of a StringWriter. This is demonstrated in the next code listing.

parseAndSaveXml.groovy : Write XML to File
#!/usr/bin/env groovy

// parseAndSaveXml.groovy
//
// Uses Groovy's XmlParser to parse provided XML file and uses Groovy's
// XmlNodePrinter to write the contents of the Node parsed from the XML with
// XmlParser to file with provided name.

if (args.length < 2)
{
   println "USAGE: groovy parseAndSaveXml.groovy <sourceXMLFile> <targetXMLFile>"
   System.exit(-1)
}

XmlParser xmlParser = new XmlParser()
Node xml = xmlParser.parse(new File(args[0]))
FileWriter fileWriter = new FileWriter(args[1])
XmlNodePrinter nodePrinter = new XmlNodePrinter(new PrintWriter(fileWriter))
nodePrinter.setPreserveWhitespace(true)
nodePrinter.print(xml)

I don't show it in this post, but the value of being able to write a Node back out as XML often comes after modifying that Node instance. Updating XML with XmlParser demonstrates the type of functionality that can be performed on a Node before serializing the modified instance back out.

Friday, January 30, 2015

Programmatically Determining Java Class's JDK Compilation Version

When it is necessary to determine which JDK version was used to compile a particular Java .class file, an approach that is often used is to use javap and to look for the listed "major version" in the javap output. I referenced this approach in my blog post Autoboxing, Unboxing, and NoSuchMethodError, but describe it in more detail here before moving onto how to accomplish this programatically.

The following code snippet demonstrates running javap -verbose against the Apache Commons Configuration class ServletFilterCommunication contained in commons-configuration-1.10.jar.

I circled the "major version" in the screen snapshot shown above. The number listed after "major version:" (49 in this case) indicates that the version of JDK used to compile this class is J2SE 5. The Wikipedia page for Java Class File lists the "major version" numerals corresponding to each JDK version:

Major VersionJDK Version
52Java SE 8
51Java SE 7
50Java SE 6
49J2SE 5
48JDK 1.4
47JDK 1.3
46JDK 1.2
45JDK 1.1

This is an easy way to determine the version of the JDK used to compile the .class file, but it can become tedious to do this on numerous classes in a directory or JAR files. It would be easier if we could programmatically check this major version so that it could be scripted. Fortunately, Java does support this. Matthias Ernst has posted "Code snippet: calling javap programmatically," in which he demonstrates use of JavapEnvironment from the JDK tools JAR to programatically perform javap functionality, but there is an easier way to identify the specific bytes of the .class file that indicate the version of JDK used for compilation.

The blog post "Identify Java Compiler version from Class Format Major/Minor version information" and the StackOverflow thread "Java API to find out the JDK version a class file is compiled for?" demonstrate reading the relevant two bytes from the Java .class file using DataInputStream.

Basic Access to JDK Version Used to Compile .class File

The next code listing demonstrates the minimalistic approach to access a .class file's JDK compilation version.

final DataInputStream input = new DataInputStream(new FileInputStream(pathFileName));
input.skipBytes(4);
final int minorVersion = input.readUnsignedShort();
final int majorVersion = input.readUnsignedShort();

The code instantiates a FileInputStream on the (presumed) .class file of interest and that FileInputStream is used to instantiate a DataInputStream. The first four bytes of a valid .class file contain numerals indicating it is a valid Java compiled class and are skipped. The next two bytes are read as an unsigned short and represent the minor version. After that comes the most important two bytes for our purposes. They are also read in as an unsigned short and represent the major version. This major version directly correlates with specific versions of the JDK. These significant bytes (magic, minor_version, and major_version) are described in Chapter 4 ("The class File Format") of The Java Virtual Machine Specification.

In the code listing above, the "magic" 4 bytes are simply skipped for convenience in understanding. However, I prefer to check those four bytes to ensure that they are what are expected for a .class file. The JVM Specification explains what should be expected for these first four bytes, "The magic item supplies the magic number identifying the class file format; it has the value 0xCAFEBABE." The next code listing revises the previous code listing and adds a check to ensure that the file in question in a Java compiled .class file. Note that the check specifically uses the hexadecimal representation CAFEBABE for readability.

final DataInputStream input = new DataInputStream(new FileInputStream(pathFileName));
// The first 4 bytes of a .class file are 0xCAFEBABE and are "used to
// identify file as conforming to the class file format."
// Use those to ensure the file being processed is a Java .class file.
final String firstFourBytes =
     Integer.toHexString(input.readUnsignedShort())
   + Integer.toHexString(input.readUnsignedShort());
if (!firstFourBytes.equalsIgnoreCase("cafebabe"))
{
   throw new IllegalArgumentException(
      pathFileName + " is NOT a Java .class file.");
}
final int minorVersion = input.readUnsignedShort();
final int majorVersion = input.readUnsignedShort();

With the most important pieces of it already examined, the next code listing provides the full listing for a Java class I call ClassVersion.java. It has a main(String[]) function so that its functionality can be easily used from the command line.

ClassVersion.java
import static java.lang.System.out;

import java.io.DataInputStream;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;

/**
 * Prints out the JDK version used to compile .class files. 
 */
public class ClassVersion
{
   private static final Map<Integer, String> majorVersionToJdkVersion;

   static
   {
      final Map<Integer, String> tempMajorVersionToJdkVersion = new HashMap<>();
      tempMajorVersionToJdkVersion.put(45, "JDK 1.1");
      tempMajorVersionToJdkVersion.put(46, "JDK 1.2");
      tempMajorVersionToJdkVersion.put(47, "JDK 1.3");
      tempMajorVersionToJdkVersion.put(48, "JDK 1.4");
      tempMajorVersionToJdkVersion.put(49, "J2SE 5");
      tempMajorVersionToJdkVersion.put(50, "Java SE 6");
      tempMajorVersionToJdkVersion.put(51, "Java SE 7");
      tempMajorVersionToJdkVersion.put(52, "Java SE 8");
      majorVersionToJdkVersion = Collections.unmodifiableMap(tempMajorVersionToJdkVersion);
   }

   /**
    * Print (to standard output) the major and minor versions of JDK that the
    * provided .class file was compiled with.
    *
    * @param pathFileName Name of (presumably) .class file from which the major
    * and minor versions of the JDK used to compile that class are to be
    * extracted and printed to standard output.
    */
   public static void printCompiledMajorMinorVersions(final String pathFileName)
   {
      try
      {
         final DataInputStream input = new DataInputStream(new FileInputStream(pathFileName));
         printCompiledMajorMinorVersions(input, pathFileName);
      }
      catch (FileNotFoundException fnfEx)
      {
         out.println("ERROR: Unable to find file " + pathFileName);
      }
   }

   /**
    * Print (to standard output) the major and minor versions of JDK that the
    * provided .class file was compiled with.
    *
    * @param input DataInputStream instance assumed to represent a .class file
    *    from which the major and minor versions of the JDK used to compile
    *    that class are to be extracted and printed to standard output.
    * @param dataSourceName Name of source of data from which the provided
    *    DataInputStream came.
    */
   public static void printCompiledMajorMinorVersions(
      final DataInputStream input, final String dataSourceName)
   {  
      try
      {
         // The first 4 bytes of a .class file are 0xCAFEBABE and are "used to
         // identify file as conforming to the class file format."
         // Use those to ensure the file being processed is a Java .class file.
         final String firstFourBytes =
              Integer.toHexString(input.readUnsignedShort())
            + Integer.toHexString(input.readUnsignedShort());
         if (!firstFourBytes.equalsIgnoreCase("cafebabe"))
         {
            throw new IllegalArgumentException(
               dataSourceName + " is NOT a Java .class file.");
         }
         final int minorVersion = input.readUnsignedShort();
         final int majorVersion = input.readUnsignedShort();
         out.println(
              dataSourceName + " was compiled with "
            + convertMajorVersionToJdkVersion(majorVersion)
            + " (" + majorVersion + "/" + minorVersion + ")");
      }
      catch (IOException exception)
      {
         out.println(
              "ERROR: Unable to process file " + dataSourceName
            + " to determine JDK compiled version - " + exception);
      }
   }

   /**
    * Accepts a "major version" and provides the associated name of the JDK
    * version corresponding to that "major version" if one exists.
    *
    * @param majorVersion Two-digit major version used in .class file.
    * @return Name of JDK version associated with provided "major version."
    */
   public static String convertMajorVersionToJdkVersion(final int majorVersion)
   {
      return  majorVersionToJdkVersion.get(majorVersion) != null
            ? majorVersionToJdkVersion.get(majorVersion)
            : "Unknown JDK version for 'major version' of " + majorVersion;
   }

   public static void main(final String[] arguments)
   {
      if (arguments.length < 1)
      {
         out.println("USAGE: java ClassVersion <nameOfClassFile.class>");
         System.exit(-1);
      }
      printCompiledMajorMinorVersions(arguments[0]);
   }
}

The next screen snapshot demonstrates running this class against its own .class file.

As the last screen snapshot of the PowerShell console indicates, the version of the class was compiled with JDK 8.

With this ClassVersion in place, we have the ability to use Java to tell us when a particular .class file was compiled. However, this is not much easier than simply using javap and looking for the "major version" manually. What makes this more powerful and easier to use is to employ it in scripts. With that in mind, I now turn focus to Groovy scripts that take advantage of this class to identify JDK versions used to compile multiple .class files in a JAR or directory.

The next code listing is an example of a Groovy script that can make using of the ClassVersion class. This script demonstrates the version of JDK used to compile all .class files in a specified directory and its subdirectories.

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

// displayCompiledJdkVersionsOfClassFilesInDirectory.groovy
//
// Displays the version of JDK used to compile Java .class files in a provided
// directory and in its subdirectories.
//

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

File directory = new File(args[0])
String directoryName = directory.canonicalPath
if (!directory.isDirectory())
{
   println "ERROR: ${directoryName} is not a directory."
   System.exit(-2)
}

print "\nJDK USED FOR .class COMPILATION IN DIRECTORIES UNDER "
println "${directoryName}\n"
directory.eachFileRecurse
{ file ->
   String fileName = file.canonicalPath
   if (fileName.endsWith(".class"))
   {
      ClassVersion.printCompiledMajorMinorVersions(fileName)
   }
}
println "\n"

An example of the output generated by the just-listed script is shown next.

Another Groovy script is shown next and can be used to identify the JDK version used to compile .class files in any JAR files in the specified directory or one of its subdirectories.

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

// displayCompiledJdkVersionsOfClassFilesInJar.groovy
//
// Displays the version of JDK used to compile Java .class files in JARs in the
// specified directory or its subdirectories.
//

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

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

String rootDir = args ? args[0] : "."
File directory = new File(rootDir)
directory.eachFileRecurse
{ file->
   if (file.isFile() && file.name.endsWith("jar"))
   {
      try
      {
         zip = new ZipFile(file)
         entries = zip.entries()
         entries.each
         { entry->
            if (entry.name.endsWith(".class"))
            {
               println "${file}"
               print "\t"
               ClassVersion.printCompiledMajorMinorVersions(new DataInputStream(zip.getInputStream(entry)), entry.name)
            }
         }
      }
      catch (ZipException zipEx)
      {
         println "Unable to open file ${file.name}"
      }
   }
}
println "\n"

The early portions of the output from running this script against the JAR used at the first of this post is shown next. All .class files contained in the JAR have the version of JDK they were compiled against printed to standard output.

Other Ideas

The scripts just shown demonstrate some of the utility achieved from being able to programatically access the version of JDK used to compile Java classes. Here are some other ideas for enhancements to these scripts. In some cases, I use these enhancements, but did not shown them here to retain better clarity and to avoid making the post even longer.

  • ClassVersion.java could have been written in Groovy.
  • ClassVersion.java's functionality would be more flexible if it returned individual pieces of information rather than printing it to standard output. Similarly, even returning of the entire Strings it produces would be more flexible than assuming callers want output written to standard output.
  • It would be easy to consolidate the above scripts to indicate JDK versions used to compile individual .class files directly accessed in directories as well as .class files contained in JAR files from the same script.
  • A useful variation of the demonstrated scripts is one that returns all .class files compiled with a particular version of JDK, before a particular version of the JDK, or after a particular version of the JDK.
Conclusion

The objective of this post has been to demonstrate programmatically determining the version of JDK used to compile Java source code into .class files. The post demonstrated determining version of JDK used for compilation based on the "major version" bytes of the JVM class file structure and then showed how to use Java APIs to read and process .class files and identify the version of JDK used to compile them. Finally, a couple of example scripts written in Groovy demonstrate the value of programatic access to this information.

Monday, January 19, 2015

Total Bummer: Pivotal Drops Groovy

Pivotal announced today that Groovy 2.4 And Grails 3.0 will be the last major releases under Pivotal sponsorship. This is big news that has not surprisingly created a lot of buzz in the blogosphere. In this post, I describe some of the questions that others and I are wondering about and speculate on Groovy's future.

After reading multiple Reddit references to this announcement, my initial thought was to see what Guillaume Laforge had to say about this. Apparently, a lot of people had the same idea because I encountered a 503 error when trying to access his page.

Fortunately, I did not have to wait for Laforge's blog to be available to get more insight from him on this announcement because there were a couple of interviews with him regarding the announcement already online: Voxxed.com's Pivotal’s "Sad and Odd" Decision to Set Groovy Adrift and InfoQ's Pivotal Pulls Groovy/Grails Funding. Since that time, Laforge's blog is available again and has a post on the subject called The Groovy project is looking for a new home. Anothrt person frequently and deservedly associated with Groovy, Graeme Rocher, has also posted on the subject: The Future of Groovy and Grails Sponsorship.

Laforge and Rocher were co-founders of G2One, which was acquired by SpringSource in late 2008. VMWare then acquired SpringSource about one year later (and VMWare had been owned by EMC since late 2003). EMC would later announce the spin-off of Pivotal in 2013 and Pivotal today announced the dropping of Groovy support as of 21 March 2015.

Questions, Answers, and Speculations

The posts referenced here in my post have collectively answered some of my questions about Groovy and at the same time presented additional questions.

Why is Pivotal dropping financial support of Groovy and Grails?
Answer: Pivotal's announcement: "The decision to conclude its sponsorship of Groovy and Grails is part of Pivotal’s larger strategy to concentrate resources on accelerating both commercial and open source projects that support its growing traction in Platform-as-a-Service, Data, and Agile development. Pivotal has determined that the time is right to let further development of Groovy and Grails be led by other interested parties in the open source community who can best serve the goals of those projects."
Who Might Sponsor Groovy and/or Grails Development?
Speculation: Many organizations benefit from Groovy and Gravy, but many probably aren't prepared to invest as fully in their development as G2One, SpringSource, VMWare, and even Pivotal have been. An example of an organization with an obvious vested interest in Groovy's future is GradleWare. Ken Kousen has tweeted and written a blog post on the opportunity of acquiring Groovy and Grails sponsorship.
What does this announcement mean for Groovy's future?
Answer Mixed with Speculation: Based on Laforge's and Rocher's posts, it seems clear that the core developers plan to continue working on Groovy. However, it is understandable that if this effort is not funded (sponsored), it will have to be at a slower pace than before (I have found through personal experience that home projects take a lot longer to complete than paid projects). I believe that Groovy has strong momentum already that will continue for some time. It is vital to Gradle, is used with other open source projects and tools such as SoapUI, and could have a promising future running on Android. I primarily use Groovy for scripting and simple "glue" tools in Java applications. The language is mature and serves these purposes well and I see no reason to stop using it at this time.
What does this mean for the future of the Spring Framework?
Speculation: There is some concern that perhaps Spring Framework could be jettisoned next from Pivotal. This seems unlikely to me, but I didn't expect Pivotal to drop Groovy either. As much as I love Groovy and as much effect on Java and JVM development as I acknowledge it has had, I think Spring Framework has been even more pervasive in Java EE development than Groovy and Grails have been in Java SE and Java EE development. That stated, Pivotal has shown that they are willing to, as most successful business are, drop a product offering that is perceived as not benefiting their strategy and bottom line. I can certainly understand if this development concerns users of Spring.
Is Standards-Based More Important than Being Open Source?
Answer: This is a difficult question to answer that often depends on numerous contextual factors including the tools being compared, the expected length of life of the products being built, etc. Fortunately, we often don't have to choose between these as many reference implementations in the Java world are also open source. However, a point can be made that any product that is not standard (including commercial or proprietary) is subject to losing support or not being available any longer. The theory is that if standards-based products are used, one can then shift to another implementation of that standard if necessary. However, a standard is only as good as its implementations and if there is only one realistic implementation of a standard, there's not much of an advantage of transferability there.

Conclusion

Although I understand Pivotal's motivation for dropping Groovy, I am still sorry to hear that news. I appreciate the effort that key Groovy contributes such as Laforge and Rocher have made and I appreciate the companies that have sponsored that work. Through this sponsorship and work, we have a really nice language to use for scripting and other purposes. I hope that a sponsor can be found for Groovy, but I plan to continue to use it either way.

Monday, January 6, 2014

Identifying Gradle Conventions

Configuration by convention has many advantages, especially in terms of conciseness because developers do not need to explicitly configure things that are implicitly configured through convention. When taking advantage of configuration by convention, however, one needs to be aware of the conventions. These conventions might be documented, but I always like it when I can programmatically determine the conventions because documentation can become outdated (same principle behind code always being correct and comments only sometimes being correct). I begin this post by looking at how to identify the specific conventions associated with the Gradle Java Plugin. I then generalize this approach to identify all properties associated with all tasks associated with the root project of a Gradle build.

The Gradle documentation on Gradle Plugins states the following regarding the importance of Gradle Plugins and what they add to a Gradle build:

Gradle at its core intentionally provides little useful functionality for real world automation. All of the useful features, such as the ability to compile Java code for example, are added by plugins. Plugins add new tasks (e.g. JavaCompile), domain objects (e.g. SourceSet), conventions (e.g. main Java source is located at src/main/java) as well as extending core objects and objects from other plugins.

This post looks at some of the tasks, domain objects, and conventions that the Java Plugin brings to a Gradle build. To start, I need a very simple Gradle build file. It consists solely of a single line that applies the Java plugin. It is shown next in the Gradle build file build-java-plugin.gradle.

build-java-plugin.gradle
apply plugin: 'java'

With that single-line Gradle build file in place, it's easy to see which Gradle tasks the plugin-provides by running the command gradle -b build-java-plugin.gradle tasks. The next two screen snapshots show the output of running an empty Gradle build file followed by the output of running the Gradle build file with only the application of the Java plugin.

By comparing the output from running Gradle "tasks" against an empty build file to the output from running Gradle "tasks" against the build file with the Java plugin applied, we can see that the Gradle has the same set of "Build Setup tasks" and "Help tasks" whether the plugin is applied or not. More significantly, we see that the Java plugin adds many new tasks categorized as "Build tasks" (assemble, build, buildDependents, buildNeeded, classes, clean, jar, testClasses), "Documentation tasks" (javadoc), "Verification tasks" (check, test), and "Rules".

One feature I enjoy in Gradle 1.10 that Gradle 1.8 (the previous version I used) did not have is the ability on the command line to ask for details on a specific Gradle task. This is demonstrated in the next screen snapshot for Java Plugin tasks compileJava, jar, and javadoc. All three tasks have details written to standard output by using the help --task <task_name> command on the command line. These details on the Java Plugin tasks can also be found in the Gradle User Guide.

Because Gradle is built on Groovy, it's fairly easy to determine characteristics of the Java Plugin using "brute force." The next code listing, for build-java-plugin-properties.gradle, demonstrates using Groovy to determine the Gradle properties (those which can be specified with -P as opposed to system properties specified with -D) available to the build script before and after applying the Java plugin and then uses Groovy's highly convenient overridden subtraction operator to find the differences. The names and values of all of the properties added to the Gradle script by the Java Plugin (except the property "properties") are presented in alphabetical order.

// build-java-plugin-properties.gradle
//
// Displays properties that Gradle Java Plugin adds beyond properties already
// specified for any Gradle build.

def propertiesBefore = this.properties

apply plugin: 'java'

def propertiesAfter = this.properties

def extraProperties = propertiesAfter - propertiesBefore

def extraPropertiesKeys = new TreeSet<String>()
extraProperties.each
{ property ->
   if (property.key != "properties")
   {
      extraPropertiesKeys.add(property.key)
   }
}

extraPropertiesKeys.each
{ key ->
   println "${key} : ${extraProperties.get(key)}"
}

The next image shows a screen snapshot with the output from running this script. The screen snapshot does not show the full output, but a larger piece of the output (all the properties) is shown in text after the image.

Output from Running Above Gradle Script to See Java Plugin Properties

apiDocTitle : gradleExample API
archivesBaseName : gradleExample
assemble : task ':assemble'
binaries : [classes 'main', classes 'test']
build : task ':build'
buildDependents : task ':buildDependents'
buildNeeded : task ':buildNeeded'
buildTasks : [build]
check : task ':check'
classes : task ':classes'
clean : task ':clean'
compileJava : task ':compileJava'
compileTestJava : task ':compileTestJava'
defaultArtifacts : org.gradle.api.internal.plugins.DefaultArtifactPublicationSet_Decorated@bc80d8
dependencyCacheDir : C:\java\examples\groovyExamples\gradleExample\build\dependency-cache
dependencyCacheDirName : dependency-cache
distsDir : C:\java\examples\groovyExamples\gradleExample\build\distributions
distsDirName : distributions
docsDir : C:\java\examples\groovyExamples\gradleExample\build\docs
docsDirName : docs
inheritedScope : org.gradle.api.internal.ExtensibleDynamicObject$InheritedDynamicObject@c10304
jar : task ':jar'
javadoc : task ':javadoc'
libsDir : C:\java\examples\groovyExamples\gradleExample\build\libs
libsDirName : libs
manifest : org.gradle.api.java.archives.internal.DefaultManifest@1ad3677
metaInf : []
module : org.gradle.api.internal.artifacts.ProjectBackedModule@d2eead
processResources : task ':processResources'
processTestResources : task ':processTestResources'
rebuildTasks : [clean, build]
reporting : org.gradle.api.reporting.ReportingExtension_Decorated@33ab8f
reportsDir : C:\java\examples\groovyExamples\gradleExample\build\reports
reportsDirName : reports
runtimeClasspath : file collection
sourceCompatibility : 1.7
sourceSets : [source set 'main', source set 'test']
sources : [[source set 'main:java', source set 'main:resources'], [source set 'test:java', source set 'test:resources']]
status : integration
targetCompatibility : 1.7
test : task ':test'
testClasses : task ':testClasses'
testReportDir : C:\java\examples\groovyExamples\gradleExample\build\reports\tests
testReportDirName : tests
testResultsDir : C:\java\examples\groovyExamples\gradleExample\build\test-results
testResultsDirName : test-results

Gradle makes it easy to see all the Gradle properties using the command "gradle properties", but this command line action shows all properties regardless of their source (Gradle itself or a plugin).

Each Gradle task that the Java Plugin adds to the build has its own set of properties. These properties can be identified in the Gradle Build Language Reference. The Task Types section of that document has links to each task type. The linked-to pages on each task type have details on the properties supported by that task type. For example, the Task Type JavaCompile is listed on its page as having properties such as classpath, destinationDir, and source.

The following rather extensive script displays the settings for the properties of the compileJava, jar, and javadoc Gradle Java Plugin tasks. This script demonstrates how powerful it can be to apply Groovy to identifying Gradle build settings. The script could be shorter if more reflection was used, but calling the tasks' properties out explicitly does have advantages in terms of readability and as a reference for what properties are available on each task.

build-java-plugin-metadata.gradle
// build-java-plugin-metadata.gradle
//
// Displays the properties associated with the Gradle Java Plugin tasks
// of "compileJava", "jar", and "javadoc".

import groovy.transform.Field

apply plugin: 'java'

@Field int MAX_COLUMNS = 80
@Field String headerSeparator = "=".multiply(MAX_COLUMNS)

printCompileJavaProperties()
printJarProperties()
printJavadocProperties()

def printCompileJavaProperties()
{
   printHeader("compileJava Task")
   println "compileJava.classpath:\n${extractStringRepresentation(compileJava.classpath)}"
   println "compileJava.destinationDir:\n${extractStringRepresentation(compileJava.destinationDir)}"
   println "compileJava.source:\n${extractStringRepresentation(compileJava.source)}"
   println "compileJava.options:\n${extractStringRepresentation(compileJava.options)}"
   println "compileJava.includes:\n${extractStringRepresentation(compileJava.includes)}"
   println "compileJava.excludes:\n${extractStringRepresentation(compileJava.excludes)}"
   println "compileJava.sourceCompatibility:\n${extractStringRepresentation(compileJava.sourceCompatibility)}"
   println "compileJava.targetCompatibility:\n${extractStringRepresentation(compileJava.targetCompatibility)}"
}

def printJarProperties()
{
   printHeader("jar Task")
   println "jar.appendix:\n${extractStringRepresentation(jar.appendix)}"
   println "jar.archiveName:\n${extractStringRepresentation(jar.archiveName)}"
   println "jar.archivePath:\n${extractStringRepresentation(jar.archivePath)}"
   println "jar.baseName:\n${extractStringRepresentation(jar.baseName)}"
   println "jar.caseSensitive:\n${extractStringRepresentation(jar.caseSensitive)}"
   println "jar.classifier:\n${extractStringRepresentation(jar.classifier)}"
   println "jar.destinationDir:\n${extractStringRepresentation(jar.destinationDir)}"
   println "jar.dirMode:\n${extractStringRepresentation(jar.dirMode)}"
   println "jar.duplicatesStrategy:\n${extractStringRepresentation(jar.duplicatesStrategy)}"
   println "jar.entryCompression:\n${extractStringRepresentation(jar.entryCompression)}"
   println "jar.excludes:\n${extractStringRepresentation(jar.excludes)}"
   println "jar.extension:\n${extractStringRepresentation(jar.extension)}"
   println "jar.fileMode:\n${extractStringRepresentation(jar.fileMode)}"
   println "jar.includeEmptyDirs:\n${extractStringRepresentation(jar.includeEmptyDirs)}"
   println "jar.includes:\n${extractStringRepresentation(jar.includes)}"
   println "jar.manifest:\n${extractStringRepresentation(jar.manifest)}"
   println "jar.source:\n${extractStringRepresentation(jar.source)}"
   println "jar.version:\n${extractStringRepresentation(jar.version)}"
}

def printJavadocProperties()
{
   printHeader("javadoc Task")
   println "javadoc.classpath:\n${extractStringRepresentation(javadoc.classpath)}"
   println "javadoc.destinationDir:\n${extractStringRepresentation(javadoc.destinationDir)}"
   println "javadoc.excludes:\n${extractStringRepresentation(javadoc.excludes)}"
   println "javadoc.executable:\n${extractStringRepresentation(javadoc.executable)}"
   println "javadoc.failOnError:\n${extractStringRepresentation(javadoc.failOnError)}"
   println "javadoc.includes:\n${extractStringRepresentation(javadoc.includes)}"
   println "javadoc.maxMemory:\n${extractStringRepresentation(javadoc.maxMemory)}"
   println "javadoc.options:\n${extractStringRepresentation(javadoc.options)}"
   println "javadoc.source:\n${extractStringRepresentation(javadoc.source)}"
   println "javadoc.title:\n${extractStringRepresentation(javadoc.title)}"
}

def String extractStringRepresentation(Object object)
{
   String returnString
   if (object in String)
   {
      returnString = "\t${object}\n"
   }
   else if (object in File)
   {
      returnString = "\t${object.canonicalPath}\n"
   }
   else if (object in FileCollection)  // FileTree is a FileCollection
   {
      StringBuilder filesStr = new StringBuilder()
      def files = object.files
      files.each
      { file ->
         filesStr << "\t" << file.canonicalPath << "\n" 
      }
      returnString = filesStr.toString()
   }
   else if (object in CompileOptions)
   {
      StringBuilder compileOptionsStr = new StringBuilder()
      def compileProperties = object.properties
      compileProperties.each
      { compileProperty ->
         if (compileProperty.value in DebugOptions)
         {
            compileOptionsStr << "\t" << compileProperty.key << ": " << extractStringRepresentation(compileProperty.value) << "\n"
         }
         else if (compileProperty.value in DependOptions)
         {
            compileOptionsStr << "\t" << compileProperty.key << ": " << extractStringRepresentation(compileProperty.value) << "\n"
         }
         else if (compileProperty.value in ForkOptions)
         {
            compileOptionsStr << "\t" << compileProperty.key << ": " << extractStringRepresentation(compileProperty.value) << "\n"
         }
         else if (compileProperty.key != "class")
         {
            compileOptionsStr << "\t" << compileProperty.key << ": " << compileProperty.value << "\n"
         } 
      }
      returnString = compileOptionsStr.toString()
   }
   else if (object in DebugOptions)
   {
      returnString = "\t${object.debugLevel}"
   }
   else if (object in DependOptions)
   {
      returnString = "\t${object.classpath}"
   }
   else if (object in ForkOptions)
   {
      returnString = "\t${object.executable} executable with ${object.tempDir} temp directory" 
   }
   else if (object in Set || object in Boolean || object in Number || object in Enum)
   {
      returnString = "\t${object.toString()}\n"
   }
   else if (object in Manifest)
   {
      StringBuilder manifestStr = new StringBuilder()
      def manifestAttributes = object.getAttributes()
      manifestAttributes.each
      { manifestAttribute ->
         manifestStr << "\t" << manifestAttribute.key << ": " << manifestAttribute.value << "\n" 
      }
      returnString = manifestStr.toString()
   }
   else if (object in MinimalJavadocOptions)
   {
      returnString = extractJavadocOptionsAsString(object)
   }
   else if (object == null)
   {
      returnString = "\tnull\n"
   }
   else
   {
      returnString = "\t${object?.class} was unexpected type.\n"
   }
   return returnString
}

def String extractJavadocOptionsAsString(MinimalJavadocOptions javadocOptions)
{
   StringBuilder javadocOptionsStr = new StringBuilder()

   javadocOptionsStr << "\tjavadoc.bootClasspath:"
   def bootClasspathFiles = javadocOptions.bootClasspath
   bootClasspathFiles.each
   { bootClasspathFile ->
      javadocOptionsStr << "\t\t" << bootClasspathFile.canonicalName << "\n" 
   }
   javadocOptionsStr << "\n"

   javadocOptionsStr << "\tjavadocOptions.classpath:"
   def classpathFiles = javadocOptions.classpath
   classpathFiles.each
   { classpathFile ->
      javadocOptionsStr << "\t\t" << classpathFile.canonicalName << "\n"
   }
   javadocOptionsStr << "\n"

   javadocOptionsStr << "\tjavadocOptions.destinationDirectory: " << javadocOptions.destinationDirectory?.canonicalName << "\n"

   javadocOptionsStr << "\tjavadocOptions.doclet: " << javadocOptions.doclet << "\n"

   javadocOptionsStr << "\tjavadocOptions.docletpath:"
   def docletpath = javadocOptions.docletpath
   docletpath.each
   { docletEntry ->
      javadocOptionsStr << "\t\t" << docletEntry.canonicalName << "\n"
   }
   javadocOptionsStr << "\n"

   javadocOptionsStr << "\tjavadocOptions.encoding: " << javadocOptions.encoding << "\n"

   javadocOptionsStr << "\tjavadocOptions.extDirs:"
   def extDirs = javadocOptions.extDirs
   extDirs.each
   { extDir ->
      javadocOptionsStr << "\t\t" << extDir.canonicalName << "\n"
   }
   javadocOptionsStr << "\n"

   javadocOptionsStr << "\tjavadocOptions.header: " << javadocOptions.header << "\n"

   javadocOptionsStr << "\tjavadocOptions.JFlags:"
   def jflags = javadocOptions.JFlags
   jflags.each
   { jflag ->
      javadocOptionsStr << "\t\t" << jflag << "\n"
   }
   javadocOptionsStr << "\n"

   javadocOptionsStr << "\tjavadocOptions.locale: " << javadocOptions.locale << "\n"

   javadocOptionsStr << "\tjavadocOptions.memberLevel: " << javadocOptions.memberLevel << "\n"

   javadocOptionsStr << "\tjavadocOptions.optionFiles:"
   def optionFiles = javadocOptions.optionFiles
   optionFiles.each
   { optionFile ->
      javadocOptionsStr << "\t\t" << optionFile.canonicalName << "\n"
   }
   javadocOptionsStr << "\n"

   javadocOptionsStr << "\tjavadocOptions.outputLevel: " << javadocOptions.outputLevel << "\n"

   javadocOptionsStr << "\tjavadocOptions.overview: " << javadocOptions.overview << "\n"

   javadocOptionsStr << "\tjavadocOptions.source: " << javadocOptions.source << "\n"

   javadocOptionsStr << "\tjavadocOptions.sourceNames:"
   def sourceNames = javadocOptions.sourceNames
   sourceNames.each
   { sourceName ->
      javadocOptionsStr << "\t\t" << sourceName << "\n"
   }
   javadocOptionsStr << "\n"

   javadocOptionsStr << "\tjavadocOptions.windowTitle: " << javadocOptions.windowTitle << "\n"

   return javadocOptionsStr.toString()
}

def printHeader(String headerText)
{
   println headerSeparator
   println "= ${headerText.center(MAX_COLUMNS-4)} ="
   println headerSeparator
}

I used the Groovy @Field annotation in this build file to make the variable to which it was applied available to methods in the build file. The @Field annotation was not available until Groovy 1.8 and this reminded me of something else significant to point out about Gradle and Groovy here: Gradle uses its own prepackaged Groovy rather than any other version of Groovy that might be installed on one's machine. You can determine which version of Groovy that is with the gradle --version command. The next screen snapshot demonstrates that my version of Groovy (2.1.6) is different than the version of Groovy (1.8.6) used by my installation of Gradle (Gradle 1.10). Because Gradle 1.10 comes with Groovy 1.8.6, I had the @Field annotation at my disposal.

Because the output from the last script is so lengthy, I show it here as text rather than in an image.

Output of Running Gradle on build-java-plugin-metadata.gradle
================================================================================
=                               compileJava Task                               =
================================================================================
compileJava.classpath:

compileJava.destinationDir:
 C:\java\examples\groovyExamples\gradleExample\build\classes\main

compileJava.source:
 C:\java\examples\groovyExamples\gradleExample\src\main\java\dustin\examples\Main.java
 C:\java\examples\groovyExamples\gradleExample\src\main\java\dustin\examples\Main2.java
 C:\java\examples\groovyExamples\gradleExample\src\main\java\dustin\examples\Main3.java
 C:\java\examples\groovyExamples\gradleExample\src\main\java\dustin\examples\Main4.java
 C:\java\examples\groovyExamples\gradleExample\src\main\java\dustin\examples\Temperature.java
 C:\java\examples\groovyExamples\gradleExample\src\main\java\dustin\examples\TemperatureScale.java
 C:\java\examples\groovyExamples\gradleExample\src\main\java\dustin\examples\TemperatureUnit.java
 C:\java\examples\groovyExamples\gradleExample\src\main\java\dustin\examples\TemperatureUnit2.java
 C:\java\examples\groovyExamples\gradleExample\src\main\java\dustin\examples\TemperatureUnit3.java

compileJava.options:
 bootClasspath: null
 fork: false
 encoding: null
 deprecation: false
 warnings: true
 forkOptions:  null executable with null temp directory
 failOnError: true
 useDepend: false
 includeJavaRuntime: false
 useAnt: false
 compilerArgs: []
 debug: true
 extensionDirs: null
 compiler: null
 debugOptions:  null
 verbose: false
 optimize: false
 dependOptions:  
 listFiles: false

compileJava.includes:
 []

compileJava.excludes:
 []

compileJava.sourceCompatibility:
 1.7

compileJava.targetCompatibility:
 1.7

================================================================================
=                                   jar Task                                   =
================================================================================
jar.appendix:
 null

jar.archiveName:
 gradleExample.jar

jar.archivePath:
 C:\java\examples\groovyExamples\gradleExample\build\libs\gradleExample.jar

jar.baseName:
 gradleExample

jar.caseSensitive:
 true

jar.classifier:
 

jar.destinationDir:
 C:\java\examples\groovyExamples\gradleExample\build\libs

jar.dirMode:
 null

jar.duplicatesStrategy:
 INCLUDE

jar.entryCompression:
 DEFLATED

jar.excludes:
 []

jar.extension:
 jar

jar.fileMode:
 null

jar.includeEmptyDirs:
 true

jar.includes:
 []

jar.manifest:
 Manifest-Version: 1.0

jar.source:
 C:\java\examples\groovyExamples\gradleExample\build\tmp\jar\MANIFEST.MF

jar.version:
 null

================================================================================
=                                 javadoc Task                                 =
================================================================================
javadoc.classpath:
 C:\java\examples\groovyExamples\gradleExample\build\classes\main
 C:\java\examples\groovyExamples\gradleExample\build\resources\main

javadoc.destinationDir:
 C:\java\examples\groovyExamples\gradleExample\build\docs\javadoc

javadoc.excludes:
 []

javadoc.executable:
 null

javadoc.failOnError:
 true

javadoc.includes:
 []

javadoc.maxMemory:
 null

javadoc.options:
 javadoc.bootClasspath:
 javadocOptions.classpath:
 javadocOptions.destinationDirectory: null
 javadocOptions.doclet: null
 javadocOptions.docletpath:
 javadocOptions.encoding: null
 javadocOptions.extDirs:
 javadocOptions.header: null
 javadocOptions.JFlags:
 javadocOptions.locale: null
 javadocOptions.memberLevel: null
 javadocOptions.optionFiles:
 javadocOptions.outputLevel: QUIET
 javadocOptions.overview: null
 javadocOptions.source: null
 javadocOptions.sourceNames:
 javadocOptions.windowTitle: null

javadoc.source:
 C:\java\examples\groovyExamples\gradleExample\src\main\java\dustin\examples\Main.java
 C:\java\examples\groovyExamples\gradleExample\src\main\java\dustin\examples\Main2.java
 C:\java\examples\groovyExamples\gradleExample\src\main\java\dustin\examples\Main3.java
 C:\java\examples\groovyExamples\gradleExample\src\main\java\dustin\examples\Main4.java
 C:\java\examples\groovyExamples\gradleExample\src\main\java\dustin\examples\Temperature.java
 C:\java\examples\groovyExamples\gradleExample\src\main\java\dustin\examples\TemperatureScale.java
 C:\java\examples\groovyExamples\gradleExample\src\main\java\dustin\examples\TemperatureUnit.java
 C:\java\examples\groovyExamples\gradleExample\src\main\java\dustin\examples\TemperatureUnit2.java
 C:\java\examples\groovyExamples\gradleExample\src\main\java\dustin\examples\TemperatureUnit3.java

javadoc.title:
 gradleExample API

:help

Welcome to Gradle 1.10.

To run a build, run gradle  ...

To see a list of available tasks, run gradle tasks

To see a list of command-line options, run gradle --help

BUILD SUCCESSFUL

Total time: 14.041 secs

The example shown above works well for identifying specific properties associated with the Java Gradle plugin. This works fine, but its limitations include the need to write explicit code for each property whose value is desired. This implies further limitations of not necessarily knowing all the properties that are available (I used the documentation to explicitly print out values in the example above). A further implied limitation is that the script above will not display any properties values that are added to those tasks in the future. The next Gradle build example is based on the previous example, but this example does not explicitly state the tasks and properties to display. Instead, it finds all Tasks associated with the root project and then prints all properties associated with each of those Tasks.

build-java-plugin-metadata-reflection.gradle
// build-java-plugin-metadata-reflection.gradle
//
// Displays the properties associated with the tasks associated with the Gradle
// root project.
//

import groovy.transform.Field

apply plugin: 'java'

@Field int MAX_COLUMNS = 80
@Field String headerSeparator = "=".multiply(MAX_COLUMNS)

def rootProject = getRootProject()
def tasks = rootProject.tasks
tasks.each
{ task ->
   printTaskProperties(task)
}


def printTaskProperties(Task task)
{
   printHeader("Task " + task.name)
   def taskProperties = task.properties
   taskProperties.each
   { taskProperty ->
      println "${task.name}.${taskProperty.key}=${extractStringRepresentation(taskProperty.value)}"
   }
}

def String extractStringRepresentation(Object object)
{
   String returnString
   if (object in String)
   {
      returnString = "\t${object}\n"
   }
   else if (object in File)
   {
      returnString = "\t${object.canonicalPath}\n"
   }
   else if (object in FileCollection)  // FileTree is a FileCollection
   {
      StringBuilder filesStr = new StringBuilder()
      def files = object.files
      files.each
      { file ->
         filesStr << "\t" << file.canonicalPath << "\n" 
      }
      returnString = filesStr.toString()
   }
   else if (object in CompileOptions)
   {
      StringBuilder compileOptionsStr = new StringBuilder()
      def compileProperties = object.properties
      compileProperties.each
      { compileProperty ->
         if (compileProperty.value in DebugOptions)
         {
            compileOptionsStr << "\t" << compileProperty.key << ": " << extractStringRepresentation(compileProperty.value) << "\n"
         }
         else if (compileProperty.value in DependOptions)
         {
            compileOptionsStr << "\t" << compileProperty.key << ": " << extractStringRepresentation(compileProperty.value) << "\n"
         }
         else if (compileProperty.value in ForkOptions)
         {
            compileOptionsStr << "\t" << compileProperty.key << ": " << extractStringRepresentation(compileProperty.value) << "\n"
         }
         else if (compileProperty.key != "class")
         {
            compileOptionsStr << "\t" << compileProperty.key << ": " << compileProperty.value << "\n"
         } 
      }
      returnString = compileOptionsStr.toString()
   }
   else if (object in DebugOptions)
   {
      returnString = "\t${object.debugLevel}"
   }
   else if (object in DependOptions)
   {
      returnString = "\t${object.classpath}"
   }
   else if (object in ForkOptions)
   {
      returnString = "\t${object.executable} executable with ${object.tempDir} temp directory" 
   }
   else if (object in Set || object in List || object in Boolean || object in Number || object in Enum || object in Class)
   {
      returnString = "\t${object.toString()}\n"
   }
   else if (object in Manifest)
   {
      StringBuilder manifestStr = new StringBuilder()
      def manifestAttributes = object.getAttributes()
      manifestAttributes.each
      { manifestAttribute ->
         manifestStr << "\t" << manifestAttribute.key << ": " << manifestAttribute.value << "\n" 
      }
      returnString = manifestStr.toString()
   }
   else if (object in MinimalJavadocOptions)
   {
      returnString = extractJavadocOptionsAsString(object)
   }
   else if (object in Convention)
   {
      StringBuilder conventionStr = new StringBuilder()
      object.plugins.each?.keyset
      { plugin ->
         conventionStr << "\t" << plugin << "\n"
      }
      returnString = conventionStr.toString()
   }
   else if (object in LoggingManager)
   {
      returnString = "\n\tCurrent Log Level: ${object.level}\n\tStandard Error: ${object.standardErrorCaptureLevel}\n\tStandard Output: ${object.standardOutputCaptureLevel}\n"
   }
   else if (object == null)
   {
      returnString = "\tnull\n"
   }
   else
   {
      returnString = "\t${object?.class} was unexpected type with value of ${object}.\n"
   }
   return returnString
}

def String extractJavadocOptionsAsString(MinimalJavadocOptions javadocOptions)
{
   StringBuilder javadocOptionsStr = new StringBuilder()

   javadocOptionsStr << "\tjavadoc.bootClasspath:"
   def bootClasspathFiles = javadocOptions.bootClasspath
   bootClasspathFiles.each
   { bootClasspathFile ->
      javadocOptionsStr << "\t\t" << bootClasspathFile.canonicalName << "\n" 
   }
   javadocOptionsStr << "\n"

   javadocOptionsStr << "\tjavadocOptions.classpath:"
   def classpathFiles = javadocOptions.classpath
   classpathFiles.each
   { classpathFile ->
      javadocOptionsStr << "\t\t" << classpathFile.canonicalName << "\n"
   }
   javadocOptionsStr << "\n"

   javadocOptionsStr << "\tjavadocOptions.destinationDirectory: " << javadocOptions.destinationDirectory?.canonicalName << "\n"

   javadocOptionsStr << "\tjavadocOptions.doclet: " << javadocOptions.doclet << "\n"

   javadocOptionsStr << "\tjavadocOptions.docletpath:"
   def docletpath = javadocOptions.docletpath
   docletpath.each
   { docletEntry ->
      javadocOptionsStr << "\t\t" << docletEntry.canonicalName << "\n"
   }
   javadocOptionsStr << "\n"

   javadocOptionsStr << "\tjavadocOptions.encoding: " << javadocOptions.encoding << "\n"

   javadocOptionsStr << "\tjavadocOptions.extDirs:"
   def extDirs = javadocOptions.extDirs
   extDirs.each
   { extDir ->
      javadocOptionsStr << "\t\t" << extDir.canonicalName << "\n"
   }
   javadocOptionsStr << "\n"

   javadocOptionsStr << "\tjavadocOptions.header: " << javadocOptions.header << "\n"

   javadocOptionsStr << "\tjavadocOptions.JFlags:"
   def jflags = javadocOptions.JFlags
   jflags.each
   { jflag ->
      javadocOptionsStr << "\t\t" << jflag << "\n"
   }
   javadocOptionsStr << "\n"

   javadocOptionsStr << "\tjavadocOptions.locale: " << javadocOptions.locale << "\n"

   javadocOptionsStr << "\tjavadocOptions.memberLevel: " << javadocOptions.memberLevel << "\n"

   javadocOptionsStr << "\tjavadocOptions.optionFiles:"
   def optionFiles = javadocOptions.optionFiles
   optionFiles.each
   { optionFile ->
      javadocOptionsStr << "\t\t" << optionFile.canonicalName << "\n"
   }
   javadocOptionsStr << "\n"

   javadocOptionsStr << "\tjavadocOptions.outputLevel: " << javadocOptions.outputLevel << "\n"

   javadocOptionsStr << "\tjavadocOptions.overview: " << javadocOptions.overview << "\n"

   javadocOptionsStr << "\tjavadocOptions.source: " << javadocOptions.source << "\n"

   javadocOptionsStr << "\tjavadocOptions.sourceNames:"
   def sourceNames = javadocOptions.sourceNames
   sourceNames.each
   { sourceName ->
      javadocOptionsStr << "\t\t" << sourceName << "\n"
   }
   javadocOptionsStr << "\n"

   javadocOptionsStr << "\tjavadocOptions.windowTitle: " << javadocOptions.windowTitle << "\n"

   return javadocOptionsStr.toString()
}

def printHeader(String headerText)
{
   println headerSeparator
   println "= ${headerText.center(MAX_COLUMNS-4)} ="
   println headerSeparator
}

Because this output is for all properties associated with all Tasks associated with the Gradle build's root project, the output is too lengthy to include here. Not all of the property value instances have classes that the extractStringRepresentation(Object object) method is prepared to handle, but those cases could be added to the if-else if structure of that method to handle them. This version of the Gradle build is more generic than the earlier one and prints out properties associated with Tasks that are grouped by Task.

Because a Gradle build is tightly coupled to Groovy, Groovy syntax and features can be used to learn more about the Gradle build. The examples in this post took advantage of numerous Groovy niceties. The reason that the Gradle build code above is so verbose is because most of the Gradle classes used for property values do NOT have overridden toString() methods and so no really useful output is shown without special code to call specific methods to get useful representations. I didn't do it in this post's examples, but another option to deal with lack of overridden toString() methods would be to use Groovy's interception capabilities (metaClass.invokeMethod) to intercept calls to toString() and provide an overridden version. That would be essentially the same code as used above, but would be encapsulated in the intercepting objects rather than contained in the script code.

Conclusion

Gradle has really nice documentation (especially the Gradle User Guide and the Gradle Build Language Reference) and most of the tasks and properties associated with the Java Plugin for Gradle (and other plugins) are easily accessible from that documentation. However, I like to know how to programmatically identify important conventions in case the documentation is ever mistaken or I use a version different than the documentation supports. Another objective of this post has been to demonstrate how useful it can be to know Groovy when working with Gradle. It is for this reason that I believe that the rising prominence of Gradle cannot help but increase interest in Groovy.