Monday, June 3, 2013

Gradle War Overlay Plugin (and why you don't need one)

Please excuse the (somewhat) misleading title, but I wanted to grab anyone actually searching for a Gradle-based solution to using the maven-war-overlay-plugin.

First some background. Certain spring-based products (such as Jasig's CAS) recommend integrating via the maven-war-overlay-plugin. Our project uses Gradle, and I wanted to replicate that behavior using that tool so we could use the same tool for everything.

Of course the first thing I looked for was the string "gradle-war-overlay-plugin." Turns out because Gradle is so flexible, I didn't need a separate plugin for this behavior. Honestly I was debating whether or not to write this functionality into a plugin, but I opted to write this post instead. First, this is what the pom.xml file would look like using the recommended setup:

Basically if you use Maven, you can declare your project as an overlay onto an existing war file that's retrieved from maven central. Here is how you reproduce that behavior in gradle:

What I've done is introduced the concept of an 'underlay'. Several reasons for this:

  • In Gradle it seemed easier to customize the behavior of my dependencies than to introduce a new project type of 'overlay'
  • Calling the dependency an 'overlay' dependency would imply that the files in those dependencies over-write the files that you've developed
  • It is a real word
So a few key differences to point out:
  • 2 new configurations:
    • warUnderlay - any war file that we want to expand "under" our project (i.e., without overwriting anything
    • pomWorkaround - In maven, a vanilla dependency will pick up a pom artifact type, but Gradle always assumes a jar dependency type. These are for dependencies that don't exist in jar form, but the transient dependencies for cas-server-webapp (for example) list them without a classifier. As far as I can tell this is the only way to override the artifact type for this dependency
  • To expand a bit more on the pomWorkaround section:
    • Download everything in pom form
    • Prevent the compile dependency from attempting to download the jar files that don't exist by subtracting the version number and '.pom' from the filename using regex
  • The compile configuration includes all the transient dependencies of the warUnderlay configuration, but does not extract them from an actual war file. An alternative to this approach is to include cas-server-core, but doing it this way matches the maven method more closely.
  • The war task has one more action tacked onto it:
    • First unwar all the warUnderlay dependencies. Gradle's AntBuilder is really helpful here
    • Then unwar the war file that was generated for us by Gradle before - the one without the war underlay. We want this to *overlay* what we've already *underlayed* in the previous step
    • Finally re-create the war file using ant again
This isn't the most terse code, nor is it the most readable either. This seemed to be the most generic I could make it..Use the following to test:

$ gradle jettyRunWar # or gradle tomcatRunWar

Here are some known weaknesses to this method:
  • IDEs will not pick up on it and will most likely generate a TON of errors
  • The project cannot be run in "exploded" mode; you must build the war and deploy it every time
  • Related to the first point, this isn't following a proper Gradle lifecycle. I'm sure there's a way to define this so that Gradle recognizes the war files as inputs into generating the final war file. I haven't spent too much time on it (past dabbling a bit with zipTree), so if you can figure this part out you could probably solve all three of these issues.
So... should I make it into a plugin? Please let me know what you think, contribute, and feel free to use this code in your project if you need to.

Cheers!
Joe

1 comment: