Nested NamedDomainObjectSets not made availabe until added as extensions themselves

apply plugin: NestedNamedDomainObjectSetsPlugin

shelf {
  name = "Mike's Shelf"
  books {
    quickStart {
    }
    userGuide {
    }
    developerGuide {
    }
  }
  magazines {
    foo {
    }
    bar {
    }
    baz {
    }
  }
}

task inspectShelf << {
  println "$shelf.name has ${shelf.books.size()} books"
  shelf.books.each { book ->
    println "  $book.name -> $book.sourceFile"
  }
  println "$shelf.name has ${shelf.magazines.size()} magazines"
  shelf.magazines.each { magazine ->
    println "  $magazine.name -> $magazine.sourceFile"
  }
}

class NestedNamedDomainObjectSetsPlugin implements Plugin<Project> {
  void apply(Project project) {
    project.extensions.create("shelf", Shelf, project)

    // MUST UNCOMMENT TO ACCESS 'shelf.books'
    // project.extensions.books = project.shelf.books

    // MUST UNCOMMENT TO ACCESS 'shelf.magazines'
    // project.extensions.magazines = project.shelf.magazines
  }
}

class Shelf {
  String name

  final NamedDomainObjectSet<Book> books
  final NamedDomainObjectSet<Magazine> magazines

  Shelf(Project project) {
    books = project.container(Book)
    books.all {
      sourceFile = project.file("src/books/$name")
    }

    magazines = project.container(Magazine)
    magazines.all {
      sourceFile = project.file("src/magazines/$name")
    }
  }
}

class Book {
  final String name
  File sourceFile

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

class Magazine {
  final String name
  File sourceFile

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

I fixed the example, test with gradle inspectShelf

I want my plugin to create an extension called ‘shelf’ of type Shelf. Shelf has a name and two NamedDomainObjectSets, books and magazines. My assumption was that when I registered Shelf as an extension, it would see books and magazines and make them available so I could configure them as I try to do in the example.

It only works if I explicitly add books and magazines as extensions:
project.extensions.books = project.shelf.books
project.extensions.magazines = project.shelf.magazines

It does not work if I try any other name:
project.extensions.hiddenBooks = project.shelf.books
project.extensions.hiddenMagazines = project.shelf.magazines

I’m relatively new to Gradle and Groovy but have lots of Java experience. It seems like what I want to do should be possible, but I’m not grasping the problem or why it works when I explicitly set books and magazines as extensions to the project. I can access them fine using shelf.books and shelf.magazines, but it will not configure properly without setting them as extensions.

Thanks,
Michael

That’s not doing what you think. You’ve made it so you can do this:

  books {
    quickStart {
      sourceFile = file('src/docs/quick-start')
    }
    userGuide {

    }
    developerGuide {

    }
  }

(note that there isn’t a shelf {})

Shelf needs to have a method like this:

void books(Closure cl) {
   ConfigureUtil.configure(cl, books)
}

Otherwise, Gradle looks for an extension called “books” on the project (which is why what you’ve suggested “works”).

I updated my example to be more complete. Could you take another look at it and help me understand what I am missing?

Thanks!
Michael

Sure. When you do:

shelf {
}

Gradle takes that to mean “configure the thing called ‘shelf’ at Project scope with the given Closure”. Gradle finds ‘shelf’ by looking for properties on Project with that name, tasks with that name and extensions with that name. Eventually, it finds an instance of Shelf called ‘shelf’.

When you do (inside ‘shelf’):

books {
}

Gradle takes that to mean “call a method called ‘books’ that takes a Closure or configure the thing called ‘books’ at Project scope with the given Closure”. Since your Shelf doesn’t have a method called books that takes a Closure, it goes back to the behavior above.

When you add ‘books’ as an extension, it seems to work because you’re sharing the same instance between ‘shelf’ and the extension.

If you changed your example to:

shelf {
   configure(books) {
   }
}

I think it would work too (without adding ‘books’ as an extension) because this says “configure this thing with this Closure” and you’re providing the thing directly (books is the container).

If you add the method as I described above:

void books(Closure cl) {
   // impl from above
}

You should be able to do what you want because you’ll now have a method called ‘books’ that takes a Closure and configures the books container.

What may be confusing is that at the Project level (i.e., shelf {}), Gradle provides the magic to call the appropriate configure method, but inside your custom types, you have to do that yourself. NamedDomainObjectSet provides a similar magic, that’s why things seem to work inside books {} again.