From Maven to Gradle – part 2

In the first part of this series, we discovered Gradle and tried to convert a simple Maven project, the domain model of my BeeWorks MoneyPile application. Be sure to read it if you haven’t already. In this second post, we will complete the domain model project by adding a custom plugin that generates DDL files for several databases.

Our goal for today

For my old/current Java development framework, I have written a Maven plugin that takes the META-INF/persistence.xml of a JPA project, and uses Hibernate SchemaExport to generate 3 ddl-files for every database dialect you specify: drop-create, create and drop. By default, this is done for Oracle, MySQL, PostgreSQL and HSQL, but you can choose any of the dialects Hibernate offers. I know what you’re thinking: why didn’t you just use the hibernate3 plugin that’s available on Codehaus? Well, I haven’t checked lately, but when I created my framework, this plugin didn’t do what I wanted and it was still in the sandbox. Plus, I liked the challenge.

Now, how will we do this with Gradle? As far as I can tell, there are several options: Since Gradle offers first-class support for Ant build files and Ant tasks, we could use the Ant tasks that come with Hibernate Tools. But again, they don’t really do what I want: for one, you need to set the dialect in the persistence.xml file, and I don’t want that. All I want to put in there are the class names of my entities.

I’ve chosen to dive into the Gradle API and write a Gradle Task and a Plugin for this purpose. My knowledge of Groovy is still too limited, so I’ll write them in Java.

So let’s create a new project, called ‘gradle-hibernate-tools’. The Gradle build file looks a lot like the one we created yesterday:

usePlugin 'java'
usePlugin 'project-reports'
usePlugin 'eclipse'

defaultTasks 'clean', 'build'

sourceCompatibility = 1.6
version = '1.0.0-SNAPSHOT'
manifest.mainAttributes(
     'Implementation-Title': 'Hibernate tools plugin for Gradle',
     'Implementation-Version': version
)
jar.baseName = 'gradle-hibernate-tools'

repositories {
     mavenCentral()
     mavenRepo urls: "http://repository.jboss.com/maven2";
}

dependencies {
     compile group: 'org.slf4j', name: 'slf4j-api', version: '1.5.2'
     compile group: 'org.slf4j', name: 'slf4j-log4j12', version: '1.5.2'
     compile group: 'org.hibernate', name: 'ejb3-persistence', version: '1.0.2.GA'
     compile group: 'org.hibernate', name: 'hibernate-annotations', version: '3.4.0.GA'
     compile group: 'org.hibernate', name: 'hibernate', version: '3.2.1.ga'
     compile group: 'org.hibernate', name: 'hibernate-entitymanager', version: '3.4.0.GA'
     compile group: 'org.hibernate', name: 'hibernate-commons-annotations', version: '3.3.0.ga'
     compile group: 'org.hibernate', name: 'hibernate-tools', version: '3.2.4.GA'
     compile group: 'dom4j', name: 'dom4j', version: '1.6.1'
     compile group: 'org.codehaus.groovy', name: 'groovy-all', version: '1.6.4'
     compile files('lib/gradle-core-0.8.jar', 'lib/gradle-open-api-0.8.jar')
}

The only difference here is the manifest section, and a number of dependencies, specifically Groovy, Gradle and Hibernate Tools. You’ll notice that the gradle dependency is a file dependency to a jar-file I put in the project lib directory. The Gradle artifacts are not yet available on any Maven repository. This is something that should be fixed as soon as possible.

The Task

The next step is writing the Task that will perform the database schema export. Now, in the Gradle manual, there are chapters on writing custom tasks and custom plugins, but they don’t go into a lot of detail. The best way for now is diving into the Gradle source code, which is exactly what I did. Let’s take a look at the Task.

public class SchemaExportTask extends ConventionTask {
	private Boolean format = Boolean.TRUE;
	private Boolean comments = Boolean.TRUE;
	private String dialects = "org.hibernate.dialect.MySQL5Dialect";
	private String persistenceUnitName = "default";
	private String delimiter = ";";
	private String targetDirectory = "build/ddl";

Our Task extends the ConventionTask class. This will allow us to configure the task from the project build file, using the Gradle Convention mechanism.

First we define some properties to control the output of the task. The dialects property for example is a comma separated list of Hibernate dialects we want to create DDL for. All these properties will be configurable in the build file later on.

