Exec Java code to generate another Java source file (and then compile the generated code for a WAR file)

Greetings,

I am in the process of converting a set of Ant build scripts to Gradle, and have run into an issue creating a WAR file – In this case, I need to first include/compile a Java class file that has been generated by another Java program.

This is a multi-project build for a GWT application. The Java source code generator exists in a different project that the WAR project is dependent upon. However, the nature of the code generator is such that it needs access to both the classpath of the project it is contained in as well as the classpath of the WAR project.

For the morbidly curious, the code generator uses Java reflection to dynamically create a list of all other Java classes in our project(s) that implement Serializable. That list of classes is then written out to a new Java class which looks like…

public class GenericRemoteServicesHandlerImpl implements GenericRemoteServicesHandler {
private static final List<Class<?>> handlers = new ArrayList<Class<?>>();
static {
handlers.add(com.acme.gwt.client.bean.BooleanFieldValue.class);
handlers.add(com.acme.gwt.client.bean.BooleanOrNull.class);
handlers.add(com.acme.gwt.client.bean.CacheStatus.class);
handlers.add(com.acme.gwt.client.bean.ContentSearch.class);
//... about a thousand more adds; that's why we generate this "handler" class. ;)

The list of serializeable classes is obtained from both our “core” project as well as the WAR project where this class needs to be compiled and included in the archive.

The build script for our “core” project (which includes our source code generator)…

apply plugin: 'java'
apply plugin: 'maven'
apply plugin: 'maven-publish'
apply from: "$rootProject.projectDir/eclipse-gwt-config.gradle"

sourceSets {
	main {
		java {
			srcDirs = ['src', 'src-defdb', 'test']
		}
		resources {
			srcDirs = ['testdata']
		}
	}
}
dependencies {
	compile project(':AcmeGwt')
	compile project(':AcmeDefaultDB')
	compile 'com.google.zxing:core:1.7'
	//... other compile/runtime dependencies omitted for brevity...
}
//JAR task and publishing closures are actually defined in our
//root project build script; I'd be happy to provide that as well
//if necessary.

Build script for our WAR project…

apply plugin: 'war'
apply plugin: 'maven'
apply plugin: 'maven-publish'
apply from: 'https://raw.github.com/akhikhl/gretty/master/pluginScripts/gretty.plugin'
apply from: "$rootProject.projectDir/eclipse-gwt-config.gradle"

sourceSets {
	main {
		java {
			srcDirs = ['src', 'test']
		}
	}
	genRpcSrc {
		java {
			srcDirs = ['../AcmeCore/src']
			include '**/GenericRemoteServicesJsonHandlerBuilder.java'
		}
	}
}

dependencies {
    compile project(':AcmeCore')
	compile 'com.jcraft:jsch:0.1.53'
	//... other compile/runtime dependencies omitted for brevity...
}

task genRmtHandlerCpJar(type: Jar) {
	dependsOn configurations.runtime
	appendix = 'launcher'
	doFirst {
		manifest {
			attributes "Class-Path": configurations.runtime.files.collect { File file-> file.name }.join(" ")
		}
	}
}

task genSource(type: JavaExec, dependsOn: genRmtHandlerCpJar) {
	classpath = files(genRmtHandlerCpJar.archivePath)
	main = 'com.acme.server.service.GenericRemoteServicesJsonHandlerBuilder'
	args "/${buildDir}/generated-sources/GenericRemoteServicesHandlerImpl.java"
	args 'false'
	
	doFirst {
		(new File("/${buildDir}/generated-sources")).mkdirs()
	}
}

/*compileJava {
    dependsOn genSource
}*/

war {
	from 'war' //i.e., additional files from the 'war' subdirectory
	//all other configuration defined in the root project
}

My current problem is just getting everything that’s needed on the classpath for the JavaExec task…

Error: Could not find or load main class com.acme.server.service.GenericRemoteServicesJsonHandlerBuilder.

Initially I was dealing with a classpath-too-long for Windows error. I worked around that by creating a “launcher” or classpath-only JAR. However, I still need to get our core project JAR (where our code generator lives) along with any additional dependencies needed by the generator itself (e.g., slf4j and reflection utilities). Also needed on the JavaExec classpath is the WAR project’s classpath – again, when the code generator executes, it needs both projects in order to correctly find all serializeable classes.

Finally, after the code generator runs, the resulting implementation class still needs to be compiled itself, so it can be included in the WAR archive.

I’m still relatively new to Gradle, and I’ve spent several days trying to figure this latest issue out. I know exactly what I want to do, but I still haven’t fully wrapped my brain around all of the magic that Gradle is doing under the covers.

I also tried creating a separate project for this particular task, but that only worked for the core project – I couldn’t figure out how to “look-ahead” so to speak into the separate WAR projects. (I’m just using one example by the way; we actually have 4 different WAR projects with customizations for different clients/customers; so I need a repeatable solution.) Once I can get this solved, I’d like to pull the code out into a separate Gradle script that I can then just apply to each WAR project. My other thought is that perhaps I need to create an actual plugin for something like this, but I haven’t really found any good, non-trivial tutorials on how to do that – at least, nothing that is similar to what I’m trying to retrieve.

Any assistance will be greatly appreciated.

Thank you!
Lee

Okay, here’s my solution. I created a new Gradle script file called ‘build-rmt-svc-handler.gradle’ which is included into each one of our custom WAR projects…

/*
 * Gradle build script fragment for generating and compiling the
 * GenericRemoteServicesHandlerImpl.class
 */
configurations {
	rmtSvcHandlerBuilder
}

dependencies {
	rmtSvcHandlerBuilder "junit:junit:$ver_junit"
	rmtSvcHandlerBuilder "org.reflections:reflections:$ver_reflections"
	rmtSvcHandlerBuilder "org.slf4j:slf4j-api:$ver_slf4j"
}

task generateRmtSvcHandlerImpl(type: JavaExec) {
	dependsOn jar
	classpath = files(
			configurations.rmtSvcHandlerBuilder,
			fileTree(dir: "$rootProject.projectDir/gwt", include: '*.jar'),
			fileTree(dir: "$rootProject.projectDir/AcmeGwt/build/libs",  include: '*.jar'),
			fileTree(dir: "$rootProject.projectDir/AcmeCore/build/libs", include: '*.jar'),
			fileTree(dir: "/${buildDir}/libs", include: '*.jar'))
	
	main = 'com.acme.server.service.GenericRemoteServicesJsonHandlerBuilder'
	args "/${buildDir}/generated-sources/GenericRemoteServicesHandlerImpl.java"
	args 'false'
	
	doFirst {
		(new File("/${buildDir}/generated-sources")).mkdirs()
	}
}

task compileRmtSvcHandlerImpl(type: JavaCompile) {
	dependsOn generateRmtSvcHandlerImpl
	classpath = files(
			fileTree(dir: "$rootProject.projectDir/AcmeGwt/build/libs", include: '*.jar'),
			fileTree(dir: "$rootProject.projectDir/AcmeCore/build/libs",  include: '*.jar'),
			fileTree(dir: "/${buildDir}/libs", include: '*.jar'))
	
	source = fileTree(dir: "/${buildDir}/generated-sources", include: '**/*.java')
	destinationDir = file("/${buildDir}/classes/java/main")
}

war {
	dependsOn compileRmtSvcHandlerImpl
}

I wish it didn’t feel so hacky, and I’m still open to other suggestions if anyone has any.

Thanks and best regards,
Lee

I think a big part of why it feels so “hacky” is due to the file system references and not relying on the underlying domain model as much as you could.

I would expect additional configurations that would extend the runtime configuration that is used by the war. Add a sourceSet for the generated classes and add that output to the war rather than modifying or trying to get it into runtime. This is more of an augment and less of a “look-ahead”.

I consider this to be an example, not exact working code that you should use instead. It’s closer to a purist approach, ignoring the long-path issue and details not relevant to using various dependencies in and out of the project to generate the code, compile, and include it. However, I hope you will see something in here that helps you come up with something that doesn’t feel like a hack.

def generatedSrcDir = file("${buildDir}/generated-sources") // define since we use it several times later

sourceSets {
    rmtSvcHandlerImpl {
        java.srcDir generatedSrcDir
    }
}

configurations {
    rmtSvcHandlerImplCompile.extendsFrom runtime // compiling the rmtSvcHanderImpl requires everything from runtime (same as war)
    rmtSvcHandlerBuilder.extendsFrom rmtSvcHandlerImplCompile // the builder itself needs a few more things
}

dependencies {
    rmtSvcHandlerBuilder project(':AcmeGwt') // this could be any project needed - depend on project over file system
    rmtSvcHandlerBuilder "junit:junit:$ver_junit"
    rmtSvcHandlerBuilder "org.reflections:reflections:$ver_reflections"
    rmtSvcHandlerBuilder "org.slf4j:slf4j-api:$ver_slf4j"
}

task generateRmtSvcHandlerImpl(type: JavaExec) {
    classpath = configurations.rmtSvcHandlerBuilder // use the configuration - runtime + build dependencies due to configuration hierarchy
    main = 'com.acme.server.service.GenericRemoteServicesJsonHandlerBuilder'
    args "${generatedSrcDir}/GenericRemoteServicesHandlerImpl.java", 'false'
    doFirst { generatedSrcDir.mkdirs() }
}
compileRmtSvcHandlerImplJava.dependsOn 'generateRmtSvcHandlerImpl' // must generate the source before compiling it

war {
    classpath sourceSets.rmtSvcHandlerImpl.output // include compile generated source in the war, no need to mess with or have it end up in runtime
}