Confusing example

The Gradle Users’ Guide section 39.6 - Maintaining multiple domain objects contains an example that I find very confusing. There are at least three distinct things called “books” in this example. This makes understanding more difficult than it should be.

My comments illustrate points of confusion, The multiple uses of the same identifier make this example more confusing than illustrative.

apply plugin: DocumentationPlugin

books {  /* books #1 - I think this is the "collection" referred to in the text*/ 
    quickStart {
        sourceFile = file('src/docs/quick-start')
    }
    userGuide {

    }
    developerGuide {

    }
}

task books << {  /* books #2 a task called books */
    books.each { book -> /* which "books" is being iterated? */
        println "$book.name -> $book.sourceFile"
    }
}

class DocumentationPlugin implements Plugin<Project> {
    void apply(Project project) {
              /* books #3 - a local variable named books */
        def books = project.container(Book)  
        books.all { 
          /* for each book in "books", define sourceFile.  
             But which "books" is being iterated - #1 or #3? */
            sourceFile = project.file("src/docs/$name")
        }
        project.extensions.books = books 
       /* Left of = sign: books #4?  
          Right of = sign: is #1 or #3 being assigned? */
    }
}

class Book {
    final String name
    File sourceFile

    Book(String name) {
        this.name = name
    }
}

I’m going to take a stab at answering this. Someone else please verify this and amend where I’m not specific enough.

I’m not going to directly address your #1-#4 references, but hopefully my explanation will help.

The first “books” block does indeed represent a collection of Book objects.

Giving the task the name “books” is definitely confusing, but the reference to “books” in the body of the task doesn’t refer to the task name. For what it’s worth, that refers to the collection defined by the earlier “books” block.

In the “apply” method, the call to “project.container(Book)” creates a NamedDomainObjectContainer. This is indeed stored in a local variable of the method. Ignoring the “all” block for a moment, note that after this block the local variable is assigned to “project.extensions.books”, which effectively connects the declaration of a “books” block in the build script to this NDOC.

What is really surprising is what the “books.all” block does. Anyone looking at the previous assignment and this “books.all” block would think “you’ve just created an empty container, and now you’re iterating through it”. Duh. That’s not going to do anything useful. The reality is that NamedDomainObjectContainer has some magic that this section in the doc, and in the DSL doc, fails to mention. Essentially, the “books.all” closure will be executed whenever a Book is added to the container, at any time in the future.

To help understand the flow of this, it would be helpful to define the plugin in a standalone Groovy file in the “buildSrc” tree (gotten to that section yet? :slight_smile: ), and then run the Gradle build in “debug” mode (lets you step through the code in the debugger, although not the build script itself). I don’t remember the syntax for that, however.

Thanks, David.

I had to modify the source with lots of printlns to show me what was happening when. After doing that, and then rereading your explanation, it finally makes sense to me. The names are not the only confusing thing here. What’s also confusing is that we are defining behavior that doesn’t happen when it appears to, based on the “normal” flow of the code but later.

Thanks. Here is my modified source

apply plugin: DocumentationPlugin

books {
    println "initializing the books object"
    quickStart {
        println "initialized the quickStart object"
        sourceFile = file('src/docs/quick-start')
        println "changed quickStart.sourceFile" 
        println "  to $quickStart.sourceFile"
    }
    userGuide {
        println "initialized the userGuide object"
    }
    developerGuide {
        println "initialized the developerGuide object"
    }
}

task books << {
    books.each { book ->
        println "$book.name -> $book.sourceFile"
    }
}

class DocumentationPlugin implements Plugin<Project> {
    void apply(Project project) {
        println("in apply()");
        def lbooks = project.container(Book)
        lbooks.all {
            sourceFile = project.file("src/docs/$name")
            println "applying lbooks.all defined in DocumentationPlugin:"
            println "  set sourceFile of book $name to $sourceFile"
        }
        project.extensions.books = lbooks
        println "exiting apply()"
    }
}

class Book {
    final String name
    File sourceFile
    /*
        constructor for Book object, obeying the "contract" prescribed for objects used with the
        NamedDomainObjectContainer
    */
    Book(String name) {
        println("Book constructor($name)")
        this.name = name
    }
}

And here is the output, which shows what happens when:

$ gradle books
in apply()
exiting apply()
initializing the books object
Book constructor(quickStart)
applying lbooks.all defined in DocumentationPlugin: 
   set sourceFile of book quickStart to /home/user/examples/books/src/docs/quickStart
initialized the quickStart object
changed quickStart.sourceFile to 
  /home/user/examples/books/src/docs/quick-start
Book constructor(userGuide)
applying lbooks.all defined in DocumentationPlugin: 
   set sourceFile of book userGuide to /home/user/examples/books/src/docs/userGuide
initialized the userGuide object
Book constructor(developerGuide)
applying lbooks.all defined in DocumentationPlugin: 
   set sourceFile of book developerGuide to /home/user/examples/books/src/docs/developerGuide
initialized the developerGuide object
:books
developerGuide -> /home/user/examples/books/src/docs/developerGuide
quickStart -> /home/user/examples/books/src/docs/quick-start
userGuide -> /home/user/examples/books/src/docs/userGuide

BUILD SUCCESSFUL

Total time: 0.673 secs