	private FileCollection classpath;

	/**
	 * Returns the classpath for the Hibernate Tools
	 */
	@InputFiles
	public FileCollection getClasspath() {
		return classpath;
	}

	/**
	 * Set the classpath for the web application
	 */
	public void setClasspath(FileCollection classpath) {
		this.classpath = classpath;
	}

The classpath property will allow us to specify the classpath that should be used by the task when trying to generate the schema. This is important because we will need the runtime classpath of the project containing the persistent entities. We’ll see how this property gets configured when we create the plugin later on. Not the @InputFiles annotation on the getter.

Next is the main method of the task.

@org.gradle.api.tasks.TaskAction
protected void start() {
	ClassLoader originalClassloader = this.getClass().getClassLoader();
	try {
		List classpath = setUpClassPath();
		URLClassLoader hibernateToolsClassloader = new URLClassLoader(
			classpath.toArray(new URL[classpath.size()]),
			originalClassloader);
		Thread.currentThread().setContextClassLoader(
			hibernateToolsClassloader);
		exportSchema();
	} catch (MalformedURLException e) {
		logger.error("Error trying to set the Hibernate Tools classpath");
	} finally {
		Thread.currentThread().setContextClassLoader(
			originalClassloader
		);
	}
}

This method is annotated with the TaskAction annotation. Note that there are 2 TaskAction classes in the Gradle API, in different packages. Only the one in the tasks package is an annotation. TaskAction indicates that the annotated method will be called when Gradle starts the task. This method creates a new classloader, containing the classpath we specified above and puts that classloader in the current thread. Then it calls the exportSchema method, which will do the actual work. Afterwards, it resets the current thread contextClassLoader to the original one. This reason for all this classloader stuff is because Hibernate uses the contextClassLoader of the current thread when instantiating a JpaConfiguration.

private void exportSchema() {
	Ejb3Configuration configuration = new Ejb3Configuration();
	Map properties = new HashMap();
	properties.put("hibernate.format_sql", format);
	properties.put("hibernate.use_sql_comments", comments);
	configuration = configuration
		.configure(persistenceUnitName, properties);

	if (configuration == null) {
		logger.error("Error: Unable to export schema");
		return;
	}

	for (final String dialect : getDialectList()) {
		try {
			export(configuration, dialect);
		} catch (final Exception e) {
			logger.error("Error exporting DDL for dialect " + dialect, e);
		}
	}
}

The exportSchema() method creates a new JPA configuration, using the persistenceUnit we configured. It looks for the configuration of this persistenceUnit in the classpath resource META-INF/persistence.xml. If the persistence.xml file can’t be found, an error is logged and the task ends. If the JPA configuration is created succesfully, an export is performed for every dialect we specified in the task properties.

That completes the hardest part. Now we’ll write a plugin so we can easily reuse this functionality in all our JPA projects. You can download the full source code of this task here.

Tasks vs. Plugins

As you have seen in the previous section, a task delivers some extra functionality to a build, in our case generating DDL. Some readily available tasks in Gradle are the Java compile and jar tasks.

A plugin is used to alter the build script, for example adding tasks, dependency configuration or conventions to the script. In other words, they are used to plug the functionality provided by the task in the build. So tasks and plugins complement each other nicely in Gradle.

The plugin

The easiest way to write a Gradle plugin seems to be in Groovy, in the build file itself. I’ll use java for this too however, for 2 reasons. First, I don’t know Groovy well enough. Second, I want to reuse my plugin in other projects, and I don’t know if that’s possible when writing it in the build file, the manual doesn’t mention this. When using good old Java, I can create a jar and put it in a repository somewhere.

What does our plugin need to do? It has to add the SchemaExport task to the project build. We would like to be able to configure the task with some properties, so the plugin will have to register a convention for it. And finally, the plugin has to pass the runtime classpath of the project to the task, so the Hibernate configuration can load our persistent classes.

public class HibernateToolsPlugin implements Plugin {

	public static final String SCHEMA_EXPORT = "schemaExport";

