Why does containter's dsl object return null for properties?

Hi everyone,

I’m working on custom gradle plugin and I’d like to be able to use the containers to allow creating of custom configuration like so:

myExtenstion {
 myContainers {
  container1 {
   property1 'value1'
  }
      container2 {
    property1 'value2'
  }
 }
 }

Created a custom plugin with the container and extensions on the project. See below.

class MyPlugin implements Plugin {
    @Override
    void apply(Project project) {
     project.extensions.create('myExtension', MyExtensionContainer)
         project.myExtension.extensions.myContainers = project.container(MyExtension)
   project.myExtension.extensions.myContainers.all { myExtension ->
            println myExtension.name // display correct values 'container1' and 'container2'
            println myExtension.property1 // always display null instead of 'value1' and 'value2'
            project.tasks.create("${myExtension}Task") {
                property1 = myExtension.property1
            }
        }
        }
}

Extension

class MyExtension {
     def name
     def property1
       MyExtension(name) {
        this.name = name
    }
}

Container:

class MyExtensionContainer {
}

I can see that the tasks and the name are set correctly based on defined DSL but other property1 always returns null.

Should the property1 be set based on defined DSL or to I am using incorrect API to setup above configuration?

Here’s an example how to use containers and extensions. Have You already checked it?

Thanks for the link. I checked the example and based on it I tried to implement similar logic that I posted above. But Still no luck.

apply plugin: DocumentationPlugin
  publisher {
   info {
       name 'Publisher'
   }
   books {
       quickStart {
           title = 'Quick Start'
           sourceFile = file('src/docs/quick-start')
       }
       userGuide {
         }
       developerGuide {
         }
   }
}
  task books << {
    publisher.books.each { book ->
        println "$book.name -> Title: $book.title, Source File: $book.sourceFile"
    }
}
  class DocumentationPlugin implements Plugin<Project> {
    void apply(Project project) {
        def publisher = project.extensions.create('publisher', Publisher)
        publisher.extensions.create('info', PublisherInfo)
        def books = project.container(Book)
        books.all { book ->
            println "$book.name -> Title: $book.title, Source File: $book.sourceFile"
            // expect to create tasks for books and setup these tasks based on the book properties
            sourceFile = project.file("src/docs/$name")
        }
        publisher.extensions.books = books
    }
}
class PublisherInfo {
   def name
}
  class Publisher {
  }
  class Book {
    final String name
    String title
    File sourceFile
      Book(String name) {
        this.name = name
    }
}

In above code I am trying to create set of the tasks and configure them based on DSL. I can create them based on the name however can’t configure them as properties for the Books are null. They are only available when tasks run which in my case is to late.

Output that I am getting:

quickStart -> Title: null, Source File: null
userGuide -> Title: null, Source File: null
developerGuide -> Title: null, Source File: null
:books
developerGuide -> Title: null, Source File: /Volumes/Data/Tools/Gradle/temp/src/docs/developerGuide
quickStart -> Title: Quick Start, Source File: /Volumes/Data/Tools/Gradle/temp/src/docs/quick-start
userGuide -> Title: null, Source File: /Volumes/Data/Tools/Gradle/temp/src/docs/userGuide

Output that I love to have:

quickStart -> Title: Quick Start, Source File: /Volumes/Data/Tools/Gradle/temp/src/docs/quick-start
userGuide -> Title: null, Source File: null
developerGuide -> Title: null, Source File: null
:books
developerGuide -> Title: null, Source File: /Volumes/Data/Tools/Gradle/temp/src/docs/developerGuide
quickStart -> Title: Quick Start, Source File: /Volumes/Data/Tools/Gradle/temp/src/docs/quick-start
userGuide -> Title: null, Source File: /Volumes/Data/Tools/Gradle/temp/src/docs/userGuide

As far as I see it works fine. Title for the book is printed where it’s set as well as sourceFile. What else You need?

Thanks for quick replay I think my last example was really unfortunate and didn’t present what I am trying to achieve. Hopefully this one will present the problem that I am struggling with.

