This is an automated email from the ASF dual-hosted git repository.

ddekany pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/freemarker-generator.git


The following commit(s) were added to refs/heads/master by this push:
     new 60f6243  initial commit
     new 22ca184  Merge commit 'refs/pull/2/head' of 
https://github.com/apache/freemarker-generator
60f6243 is described below

commit 60f6243b3716e1a543ebbc1eb4e9d9fa12c3cda7
Author: bjackson <[email protected]>
AuthorDate: Sat Sep 29 19:10:36 2018 -0500

    initial commit
---
 README.md                                          | 147 +++++++++
 pom.xml                                            | 172 +++++++++++
 .../apache/freemarker/generator/FactoryUtil.java   |  50 +++
 .../freemarker/generator/FreeMarkerMojo.java       | 125 ++++++++
 .../generator/GeneratingFileVisitor.java           |  78 +++++
 .../generator/JsonPropertiesProvider.java          |  92 ++++++
 .../freemarker/generator/OutputGenerator.java      | 166 ++++++++++
 .../OutputGeneratorPropertiesProvider.java         |  32 ++
 src/test/data/freemarker-mojo/data/test.txt.json   |   3 +
 .../data/freemarker-mojo/freemarker.properties     |   3 +
 src/test/data/freemarker-mojo/template/test.ftl    |   1 +
 .../badPath/success-test.txt.json                  |   4 +
 .../data/badParent/bad-parent-test.txt.json        |   6 +
 .../data/mydir/bad-extension-test.txt              |   6 +
 .../data/mydir/bad-template-name.txt.json          |   6 +
 .../data/mydir/missing-template-name.txt.json      |   6 +
 .../data/mydir/missing-var-test.txt.json           |   4 +
 .../data/mydir/success-test-2.txt.json             |   4 +
 .../data/mydir/success-test.txt.json               |   6 +
 .../template/test-pom-only.ftl                     |   1 +
 .../data/generating-file-visitor/template/test.ftl |   1 +
 .../freemarker/generator/FreeMarkerMojoTest.java   | 340 +++++++++++++++++++++
 .../generator/GeneratingFileVisitorTest.java       | 181 +++++++++++
 .../generator/JsonPropertiesProviderTest.java      | 121 ++++++++
 .../freemarker/generator/OutputGeneratorTest.java  | 295 ++++++++++++++++++
 25 files changed, 1850 insertions(+)

diff --git a/README.md b/README.md
index 1ab5a81..26b8923 100644
--- a/README.md
+++ b/README.md
@@ -28,6 +28,153 @@ Currently it can be invoked as a Maven plug-in, but in the 
future it might
 will be callable on other ways (say, from Gradle, or as a standalone command
 line tool).
 
