nilgun nilgun - 4 months ago 30
Groovy Question

Gradle looping inside ProcessResources and filter

In a jar task, I want to replace some texts in a conf file.

jar {
ext {
excludedClasses = ["com.MyClass1", "com.MyClass2"]
}
doFirst {
println 'Jar task started execution'
println 'Excluded classes ' + excludedClasses
exclude(excludedClasses)
}
doLast {
println 'Jar task finished execution'
}
processResources {
filesMatching('**/moduleconfiguration/conf.json') { f ->
excludedClasses.each { c ->
filter {
println it
it.replace(c, "com.MyClass3")
}
}
}
}
}


But the above code tries to replace c from all *.class files, resulting in an illegal jar. I want it to make replacements only in '**/moduleconfiguration/conf.json' file.

How can I achieve that?

UPDATE

Looks like I am suffering from the same problem happening here: https://issues.gradle.org/browse/GRADLE-1566. This issue has already been resolved but reoccurs if I use an
each
loop inside
processResources
.

Meanwhile, I have found 2 solutions to my problem as follows:

Solution 1: Changing order of
filter
and
each
loop. i.e. Looping inside
filter


filesMatching('**/moduleconfiguration/conf.json') { f ->
filter {
excludedClasses.each { c ->
println it
it = it.replace(c, "com.MyClass3")
}
it
}
}


Solution 2: Using regex instead of
each
loop

filesMatching('**/moduleconfiguration/conf.json') { f ->
filter {
println it
def regex = excludedClasses.join("|") // watch for .(dot) or other regex chars here
it.replaceAll(regex, "com.MyClass3")
}
}


I am still wondering why the scope of filtering changes to all files if I use
each
loop within the
filesMatching
method closure. Is this a groovy thing or gradle thing? I would be very thankful if someone could explain what is happening there.

UPDATE 2

println
output of values of delegate, this and owner at different positions for problematic case:

:processResources

Inside filesMatching. delegate:file '.../configuration/conf.json' this:root project 'projectName' owner:build_95q5jrf5z5ao0hk03tsevn2t0$_run_closure10_closure90@6f3e18b8

Problematic Case inside loop before filter. delegate:build_95q5jrf5z5ao0hk03tsevn2t0$_run_closure10_closure90_closure91@4587ec31 this:root project 'projectName' owner:build_95q5jrf5z5ao0hk03tsevn2t0$_run_closure10_closure90_closure91@4587ec31

Problematic Case inside loop before filter. delegate:build_95q5jrf5z5ao0hk03tsevn2t0$_run_closure10_closure90_closure91@4587ec31 this:root project 'projectName' owner:build_95q5jrf5z5ao0hk03tsevn2t0$_run_closure10_closure90_closure91@4587ec31

:classes
:jar
Jar task started execution

Excluded classes [MyClass1.class, MyClass2.class]

Problematic Case inside loop inside filter. delegate:build_95q5jrf5z5ao0hk03tsevn2t0$_run_closure10_closure90_closure91_closure92@3a0d0128 this:root project 'projectName' owner:build_95q5jrf5z5ao0hk03tsevn2t0$_run_closure10_closure90_closure91_closure92@3a0d0128
.
.
.
.
.


println
output of values of delegate, this and owner at different positions for solution 1:

:processResources

Inside filesMatching. delegate:file '.../configuration/conf.json' this:root project 'projectName' owner:build_95q5jrf5z5ao0hk03tsevn2t0$_run_closure10_closure90@6ece61a3

Solution 1 Inside filter before loop. delegate:build_95q5jrf5z5ao0hk03tsevn2t0$_run_closure10_closure90_closure91@64af2ad7 this:root project 'projectName' owner:build_95q5jrf5z5ao0hk03tsevn2t0$_run_closure10_closure90_closure91@64af2ad7

Solution 1 Inside filter inside loop. delegate:build_95q5jrf5z5ao0hk03tsevn2t0$_run_closure10_closure90_closure91_closure92@22c74276 this:root project 'projectName' owner:build_95q5jrf5z5ao0hk03tsevn2t0$_run_closure10_closure90_closure91_closure92@22c74276

Solution 1 Inside filter inside loop. delegate:build_95q5jrf5z5ao0hk03tsevn2t0$_run_closure10_closure90_closure91_closure92@22c74276 this:root project 'projectName' owner:build_95q5jrf5z5ao0hk03tsevn2t0$_run_closure10_closure90_closure91_closure92@22c74276
.
.
.
.
.
:classes
:jar
Jar task started execution
Excluded classes [MyClass1.class, MyClass2.class]

Answer

Update

Based on your second update and some testing on my side, it doesn't seem to directly be what I originally suggested. It does definitely appear to be something with delegation, but I can't pin it down.

You can illustrate the difference in your original (problematic) example by changing the filter { line to f.filter {. This explicitly tries to execute the FileCopyDetails#filter method instead of whichever happens to be in scope.

You should be able to see that when calling f.filter only the matched file is filtered. When you call filter on its own, it is calling the task level one. However, since you are already in the middle of copying files, it only applies the filter to files that occur alphabetically after the first one that matches.

For example, if you have this folder structure:

+ resources/main
  - abc.json
  - configuration.json
  - def.json
  - efg.json

The def.json and efg.json will be filtered, but the first two will not.

This still doesn't answer why it doesn't call the correct filter method, but it at least confirms that it is calling the task level one.

Original Answer

I believe this is due to the Groovy closures delegating to different objects. I believe you are actually calling Copy#filter in the original (problematic) case, and FileCopyDetails#filter in the two solutions. The copy task's filter method will apply a filter to everything, whereas the details filter method will be for the specific file in your filesMatching.

You should be able to illustrate the different delegates by printing them out:

Original

processResources {
    filesMatching('**/moduleconfiguration/conf.json') { f ->
        excludedClasses.each { c ->
            // print the class of this closure's delegate
            println delegate.class
            filter {
                it.replace(c, "com.MyClass3")
            }
        }
    }
}

Solution 1

filesMatching('**/moduleconfiguration/conf.json') { f ->
     filter {
        // print the class of this closure's delegate
        println delegate.class
        excludedClasses.each { c ->
            println it
            it = it.replace(c, "com.MyClass3")
       }
       it
    }
}

My expectation is that you will see the original delegating to your Copy task (i.e. processResources) and the solutions delegating to the FileCopyDetails. By default all closures in Groovy are "owned" by the object in which they were declared, and also will "delegate" to the owner. When Gradle has API's that take a closure as an argument (such as the filter methods), they are usually reconfigured to have a specific delegate (usually the enclosing container object) and to use the "delegate first" strategy to lookup methods.

See Groovy's documentation on delegation strategies.