	@Override
	public void use(Project project, ProjectPluginsContainer container) {
		System.out.println("Using Hibernate Tools Plugin");
		container.usePlugin(JavaPlugin.class, project);

		HibernateToolsConvention hibernateToolsConvention = new HibernateToolsConvention();
		Convention convention = project.getConvention();
		convention.getPlugins().put("hibernateTools", hibernateToolsConvention);

		configureSchemaExport(project, hibernateToolsConvention);
	}

A plugin needs to implement the Plugin interface. This interface has only 1 method: use. This method is called when we add the method usePlugin() to a build file.

Our use() method does 3 things:

  1. Make sure the build also uses the Java plugin. Not much use for Hibernate without Java.
  2. Add a convention to the build, which will allow us to configure the schemaExport task. We will look at the HibernateToolsConvention class later.
  3. Configure and register the schemaExport task.
private void configureSchemaExport(final Project project, final HibernateToolsConvention hibernateToolsConvention) {
	project.getTasks().withType(SchemaExportTask.class).whenTaskAdded(new Action() {
		public void execute(SchemaExportTask schemaExport) {
			schemaExport.getConventionMapping().map("classpath", new ConventionValue() {
				public Object getValue(Convention convention, IConventionAware conventionAwareObject) {
					return getJavaConvention(project).getSourceSets().
						getByName(SourceSet.MAIN_SOURCE_SET_NAME).
						getRuntimeClasspath();
				}
			});
			schemaExport.getConventionMapping().map("format", new ConventionValue() {
				public Object getValue(Convention convention, IConventionAware conventionAwareObject) {
					return hibernateToolsConvention.getFormat();
				}
			});
			schemaExport.getConventionMapping().map("comments", new ConventionValue() {
				public Object getValue(Convention convention, IConventionAware conventionAwareObject) {
					return hibernateToolsConvention.getComments();
				}
			});
			schemaExport.getConventionMapping().map("dialects", new ConventionValue() {
				public Object getValue(Convention convention, IConventionAware conventionAwareObject) {
					return hibernateToolsConvention.getDialects();
				}
			});
			schemaExport.getConventionMapping().map("persistenceUnitName", new ConventionValue() {
				public Object getValue(Convention convention, IConventionAware conventionAwareObject) {
					return hibernateToolsConvention.getPersistenceUnitName();
				}
			});
			schemaExport.getConventionMapping().map("delimiter", new ConventionValue() {
				public Object getValue(Convention convention, IConventionAware conventionAwareObject) {
					return hibernateToolsConvention.getDelimiter();
				}
			});
			schemaExport.getConventionMapping().map("targetDirectory", new ConventionValue() {
				public Object getValue(Convention convention, IConventionAware conventionAwareObject) {
					return hibernateToolsConvention.getTargetDirectory();
				}
			});

		}
	});

	SchemaExportTask schemaExport = project.getTasks().add(SCHEMA_EXPORT, SchemaExportTask.class);
	schemaExport.setDescription("Generates DDL scripts for your database from your JPA persistent classes.");
}

What’s going on here? The first thing we do is registering a listener that fires an action when the SchemaExport task is added to the project. This action injects the runtime classpath of the project in our SchemaExport task. Remember we need this to run the schema export. Notice how convenient Gradle makes it to retrieve that classpath from the project. The rest of this action takes the properties from the schemaExport convention and injects them in the task.

After registering this listener, the task itself is added to the project, as well as a description.

So that was the plugin. The only thing we need to take a look at now is the convention object, but there is really is nothing much to say about it. It’s just a simple bean with a number of properties.

You can download the source code of the plugin as well as the convention object here.

Using the plugin

In order to use this plugin in our domain model project, we will need to add it as a dependency to our build script. According to the manual, you do this using the buildscript() method, passing in a closure which declares the dependencies.

buildscript {
        dependencies {
                classpath fileTree(dir: "D:/meiji-framework/gradle/hibernate-tools/build/libs", includes: ['*.jar'])
        }
}

This adds the jar-file of the plugin to the build classpath. That’s not enough however. Since we are using a file dependency at the moment, there are no transitive dependencies. But we also need hibernate. The hibernate dependencies are on the Maven central repository. So we need to add these lines as well. The complete buildscript method now looks like this:

buildscript {
        repositories {
                mavenCentral()
                mavenRepo urls: "http://repository.jboss.com/maven2"
        }

        dependencies {
                classpath group: 'org.hibernate', name: 'hibernate-entitymanager', version: '3.4.0.GA'
                classpath fileTree(dir: "D:/meiji-framework/gradle/hibernate-tools/build/libs", includes: ['*.jar'])
        }
}

Now we can add the plugin itself. This is easy:

usePlugin(org.meijiframework.gradle.plugins.HibernateToolsPlugin)

Configuring the plugin is easy as well. Let’s say we want DDL files for 4 databases: Oracle, MySQL, PostgreSQL and HSQL:

schemaExport.dialects = 'org.hibernate.dialect.Oracle10gDialect, org.hibernate.dialect.MySQL5Dialect, org.hibernate.dialect.PostgreSQLDialect, org.hibernate.dialect.HSQLDialect'

That’s it! Now when we start the command ‘gradle schemaExport’, we get 4 directories in build/ddl: hsql, mysql5, oracle10g and postgresql. Each of these contains 3 files: create.sql, drop.sql and drop_create.sql.

Day 2 conclusion

This is all very nice, but it’s not perfect yet. For one, having to specify all transitive dependencies for the plugin is not acceptable. Luckily, that should be easily fixed once I deploy the plugin to a repository, along with a descriptor. That’s for the next blog post though.

What would be really great, is if you could use the dependency notation with the usePlugin() method, like this:

usePlugin('org.meijiframework:gradle-hibernate-tools:1.0')

I don’t think this would be very hard to implement, so who knows, maybe it will be added before they reach version 1.0.

A slight concern

As I’m getting more acquainted with Gradle, I do have a slight concern. One of the great things about Maven is the rather strict convention. I mean, if you download a project with a POM, you know for sure that an ‘mvn clean install’ will build, test and package the project, and all plugins the developer added to each phase will be executed.

Gradle also has conventions to a certain level, but they are not enforced and I have a feeling that the philosophy is more like Ant. You create your own tasks (or targets, in Ant terms), mark some of them as default, but there is no guarantee that ‘gradle build’ will even work or that every plugin in the buildfile is used when you run a task. In short, I’m afraid that in the end, you will have to investigate the build file of project when you download it, just like you did in the Ant days. With Maven, you don’t have to worry about it.

Reacties op “From Maven to Gradle – part 2”

