Task development: How to combine extensions and conventions for a richer DSL?


(Mike Meessen) #1

Hi everyone,

I’m working on our custom gradle plugins and I’d like to be able to use a rich(er) DSL for configuring aspects of the task like so:

myTask {
    taskProperty = "value"
    feature1.enabled = false
    feature2 {
        enabled = true
        featureProperty = "someOtherValue"
    }
}

The Plugin/Task development documentation is quite fine, but it lacks some details, so I’m having trouble sorting out the APIs that I should use to achieve this. I’ll just write down my thoughts and the question would be: am I right, is this as it’s meant to be used (in the current 1.0-milestone-9 state)? Maybe this topic could meanwhile serve as input for enhancing the documentation.

If I understand the documentation and API right, extensions would be the way to go to add the “feature1” and “feature2” containers to the task. So in the constructor of my task (which extends from AbstractTask), I’d use something like:

this.getExtensions().add("feature1", new Feature1Container());

.

So far so good. Now, all of the task’s properties actually have reasonable defaults, so for each property, I’d like to define a convention mapping so that projects don’t have to configure each property, in the spirit of convention-over-configuration.

For “direct” task properties, I can write the following in the plugin:

myTask.conventionMapping("taskProperty", new Callable<PropertyType>() {
    public PropertyType call() throws Exception {
        // This might even be dynamic or computed, not necessarily static.
        return someDefaultValue;
    }
});

However, for the nested containers, this seems more complicated, as the “Feature1Container” and “Feature2Container” classes are only plain java-beans at this point. To be able to define convention mappings on them, they’d have to implement IConventionAware. However, there are 2 problems here: - First, IConventionAware is a Gradle internal API, so I guess I shouldn’t rely on it - Second, if I decorate my container javabeans with IConventionAware by hand, I’d have to write all special getters, which first check if the property is set and use the convention value as fallback. Writing that by hand just seems horribly wrong.

I must have taken a wrong turn somewhere I guess? What would be the correct path to success here? :wink:

Thanks in advance, Mike


(Andrew Oberstar) #2

That’s an interesting question. I would agree, avoid direct use of IConventionAware. I don’t understand how and in what circumstances it happens, but Gradle does “enhance” some of your classes with additional methods. I believe adding the required methods to properly support conventionMapping happens through this mechanism.

Hopefully one of the devs can shed light on this…


(Luke Daley) #3

This became possible in m9, but you need to change:

this.getExtensions().add(“feature1”, new Feature1Container());

To…

this.getExtensions().create(“feature1”, Feature1Container);

This way, the extension gets the “Gradle magic” added to it.

There is a way to do this pre m9 but it would use an internal API that is likely to change sometime in the future. Let me know if you need to support pre m9.


(Mike Meessen) #4

Hi Luke,

Thanks for your reply. That seems to be the missing link I was looking for. m9 is fine for me.

I’m still playing devil’s advocate here: what about the convention mapping inside the “created” extension? The javadoc for “create” says:

/**
     * Adds a new extension to this container, that itself is dynamically made {@link ExtensionAware}.
     *
     * A new instance of the given {@code type} will be created using the given {@code constructionArguments}. The new
     * instance will have been dynamically which means that you can cast the object to {@link ExtensionAware}.
     *
     * @see #add(String, Object)
     * @param name The name for the extension
     * @param type The type of the extension
     * @param constructionArguments The arguments to be used to construct the extension instance
     * @return The created instance
     */
    <T> T create(String name, Class<T> type, Object... constructionArguments);

… “you can cast the object to {@link ExtensionAware}”. For extensions that’s fine. What about convention mappings in there? I’d still have to cast it to IConventionAware, wouldn’t I? It seems to me like, however we put it, IConventionAware should be made public, just like ExtensionAware?

Best regards, Mike


(Luke Daley) #5

ConventionMapping is not quite public yet, which is why it’s not mentioned in the docs there. The created extension does have convention mapping capabilities though.

We are working towards moving convention mapping to the public API. There are a few more issues and details to sort out before though. It won’t be for 1.0, but definitely will happen soon after as we shift the focus on making the life of plugin developers easier.


(Mike Meessen) #6

Okay, so I guess if I want to write my plugin using java, I’ll have to rely on the non-public API for now.

At least I know that’s how it is for now and I’m not heading in a totally wrong direction :wink:

Thanks Luke!

  • Mike

(Mike Meessen) #7

Little update on this:

Playing around with this in java, I found extensions to be quite a pain atm. Here’s what I ended up with (not even came to conventions yet):

What I want to achieve:

myTask {
    featureset {
        feature1 {
            enabled = true
        }
    }
}

Registering the Extension Container in MyTask:

public MyTask() {
        this.getExtensions().create("featureset", FeaturesetExtension.class);
    }

The FeaturesetExtension Bean:

public class FeaturesetExtension {
    public FeaturesetExtension() {
        // "ExtensionAware" is mixed in by gradle at runtime
        ((ExtensionAware) this).getExtensions().create("feature1", Feature1Extension.class);
    }
}

The Feature1Extension Bean:

public class Feature1Extension {
    @Input
    private Boolean enabled = false;
      public Boolean getEnabled() {
        return enabled;
    }
      public void setEnabled(Boolean enabled) {
        this.enabled = enabled;
    }
}

Retrieving the nested boolean in the task is very cumbersome:

Boolean nestedEnabled = ((ExtensionAware) this.getExtensions()
            .getByType(FeaturesetExtension.class)).getExtensions()
            .getByType(Feature1Extension.class).getEnabled();

