How to evolve a custom model in the tooling api?


This is somewhat related to @luke_daley’s reply here

We’ve written some functionality with the tooling api that we now need to evolve with a backward-compatible API change to the custom model. We’re wondering if we’re totally missing something or we aren’t following the correct pattern for this.

Our first iteration contains a model like:

// Version 1.0
interface MyModel
     public String method()

There is a corresponding builder that is registered when the plugin is applied.

We want to add a method to the model to expose more information about the build to the tooling api.

// Version 1.1
interface MyModel
     public String method()

     public String newMethod()

We’re currently adding functionality to the existing builder to define the result of newMethod().

What’s the correct way to evolve our main class that is using the model?

public static void main( String[] args )
    GradleConnector connector = GradleConnector.newConnector()
    ProjectConnection connection = connector.connect()

    final MyModel model = connection.getModel( MyModel.class )
    String strMethod = model.method()
    // Do something with strMethod

    // Make intelligent decision if model.newMethod() exists
    if( ... )
        String strNewMethod = model.newMethod()

There will be instances where a new invocation of the tooling api will need to be backwards-compatible with Version 1.0 of the model.

The thought was that groovy could provide some information if the newMethod exists but the model returned by connection.getModel() refers to the updated model. It’s clear that since model is a Proxy object none of these options can help:

MyModel.metaClass.respondsTo( model, "newMethod" )
model.metaClass.respondsTo( model, "newMethod" )
model.metaClass.getMetaMethod( "newMethod" )

It’s only when model.newMethod() is invoked that the break to the interface causes the exception:

Exception in thread “main” org.gradle.tooling.model.UnsupportedMethodException: Unsupported method: MyModel.newMethod().

We can see a few options that address the issue:

  • we should have exposed a “version” attribute/method in our initial model. Break the functionality and roll out the correct implementation with a version included with the model so future improvements can be compatible.
  • catch the exception and take action appropriately?
  • we’ve completely missing something that can help keep this backwards compatible.

Any ideas or thoughts are greatly appreciated. Thanks,

The fallback strategy is to indeed catch org.gradle.tooling.model.UnsupportedMethodException. There is however some support for dealing with getters.

In the ToolingModelBuilder, you don’t actually have to return an object that implements the interface, it just has to structurally match. There are certain “magic” methods that you can put on the interface that Gradle will generate the implementation for.

interface MyToolingModel {
  String getName()

  // magic methods
  boolean isNameSupported()
  String getName(String valueIfMissing)

Those magic methods apply to any getter. You don’t need to provide impls of methods matching this pattern on the provider side.

Unfortunately, we haven’t documented this yet. The gory details are in ProtocolToModelAdapter if you’re interested.