How to Generate Java Sources Using buildSrc Gradle Project and Codemodel
Generate Java sources using buildSrc Gradle project and Codemodel. Check out these helpful tips.
Introduction
Assume that you have to embed some data in your Android application. Data which is gathered from some external source and needs to be parsed to be usable inside an app. In this example, we will use list of Internet top-level domains. As you can see at ICANN TLD Report, there are over 1000 TLDs and new ones are being added once in a while. Furthermore, over the time some domains have been retired.
API for managing TLDs in Android framework has been deprecated because it will become out-of-date very quickly. So where to get up-to-date TLD list from? Fortunately, there is machine-friendly source maintained by IANA: IANA — Root Zone Database.
OK, we’ve got a source, so how to embed that data in app? There are many ways to do it, for example we can just put downloaded text file in assets
or res/raw
directory and parse it at runtime. But there is more efficient way – parse data before app compilation and use it ready-made at runtime. We can generate Java code which will provide method say getTldList()
which will always return TLDs up-to-date at compilation time. Just like Android build tools generate fresh R
class at every compilation.
How generated code should look like?
In this example data is a list of strings so it can be represented as List<String>
. Actually it is a set of strings since domains are unique, but it will be never modified and List interface gives a little bit more opportunities eg. indexing, so we will use a list. Finally we are going to make utility class with one method which should look like that:
1 2 3 4 5 6 7 8 9 10 11 | public final class TldList { private static final List TLD_LIST = Collections.unmodifiableList(Arrays.asList(<TLDs here>)); /** * javadoc here */ public static List<String> getTldList() { return TLD_LIST; } private TldList() {} } |
How to generate Java code automatically?
To generate Java code we need to download source data and rewrite it embedding in Java source file. The latter can be done manually by just printing Java syntax elements to file. But more elegant way is to use dedicated library. In such case you don’t need to worry about braces, newlines and other syntactic elements and focus on the logic. One of the libraries for Java code generation is Codemodel which will be used in this example. With Codemodel code generation is straightforward. Our generator will be written in Groovy like Gradle itself and most of its plugins.
Here is complete code generator:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 | public class TldListGenerator { private static final URL IANA_TLDS_URL = new URL('https://data.iana.org/TLD/tlds-alpha-by-domain.txt') /** * Generates <code>pl.droidsonroids.domainnameutils.TldList</code> * and places it into <code>outputDir</code>. * @param outputDir directory where generated sources will be written to * @param useSavedVersion if true then saved TLD data will be used instead of downloading it * from IANA's website */ public static void generateTldListClass(final File outputDir, final boolean useSavedVersion) { def javadocConfig = new ConfigSlurper() .parse(TldListGenerator.class.getResource('javadoc.properties')) def sourceUrl = useSavedVersion ? TldListGenerator.class.getResource('tlds-alpha-by-domain.txt') : IANA_TLDS_URL def codeModel = new JCodeModel() def fqcn = TldListGenerator.class.getPackage().getName() + '.TldList' def tldListClass = codeModel._class(PUBLIC | FINAL, fqcn, ClassType.CLASS) def classJavadoc = tldListClass.javadoc() classJavadoc.append(javadocConfig.getProperty('classJavadoc')) def listStringType = codeModel.ref(List.class).narrow(codeModel.ref(String.class)) def asListInvocation = codeModel.directClass(Arrays.class.getName()).staticInvoke('asList') sourceUrl.eachLine { if (!it.startsWith('#')) { asListInvocation.arg(it.toLowerCase(Locale.ENGLISH)) } else { classJavadoc.append('\n<br/>').append(it.replaceFirst('#\\s+', '')) } return } def constant = codeModel .directClass(Collections.class.getName()) .staticInvoke('unmodifiableList') .arg(asListInvocation) def field = tldListClass.field(PRIVATE | STATIC | FINAL, listStringType, 'TLD_LIST', constant) def method = tldListClass.method(PUBLIC | STATIC, listStringType, 'getTldList') method.javadoc() .append(javadocConfig.getProperty('methodJavadoc')) .addReturn() .append(javadocConfig.getProperty('methodReturnJavadoc')) method.body()._return(field) tldListClass.constructor(PRIVATE) if (!outputDir.isDirectory() && !outputDir.mkdirs()) { throw new IOException('Could not create directory: ' + outputDir) } codeModel.build(outputDir) } } |
Let’s explain key code parts. At the beginning, we are loading javadocs from properties using ConfigSlurper. Properties are key-value pairs and are located in a separate file, so the code is not mixed with data and all the javadoc text is available in one place similar to Android String resources. Codemodel API are self-explanatory, we call its methods just like we will write the code manually.
Input is read using ResourceGroovyMethods#eachLine() method, like the name suggests the closure (code fragment enclosed with braces) will be executed for each line read from the URL. Special variable it
is a String with each line contents respectively. Lines beginning with hash are comments so we are putting them into javadoc. Other lines contain TLDs so we are passing them lowercased to the generated code. After all lines are processed the source is closed automatically, like in Java’s try-catch-finally or try-with-resources statements.
It is important to specify English Locale explicitly while lowercasing TLDs since that operation is language-dependent. Without Locale specified default one from host invoking generator will be used. For example if that Locale is set to Turkish or Azeri then non-ASCII character (small dotless ı) will be produced as a result of lowercasing ASCII letter I and some of our generated TLDs will be invalid. See Internationalizing Turkish for more info. Finally, we are creating output directory structure and saving generated Java file.
What about error handling, what will happen if there is no Internet connection and data cannot be downloaded? As you can see there is no catch
statements nor throws
clause. In Groovy they are not needed, all exceptions are treated like unchecked. If the exception is thrown from our method it will simply cause Gradle build to fail and will be shown in Gradle Console and Messages windows in Android Studio.
Where to place code generator?
Gradle gives us several opportunities to helpful our code generator eg.:
- direct embedding into
build.gradle
of an app project - separate file eg.
generator.gradle
and usingapply from: 'generator.gradle'
inbuild.gradle
- buildSrc project
- standalone project
First two options gives us the least flexibility eg. our code generator cannot be easily tested and (especially in the first case) code generation logic is mixed with build configuration causing poor readability. The last two options differs mainly by the manner how plugin will be applied to the app project. Standalone project is useful when plugin will be used in many projects and requires a repository or at least copying a JAR file. In this example we are going to use our generator in single application project and we will place it in buildSrc
project.
What is buildSrc
project?
When directory called buildSrc
is found in project’s root directory by Gradle it is treated in a special way. Subproject buildSrc
(and submodule in Android Studio/IntelliJ) is created automatically (no need to declare it in settings.gradle
). Moreover even build.gradle
for that project is not needed since default one is applied implicitly. See Organizing Build Logic in Gradle documentation for more information. That project is added to the classpath of buildsript so it contents will be available in build.gradle files in the same way as classpath dependencies eg. classpath 'com.android.tools.build:gradle:1.2.3'
. buildSrc
project like normal ones can contain unit tests and resources. Its tests are executed on each Gradle invocation on any of other projects.
How to use our generator?
We need to invoke our generateTldListClass()
method somewhere to see any results. We can create a complete Gradle plugin but for this simple purpose, we can just add a custom task to build.gradle
file in app project. A sample implementation can look like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | import pl.droidsonroids.domainnameutils.TldListGenerator apply plugin: 'com.android.application' def generatedSrcDir = new File(buildDir, "generated/tld/src/main/java/") task generateTldList << { TldListGenerator.generateTldListClass(generatedSrcDir, true) } preBuild.dependsOn generateTldList android { sourceSets { main { java { srcDirs += generatedSrcDir } } } <rest of the android closure> } |
Let’s analyze a buildscript. By default files produced during Gradle builds are placed in build
directory located in the project’s root directory. It can be retrieved as buildDir in build.gradle
. So we are building path to our output directory upon it.
We are also creating custom task called generateTldList
. Note that <<
is a shortcut to define an action. Look at Gradle tasks documentation for more info about tasks. Next we are adding our task as a dependency of preBuild
task from Android Gradle Plugin which is executed at the beginning of each project build. Finally we are appending our output directory to the main source set, so code from it will be accessible from app source code.
How to use generated code?
We can use our generated class like any other class. An example below shows how to create a simple Spinner containing TLDs:
1 2 3 | final Spinner spinner = (Spinner) findViewById(R.id.spinner_tld); spinner.setAdapter(new ArrayAdapter<>(this, android.R.layout.simple_dropdown_item_1line, TldList.getTldList())); |
Sample project
A sample project with code generator and simple Android app can be found on Github: koral–/buildsrc-sample. Note that editor in Android Studio (version 1.3) is complaining about Class 'TldListGenerator' already exists in 'pl.droidsonroids.domainnameutils'
however that is bogus error and does not prevent project from building. Running app looks like that:
References
About the author
Ready to take your business to the next level with a digital product?
We'll be with you every step of the way, from idea to launch and beyond!