Why write templates in pure Java? Objectos 0.4.2 released
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:
-
Objectos HTML; and
-
Objectos CSS.
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:
-
these are problems I faced. I am not trying to imply they are common;
-
this goes back to 2012 at least. And memory can be a tricky thing; and
-
I don't claim to be a good programmer. But in 2012 I was definitely a worse programmer than I am today.
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:
-
the path of
loadTemplate("iface.tmpl")
is now wrong; and -
the
maven-resources-plugin
does not know we moved our templates tosrc/main/java
.
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.