And finally, it doesn’t work :wink: :

Caused by: java.lang.NullPointerException
        at FeaturesetExtension_Decorated.getConvention(Unknown Source)
        at FeaturesetExtension_Decorated.getExtensions(Unknown Source)
        at FeaturesetExtension.<init>(FeaturesetExtension.java:27) // The extensions call in the constructor
        at FeaturesetExtension_Decorated.<init>(Unknown Source)
        at org.gradle.api.internal.DirectInstantiator.newInstance(DirectInstantiator.java:40)
        at org.gradle.api.internal.ClassGeneratorBackedInstantiator.newInstance(ClassGeneratorBackedInstantiator.java:34)
        at org.gradle.api.internal.plugins.DefaultConvention.create(DefaultConvention.java:114)
        at MyTask.<init>(MyTask.java:79)
        at MyTask_Decorated.<init>(Unknown Source)
        at org.gradle.api.internal.project.taskfactory.TaskFactory$1.call(TaskFactory.java:110)
        ... 56 more

Well anyway, looks like all of that isn’t ready for primetime yet…

Greetings, Mike


(Peter Niederwieser) #8

I think the problem is that you are accessing members in the constructor that are only declared and initialized in the dynamically generated subclass. You should do this work in the plugin instead.

You wouldn’t get the nested extension object each time you access a property. Instead, you would save the extension object in a local variable or field.

Java is a cumbersome language for the sort of things that plugins do (extend objects dynamically, register callbacks, etc.). Nevertheless, we go to great lengths to make it at least possible to implement plugins in Java. With Groovy, you’ll get a much nicer experience.

The only real downside of implementing a plugin in Groovy is that it will only work with Gradle versions that are based on the same major Groovy version that you compiled the plugin against. In many cases, this is an acceptable compromise.


(Mike Meessen) #9

Yes, I know that groovy would make some things easier here, but company policies, colleagues that can’t read groovy well, yada yada, you know the chorus :wink: (I managed to get Gradle through nevertheless since it’s so much better than all other alternatives ;)) In short, I’m trying to keep our Gradle Tasks / Plugins in Java and only have the groovy going on in the build scripts, which works fine thanks to the effort you guys put into it.

Yes, accessing the Extension Container in the constructor is probably the problem here, but having to “assemble” the task at runtime in the plugin (though my DSL structure is actually static here) seems odd, too.

It would be much easier if extensions were task properties (like gradle does it with the “reports” property in the new checkstyle task).

Maybe something along the lines of:

@Extension
private FeaturesetExtension featureset;

I don’t know if that’s feasible, but it would make it a lot simpler to use I think?


(Luke Daley) #10

I think the core of the problem here is that you are using extensions in a way that they are not intended to be used. They are essentially mixins, but you want it to be part of the static API. That’s completely understandable though as right now the extension mechanism is the only public way to create “dsl objects” (i.e. with convention mapping etc).

I can see two options.

1 - Go deeper into public API and don’t use the extension mechanism (with all the usual disclaimer about private API being subject to change without notice)

import org.gradle.api.internal.plugins.DslObject
import org.gradle.api.internal.ThreadGlobalInstantiator
import org.gradle.api.internal.Instantiator
  class MyTask extends DefaultTask {
    FeatureSet featureset
      MyTask() {
    featureset = ThreadGlobalInstantiator.getOrCreate().newInstance(FeatureSet)
    DslObject featuresetDslObject = new DslObject(featureset)
    featuresetDslObject.getConventionMapping().…
  }
      // In a future version of Gradle, we'll create this method for you but you need it now.
  void featureset(Closure c) {
    project.configure(featureset, c)
  }
  }

It seems to me though that what you might be after is a domain object container.

import org.gradle.api.NamedDomainObjectContainer
import org.gradle.api.Named
  class Feature implements Named {
  private final String name
  boolean enabled
      Feature(String name) {
    this.name = name
  }
    }
class MyTask extends DefaultTask {
    NamedDomainObjectContainer<Feature> featureset
      MyTask() {
    featureset = project.container(Feature)
    featureset.create("feature1")
    featureset.create("feature2") {
      enabled
    }
  }
      // In a future version of Gradle, we'll create this method for you but you need it now.
  void featureset(Closure c) {
    featureset.configure(c)
  }
  }

With that, you could do…

task mytask(type: MyTask) {
  features {
    feature1 {
      enabled = true
    }
  }
}

(Mike Meessen) #11

Luke,

The NamedDomainObjectContainer seems interesting, but it looks like it can only contain objects of the same type, right? In my case, my features have different configuration options (well, besides “enabled” ;)).

So I guess your first option would be my best bet for now.


(Luke Daley) #12

It only requires the same type in the same way that a java List does.


(Mike Meessen) #13

Yes, but in a java list, I can put in any concrete (sub)types I want, whereas here (if I understood it right), I can only have instances of the exact same class, the one I specify in:

featureset = project.container(Feature)

. Instances of this exact class are then created when I call

featureset.create("feature1")

right?


(Luke Daley) #14

Two things:

  1. The container is a java.util.Collection, so it has an add() method. 2. Check out the other project.container() methods that allow you to use custom factories.

(Mike Meessen) #15

Awesome, I had overseen these, that’s pretty much what I need then :slight_smile:

Thanks Luke! :slight_smile:


(Luke Daley) #16

Cool, glad it’s going to work for you.