Why write templates in pure Java? Objectos 0.4.2 released

Marcio Endo
February 19, 2023

Welcome to Objectos Weekly issue #014.

I have released Objectos 0.4.2. It is an "epsilon" release focused on Objectos Code. It adds small increments in an attempt to make its API more "programmer-friendly" if you will.

I will quickly show how the effort is going at the end of this article. In this issue I want to discuss the "why Objectos Code?" question for a while. I will probably continue this discussion in subsequent issues.

This may be too many words written about a Java source code generation library. And I agree to some extent. But know that the same idea of "templates in pure Java" is used by:

Both are currently unreleased. Objectos Code is used to generate code for both libraries. And any idea developed in Objectos Code will most likely be reused in both of them.

At some point I hope they will help me (and others perhaps) to build webapps. And do so in a memory and CPU cost conscious way.

Let's begin.

Text-based template engines

Back in 2012, I was a big user of what I call text-based template engines.

By text-based templates I mean template engines whose template source is a plain text file. Well, mostly plain text: the template source usually contain bits of the template language.

Though I lack actual numbers, I believe it is safe to say that they are still a commonly used technology in 2023.

A hypothetical engine

As an example, suppose the following template to generate a Java file:

package {{pkgName}};

public interface {{name}} {}

It could be a file named iface.tmpl which is accessible in the class path.

Our hypothetical library would provide an API to process this template. The code to generate a HelloWorld.java file from the template could be similar to the following:

var engine = HypotheticalLibrary.createEngine();

Template tmpl;

try {
  tmpl = engine.loadTemplate("iface.tmpl")
} catch (IOException e) {
  System.err.println("Failed to load template");
  return;  
}

var tmpdir = System.getProperty("java.io.tmpdir");
var target = Path.of(tmpdir, "HelloWorld.java");

var data = Map.of(
  "pkgName", "com.example",
  "name", "HelloWorld"
);

try {
  tmpl.write(target, data);
} catch (IOException e) {
  System.err.println("Failed to write HelloWorld.java");
  return;
}

Just to be clear, this is all for a hypothetical library.

Assuming a "happy path" kind-of scenario it would generate:

package com.example;

public interface HelloWorld {}

As mentioned, I was a big user of template engines such as this.

Issues

Let's consider the last code sample our canonical example. Next, I will discuss a few issues I had while using code like that.

Before I do, please try to keep in mind the following:

Loading external resources

So the first issue is the loading of external resources:

Template tmpl;

try {
  tmpl = engine.loadTemplate("iface.tmpl")
} catch (IOException e) {
  System.err.println("Failed to load template");
  
  return;  
}

And, when performing I/O, one has to deal with IOException.

Back in 2012 I probably did not handle exceptions as well I should have. In some ways, my appreciation for the try and try-with-resources statements is quite recent. It is just easier to have the enclosing method declare that it throws the IOException instead.

Of course, this being a Java file generation library one has to perform I/O at some point. After all, the generated Java file has to be written eventually.

But it is probably nice if we can minimize that.

Project and file organization

Let's stay at the same try statement for a while. The actual loading of the external resource happens at this method invocation:

tmpl = engine.loadTemplate("iface.tmpl")

The question is: where is the file iface.tmpl located?

Let's assume we are running the code in the class path. In other words, the application is not modular or it is not being run in the module path. Additionally, let's assume our project is a Maven project.

So we should create the iface.tmpl file in the src/main/resources directory.

I find this works fine if the number of templates is small. However, as the number of Java files and template files starts to grow, I find it cumbersome having to constantly navigate between src/main/java and src/main/resources in my IDE.

So one solution is to move all of the templates to live alongside the corresponding Java file in the src/main/java directory.

But this creates two new problems:

So there's that.

Separation of concerns

I understand that separation of concerns is generally considered to be a good practice.

This is probably just a sign of my age, but the older I get, I find that the fewer the files the better. Of course this is not an absolute rule. In any case, it generally means going against the 'separation of concerns' principle.

With text-based templates, as the template is a separate concern, you need to learn its particular template language. In our example:

package {{pkgName}};

public interface {{name}} {}

One needs to learn the syntax for variables. Also there's special syntax for iterating over the elements of a collection for example.

The data is also a separate concern. It is represented by a Map in our example:

var data = Map.of(
  "pkgName", "com.example",
  "name", "HelloWorld"
);

It could also have been a POJO like:

class MyInterface {
  String pkgName;
  String name;
}

An alternative is to merge both together:

import objectos.code.JavaTemplate;

class MyInterface extends JavaTemplate {
  String pkgName;
  String name;

  @Override
  protected void definition() {
    _package(pkgName);
    
    _public(); _interface(name); body();
  }
}

And as a bonus, it allows you to use plain Java language constructs like for and if statements.

Keeping variable names in sync

The last code example also shows that keeping variables names in sync between template and data model becomes trivial.

IDEs can help, but, with text-based templates, if we were to change the pkgName variable name to packageName:

package {{packageName}};

public interface {{name}} {}

We must not forget to also update our data model:

var data = Map.of(
  "packageName", "com.example",
  "name", "HelloWorld"
);

Otherwise generation might fail at runtime or, worse, it might be silently ignored. This might lead to sending out emails saying:

Hello null!

Objectos 0.4.2 released

As mentioned this is an ongoing effort.

The idea is to allow expressing a method declaration in the following Objectos Code:

static final ClassTypeName STRING = classType(String.class);

method(
  annotation(Override.class),
  PUBLIC, FINAL, STRING, name("toString"),
  RETURN, s("Objectos Code")
)

To generate the following:

@java.lang.Override
public final java.lang.String toString() {
  return "Objectos Code";
}

You can read the full release notes here.

Until the next issue of Objectos Weekly

So that's it for today. I hope you enjoyed reading.

Please send me an e-mail if you have comments, questions or corrections regarding this post.