+freemarker-generator-maven-plugin
+---------------------------------
+## Table of contents
+
+- [Background](#background)
+- [Install](#install)
+- [Usage](#usage)
+  - [FreeMarker Template Files](#freemarker-template-files)
+  - [JSON Generator Files](#json-generator-files)
+  - [Using POM Properties During 
Generation](#using-pom-properties-during-generation)
+  - [FreeMarker Configuration](#freemarker-configuration)
+  - [Incremental Builds](#incremental-builds)
+- [Code Coverage](#code-coverage)
+- [Contributing](#contributing)
+- [License](#license)
+
+## Background
+This plugin generates source files from FreeMarker templates with a flexible 
process that includes the ability to:
+
+- Generate multiple source files from a single template,
+- Generate source files during multiple steps in the build process such as 
testing, and
+- Specify distinct locations for the templates and data models for different 
stages of the build. 
+
+## Install
+### pom.xml
+
+Add the following snippet within the `<plugins>` tag of your pom.xml:
+
+```xml
+      <plugin>
+        <groupId>com.oath</groupId>
+        <artifactId>freemarker-maven-plugin</artifactId>
+        <version>${freemarker-maven-plugin.version}</version>
+        <configuration>
+          <!-- Required. Specifies the compatibility version for template 
processing -->
+          <freeMarkerVersion>2.3.23</freeMarkerVersion>
+        </configuration>
+        <executions>
+          <!-- If you want to generate files during other phases, just add 
more execution
+               tags and specify appropriate phase, sourceDirectory and 
outputDirectory values.
+          -->
+          <execution>
+            <id>freemarker</id>
+            <!-- Optional, defaults to generate-sources -->
+            <phase>generate-sources</phase>
+            <goals>
+              <!-- Required, must be generate -->
+              <goal>generate</goal>
+            </goals>
+            <configuration>
+              <!-- Optional, defaults to src/main/freemarker/generator -->
+              <sourceDirectory>src/main/freemarker</templateDirectory>
+              <!-- Optional, defaults to 
src/main/freemarker/generator/template -->
+              
<templateDirectory>src/main/freemarker/template</templateDirectory>
+              <!-- Optional, defaults to src/main/freemarker/generator -->
+              
<generatorDirectory>src/main/freemarker/generator/generator</generatorDirectory>
+              <!-- Optional, defaults to target/generated-sources/freemarker 
-->
+              
<outputDirectory>target/generated-sources/freemarker/generator</outputDirectory>
+            </configuration>
+          </execution>
+        </executions>
+      </plugin>
+```
+
+## Usage
+
+### FreeMarker Template Files
+FreeMarker template files must reside in the `templateDirectory`. For the 
default configuration,
+this is: `src/main/freemarker/generator/template`.
+
+By convention, file names for FreeMarker template files use the .ftl 
extension. For details on the FreeMarker
+template syntax, see: [Getting 
Started](https://freemarker.apache.org/docs/dgui_quickstart.html) and
+[Template Language Reference](https://freemarker.apache.org/docs/ref.html).
+
+### JSON Generator Files
+The JSON generator files must reside in the `generatorDirectory`. For the 
default
+configuration, this is: `src/main/freemarker/generator/generator`.
+
+For each JSON generator file, freemarker-maven-plugin will generate a file 
under the outputDirectory.
+The name of the generated file will be based on the name of the JSON data 
file. For example,
+the following JSON file: 
+```
+    <sourceDirectory>/data/my/package/MyClass.java.json
+```
+will result in the following file being generated:
+```
+    <outputDirectory>/my/package/MyClass.java
+```
+
+This plugin parses the JSON generator file's `dataModel` field into a 
`Map<String, Object>` instance (hereafter, referred
+to as the data model). If the dataModel field is empty, an empty map will be 
created.
+
+Here are some additional details you need to know.
+
+  - This plugin *requires* one top-level field in the JSON data file: 
`templateName`. This field is used to locate the template file under 
`<sourceDirectory>/template` that is used to generate the file. This plugin 
provides the data model to FreeMarker as the data model to process the template 
identified by `templateName`.
+  - The parser allows for comments.
+  - This plugin currently assumes that the JSON data file encoded using UTF-8.
+
+Here is an example JSON data file:
+```json
+{
+  // An end-of-line comment.
+  # Another end-of-line comment
+  "templateName": "my-template.ftl", #Required
+  "dataModel": { #Optional
+      /* A multi-line
+         comment */
+      "myString": "a string",
+      "myNumber": 1,
+      "myListOfStrings": ["s1", "s2"],
+      "myListOfNumbers": [1, 2],
+      "myMap": {
+        "key1": "value1",
+        "key2": 2
+      }
+  }
+}
+```
+
+### Using POM Properties During Generation
+After parsing the JSON file, the plugin will add
+a `pomProperties` entry into the data model, which is a map itself, that 
contains the properties defined in the pom. Thus, your template can reference 
the pom property `my_property` using `${pomProperties.my_property}`. If you 
have a period or dash in the property name, use 
`${pomProperties["my.property"]}`.
+
+
+
+### FreeMarker Configuration
+
+Typically, users of this plugin do not need to mess with the FreeMarker 
configuration. This plugin explicitly sets two FreeMarker configurations:
+
+ 1. the default encoding is set to UTF-8
+ 2. the template loader is set to be a FileTemplateLoader that reads from 
`templateDirectory`.
+ 
+If you need to override these configs or set your own, you can put them in a 
+`<sourceDirectory>/freemarker.properties` file. If that file exists, this 
plugin will read it into a java Properties instance and pass it to 
freemarker.core.Configurable.setSettings() to establish the FreeMarker 
configuration. See this 
[javadoc](https://freemarker.apache.org/docs/api/freemarker/template/Configuration.html#setSetting-java.lang.String-java.lang.String-)
 for configuration details.
+
+
+### Incremental Builds
+This plugin supports incremental builds; it only generates sources if the 
generator file, template file, or pom file have timestamps newer than any 
existing output file.  To force a rebuild if these conditions are not met (for 
example, if you pass in a model parameter on the command line), first run `mvn 
clean`.
+
+## Code Coverage
+
+By default, the code coverage report is not generated. It is generated by 
screwdriver jobs. You can generate code coverage on your dev machine with the 
following maven command:
+```bash
+mvn clean initialize -Dclover-phase=initialize 
+``` 
+Bring up the coverage report by pointing your browser to 
target/site/clover/dashboard.html under the root directory of the local 
repository.
+
 
 Licensing
 ---------
diff --git a/pom.xml b/pom.xml
new file mode 100644
index 0000000..4edc9e9
--- /dev/null
+++ b/pom.xml
@@ -0,0 +1,172 @@
+<project xmlns="http://maven.apache.org/POM/4.0.0"; 
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance";
+  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 
http://maven.apache.org/xsd/maven-4.0.0.xsd";>
+  <modelVersion>4.0.0</modelVersion>
+
+  <groupId>org.apache.freemarker</groupId>
+  <artifactId>freemarker-generator-maven-plugin</artifactId>
+  <version>1.0.${build}</version>
+  <packaging>maven-plugin</packaging>
+
+  <name>Freemarker Generator Maven Plugin</name>
+  <url>http://freemarker.apache.org/</url>
+
+  <properties>
+    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+    <maven.compiler.source>1.8</maven.compiler.source>
+    <maven.compiler.target>1.8</maven.compiler.target>
+    <maven-core.version>3.5.2</maven-core.version>
+    <maven-plugin-api.version>3.5.2</maven-plugin-api.version>
+    <maven-plugin-annotations.version>3.5</maven-plugin-annotations.version>
+    <fastutil.version>8.1.0</fastutil.version>
+    <freemarker.version>2.3.23</freemarker.version>
+    <gson.version>2.8.2</gson.version>
+    <jmockit.version>1.32</jmockit.version>
+    <org.testng.version>6.8</org.testng.version>
+    <assertj-core.version>3.8.0</assertj-core.version>
+    <clover-target-percentage>100</clover-target-percentage>
+    <clover-phase>pre-site</clover-phase>
+    <target-jdk-version>1.8</target-jdk-version>
+  </properties>
+
+  <dependencies>
+    <dependency>
+      <groupId>org.apache.maven</groupId>
+      <artifactId>maven-core</artifactId>
+      <version>${maven-core.version}</version>
+      <scope>provided</scope>
+      <exclusions>
+        <exclusion>
+          <groupId>com.google.guava</groupId>
+          <artifactId>guava</artifactId>
+        </exclusion>
+        <exclusion>
+          <groupId>org.codehaus.plexus</groupId>
+          <artifactId>plexus-utils</artifactId>
+        </exclusion>
+      </exclusions>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.maven</groupId>
+      <artifactId>maven-plugin-api</artifactId>
+      <version>${maven-plugin-api.version}</version>
+      <scope>provided</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.maven.plugin-tools</groupId>
+      <artifactId>maven-plugin-annotations</artifactId>
+      <version>${maven-plugin-annotations.version}</version>
+      <scope>provided</scope>
+      <exclusions>
+        <exclusion>
+          <groupId>org.apache.maven</groupId>
+          <artifactId>maven-artifact</artifactId>
+        </exclusion>
+      </exclusions>
+    </dependency>
+    <dependency>
+      <groupId>org.eclipse.sisu</groupId>
+      <artifactId>org.eclipse.sisu.plexus</artifactId>
+      <version>0.3.3</version>
+      <exclusions>
+        <exclusion>
+          <groupId>org.codehaus.plexus</groupId>
+          <artifactId>plexus-utils</artifactId>
+        </exclusion>
+      </exclusions>
+    </dependency>
+    <dependency>
+      <groupId>org.jmockit</groupId>
+      <artifactId>jmockit</artifactId>
+      <version>${jmockit.version}</version>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>com.google.code.gson</groupId>
+      <artifactId>gson</artifactId>
+      <version>${gson.version}</version>
+    </dependency>
+    <dependency>
+      <groupId>org.freemarker</groupId>
+      <artifactId>freemarker</artifactId>
+      <version>${freemarker.version}</version>
+    </dependency>
+    <dependency>
+      <groupId>org.testng</groupId>
+      <artifactId>testng</artifactId>
+      <version>${org.testng.version}</version>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.assertj</groupId>
+      <artifactId>assertj-core</artifactId>
+      <version>${assertj-core.version}</version>
+      <scope>test</scope>
+    </dependency>
+  </dependencies>
+
+  <build>
+    <plugins>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-compiler-plugin</artifactId>
+        <version>3.7.0</version>
+        <configuration>
+          <source>1.8</source>
+          <target>1.8</target>
+        </configuration>
+      </plugin>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-source-plugin</artifactId>
+        <version>3.0.1</version>
+        <executions>
+          <execution>
+            <id>attach-sources</id>
+            <goals>
+              <goal>jar</goal>
+            </goals>
+          </execution>
+        </executions>
+      </plugin>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-plugin-plugin</artifactId>
+        <version>3.5</version>
+        <configuration>
+          <goalPrefix>freemarker</goalPrefix>
+        </configuration>
+        <executions>
+          <execution>
+            <id>default-descriptor</id>
+            <goals>
+              <goal>descriptor</goal>
+            </goals>
+            <phase>process-classes</phase>
+          </execution>
+        </executions>
+      </plugin>
+      <plugin>
+        <groupId>org.openclover</groupId>
+        <artifactId>clover-maven-plugin</artifactId>
+        <version>4.2.0</version>
+        <executions>
+          <execution>
+            <id>clover</id>
+            <phase>${clover-phase}</phase>
+            <goals>
+              <goal>instrument-test</goal>
+              <goal>clover</goal>
+              <goal>check</goal>
+            </goals>
+            <configuration>
+              <targetPercentage>${clover-target-percentage}</targetPercentage>
+              <generateHtml>true</generateHtml>
+              <generateXml>true</generateXml>
+              <jdk>${target-jdk-version}</jdk>
+            </configuration>
+          </execution>
+        </executions>
+      </plugin>
+    </plugins>
+  </build>
+</project>
diff --git a/src/main/java/org/apache/freemarker/generator/FactoryUtil.java 
b/src/main/java/org/apache/freemarker/generator/FactoryUtil.java
new file mode 100644
index 0000000..359dd3f
--- /dev/null
+++ b/src/main/java/org/apache/freemarker/generator/FactoryUtil.java
@@ -0,0 +1,50 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.freemarker.generator;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+
+import freemarker.template.Configuration;
+import freemarker.template.Version;
+
+/**
+ * Simple utility class to call various constructors.
+ * Needed because some jmockit features don't work well with constructors.
+ */
+public class FactoryUtil {
+
+  public static Configuration createConfiguration(String freeMarkerVersion) {
+    return new Configuration(new Version(freeMarkerVersion));
+  }
+
+  public static File createFile(File parent, String child) {
+    return new File(parent, child);
+  }
+
+  public static FileInputStream createFileInputStream(File file) throws 
FileNotFoundException {
+    return new FileInputStream(file);
+  }
+  
+  public static File createFile(String name) {
+    return new File(name);
+  }
+}
diff --git a/src/main/java/org/apache/freemarker/generator/FreeMarkerMojo.java 
b/src/main/java/org/apache/freemarker/generator/FreeMarkerMojo.java
new file mode 100644
index 0000000..5b0454a
--- /dev/null
+++ b/src/main/java/org/apache/freemarker/generator/FreeMarkerMojo.java
@@ -0,0 +1,125 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.freemarker.generator;
+
+import java.io.File;
+import java.io.InputStream;
+import java.nio.file.Files;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Properties;
+
+import org.apache.maven.execution.MavenSession;
+import org.apache.maven.plugin.AbstractMojo;
+import org.apache.maven.plugin.MojoExecution;
+import org.apache.maven.plugin.MojoExecutionException;
+import org.apache.maven.plugin.MojoFailureException;
+import org.apache.maven.plugins.annotations.LifecyclePhase;
+import org.apache.maven.plugins.annotations.Mojo;
+import org.apache.maven.plugins.annotations.Parameter;
+
+import freemarker.cache.FileTemplateLoader;
+import freemarker.template.Configuration;
+
+@Mojo(name = "generate", defaultPhase = LifecyclePhase.GENERATE_SOURCES)
+public class FreeMarkerMojo extends AbstractMojo {
+
+  /** FreeMarker version string used to build FreeMarker Configuration 
instance. */
+  @Parameter
+  private String freeMarkerVersion;
+
+  @Parameter(defaultValue = "src/main/freemarker/generator")
+  private File sourceDirectory;
+
+  @Parameter(defaultValue = "src/main/freemarker/generator/template")
+  private File templateDirectory;
+
+  @Parameter(defaultValue = "src/main/freemarker/generator/generator")
+  private File generatorDirectory;
+
+  @Parameter(defaultValue = "target/generated-sources/freemarker/generator")
+  private File outputDirectory;
+
+  @Parameter(defaultValue = "${session}", readonly = true)
+  private MavenSession session;
+
+  @Parameter(defaultValue = "${mojoExecution}", readonly = true)
+  private MojoExecution mojo;
+
+  @Override
+  public void execute() throws MojoExecutionException, MojoFailureException {
+
+    if (freeMarkerVersion == null || freeMarkerVersion.length() == 0) {
+      throw new MojoExecutionException("freeMarkerVersion is required");
+    }
+
+    if (!generatorDirectory.isDirectory()) {
+      throw new MojoExecutionException("Required directory does not exist: " + 
generatorDirectory);
+    }
+
+    Configuration config = FactoryUtil.createConfiguration(freeMarkerVersion);
+
+    config.setDefaultEncoding("UTF-8");
+
+    if (!templateDirectory.isDirectory()) {
+      throw new MojoExecutionException("Required directory does not exist: " + 
templateDirectory);
+    }
+    try {
+      config.setTemplateLoader(new FileTemplateLoader(templateDirectory));
+    } catch (Throwable t) {
+      getLog().error("Could not establish file template loader for directory: 
" + templateDirectory, t);
+      throw new MojoExecutionException("Could not establish file template 
loader for directory: " + templateDirectory);
+    }
+
+    File freeMarkerProps = FactoryUtil.createFile(sourceDirectory, 
"freemarker.properties");
+    if (freeMarkerProps.isFile()) {
+      Properties configProperties = new Properties();
+      try (InputStream is = 
FactoryUtil.createFileInputStream(freeMarkerProps)) {
+        configProperties.load(is);
+      } catch (Throwable t) {
+        getLog().error("Failed to load " + freeMarkerProps, t);
+        throw new MojoExecutionException("Failed to load " + freeMarkerProps);
+      }
+      try {
+        config.setSettings(configProperties);
+      } catch (Throwable t) {
+        getLog().error("Invalid setting(s) in " + freeMarkerProps, t);
+        throw new MojoExecutionException("Invalid setting(s) in " + 
freeMarkerProps);
+      }
+    }
+    
+    if ("generate-sources".equals(mojo.getLifecyclePhase())) {
+      
session.getCurrentProject().addCompileSourceRoot(outputDirectory.toString());
+    } else if ("generate-test-sources".equals(mojo.getLifecyclePhase())) {
+      
session.getCurrentProject().addTestCompileSourceRoot(outputDirectory.toString());
+    }
+
+    Map<String, OutputGeneratorPropertiesProvider> extensionToBuilders = new 
HashMap<>(1);
+    extensionToBuilders.put(".json", 
JsonPropertiesProvider.create(generatorDirectory,templateDirectory,outputDirectory));
+
+    GeneratingFileVisitor fileVisitor = GeneratingFileVisitor.create(config, 
session, extensionToBuilders);
+    try {
+      Files.walkFileTree(generatorDirectory.toPath(), fileVisitor);
+    } catch (Throwable t) {
+      getLog().error("Failed to process files in generator dir: " + 
generatorDirectory, t);
+      throw new MojoExecutionException("Failed to process files in generator 
dir: " + generatorDirectory);
+    }
+  }
+}
diff --git 
a/src/main/java/org/apache/freemarker/generator/GeneratingFileVisitor.java 
b/src/main/java/org/apache/freemarker/generator/GeneratingFileVisitor.java
new file mode 100644
index 0000000..a130257
--- /dev/null
+++ b/src/main/java/org/apache/freemarker/generator/GeneratingFileVisitor.java
@@ -0,0 +1,78 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.freemarker.generator;
+
+import java.nio.file.FileVisitResult;
+import java.nio.file.Path;
+import java.nio.file.SimpleFileVisitor;
+import java.nio.file.attribute.BasicFileAttributes;
+import java.util.Map;
+
+import org.apache.maven.execution.MavenSession;
+
+import freemarker.template.Configuration;
+
+/**
+ * FileVisitor designed to process json data files. The json file parsed into
+ * a map and given to FreeMarker to 
+ */
+public class GeneratingFileVisitor extends SimpleFileVisitor<Path> {
+
+  private final Configuration config;
+  private final MavenSession session;
+  private final long pomLastModifiedTimestamp;
+  private final Map<String, OutputGeneratorPropertiesProvider > 
extensionToBuilder;
+
+  private GeneratingFileVisitor(Configuration config, MavenSession session, 
Map<String, OutputGeneratorPropertiesProvider> extensionToBuilder) {
+    this.config = config;
+    this.session = session;
+    this.extensionToBuilder = extensionToBuilder;
+    this.pomLastModifiedTimestamp = session.getAllProjects().stream()
+        .map(project->project.getFile().lastModified())
+        .reduce(Long::max)
+        .orElse(0L);
+  }
+
+  /**
+   * Factory method that calls constructor, added to facilitate testing with 
jmockit.
+   */
+  public static GeneratingFileVisitor create(Configuration config, 
MavenSession session, Map<String, OutputGeneratorPropertiesProvider> 
extensionToBuilder) {
+    return new GeneratingFileVisitor(config, session, extensionToBuilder);
+  }
+
+  @Override
+  public FileVisitResult visitFile(Path path, BasicFileAttributes attrs) {
+    if (attrs.isRegularFile()) {
+      OutputGenerator.OutputGeneratorBuilder builder = 
OutputGenerator.builder()
+          .addGeneratorLocation(path)
+          .addPomLastModifiedTimestamp(pomLastModifiedTimestamp);
+      String fileName = path.getFileName().toString();
+      String extenstion = fileName.substring(fileName.lastIndexOf('.'));
+      OutputGeneratorPropertiesProvider pathProcessor = 
extensionToBuilder.get(extenstion);
+      if (pathProcessor == null) {
+        throw new RuntimeException("Unknown file extension: " + path);
+      }
+      pathProcessor.providePropertiesFromFile(path, builder);
+      builder.addToDataModel("pomProperties", 
session.getCurrentProject().getProperties());
+      builder.create().generate(config);
+    }
+    return FileVisitResult.CONTINUE;
+  }
+}
diff --git 
a/src/main/java/org/apache/freemarker/generator/JsonPropertiesProvider.java 
b/src/main/java/org/apache/freemarker/generator/JsonPropertiesProvider.java
new file mode 100644
index 0000000..ea9e678
--- /dev/null
+++ b/src/main/java/org/apache/freemarker/generator/JsonPropertiesProvider.java
@@ -0,0 +1,92 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.freemarker.generator;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.reflect.TypeToken;
+import com.google.gson.stream.JsonReader;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.InputStreamReader;
+import java.lang.reflect.Type;
+import java.nio.file.Path;
+import java.util.HashMap;
+import java.util.Map;
+
+public class JsonPropertiesProvider implements 
OutputGeneratorPropertiesProvider {
+       private final Gson gson;
+       private final Type stringObjectMap;
+       private final File dataDir;
+       private final File templateDir;
+       private final File outputDir;
+
+       private JsonPropertiesProvider(File dataDir, File templateDir, File 
outputDir) {
+               this.dataDir = dataDir;
+               this.templateDir = templateDir;
+               this.outputDir = outputDir;
+               gson = new GsonBuilder().setLenient().create();
+               stringObjectMap = new TypeToken<Map<String, Object>>() { } 
.getType();
+       }
+
+       public static JsonPropertiesProvider create(File dataDir, File 
templateDir, File outputDir) {
+               return new JsonPropertiesProvider(dataDir, templateDir, 
outputDir);
+       }
+
+       @Override
+       public void providePropertiesFromFile(Path path, 
OutputGenerator.OutputGeneratorBuilder builder) {
+               File jsonDataFile = path.toFile();
+               Map<String,Object> data = parseJson(jsonDataFile);
+
+               Object obj = data.get("dataModel");
+               if (obj != null) {
+                       builder.addDataModel((Map<String, Object>) obj);
+               } else {
+                       builder.addDataModel(new HashMap<String,Object>());
+               }
+
+               obj = data.get("templateName");
+               if (obj == null) {
+                       throw new RuntimeException("Require json data property 
not found: templateName");
+               }
+               
builder.addTemplateLocation(templateDir.toPath().resolve(obj.toString()));
+
+               String dataDirName = dataDir.getAbsolutePath();
+               String jsonFileName = jsonDataFile.getAbsolutePath();
+               if (!jsonFileName.startsWith(dataDirName)) {
+                       throw new IllegalStateException("visitFile() given file 
not in sourceDirectory: " + jsonDataFile);
+               }
+
+               String outputFileName = 
jsonFileName.substring(dataDirName.length()+1);
+               outputFileName = outputFileName.substring(0, 
outputFileName.length() - 5);
+               Path outputPath = outputDir.toPath();
+               Path resolved = outputPath.resolve(outputFileName);
+               builder.addOutputLocation(resolved);
+       }
+
+       private Map<String, Object> parseJson(File jsonDataFile) {
+               try (JsonReader reader = new JsonReader(new 
InputStreamReader(new FileInputStream(jsonDataFile), "UTF-8"))) {
+                       return gson.fromJson(reader, stringObjectMap);
+               } catch (Throwable t) {
+                       throw new RuntimeException("Could not parse json data 
file: " + jsonDataFile, t);
+               }
+       }
+}
diff --git a/src/main/java/org/apache/freemarker/generator/OutputGenerator.java 
b/src/main/java/org/apache/freemarker/generator/OutputGenerator.java
new file mode 100644
index 0000000..e639392
--- /dev/null
+++ b/src/main/java/org/apache/freemarker/generator/OutputGenerator.java
@@ -0,0 +1,166 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.freemarker.generator;
+
+import freemarker.template.Configuration;
+import freemarker.template.Template;
+
+import java.io.File;
+import java.io.FileWriter;
+import java.nio.file.Path;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Knows how to generate an output file given five things:
+ * <ul>
+ * <li>The latest update time of the <b>pom file(s)</b> for the project</li>
+ * <li>The location of the <b>generator file</b></li>
+ * <li>The location of the <b>template file</b></li>
+ * <li>The location of the <b>output file</b></li>
+ * <li>A <b>data model<b> used to fill out the template.</li>
+ * </ul>
+ *<p>Given these five pieces of information, the generator will generate a new 
output file, but only if any existing
+ * generated file is not newer than the inputs (pom, generator, and 
template).</p>
+ */
+class OutputGenerator {
+       public final long pomModifiedTimestamp;
+       public final Path generatorLocation;
+       public final Path templateLocation;
+       public final Path outputLocation;
+       public final Map<String,Object> dataModel;
+       private OutputGenerator(
+                long pomModifiedTimestamp,
+                Path generatorLocation,
+                Path templateLocation,
+                Path outputLocation,
+                Map<String, Object> dataModel) {
+               this.pomModifiedTimestamp = pomModifiedTimestamp;
+               this.generatorLocation = generatorLocation;
+               this.templateLocation = templateLocation;
+               this.outputLocation = outputLocation;
+               this.dataModel = dataModel;
+       }
+
+       /**
+        * Uses a fluent builder to make the code more legible in place.
+        * Also allows the output generator to be built from multiple locations 
in source by passing the builder.
+        * @return A new fluent builder for the OutputGenerator class.
+        */
+       public static OutputGeneratorBuilder builder() {
+               return new OutputGeneratorBuilder();
+       }
+
+       public static class OutputGeneratorBuilder {
+               private long pomModifiedTimestamp = Long.MAX_VALUE;
+               private Path generatorLocation = null;
+               private Path templateLocation = null;
+               private Path outputLocation = null;
+               private Map<String,Object> dataModel = null;
+
+               public OutputGeneratorBuilder addPomLastModifiedTimestamp(long 
pomModifiedTimestamp) {
+                       this.pomModifiedTimestamp = pomModifiedTimestamp;
+                       return this;
+               }
+
+               public OutputGeneratorBuilder addGeneratorLocation(Path 
generatorLocation) {
+                       this.generatorLocation = generatorLocation;
+                       return this;
+               }
+
+               public OutputGeneratorBuilder addTemplateLocation(Path 
templateLocation) {
+                       this.templateLocation = templateLocation;
+                       return this;
+               }
+
+               public OutputGeneratorBuilder addOutputLocation(Path 
outputLocation) {
+                       this.outputLocation = outputLocation;
+                       return this;
+               }
+
+               public OutputGeneratorBuilder addDataModel(Map<String,Object> 
dataModel) {
+                       this.dataModel = dataModel;
+                       return this;
+               }
+
+               public OutputGeneratorBuilder addToDataModel(String key, Object 
val) {
+                       if (this.dataModel == null) {
+                               this.dataModel = new HashMap<>(4);
+                       }
+                       this.dataModel.put(key,val);
+                       return this;
+               }
+
+               /**
+                * @throws IllegalStateException if any of the parts of the 
OutputGenerator were not set.
+                * @return A new output generator (which is immutable).
+                */
+               public OutputGenerator create() {
+                       if (pomModifiedTimestamp == Long.MAX_VALUE) throw new 
IllegalStateException("Must set the pomModifiedTimestamp");
+                       if (generatorLocation == null) throw new 
IllegalStateException("Must set a non-null generatorLocation");
+                       if (templateLocation == null) throw new 
IllegalStateException("Must set a non-null templateLocation");
+                       if (outputLocation == null) throw new 
IllegalStateException("Must set a non-null outputLocation");
+                       if (dataModel == null) throw new 
IllegalStateException("Must set a non-null dataModel");
+                       return new OutputGenerator(pomModifiedTimestamp, 
generatorLocation, templateLocation, outputLocation, dataModel);
+               }
+       }
+
+       /**
+        * <p>Generates an output by applying the model to the template.</p>
+        * <p>Checks the ages of the inputs against an existing output file to 
early exit if there is no update.</p>
+        * @param config Used to load the template from the template name.
+        */
+       public void generate(Configuration config) {
+               //Use "createFile" for testing purposes only
+               File outputFile = 
FactoryUtil.createFile(outputLocation.toFile().toString());
+               File templateFile = templateLocation.toFile();
+               File generatorFile = generatorLocation.toFile();
+               if (outputFile.exists()) {
+                       //early exit only if the output file is newer than all 
files that contribute to its generation
+                       if (outputFile.lastModified() > 
generatorFile.lastModified()
+                                && outputFile.lastModified() > 
templateFile.lastModified()
+                                && outputFile.lastModified() > 
pomModifiedTimestamp) {
+                               return;
+                       }
+               } else {
+                       File parentDir = outputFile.getParentFile();
+                       if (parentDir.isFile()) {
+                               throw new RuntimeException("Parent directory of 
output file is a file: " + parentDir.getAbsoluteFile());
+                       }
+                       parentDir.mkdirs();
+                       if (!parentDir.isDirectory()) {
+                               throw new RuntimeException("Could not create 
directory: " + parentDir.getAbsoluteFile());
+                       }
+               }
+
+               Template template;
+               try {
+                       template = config.getTemplate(templateFile.getName());
+               } catch (Throwable t) {
+                       throw new RuntimeException("Could not read template: " 
+ templateFile.getName(), t);
+               }
+
+               try (FileWriter writer = new FileWriter(outputFile)) {
+                       template.process(dataModel, writer);
+               } catch (Throwable t) {
+                       throw new RuntimeException("Could not process template 
associated with data file: " + generatorLocation, t);
+               }
+       }
+}
diff --git 
a/src/main/java/org/apache/freemarker/generator/OutputGeneratorPropertiesProvider.java
 
b/src/main/java/org/apache/freemarker/generator/OutputGeneratorPropertiesProvider.java
new file mode 100644
index 0000000..019373b
--- /dev/null
+++ 
b/src/main/java/org/apache/freemarker/generator/OutputGeneratorPropertiesProvider.java
@@ -0,0 +1,32 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.freemarker.generator;
+
+import java.nio.file.Path;
+
+public interface OutputGeneratorPropertiesProvider {
+       /**
+        * Must add three properties to the builder: the 
<b>templateLocation</b>, <b>outputLocation</b>, and <b>dataModel</b>
+        * The <b>pom updated timestamp</b> and <b>generatorLocation</b> are 
added elsewhere.
+        * @param path The path to the generator file, to be used to decide on 
the three properties above.
+        * @param builder The builder to which to add the properties.
+        */
+       public void providePropertiesFromFile(Path path, 
OutputGenerator.OutputGeneratorBuilder builder);
+}
diff --git a/src/test/data/freemarker-mojo/data/test.txt.json 
b/src/test/data/freemarker-mojo/data/test.txt.json
new file mode 100644
index 0000000..0702dca
--- /dev/null
+++ b/src/test/data/freemarker-mojo/data/test.txt.json
@@ -0,0 +1,3 @@
+{
+  "templateName": "test.ftl"
+}
\ No newline at end of file
diff --git a/src/test/data/freemarker-mojo/freemarker.properties 
b/src/test/data/freemarker-mojo/freemarker.properties
new file mode 100644
index 0000000..17cefa1
--- /dev/null
+++ b/src/test/data/freemarker-mojo/freemarker.properties
@@ -0,0 +1,3 @@
+# Test properties file used by FreeMarkerMojoTest to verify FreeMarkerMojo
+# passes these settings to the FreeMarker configuration.
+boolean_format: T,F
\ No newline at end of file
diff --git a/src/test/data/freemarker-mojo/template/test.ftl 
b/src/test/data/freemarker-mojo/template/test.ftl
new file mode 100644
index 0000000..6986c80
--- /dev/null
+++ b/src/test/data/freemarker-mojo/template/test.ftl
@@ -0,0 +1 @@
+This is a dummy test file. It is only here to make sure the directory exists.
\ No newline at end of file
diff --git 
a/src/test/data/generating-file-visitor/badPath/success-test.txt.json 
b/src/test/data/generating-file-visitor/badPath/success-test.txt.json
new file mode 100644
index 0000000..89b6d13
--- /dev/null
+++ b/src/test/data/generating-file-visitor/badPath/success-test.txt.json
@@ -0,0 +1,4 @@
+{
+  "templateName": "test.ftl",
+  "testVar": "test value"
+}
\ No newline at end of file
diff --git 
a/src/test/data/generating-file-visitor/data/badParent/bad-parent-test.txt.json 
b/src/test/data/generating-file-visitor/data/badParent/bad-parent-test.txt.json
new file mode 100644
index 0000000..ad68303
--- /dev/null
+++ 
b/src/test/data/generating-file-visitor/data/badParent/bad-parent-test.txt.json
@@ -0,0 +1,6 @@
+{
+  "templateName": "test.ftl",
+  "dataModel": {
+    "testVar": "test value"
+  }
+}
\ No newline at end of file
diff --git 
a/src/test/data/generating-file-visitor/data/mydir/bad-extension-test.txt 
b/src/test/data/generating-file-visitor/data/mydir/bad-extension-test.txt
new file mode 100644
index 0000000..e4747b5
--- /dev/null
+++ b/src/test/data/generating-file-visitor/data/mydir/bad-extension-test.txt
@@ -0,0 +1,6 @@
+{
+  "templateName": "test.ftl",
+  "dataModel": {
+    "testVar": "test 2 value"
+  }
+}
\ No newline at end of file
diff --git 
a/src/test/data/generating-file-visitor/data/mydir/bad-template-name.txt.json 
b/src/test/data/generating-file-visitor/data/mydir/bad-template-name.txt.json
new file mode 100644
index 0000000..5f01033
--- /dev/null
+++ 
b/src/test/data/generating-file-visitor/data/mydir/bad-template-name.txt.json
@@ -0,0 +1,6 @@
+{
+  "templateName": "missing.ftl",
+  "dataModel": {
+    "testVar": "test value"
+  }
+}
\ No newline at end of file
diff --git 
a/src/test/data/generating-file-visitor/data/mydir/missing-template-name.txt.json
 
b/src/test/data/generating-file-visitor/data/mydir/missing-template-name.txt.json
new file mode 100644
index 0000000..f80ce71
--- /dev/null
+++ 
b/src/test/data/generating-file-visitor/data/mydir/missing-template-name.txt.json
@@ -0,0 +1,6 @@
+{
+  //not ok to be missing the templateName
+  "dataModel": {
+    "testVar": "test value"
+  }
+}
\ No newline at end of file
diff --git 
a/src/test/data/generating-file-visitor/data/mydir/missing-var-test.txt.json 
b/src/test/data/generating-file-visitor/data/mydir/missing-var-test.txt.json
new file mode 100644
index 0000000..b97ea16
--- /dev/null
+++ b/src/test/data/generating-file-visitor/data/mydir/missing-var-test.txt.json
@@ -0,0 +1,4 @@
+{
+  "templateName": "test.ftl"
+  //missing dataModel is not ok, since we are missing a variable needed to 
fill out the template
+}
\ No newline at end of file
diff --git 
a/src/test/data/generating-file-visitor/data/mydir/success-test-2.txt.json 
b/src/test/data/generating-file-visitor/data/mydir/success-test-2.txt.json
new file mode 100644
index 0000000..5dc549c
--- /dev/null
+++ b/src/test/data/generating-file-visitor/data/mydir/success-test-2.txt.json
@@ -0,0 +1,4 @@
+{
+  "templateName": "test-pom-only.ftl"
+  //missing dataModel, is OK, assuming all required properies are found in pom
+}
diff --git 
a/src/test/data/generating-file-visitor/data/mydir/success-test.txt.json 
b/src/test/data/generating-file-visitor/data/mydir/success-test.txt.json
new file mode 100644
index 0000000..ad68303
--- /dev/null
+++ b/src/test/data/generating-file-visitor/data/mydir/success-test.txt.json
@@ -0,0 +1,6 @@
+{
+  "templateName": "test.ftl",
+  "dataModel": {
+    "testVar": "test value"
+  }
+}
\ No newline at end of file
diff --git a/src/test/data/generating-file-visitor/template/test-pom-only.ftl 
b/src/test/data/generating-file-visitor/template/test-pom-only.ftl
new file mode 100644
index 0000000..7168154
--- /dev/null
+++ b/src/test/data/generating-file-visitor/template/test-pom-only.ftl
@@ -0,0 +1 @@
+This is a test freemarker template. Test pom data: '${pomProperties.pomVar}'.
diff --git a/src/test/data/generating-file-visitor/template/test.ftl 
b/src/test/data/generating-file-visitor/template/test.ftl
new file mode 100644
index 0000000..1edee95
--- /dev/null
+++ b/src/test/data/generating-file-visitor/template/test.ftl
@@ -0,0 +1 @@
+This is a test freemarker template. Test json data: '${testVar}'. Test pom 
data: '${pomProperties.pomVar}'.
\ No newline at end of file
diff --git 
a/src/test/java/org/apache/freemarker/generator/FreeMarkerMojoTest.java 
b/src/test/java/org/apache/freemarker/generator/FreeMarkerMojoTest.java
new file mode 100644
index 0000000..f5b846b
--- /dev/null
+++ b/src/test/java/org/apache/freemarker/generator/FreeMarkerMojoTest.java
@@ -0,0 +1,340 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.freemarker.generator;
+
+import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.FileVisitor;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Comparator;
+import java.util.Map;
+import java.util.Properties;
+
+import org.apache.maven.execution.MavenSession;
+import org.apache.maven.plugin.MojoExecution;
+import org.apache.maven.plugin.MojoExecutionException;
+import org.apache.maven.plugin.MojoFailureException;
+import org.apache.maven.project.MavenProject;
+import org.junit.Assert;
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.Test;
+
+import freemarker.cache.FileTemplateLoader;
+import freemarker.cache.TemplateLoader;
+import freemarker.template.Configuration;
+import mockit.Deencapsulation;
+import mockit.Expectations;
+import mockit.Mocked;
+import mockit.Verifications;
+
+public class FreeMarkerMojoTest extends Assert {
+
+  public static final File testOutputDir = new 
File("target/test-output/freemarker-mojo");
+  
+  @BeforeClass
+  public static void beforeClass() throws IOException {
+    // Clean output dir before each run.
+    if (testOutputDir.exists()) {
+      // Recursively delete output from previous run.
+      Files.walk(testOutputDir.toPath())
+       .sorted(Comparator.reverseOrder())
+       .map(Path::toFile)
+       .forEach(File::delete);
+    }
+  }
+
+  @Test
+  public void executeTest(
+      @Mocked MavenSession session,
+      @Mocked MavenProject project,
+      @Mocked MojoExecution mojoExecution,
+      @Mocked GeneratingFileVisitor generatingFileVisitor,
+      @Mocked Files files
+      ) throws MojoExecutionException, MojoFailureException, IOException {
+
+    new Expectations(mojoExecution, generatingFileVisitor) {{
+      mojoExecution.getLifecyclePhase(); result = "generate-sources";
+      session.getCurrentProject(); result = project;
+    }};
+
+    FreeMarkerMojo mojo = new FreeMarkerMojo();
+    
+    // Validate freeMarkerVersion is required.
+    assertThatExceptionOfType(MojoExecutionException.class).isThrownBy(() -> {
+      mojo.execute();
+    }).withMessage("freeMarkerVersion is required");
+    
+    Deencapsulation.setField(mojo, "freeMarkerVersion", "");
+    assertThatExceptionOfType(MojoExecutionException.class).isThrownBy(() -> {
+      mojo.execute();
+    }).withMessage("freeMarkerVersion is required");
+
+    File testCaseOutputDir = new File(testOutputDir, "executeTest");
+    Deencapsulation.setField(mojo, "freeMarkerVersion", "2.3.23");
+    Deencapsulation.setField(mojo, "sourceDirectory", testCaseOutputDir);
+    Deencapsulation.setField(mojo, "templateDirectory", new 
File(testCaseOutputDir, "template"));
+    Deencapsulation.setField(mojo, "generatorDirectory", new 
File(testCaseOutputDir, "data"));
+    Deencapsulation.setField(mojo, "outputDirectory", new 
File(testCaseOutputDir, "generated-files"));
+    Deencapsulation.setField(mojo, "mojo", mojoExecution);
+    Deencapsulation.setField(mojo, "session", session);
+
+    // Validate source directory.
+    assertThatExceptionOfType(MojoExecutionException.class).isThrownBy(() -> {
+      mojo.execute();
+    }).withMessage("Required directory does not exist: 
target/test-output/freemarker-mojo/executeTest/data");
+    
+    new File(testCaseOutputDir, "data").mkdirs();
+    assertThatExceptionOfType(MojoExecutionException.class).isThrownBy(() -> {
+      mojo.execute();
+    }).withMessage("Required directory does not exist: 
target/test-output/freemarker-mojo/executeTest/template");
+    new File(testCaseOutputDir, "template").mkdirs();
+    
+    // Validate minimum configuration.
+    mojo.execute();
+    
+    new Verifications() {{
+      
project.addCompileSourceRoot("target/test-output/freemarker-mojo/executeTest/generated-files");
 times = 1;
+
+      Configuration config;
+      MavenSession capturedSession;
+      Map<String, OutputGeneratorPropertiesProvider> builders;
+
+      GeneratingFileVisitor.create(
+          config = withCapture(), 
+          capturedSession = withCapture(), 
+          builders = withCapture()); times = 1;
+
+      assertEquals("UTF-8", config.getDefaultEncoding());
+      assertEquals(session, capturedSession);
+      TemplateLoader loader = config.getTemplateLoader();
+      assertTrue(loader instanceof FileTemplateLoader);
+
+      Path path;
+      FileVisitor<Path> fileVisitor;
+      
+      Files.walkFileTree(path = withCapture(), fileVisitor = withCapture()); 
times = 1;
+      
+      assertEquals(new File(testCaseOutputDir, "data").toPath(), path);
+      assertTrue(fileVisitor instanceof GeneratingFileVisitor);
+    }};
+  }
+  
+  @Test
+  public void execute_generateTestSourceTest(
+      @Mocked MavenSession session,
+      @Mocked MavenProject project,
+      @Mocked MojoExecution mojoExecution,
+      @Mocked GeneratingFileVisitor generatingFileVisitor,
+      @Mocked Files files
+      ) throws MojoExecutionException, MojoFailureException, IOException {
+
+    new Expectations(mojoExecution, generatingFileVisitor) {{
+      mojoExecution.getLifecyclePhase(); result = "generate-test-sources";
+      session.getCurrentProject(); result = project;
+    }};
+
+    FreeMarkerMojo mojo = new FreeMarkerMojo();
+    
+    File testCaseOutputDir = new File(testOutputDir, "generateTestSourceTest");
+    Deencapsulation.setField(mojo, "freeMarkerVersion", "2.3.23");
+    Deencapsulation.setField(mojo, "sourceDirectory", testCaseOutputDir);
+    Deencapsulation.setField(mojo, "templateDirectory", new 
File(testCaseOutputDir, "template"));
+    Deencapsulation.setField(mojo, "generatorDirectory", new 
File(testCaseOutputDir, "data"));
+    Deencapsulation.setField(mojo, "outputDirectory", new 
File(testCaseOutputDir, "generated-files"));
+    Deencapsulation.setField(mojo, "mojo", mojoExecution);
+    Deencapsulation.setField(mojo, "session", session);
+
+    new File(testCaseOutputDir, "data").mkdirs();
+    new File(testCaseOutputDir, "template").mkdirs();
+    
+    mojo.execute();
+    
+    new Verifications() {{
+      
project.addTestCompileSourceRoot("target/test-output/freemarker-mojo/generateTestSourceTest/generated-files");
 times = 1;
+    }};
+  }
+
+  @SuppressWarnings({"rawtypes", "unchecked"})
+  @Test
+  public void execute_walkFileTreeExceptionTest(
+      @Mocked MavenSession session,
+      @Mocked MavenProject project,
+      @Mocked MojoExecution mojoExecution,
+      @Mocked GeneratingFileVisitor generatingFileVisitor,
+      @Mocked Files files
+      ) throws MojoExecutionException, MojoFailureException, IOException {
+
+    new Expectations(mojoExecution, generatingFileVisitor) {{
+      mojoExecution.getLifecyclePhase(); result = "generate-test-sources";
+      session.getCurrentProject(); result = project;
+      Files.walkFileTree((Path) any,(FileVisitor) any); result = new 
RuntimeException("test exception");
+    }};
+
+    FreeMarkerMojo mojo = new FreeMarkerMojo();
+    
+    File testCaseOutputDir = new File(testOutputDir, "generateTestSourceTest");
+    Deencapsulation.setField(mojo, "freeMarkerVersion", "2.3.23");
+    Deencapsulation.setField(mojo, "sourceDirectory", testCaseOutputDir);
+    Deencapsulation.setField(mojo, "templateDirectory", new 
File(testCaseOutputDir, "template"));
+    Deencapsulation.setField(mojo, "generatorDirectory", new 
File(testCaseOutputDir, "data"));
+    Deencapsulation.setField(mojo, "outputDirectory", new 
File(testCaseOutputDir, "generated-files"));
+    Deencapsulation.setField(mojo, "mojo", mojoExecution);
+    Deencapsulation.setField(mojo, "session", session);
+
+    new File(testCaseOutputDir, "data").mkdirs();
+    new File(testCaseOutputDir, "template").mkdirs();
+    
+    assertThatExceptionOfType(MojoExecutionException.class).isThrownBy(() -> {
+      mojo.execute();
+    }).withMessage("Failed to process files in generator dir: 
target/test-output/freemarker-mojo/generateTestSourceTest/data");
+  }
+  
+  @Test
+  public void execute_setTemplateLoaderExceptionTest(
+      @Mocked MavenSession session,
+      @Mocked MavenProject project,
+      @Mocked MojoExecution mojoExecution,
+      @Mocked FactoryUtil factoryUtil,
+      @Mocked Configuration config) {
+    
+    new Expectations(config, FactoryUtil.class) {{
+      FactoryUtil.createConfiguration("2.3.23"); result = config;
+      config.setTemplateLoader((TemplateLoader) any); result = new 
RuntimeException("test exception");
+    }};
+
+    FreeMarkerMojo mojo = new FreeMarkerMojo();
+
+    File testCaseOutputDir = new File(testOutputDir, 
"setTemplateLoaderException");
+
+    Deencapsulation.setField(mojo, "freeMarkerVersion", "2.3.23");
+    Deencapsulation.setField(mojo, "sourceDirectory", testCaseOutputDir);
+    Deencapsulation.setField(mojo, "templateDirectory", new 
File(testCaseOutputDir, "template"));
+    Deencapsulation.setField(mojo, "generatorDirectory", new 
File(testCaseOutputDir, "data"));
+    Deencapsulation.setField(mojo, "outputDirectory", new 
File(testCaseOutputDir, "generated-files"));
+    Deencapsulation.setField(mojo, "mojo", mojoExecution);
+    Deencapsulation.setField(mojo, "session", session);
+
+    new File(testCaseOutputDir, "data").mkdirs();
+    new File(testCaseOutputDir, "template").mkdirs();
+
+    assertThatExceptionOfType(MojoExecutionException.class).isThrownBy(() -> {
+      mojo.execute();
+    }).withMessage("Could not establish file template loader for directory: 
target/test-output/freemarker-mojo/setTemplateLoaderException/template");
+  }
+  
+  @Test
+  public void execute_loadFreemarkerPropertiesTest(
+      @Mocked MavenSession session,
+      @Mocked MavenProject project,
+      @Mocked MojoExecution mojoExecution,
+      @Mocked Configuration config) throws Exception {
+    
+    FreeMarkerMojo mojo = new FreeMarkerMojo();
+
+    File sourceDirectory = new File("src/test/data/freemarker-mojo");
+    File testCaseOutputDir = new File(testOutputDir, 
"loadFreemarkerProperties");
+
+    Deencapsulation.setField(mojo, "freeMarkerVersion", "2.3.23");
+    Deencapsulation.setField(mojo, "sourceDirectory", sourceDirectory);
+    Deencapsulation.setField(mojo, "templateDirectory", new File( 
sourceDirectory, "template"));
+    Deencapsulation.setField(mojo, "generatorDirectory", new File( 
sourceDirectory, "data"));
+    Deencapsulation.setField(mojo, "outputDirectory", new 
File(testCaseOutputDir, "generated-files"));
+    Deencapsulation.setField(mojo, "mojo", mojoExecution);
+    Deencapsulation.setField(mojo, "session", session);
+
+    mojo.execute();
+    
+    new Verifications() {{
+      Properties properties;
+      
+      config.setSettings(properties = withCapture()); times = 1;
+      
+      assertEquals("T,F", properties.getProperty("boolean_format"));
+    }};
+  }
+  
+  @Test
+  public void execute_loadFreemarkerPropertiesExceptionTest(
+      @Mocked MavenSession session,
+      @Mocked MavenProject project,
+      @Mocked MojoExecution mojoExecution,
+      @Mocked FactoryUtil factoryUtil,
+      @Mocked Configuration config) throws Exception {
+    
+    new Expectations(FactoryUtil.class) {{
+      FactoryUtil.createFileInputStream((File) any); result = new 
RuntimeException("test exception");
+    }};
+    
+    FreeMarkerMojo mojo = new FreeMarkerMojo();
+
+    File sourceDirectory = new File("src/test/data/freemarker-mojo");
+    File testCaseOutputDir = new File(testOutputDir, 
"loadFreemarkerPropertiesExceptionTest");
+
+    Deencapsulation.setField(mojo, "freeMarkerVersion", "2.3.23");
+    Deencapsulation.setField(mojo, "sourceDirectory", sourceDirectory);
+    Deencapsulation.setField(mojo, "templateDirectory", new File( 
sourceDirectory, "template"));
+    Deencapsulation.setField(mojo, "generatorDirectory", new File( 
sourceDirectory, "data"));
+    Deencapsulation.setField(mojo, "outputDirectory", new 
File(testCaseOutputDir, "generated-files"));
+    Deencapsulation.setField(mojo, "mojo", mojoExecution);
+    Deencapsulation.setField(mojo, "session", session);
+
+    System.out.println("==== before mojo execute");
+    try {
+    assertThatExceptionOfType(MojoExecutionException.class).isThrownBy(() -> {
+      mojo.execute();
+    }).withMessage("Failed to load 
src/test/data/freemarker-mojo/freemarker.properties");
+    } catch ( Throwable t) {
+      t.printStackTrace();
+    }
+  }
+
+  @Test
+  public void execute_setSettingsExceptionTest(
+      @Mocked MavenSession session,
+      @Mocked MavenProject project,
+      @Mocked MojoExecution mojoExecution,
+      @Mocked Configuration config) throws Exception {
+    
+    new Expectations() {{
+      config.setSettings((Properties) any); result = new 
RuntimeException("test exception");
+    }};
+    
+    FreeMarkerMojo mojo = new FreeMarkerMojo();
+
+    File sourceDirectory = new File("src/test/data/freemarker-mojo");
+    File testCaseOutputDir = new File(testOutputDir, 
"loadFreemarkerProperties");
+
+    Deencapsulation.setField(mojo, "freeMarkerVersion", "2.3.23");
+    Deencapsulation.setField(mojo, "sourceDirectory", sourceDirectory);
+    Deencapsulation.setField(mojo, "templateDirectory", new File( 
sourceDirectory, "template"));
+    Deencapsulation.setField(mojo, "generatorDirectory", new File( 
sourceDirectory, "data"));
+    Deencapsulation.setField(mojo, "outputDirectory", new 
File(testCaseOutputDir, "generated-files"));
+    Deencapsulation.setField(mojo, "mojo", mojoExecution);
+    Deencapsulation.setField(mojo, "session", session);
+
+    assertThatExceptionOfType(MojoExecutionException.class).isThrownBy(() -> {
+      mojo.execute();
+    }).withMessage("Invalid setting(s) in 
src/test/data/freemarker-mojo/freemarker.properties");
+  }
+  
+}
diff --git 
a/src/test/java/org/apache/freemarker/generator/GeneratingFileVisitorTest.java 
b/src/test/java/org/apache/freemarker/generator/GeneratingFileVisitorTest.java
new file mode 100644
index 0000000..8770a27
--- /dev/null
+++ 
b/src/test/java/org/apache/freemarker/generator/GeneratingFileVisitorTest.java
@@ -0,0 +1,181 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.freemarker.generator;
+
+import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.FileVisitResult;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.attribute.BasicFileAttributes;
+import java.util.*;
+
+import org.apache.maven.execution.MavenSession;
+import org.apache.maven.project.MavenProject;
+import org.junit.Assert;
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import freemarker.cache.FileTemplateLoader;
+import freemarker.template.Configuration;
+import mockit.Expectations;
+import mockit.Mocked;
+
+public class GeneratingFileVisitorTest extends Assert {
+
+  private static File testDir = new 
File("src/test/data/generating-file-visitor");
+  private static File dataDir = new File(testDir, "data");
+  private static File templateDir = new File(testDir, "template");
+  private static File outputDir = new 
File("target/test-output/generating-file-visitor");
+  private static Map<String, OutputGeneratorPropertiesProvider> builders = new 
HashMap<>();
+  private Configuration config;
+  private Properties pomProperties = new Properties();
+  
+  @BeforeClass
+  public static void beforeClass() throws IOException {
+        builders.put(".json", 
JsonPropertiesProvider.create(dataDir,templateDir,outputDir));
+    // Clean output dir before each run.
+    File outputDir = new File("target/test-output/generating-file-visitor");
+    if (outputDir.exists()) {
+      // Recursively delete output from previous run.
+      Files.walk(outputDir.toPath())
+       .sorted(Comparator.reverseOrder())
+       .map(Path::toFile)
+       .forEach(File::delete);
+    }
+  }
+
+  @BeforeMethod
+  public void before() throws IOException {
+    if (!testDir.isDirectory()) {
+      throw new RuntimeException("Can't find required test data directory. "
+          + "If running test outside of maven, make sure working directory is 
the project directory. "
+          + "Looking for: " + testDir);
+    }
+
+    config = new Configuration(Configuration.VERSION_2_3_23);
+    config.setDefaultEncoding("UTF-8");
+    config.setTemplateLoader(new FileTemplateLoader(templateDir));
+    pomProperties.put("pomVar", "pom value");
+  }
+  
+  @Test
+  public void functionalHappyPathTestNoDataModel(
+      @Mocked MavenSession session, 
+      @Mocked MavenProject project,
+      @Mocked File mockFile,
+      @Mocked BasicFileAttributes attrs) throws IOException {
+    List<MavenProject> projects = new ArrayList<>();
+    projects.add(project);
+    new Expectations(session, project, mockFile) {{
+      session.getCurrentProject(); result = project;
+      session.getAllProjects(); result = projects;
+      project.getProperties(); result = pomProperties;
+      attrs.isRegularFile(); result = true;
+      project.getFile(); result = mockFile;
+      mockFile.lastModified(); result = 10;
+    }};
+    
+    File file = new File(dataDir, "mydir/success-test-2.txt.json");
+    GeneratingFileVisitor gfv = GeneratingFileVisitor.create(config, session, 
builders);
+    assertEquals(FileVisitResult.CONTINUE, gfv.visitFile(file.toPath(), 
attrs));
+    
+    File outputFile = new File(outputDir, "mydir/success-test-2.txt");
+    assertTrue(outputFile.isFile());
+    List<String> lines = Files.readAllLines(outputFile.toPath(), 
StandardCharsets.UTF_8);
+    assertEquals(1, lines.size());
+    assertEquals("This is a test freemarker template. Test pom data: 'pom 
value'.", lines.get(0));
+  }
+
+  @Test
+  public void functionalHappyPathTest(
+      @Mocked MavenSession session,
+      @Mocked MavenProject project,
+      @Mocked File mockFile,
+      @Mocked BasicFileAttributes attrs) throws IOException {
+    List<MavenProject> projects = new ArrayList<>();
+    projects.add(project);
+    new Expectations(session, project, mockFile) {{
+      session.getCurrentProject(); result = project;
+      session.getAllProjects(); result = projects;
+      project.getProperties(); result = pomProperties;
+      attrs.isRegularFile(); result = true;
+      project.getFile(); result = mockFile;
+      mockFile.lastModified(); result = 10;
+    }};
+
+    File file = new File(dataDir, "mydir/success-test.txt.json");
+    GeneratingFileVisitor gfv = GeneratingFileVisitor.create(config, session, 
builders);
+    assertEquals(FileVisitResult.CONTINUE, gfv.visitFile(file.toPath(), 
attrs));
+
+    File outputFile = new File(outputDir, "mydir/success-test.txt");
+    assertTrue(outputFile.isFile());
+    List<String> lines = Files.readAllLines(outputFile.toPath(), 
StandardCharsets.UTF_8);
+    assertEquals(1, lines.size());
+    assertEquals("This is a test freemarker template. Test json data: 'test 
value'. Test pom data: 'pom value'.", lines.get(0));
+  }
+
+  @Test
+  public void visitFile_badExtensionTest(
+      @Mocked MavenSession session,
+      @Mocked MavenProject project,
+      @Mocked File mockFile,
+      @Mocked BasicFileAttributes attrs) throws IOException {
+    List<MavenProject> projects = new ArrayList<>();
+    projects.add(project);
+    new Expectations(session, project, mockFile) {{
+      attrs.isRegularFile(); result = true;
+      session.getAllProjects(); result = projects;
+      project.getFile(); result = mockFile;
+      mockFile.lastModified(); result = 10;
+    }};
+    // Test file without .json suffix.
+    File file = new File(dataDir, "mydir/bad-extension-test.txt");
+    GeneratingFileVisitor gfv = GeneratingFileVisitor.create(config, session, 
builders);
+    assertThatExceptionOfType(RuntimeException.class).isThrownBy(() -> {
+      gfv.visitFile(file.toPath(), attrs);
+    }).withMessage("Unknown file extension: " + file.toPath());
+  }
+
+  @Test 
+  public void visitFile_notRegularFileTest(@Mocked MavenSession session,
+                                           @Mocked MavenProject project,
+                                           @Mocked BasicFileAttributes attrs,
+                                           @Mocked File mockFile
+                                           ) {
+    List<MavenProject> projects = new ArrayList<>();
+    projects.add(project);
+    new Expectations(session, project, mockFile) {{
+      attrs.isRegularFile(); result = false;
+      session.getAllProjects(); result = projects;
+      project.getFile(); result = mockFile;
+      mockFile.lastModified(); result = 10;
+    }};
+    // FYI: if you change above result to true, test will fail trying to read 
the 'mydir' directory
+    // as a json file.
+    File dir = new File(dataDir, "mydir");
+    GeneratingFileVisitor gfv = GeneratingFileVisitor.create(config, session, 
builders);
+    assertEquals(FileVisitResult.CONTINUE, gfv.visitFile(dir.toPath(), attrs));
+  }
+}
diff --git 
a/src/test/java/org/apache/freemarker/generator/JsonPropertiesProviderTest.java 
b/src/test/java/org/apache/freemarker/generator/JsonPropertiesProviderTest.java
new file mode 100644
index 0000000..3f5b2ff
--- /dev/null
+++ 
b/src/test/java/org/apache/freemarker/generator/JsonPropertiesProviderTest.java
@@ -0,0 +1,121 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.freemarker.generator;
+
+import com.google.gson.Gson;
+import com.google.gson.stream.JsonReader;
+import mockit.Expectations;
+import mockit.Mocked;
+import mockit.Verifications;
+import org.testng.annotations.Test;
+
+import java.io.File;
+import java.lang.reflect.Type;
+import java.nio.file.Path;
+import java.util.HashMap;
+import java.util.Map;
+
+import static 
org.assertj.core.api.AssertionsForClassTypes.assertThatExceptionOfType;
+import static org.testng.AssertJUnit.assertEquals;
+import static org.testng.internal.junit.ArrayAsserts.assertArrayEquals;
+
+public class JsonPropertiesProviderTest {
+       private File testDir = new 
File("src/test/data/generating-file-visitor");
+       private File dataDir = new File(testDir, "data");
+       private File templateDir = new File(testDir, "template");
+       private File outputDir = new 
File("target/test-output/generating-file-visitor");
+
+       @Test
+       public void testSuccess(@Mocked OutputGenerator.OutputGeneratorBuilder 
builder) {
+               Path path = 
dataDir.toPath().resolve("mydir/success-test.txt.json");
+               Path expectedTemplateLocation = 
templateDir.toPath().resolve("test.ftl");
+               Path expectedOutputLocation = 
outputDir.toPath().resolve("mydir/success-test.txt");
+               Map<String,Object> expectedMap = new HashMap<String,Object>(4);
+               expectedMap.put("testVar", "test value");
+               JsonPropertiesProvider toTest = 
JsonPropertiesProvider.create(dataDir, templateDir, outputDir);
+               toTest.providePropertiesFromFile(path, builder);
+               new Verifications(){{
+                       Path templateLocation;
+                       builder.addTemplateLocation(templateLocation = 
withCapture());
+                       Path outputLocation;
+                       builder.addOutputLocation(outputLocation = 
withCapture());
+                       Map<String,Object> actualMap;
+                       builder.addDataModel(actualMap = withCapture());
+
+                       assertEquals(expectedTemplateLocation, 
templateLocation);
+                       assertEquals(expectedOutputLocation, outputLocation);
+                       assertArrayEquals(expectedMap.entrySet().toArray(), 
actualMap.entrySet().toArray());
+               }};
+       }
+
+       @Test
+       public void testSuccessNoDataModel(@Mocked 
OutputGenerator.OutputGeneratorBuilder builder) {
+               Path path = 
dataDir.toPath().resolve("mydir/success-test-2.txt.json");
+               Path expectedTemplateLocation = 
templateDir.toPath().resolve("test-pom-only.ftl");
+               Path expectedOutputLocation = 
outputDir.toPath().resolve("mydir/success-test-2.txt");
+               Map<String,Object> expectedMap = new HashMap<String,Object>(4);
+               JsonPropertiesProvider toTest = 
JsonPropertiesProvider.create(dataDir, templateDir, outputDir);
+               toTest.providePropertiesFromFile(path, builder);
+               new Verifications(){{
+                       Path templateLocation;
+                       builder.addTemplateLocation(templateLocation = 
withCapture());
+                       Path outputLocation;
+                       builder.addOutputLocation(outputLocation = 
withCapture());
+                       Map<String,Object> actualMap;
+                       builder.addDataModel(actualMap = withCapture());
+
+                       assertEquals(expectedTemplateLocation, 
templateLocation);
+                       assertEquals(expectedOutputLocation, outputLocation);
+                       assertArrayEquals(expectedMap.entrySet().toArray(), 
actualMap.entrySet().toArray());
+               }};
+       }
+
+       @Test
+       public void testParsingException(@Mocked 
OutputGenerator.OutputGeneratorBuilder builder, @Mocked Gson gson) {
+               Path path = 
dataDir.toPath().resolve("mydir/success-test.txt.json");
+               new Expectations() {{
+                       gson.fromJson((JsonReader) any, (Type) any); result = 
new RuntimeException("test exception");
+               }};
+               JsonPropertiesProvider toTest = 
JsonPropertiesProvider.create(dataDir, templateDir, outputDir);
+
+               assertThatExceptionOfType(RuntimeException.class).isThrownBy(() 
-> {
+                       toTest.providePropertiesFromFile(path, builder);
+               }).withMessage("Could not parse json data file: 
src/test/data/generating-file-visitor/data/mydir/success-test.txt.json");
+       }
+
+       @Test
+       public void testMissingTemplateName(@Mocked 
OutputGenerator.OutputGeneratorBuilder builder) {
+               Path path = 
dataDir.toPath().resolve("mydir/missing-template-name.txt.json");
+               JsonPropertiesProvider toTest = 
JsonPropertiesProvider.create(dataDir, templateDir, outputDir);
+
+               assertThatExceptionOfType(RuntimeException.class).isThrownBy(() 
-> {
+                       toTest.providePropertiesFromFile(path, builder);
+               }).withMessage("Require json data property not found: 
templateName");
+       }
+
+       @Test
+       public void testBadPath(@Mocked OutputGenerator.OutputGeneratorBuilder 
builder) {
+               Path path = 
testDir.toPath().resolve("badPath/success-test.txt.json");
+               JsonPropertiesProvider toTest = 
JsonPropertiesProvider.create(dataDir, templateDir, outputDir);
+               
assertThatExceptionOfType(IllegalStateException.class).isThrownBy(() -> {
+                       toTest.providePropertiesFromFile(path, builder);
+               }).withMessage("visitFile() given file not in sourceDirectory: 
src/test/data/generating-file-visitor/badPath/success-test.txt.json");
+       }
+}
diff --git 
a/src/test/java/org/apache/freemarker/generator/OutputGeneratorTest.java 
b/src/test/java/org/apache/freemarker/generator/OutputGeneratorTest.java
new file mode 100644
index 0000000..63e5eeb
--- /dev/null
+++ b/src/test/java/org/apache/freemarker/generator/OutputGeneratorTest.java
@@ -0,0 +1,295 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.freemarker.generator;
+
+import freemarker.cache.FileTemplateLoader;
+import freemarker.template.Configuration;
+import mockit.Expectations;
+import mockit.Mocked;
+import org.assertj.core.api.Assertions;
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.Test;
+import org.testng.annotations.BeforeMethod;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.*;
+
+import static junit.framework.Assert.assertEquals;
+import static 
org.assertj.core.api.AssertionsForClassTypes.assertThatExceptionOfType;
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertTrue;
+
+public class OutputGeneratorTest {
+
+       private File testDir = new 
File("src/test/data/generating-file-visitor");
+       private File dataDir = new File(testDir, "data");
+       private File templateDir = new File(testDir, "template");
+       private File outputDir = new 
File("target/test-output/generating-file-visitor");
+       private Configuration config;
+       private Map<String, Object> dataModel = new HashMap<String,Object>();
+
+       @BeforeMethod
+       public void setupDataModel() {
+               dataModel.clear();
+               dataModel.put("testVar", "test value");
+               dataModel.put("pomProperties", new HashMap<String,String>());
+               
((Map<String,String>)dataModel.get("pomProperties")).put("pomVar", "pom value");
+       }
+
+       @BeforeClass
+       public static void cleanFields() throws IOException {
+               // Clean output dir before each run.
+               File outputDir = new 
File("target/test-output/generating-file-visitor");
+               if (outputDir.exists()) {
+                       // Recursively delete output from previous run.
+                       Files.walk(outputDir.toPath())
+                                .sorted(Comparator.reverseOrder())
+                                .map(Path::toFile)
+                                .forEach(File::delete);
+               }
+       }
+
+       @BeforeMethod
+       public void before() throws IOException {
+               if (!testDir.isDirectory()) {
+                       throw new RuntimeException("Can't find required test 
data directory. "
+                                + "If running test outside of maven, make sure 
working directory is the project directory. "
+                                + "Looking for: " + testDir);
+               }
+
+               config = new Configuration(Configuration.VERSION_2_3_23);
+               config.setDefaultEncoding("UTF-8");
+               config.setTemplateLoader(new FileTemplateLoader(templateDir));
+       }
+
+       @Test
+       public void createTest() {
+               OutputGenerator.OutputGeneratorBuilder builder = 
OutputGenerator.builder();
+               
assertThatExceptionOfType(IllegalStateException.class).isThrownBy(() -> {
+                       builder.create();
+               }).withMessage("Must set the pomModifiedTimestamp");
+
+               builder.addPomLastModifiedTimestamp(0);
+               
assertThatExceptionOfType(IllegalStateException.class).isThrownBy(() -> {
+                       builder.create();
+               }).withMessage("Must set a non-null generatorLocation");
+
+               File file = new File(dataDir, "mydir/success-test.txt.json");
+               builder.addGeneratorLocation(file.toPath());
+               
assertThatExceptionOfType(IllegalStateException.class).isThrownBy(() -> {
+                       builder.create();
+               }).withMessage("Must set a non-null templateLocation");
+
+               File templateFile = new File(templateDir, "test.ftl");
+               builder.addTemplateLocation(templateFile.toPath());
+               
assertThatExceptionOfType(IllegalStateException.class).isThrownBy(() -> {
+                       builder.create();
+               }).withMessage("Must set a non-null outputLocation");
+
+               File outputFile = new File(outputDir, "mydir/success-test.txt");
+               builder.addOutputLocation(outputFile.toPath());
+
+               
assertThatExceptionOfType(IllegalStateException.class).isThrownBy(() -> {
+                       builder.create();
+               }).withMessage("Must set a non-null dataModel");
+
+               builder.addDataModel(dataModel);
+               OutputGenerator generator = builder.create();
+
+               assertEquals(0, generator.pomModifiedTimestamp);
+               assertEquals(file.toPath(), generator.generatorLocation);
+               assertEquals(templateFile.toPath(), generator.templateLocation);
+               assertEquals(outputFile.toPath(), generator.outputLocation);
+               assertEquals(dataModel.size(), generator.dataModel.size());
+               assertArrayEquals(dataModel.entrySet().toArray(), 
generator.dataModel.entrySet().toArray());
+       }
+
+       @Test
+       public void addToDataModelTest() {
+               OutputGenerator.OutputGeneratorBuilder builder = 
OutputGenerator.builder();
+               
assertThatExceptionOfType(IllegalStateException.class).isThrownBy(() -> {
+                       builder.create();
+               }).withMessage("Must set the pomModifiedTimestamp");
+
+               builder.addPomLastModifiedTimestamp(0);
+               
assertThatExceptionOfType(IllegalStateException.class).isThrownBy(() -> {
+                       builder.create();
+               }).withMessage("Must set a non-null generatorLocation");
+
+               File file = new File(dataDir, "mydir/success-test.txt.json");
+               builder.addGeneratorLocation(file.toPath());
+               
assertThatExceptionOfType(IllegalStateException.class).isThrownBy(() -> {
+                       builder.create();
+               }).withMessage("Must set a non-null templateLocation");
+
+               File templateFile = new File(templateDir, "test.ftl");
+               builder.addTemplateLocation(templateFile.toPath());
+               
assertThatExceptionOfType(IllegalStateException.class).isThrownBy(() -> {
+                       builder.create();
+               }).withMessage("Must set a non-null outputLocation");
+
+               File outputFile = new File(outputDir, "mydir/success-test.txt");
+               builder.addOutputLocation(outputFile.toPath());
+
+               
assertThatExceptionOfType(IllegalStateException.class).isThrownBy(() -> {
+                       builder.create();
+               }).withMessage("Must set a non-null dataModel");
+
+               builder.addToDataModel("testVar", "testVal");
+               OutputGenerator generator = builder.create();
+
+               assertEquals(1, generator.dataModel.size());
+               assertEquals( "testVal", generator.dataModel.get("testVar"));
+
+               builder.addDataModel(dataModel);
+               builder.addToDataModel("testVar2", "testVal2");
+
+               generator = builder.create();
+
+               assertEquals(3, generator.dataModel.size());
+               assertEquals( "test value", generator.dataModel.get("testVar"));
+               assertEquals( "testVal2", generator.dataModel.get("testVar2"));
+       }
+
+       @Test
+       public void generate_SuccessTest()
+                 throws IOException {
+               OutputGenerator.OutputGeneratorBuilder builder = 
OutputGenerator.builder();
+               builder.addPomLastModifiedTimestamp(0);
+               File file = new File(dataDir, "mydir/success-test.txt.json");
+               builder.addGeneratorLocation(file.toPath());
+               File outputFile = new File(outputDir, "mydir/success-test.txt");
+               builder.addOutputLocation(outputFile.toPath());
+               File templateFile = new File(templateDir, "test.ftl");
+               builder.addTemplateLocation(templateFile.toPath());
+               builder.addDataModel(dataModel);
+               OutputGenerator generator = builder.create();
+               generator.generate(config);
+
+               assertTrue(outputFile.isFile());
+               List<String> lines = Files.readAllLines(outputFile.toPath(), 
StandardCharsets.UTF_8);
+               assertEquals(1, lines.size());
+               assertEquals("This is a test freemarker template. Test json 
data: 'test value'. Test pom data: 'pom value'.", lines.get(0));
+
+               // Process same file again, should not regenerate file.
+               long lastMod = outputFile.lastModified();
+               generator.generate(config);
+               assertEquals(lastMod, outputFile.lastModified());
+
+               // Set mod time to before json file.
+               lastMod = file.lastModified() - 1000; // File system may only 
keep 1 second precision.
+               outputFile.setLastModified(lastMod);
+               generator.generate(config);
+               assertTrue(lastMod < outputFile.lastModified());
+
+               // Set mod time to before template file.
+               lastMod = templateFile.lastModified() - 1000; // File system 
may only keep 1 second precision.
+               outputFile.setLastModified(lastMod);
+               generator.generate(config);
+               assertTrue(lastMod < outputFile.lastModified());
+       }
+
+       @Test
+       public void generate_badTemplateNameTest(){
+               OutputGenerator.OutputGeneratorBuilder builder = 
OutputGenerator.builder();
+               builder.addPomLastModifiedTimestamp(0);
+               File file = new File(dataDir, 
"mydir/bad-template-name.txt.json");
+               builder.addGeneratorLocation(file.toPath());
+               File outputFile = new File(outputDir, 
"mydir/bad-template-name.txt");
+               builder.addOutputLocation(outputFile.toPath());
+               File templateFile = new File(templateDir, "missing.ftl"); 
//this doesn't exist
+               builder.addTemplateLocation(templateFile.toPath());
+               builder.addDataModel(dataModel);
+               OutputGenerator generator = builder.create();
+               
Assertions.assertThatExceptionOfType(RuntimeException.class).isThrownBy(() -> {
+                       generator.generate(config);
+               }).withMessage("Could not read template: missing.ftl");
+       }
+
+       @Test
+       public void generate_missingVarTest() {
+               OutputGenerator.OutputGeneratorBuilder builder = 
OutputGenerator.builder();
+               builder.addPomLastModifiedTimestamp(0);
+               File file = new File(dataDir, 
"mydir/missing-var-test.txt.json");
+               builder.addGeneratorLocation(file.toPath());
+               File outputFile = new File(outputDir, 
"mydir/missing-var-test.txt");
+               builder.addOutputLocation(outputFile.toPath());
+               File templateFile = new File(templateDir, "test.ftl"); //this 
is missing a
+               builder.addTemplateLocation(templateFile.toPath());
+               dataModel.remove("testVar");
+               builder.addDataModel(dataModel);
+               OutputGenerator generator = builder.create();
+               
Assertions.assertThatExceptionOfType(RuntimeException.class).isThrownBy(() -> {
+                       generator.generate(config);
+               }).withMessage("Could not process template associated with data 
file: 
src/test/data/generating-file-visitor/data/mydir/missing-var-test.txt.json");
+       }
+
+       @Test
+       public void generate_badParentTest() throws IOException {
+               OutputGenerator.OutputGeneratorBuilder builder = 
OutputGenerator.builder();
+               builder.addPomLastModifiedTimestamp(0);
+               File file = new File(dataDir, 
"badParent/bad-parent-test.txt.json");
+               builder.addGeneratorLocation(file.toPath());
+               File outputFile = new File(outputDir, 
"badParent/bad-parent-test.txt");
+               builder.addOutputLocation(outputFile.toPath());
+               File templateFile = new File(templateDir, "test.ftl"); //this 
is missing a
+               builder.addTemplateLocation(templateFile.toPath());
+               builder.addDataModel(dataModel);
+               OutputGenerator generator = builder.create();
+               outputDir.mkdirs();
+               outputFile.getParentFile().createNewFile();
+
+               
Assertions.assertThatExceptionOfType(RuntimeException.class).isThrownBy(() -> {
+                       generator.generate(config);
+               }).withMessage("Parent directory of output file is a file: " + 
outputFile.getParentFile().getAbsolutePath());
+       }
+
+       @Test
+       public void generate_cantCreateOutputFileParentDirTest(
+                @Mocked FactoryUtil factoryUtil,
+                @Mocked File mockOutputFile) throws IOException {
+
+               File parentDir = new 
File("target/test-output/generating-file-visitor/mydir");
+               new Expectations(mockOutputFile, parentDir) {{
+                       FactoryUtil.createFile(anyString); result = 
mockOutputFile;
+                       mockOutputFile.exists(); result = false;
+                       mockOutputFile.getParentFile(); result = parentDir;
+                       parentDir.isDirectory(); result = false;
+               }};
+
+               OutputGenerator.OutputGeneratorBuilder builder = 
OutputGenerator.builder();
+               builder.addPomLastModifiedTimestamp(0);
+               File file = new File(dataDir, 
"mydir/missing-var-test.txt.json");
+               builder.addGeneratorLocation(file.toPath());
+               File outputFile = new File(outputDir, 
"mydir/missing-var-test.txt");
+               builder.addOutputLocation(outputFile.toPath());
+               File templateFile = new File(templateDir, "test.ftl"); //this 
is missing a
+               builder.addTemplateLocation(templateFile.toPath());
+               builder.addDataModel(dataModel);
+               OutputGenerator generator = builder.create();
+               
Assertions.assertThatExceptionOfType(RuntimeException.class).isThrownBy(() -> {
+                       generator.generate(config);
+               }).withMessage("Could not create directory: " + 
parentDir.getAbsoluteFile().toString());
+       }
+}

Reply via email to