Before you look for validation in others, try and find it in yourself
Greg Behrendt
A further step on the way to static templates is the automation of their creation. A Maven plugin can be used to convert the FreshMarker template sources into Java source code. Although there are now more modern build tools, Maven is still used in many projects. The first task of the Maven plugin should be the validation of template sources.
There have already been several articles on developing your own Maven plugins here, so only the implementation will be explained here. Further details on the development of Maven plugins can be found in the articles Build Automatisierung mit eigenen Maven Plugins verbessern and Trivial Pursuit – API MarkDown.
The verification of FreshMarker templates is quite simple, here only the list of templates to be verified must be determined and then each template must be loaded by a TemplateBuilder
.
The following example shows how the plugin can be used for verification.
<plugin> <groupId>de.schegge</groupId> <artifactId>freshmarker-compiler-maven-plugin</artifactId> <version>0.1.0</version> <executions> <execution> <goals> <goal>validate-templates</goal> </goals> <configuration> <templates> <directory>src/main/resources</directory> <includes> <include>**/*.fmt</include> </includes> </templates> </configuration> </execution> </executions> </plugin>
This reads all templates from the src/main/resource
directory that are located in any subfolder and have the file suffix .ftm
.
A Mojo must be created for each task of a plugin that contains the name of the task in its @Mojo
annotation. In addition, the phase of processing in which the Mojo is to be called can also be specified here. For this Mojo, we have chosen the PROCESS_RESOURCES
phase.
@Mojo(name = "validate-templates", threadSafe = true, defaultPhase = LifecyclePhase.PROCESS_RESOURCES) public class TemplateValidateMojo extends AbstractTemplateMojo { @Override protected void executeTemplates() throws MojoExecutionException, MojoFailureException { getLog().info("Validate template"); FileSet templates = getTemplates(); FileSetManager fileSetManager = new FileSetManager(); String[] includedFiles = fileSetManager.getIncludedFiles(templates); if (includedFiles.length == 0) { getLog().info("No templates found"); return; } Path inputPath = Path.of(templates.getDirectory()); Verifier verifier = new Verifier(new Configuration().builder(), charset(), getLog()); for (String includedFile : includedFiles) { verifier.verify(inputPath.resolve(includedFile)); } if (!verifier.getFail().isEmpty()) { throw new MojoExecutionException("Error validate template on " + verifier.getFail().size() + "templates"); } } }
All processing takes place in the executeTemplate
method. This method is called by the execute
method of the AbstractTemplateMojo
base class. But more on this later.
At the beginning of the method, the list of template sources is determined with a FileSet
and a FileSetManager
. The use of these classes provides a real Maven feeling in the configuration, unfortunately these classes, like so many things in the Maven API, are quite old school.
Next, a Verifier
instance is created and all template sources are checked with the it. At the end of the check, the Verifier
contains a list of failed checks. If this list is not empty, processing in Mojo is terminated with an MojoExecutionException
.
The Mojo only takes care of the provision of resources and the control of processing, the actual work has been moved to the Verifier
class. This class receives a TemplateBuilder
, a CharSet
and a Logger
from the Mojo to do its work.
class Verifier { private final TemplateBuilder builder; private final List<Path> fail = new LinkedList<>(); private final Charset charset; private final Log log; private Verifier(TemplateBuilder builder, Charset charset, Log log) { this.builder = builder; this.charset = charset; this.log = log; } public void verify(Path path) { log.info("validate: " + path); try { builder.getTemplate("validate", new InputStreamReader(new FileInputStream(path.toFile()), charset)); } catch (ParseException e) { log.info("cannot parse: " + path); fail.add(path); } catch (FileNotFoundException e) { log.info("cannot read: " + path); fail.add(path); } } public List<Path> getFail() { return fail; } }
In the verify
method, the TemplateBuilder
reads in the corresponding template with the help of the given Path
. If an error occurs during import, the path is saved in the fail
list for later analysis.
Some processing details have been outsourced to the AbstractTemplateMojo
class because they are also to be used by other Mojos.
public abstract class AbstractTemplateMojo extends AbstractMojo { /** * Skip the generation of the template. */ @Parameter(defaultValue = "false") protected boolean skip; /** * The character encoding. */ @Parameter(defaultValue = "${project.build.sourceEncoding}") protected String encoding; /** * The base directory. */ @Parameter(defaultValue = "${project.basedir}", readonly = true) protected File baseDirectory; /** * A specific <code>fileSet</code> rule to select files and directories. */ @Parameter protected FileSet templates; protected Charset charset() throws MojoFailureException { if (encoding == null) { return Charset.defaultCharset(); } try { return Charset.forName(encoding); } catch (UnsupportedCharsetException e) { throw new MojoFailureException("Invalid 'encoding' option: " + encoding); } } protected FileSet getTemplates() { if (templates == null) { templates = new FileSet(); templates.setDirectory(baseDirectory.toPath().resolve("src/main/resources/freshmarker").toString()); templates.setIncludes(List.of("**/*")); } else { templates.setDirectory(baseDirectory.toPath().resolve(templates.getDirectory()).toString()); } return templates; } }
This class provides four parameters for the Mojo implementations. A skip
flag to suppress processing in the respective Mojo. An encoding
parameter to change the character encoding of the input files. There is also a templates
parameter that defines the FileSet
for the input and finally the baseDirectory
parameter to resolve relative paths against the project directory.
The FileSet
parameter is somewhat annoying because it is difficult to configure. The @Parameter
annotation is not able to influence the individual components and, of course, Path
instances are not supported as results.
Anyone who has taken a closer look will have recognized that our Mojo does not require any configuration. The following plugin description is sufficient to verify all templates in the src/main/resources/freshmarker
directory.
<plugin> <groupId>de.schegge</groupId> <artifactId>freshmarker-compiler-maven-plugin</artifactId> <version>0.1.0</version> <executions> <execution> <goals> <goal>validate-templates</goal> </goals> </execution> </executions> </plugin>
This means that the first Mojo for automating our FreshMarker projects is already available and can be used in your own CI/CD process to validate FreshMarker templates in advance.