  • Regarding you worries about standardized ways to execute a build. We are aware of this problem. Yet we don’t think the answer is rigidity. There are a couple of things how we will address this issue. The Java Plugin is providing the standard tasks you usually need to use the build.

    1.) Provide best practices documentation to our users that the normal usage of the build should be possible via the standard tasks. Their custom tasks should hook into the standard tasks. The same is the case for custom plugins. Right now people might be not so aware for example about the existence of the check task (which executes code- quality checks like unit tests, checkstyle, etc …).

    2.) Possibly provide some missing standard tasks (e.g. docs).

    3.) Distinguish between public and private tasks. If people do a gradle -t, they would see the classes task but not the processResources task (which they could see with gradle -t –all).

    But we still want to have the flexibility. For example the Gradle build has a developerBuild and a ciBuild entry point. Those are internal tasks usually not of interest for the public build. But they are very useful for the use case they are serving.

    Regarding the plugin handling. We know that we can make this more convenient. We have discussed something like you have proposed above: (usePlugin(’org.meijiframework:gradle-hibernate-tools:1.0′))

    This is definitely a pre-1.0 issue. It is important to have a plugin ecosystem that makes it very easy to share stuff.

  • [...] I saved the hardest part for last of course. For my Maven builds, I wrote a custom plugin that uses Hibernate Tools to generate DDL files for a number of databases, configurable with a plugin property. As far as I can see, there are several options for doing this with Gradle, but I’ll explore those in the next part of this series. [...]

  • [...] the first 2 parts of this series on Gradle, we have migrated a simple project from Maven to Gradle, and written [...]

Reageer