FreshMarker – compile it! (4)

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.

Leave a Comment