apply plugin: DocumentationPlugin
  publisher {
    info {
        name 'Publisher'
    }
    books {
        quickStart {
            title = 'Quick Start'
        }
        userGuide {
            title = 'User Guide'
        }
        developerGuide {
            title = 'Developer Guide'
        }
    }
}
  class DocumentationPlugin implements Plugin<Project> {
    void apply(Project project) {
        def publisher = project.extensions.create('publisher', Publisher)
        def publisherInfo = publisher.extensions.create('info', PublisherInfo)
        def books = project.container(Book)
        books.all { book ->
            println "Creating task $book.name -> Title: $book.title, "
            project.tasks.create("displayBook${book.name.capitalize()}", DisplayBook) {
                publisherName = publisherInfo.name
                title = book.title
            }
        }
        publisher.extensions.books = books
    }
}
class PublisherInfo {
    String name
}
  class Publisher {
  }
  class Book {
    final String name
    String title
      Book(String name) {
        this.name = name
    }
}
  class DisplayBook extends DefaultTask {
      String publisherName
    String title
      @TaskAction
    def displayBook() {
        println "$name -> Title: $title, Publisher : $publisherName"
    }
}

I am trying to create a task that will use DSL to sets itself. Based on the DSL also it will create a list of the tasks. If you run following code you will get following list of the tasks:

displayBookDeveloperGuide
displayBookQuickStart
displayBookUserGuide

Now this is what I am struggling with and what I am expecting to get after running e.g. displayBookDeveloperGuide

// Expected
 displayBookDeveloperGuide -> Title: Developer Guide, Publisher : Publisher

But what I am actually getting is:

// This is what happens title is not set
 displayBookDeveloperGuide -> Title: null, Publisher : Publisher

Hopefully this example presents the problem better.

No idea why this is not working. I wonder if the structure isn’t too complicated :confused: And what exactly You want to achieve…

‘books.all’ fires whenever a new book has been added to the container, but before the book has been configured. You need to take additional measures to defer configuration of the task, such as ‘doFirst {}’, convention mapping, or a similar technique. As this is a common question, you should find more on this on the forums and on Stack Overflow.

Thanks for update Peter and Opal.

I just got this working with convention mapping but it requires changing tasks to work with getters. It is not a problem but one have to remember to access properties through getters.

Also noticed that if I defer tasks creation to the ‘project.afterEvaluate’ method everything works correctly. I haven’t seen this being suggested anywhere.

Is there any particular reason why it is not being suggested as an option ?

Example below:

apply plugin: DocumentationPlugin
    publisher {
    info {
        name 'Publisher'
    }
    books {
        quickStart {
            title = 'Quick Start'
        }
        userGuide {
            title = 'User Guide'
        }
        developerGuide {
            title = 'Developer Guide'
        }
    }
}
  class DocumentationPlugin implements Plugin<Project> {
    void apply(Project project) {
        def publisher = project.extensions.create('publisher', Publisher)
        def publisherInfo = publisher.extensions.create('info', PublisherInfo)
        def books = project.container(Book)
          project.afterEvaluate({
            println "After evaluation"
            books.all { book ->
                println "Creating task $book.name -> Title: $book.title, "
                project.tasks.create("displayBook${book.name.capitalize()}", DisplayBook) {
                    publisherName = publisherInfo.name
                    title = book.title
                }
            }
        })
        publisher.extensions.books = books
    }
}
class PublisherInfo {
    String name
}
  class Publisher {
  }
  class Book {
    final String name
    String title
      Book(String name) {
        this.name = name
    }
}
  class DisplayBook extends DefaultTask {
      String publisherName
    String title
      @TaskAction
    def displayBook() {
        println "$name -> Title: ${title}, Publisher : ${publisherName}"
    }
}

Also noticed that if I defer tasks creation to the project.afterEvaluate method everything works correctly. I haven’t seen this being suggested anywhere. Is there any particular reason why it is not being suggested as an option ?

It’s often suggested as an option, but like other options, it has its limitations. For example, you can run into problems when one property depends on another property set in ‘afterEvaluate’, because you can’t control the order of ‘afterEvaluate’ callbacks. Also it’s difficult to test.

Great thanks for update, than I will go with the option of using convention mapping.

Note that convention mapping isn’t considered a public API. That said, currently all available options have their drawbacks. Fortunately, a new solution is in the works.

That’s good to know. Will keep checking for new solution :slight_smile: