Extending Gradle war task, where to put the code?


(Mark Waschkowski) #1

After much trial and error, I seem to have a working, custom war task, as shown below. However, I’m having an issue where I can’t configure a property of the class from in the build.gradle of a consumer of my custom war task.

I’m sure that I’ve put the code in the custom war task in the wrong spot, but couldn’t find a working example of extending the gradle war, so advice appreciated. I tried putting the code in doTask or doFirst, but my custom war file was not generated in either of those spots, it was only generated when I put the code into the constructor, as shown below.

The following is not working - directoryName always ends up as the default value assigned in the custom war task (ie. bentallProd), unless I put the RdsWarTask code into doTask, but then the war itself is not generated!

How can I get the code to work properly and still be able to configure the task from the consumer?

task bentallUatWar(type: com.refineddata.RdsWarTask){
  directoryName = 'bentallUat'
}
package com.refineddata
  import org.gradle.api.tasks.bundling.War
import org.gradle.api.tasks.TaskAction
import org.apache.tools.ant.filters.ReplaceTokens
  /**
* Custom War task for rds
*/
class RdsWarTask extends War {
   String directoryName = 'bentallProd'
   RdsWarTask(){
  super()
      println 'RdsWarTask started... '
    println 'directoryName=' + directoryName
  description = 'rds war task'
  baseName = directoryName
   //exclude the local web.xml from the build
  exclude '**/web.xml'
  //copy properties files to classes
  from ('env/' + directoryName){
   include '*.properties'
   into '/WEB-INF/classes'
  }
  // copies a file to WEB-INF/web.xml
  def envWebXmlName = 'env/' + directoryName + '/web.xml'
  //check to make sure default web.xml and the web.xml.properties file exist
  assert project.file('env/defaultWeb.xml').exists()
  assert project.file('env/' + directoryName + '/web.xml.properties').exists()
  //copy and filter default web.xml file
  def Properties webProps = new Properties()
  webProps.load(new FileInputStream('env/' + directoryName + '/web.xml.properties'))
  from ('env'){
   include 'defaultWeb.xml'
   into '/WEB-INF'
   rename { 'web.xml'}
   filter(ReplaceTokens, tokens: webProps )
  }
    println 'RdsWarTask done.'
    doLast{
     }
        }
    @TaskAction
    def doTask() {
     }
}

(sethgoings) #2

This thread might help: http://forums.gradle.org/gradle/topics/war_task_related_question


(Mark Waschkowski) #3

Thanks for the suggestion. I tried what was discussed in that thread, but got a strange error message

I tried:

class RdsWarTask extends War {
   private String directoryName;
    private File envFolder = project.file('env');
     RdsWarTask(){
  super()
    println "directoryName=" + getDirectoryName()
    println "baseName=" + baseName
    println "baseName=" + getBaseName()
    println envFolder.exists()
      //baseName = getDirectoryName()
        //exclude the local web.xml from the build
  exclude '**/web.xml'
        //copy properties files to classes
  from ('env/' + from{directoryName}){
   include '*.properties'
   into '/WEB-INF/classes'
  }
                ...
task bentallProdWar(type: com.refineddata.RdsWarTask){
  directoryName = 'bentallUat'
}

but I get an internal error from Gradle:

FAILURE: Build aborted because of an internal error.
  * What went wrong:
Build aborted because of an unexpected internal error. Please file an issue at: http://forums.gradle.org.
  * Try:
Run with --debug option to get additional debug info.
  * Exception is:
org.gradle.api.UncheckedIOException: Could not normalize path for file 'C:\eclipseProjects\sustainability\env\task ':bentallProdWar''.

(Mark Waschkowski) #4

I tried various ways to use from{} in various places, but couldn’t resolve this. This is basically the problem in its shortest form:

I would like to extend the war task and have the consumer set properties on the custom task, AND the values have to be available in the constructor (I’m not sure why, but others have noted the same behavior) , but so far values have come through as null.

The below does NOT work.

class RdsWarTask extends War {
   private String directoryName
   RdsWarTask(){
  super()
                //alwasy prints null
                println "directoryName=" + directoryName
                ...

Consumer code

task bentallProd(type: com.refineddata.RdsWarTask){
  directoryName = 'env/bentallProd'
}

Appears to relate to:

Any help to resolve this much appreciated!

Thank you,

Mark


(Mark Waschkowski) #5

Just tried

RdsWarTask(){
  super()
    //exclude the local web.xml from the build
    exclude '**/web.xml'
    //copy properties files to classes
    from ({'env/' + directoryName}){
      exclude 'web.xml.properties'
      include '*.properties'
      into '/WEB-INF/classes'
    }

(note the {} use in the from statement)

but still no go. If directory name is hardcoded, then the properties files come through in the war no problem.

I’m really dead in the water here - is it possible to extend a War class and reconfigure its properties, or should I just give up and create my own War task?

Thanks,

Mark


(Mark Waschkowski) #6

btw - also tried the following, but no success:

RdsWarTask(){
  super()
           from ('${->project.tasks.bentallProd.getDirectoryName()}'){
      exclude 'web.xml.properties'
      include '*.properties'
      into '/WEB-INF/classes'
    }
...

(sethgoings) #7

As for the original question you posed: “Where to put code that extends the War task”… I’m wondering why you didn’t follow Peter’s response here: http://forums.gradle.org/gradle/topics/extend_war_task

In other words, is there any reason why you’d need to drop into Groovy/Java land to solve your problem? Why not do something outlined like the code block below instead of creating your own War class extending from Gradle’s implementation?

task rdsWar(type: War) {
  from '...'
  into '...'
  exclude '...'
  include '...'
}

You can then use apply from: ‘…’ to apply this Gradle script in any other build.

If you’d like to solve the most recent problem though, check out this documentation page: http://gradle.org/docs/current/userguide/custom_tasks.html (specifically the stuff starting in 51.2)

There are two key difference between this example and yours.

One is the underlying Groovy magic related to the automatically created getters and setters when properties are defined without any specific scope. See: http://www.ibm.com/developerworks/java/library/j-pg09196/index.html

Another is utilizing the knowledge of the separation of configuration phase vs. execution phase. I believe that the constructor is always called before the override variables are set, so you want to set your defaults in your class/constructor, and then in the @TaskAction method’s scope you want to just use the variables in your task’s work.

The basic version of your RdsWarTask works fine if you do a few modifications:

class RdsWarTask extends War {
  String directoryName
    RdsWarTask(){
    super()
  }
    @TaskAction
  void run() {
    println "directoryName=" + directoryName
  }
}
  task war_default(type: RdsWarTask)
  task war_over(type: RdsWarTask) {
  directoryName = 'test'
}

For future reference, from my perspective… there’s a lot going on in this thread (which is why I think people have passed up on answering this post). If you’ve got several questions or problems, try breaking them out into individual answerable tidbits instead of tacking on slightly different problem after problem.


(Mark Waschkowski) #8

Thanks for the reply Seth!

I did try to follow Peter’s response:

‘You just need to write a build script or plugin that adds a few War tasks and configures them to your liking.’

So I tried to create a plugin that would do what I want, seemed like a good idea, but found it to be a lot more difficult than I imagined.

I can try your suggestion about the custom war task and the apply from: ‘…’, that sounds good, and would avoid the issues that I’m running into! I’m going to try it right away, will let you know shortly.

I would love to know the answer to the ‘directoryName’ question since it seems like an issue I could run into again.

The modified example you provided doesn’t work for the war case however - if you don’t make changes in the Groovy constructor that extends War, that a lot of things just don’t work - see that other thread I referenced:

and I really don’t know why the code must be in the constructor. Since other people have already noticed the same I’m hoping to make a nice clean example and put it on the cookbook…

In terms of the thread, you are right, there are several things going on, but all from the same thought. I don’t really know what the protocol is - some places hate starting new threads for things, but I’ll just break it out next time!


(sethgoings) #9

In terms of the War task extension, you are right… but it seems like that problem has been solved in another way (via the forum post you referenced). You were having other problems that put you in another problem bucket… which I essentially addressed here.


(Mark Waschkowski) #10

Hi Seth,

Am I able to set properties on a custom task?

task rdsWar(type: War) {
  def dirName
  from '...'
  into '...'
  exclude '...'
  include '...'
}
  task prodWar(type: rdsWar){
  dirName 'bentallProd'
}

? I’m sure the syntax is incorrect, but thats along the lines of what I would like to do…

Thanks,

Mark


(sethgoings) #11

Why do you need a dirName property? You already have access to ‘from’ and ‘into’ via the normal War task…


(Mark Waschkowski) #12

Well, I have 3 different environments that all need a common set of customizations applied. I could end up with 3 war tasks that all repeat the same customizations, but that doesn’t follow the DRY principle and has its associated pitfalls…


(Peter Niederwieser) #13

It’s almost always better to have separate tasks in Gradle than to rely on conditional configuration, and it’s typically no less DRY.


(Mark Waschkowski) #14

Well, I have over 10 projects, with several targets per project, and my initial build has about 40 lines of script code. If I have a custom task that does all that, if I have to add in a file check, a standard cleanup operation, etc., I won’t have to edit 10 different files in several spots per file, I’ll just edit my custom task, build it to our repo, and then all my builds will have the extra functionality. It sounds great to me, and something that Gradle will handle far better than Ant ever could…It would be much more error prone and inefficient having to manually do that, so I’m unsure as why you would say its typically no less DRY?


(Peter Niederwieser) #15

Because in Gradle, it’s as easy to configure ten tasks as it is to configure a single task. Typically, configuration isn’t baked into a task but is the responsibility of plugins.

From your explanation, I have a feeling that you want to model each variation in configuration as a separate task class. Typically, you would instead have multiple instances of the same task class which are configured individually by a plugin. In the simplest case, a plugin is a build script applied with ‘apply from:’.


(Mark Waschkowski) #16

Oh no, that’s not what I’m looking for. I’ve been using Ant across all our projects and each project tend do the same things. I’ve made lots of ant macro’s to do standard stuff and want to have Gradle work at the next level. Standard stuff includes: -checking for required files to build war -copying over required properties files for each environment -creating the web.xml appropriate for each environment etc.

Currently we have a folder called ‘env’ with multiple folders underneath them that hold various artifacts for each environment. ie:

env/bentallProd env/bentallUat env/local env/sandbox

that contain various files, some of which I can just incorporate into the gradle way (with gradle properties) and some of which I can’t - logo’s, login pages etc.

I’m trying to create a war task that will handle all the stuff that has been standardized across ant scripts and put it into one task class that can be shared across projects. There may be some exceptions, but by and large this should be possible in my environment. Is there some resistance to users of gradle customizing built in tasks?


(Peter Niederwieser) #17

Inheriting from a built-in task class is typically not the right means to customize. Copying over artifacts should be done by a ‘Copy’ task. (Or, if the files should be copied into a War, a ‘War’ task should be configured to include these files.) Creating the ‘web.xml’ should be done by a custom task, potentially implemented as a task class. Checking for required files might be its own task too. All this would be held together by a custom plugin that would be shared across teams. So the general approach is to favor composition over inheritance, and to compose at the plugin rather than the task level.


(Mark Waschkowski) #18

Thank you for your response. I’m certainly willing to consider the approach you have suggested, although I have some reservations. However, the easiest way to discuss the approaches would probably be against an example. I looked through the java samples but nothing there looked appropriate for what I’m doing:

  1. multiple environment builds with the web.xml configured per environment 2. compilation of java classes into HTML/javascript for deployment (we are using GWT via Vaadin) 3. various other steps that probably aren’t interesting

I’ll provide an example and perhaps you would consider critiquing it so we don’t end up having difficulties in our discussion around semantics.


(Mark Waschkowski) #19

For anyone reading this, I found out that the code I was interested in running required being in the constructor, and the following examples show 2 ways of deferring the code eval until the execution phase, which helped me greatly in dealing with my issues:

class RdsWarTask extends War {
   String directoryName
   RdsWarTask(){
  super()
    //exclude the local web.xml from the build
  exclude '**/web.xml'
        //note closure to defer until execution phase
    from ({directoryName}){
      exclude 'web.xml.properties'
      include '*.properties'
      into '/WEB-INF/classes'
    }
          project.gradle.projectsEvaluated {
      //copy and filter default web.xml file
      def Properties webProps = new Properties()
      webProps.load(new FileInputStream(directoryName + '/web.xml.properties'))
      from ('env'){
        include 'defaultWeb.xml'
        into '/WEB-INF'
        rename { 'web.xml'}
        filter(ReplaceTokens, tokens: webProps )
      }
    }
  }
  ...

consumer code

task bentallProd(type: com.refineddata.RdsWarTask) {
  directoryName 'env/bentallProd'
}