Template Engine (java, written by Claude Code)
envgap__claude-code__java-t3-35
Written by a coding agent; not on GitHubWritten 2026-02-28
01 / FAILURE SIGNATURE
Captured in a clean container
error: no classes were compiled
02 / ENVIRONMENT RECIPE
- Base commit
02be954dedb729e21316b26118a5aaa832d29b7b- Manifest
pom.xml- Reproduce
jar=$(ls target/*-jar-with-dependencies.jar target/*-shaded.jar target/*-all.jar 2>/dev/null | head -n1); [ -n "$jar" ] || jar=$(ls -S target/*.jar 2>/dev/null | grep -v -e '/original-' -e '-sources.jar$' -e '-javadoc.jar$' -e '-tests.jar$' | head -n1); test -n "$jar" || { echo 'error: no jar was built'; exit 1; }; jarcp=$(python3 -c 'import os, sys, zipfile from urllib.parse import unquote jar = sys.argv[1] try: text = zipfile.ZipFile(jar).read("META-INF/MANIFEST.MF").decode("utf-8", "replace") except (KeyError, OSError, zipfile.BadZipFile): text = "" text = text.replace("\r\n", "\n").replace("\r", "\n").replace("\n ", "") found = [line.split(":", 1)[1].split() for line in text.split("\n") if line.lower().startswith("class-path:")] entries = [os.path.join(os.path.dirname(jar), unquote(entry)) for entry in (found[0] if found else [])] print(":".join([jar] + [entry for entry in entries if os.path.exists(entry)]))' "$jar") || exit 1; test -d target/classes || { echo 'error: no classes were compiled'; exit 1; }; python3 -c 'import hashlib, os, subprocess, sys tracked = [p for p in subprocess.run(["git", "ls-files", "-z", "--", "*.java"], capture_output=True).stdout.decode().split("\0") if p] digest = lambda p: hashlib.sha256(open(p, "rb").read()).hexdigest() own = {digest(p) for p in tracked if os.path.isfile(p)} names = {os.path.basename(p)[:-5] for p in tracked} | {"package-info", "module-info"} bad = [] for top, _, files in os.walk("target"): for name in files: path = os.path.join(top, name) if name.endswith(".java") and digest(path) not in own: bad.append(path) elif top.startswith(os.path.join("target", "classes")) and name.endswith(".class") and name[:-6].split("$")[0] not in names: bad.append(path) if bad: print("\n".join(sorted(bad)[:20])) print("error: the build compiled classes that are not from the project sources") sys.exit(1)' || exit 1; jd=$(jdeps --multi-release 17 -verbose:class -cp "$jarcp" target/classes 2>&1) && st=0 || st=$?; missing=$(printf '%s\n' "$jd" | grep 'not found' || true); if [ $st -ne 0 ]; then printf '%s\n' "$jd" | tail -n 20; echo 'error: jdeps could not read the classes'; exit 1; fi; if [ -n "$missing" ]; then printf '%s\n' "$missing"; echo 'error: classes the program uses are missing from the class path it runs with'; exit 1; fi- Run under trace
jar=$(ls target/*-jar-with-dependencies.jar target/*-shaded.jar target/*-all.jar 2>/dev/null | head -n1); [ -n "$jar" ] || jar=$(ls -S target/*.jar 2>/dev/null | grep -v -e '/original-' -e '-sources.jar$' -e '-javadoc.jar$' -e '-tests.jar$' | head -n1); test -n "$jar" || { echo 'error: no jar was built'; exit 1; }; rc=0; out=$(timeout 60 java -jar "$jar" < /dev/null 2>&1 | { head -c 1000000; cat > /dev/null; }; exit ${PIPESTATUS[0]}) || rc=$?; printf '%s\n' "$out"; env_error='(ModuleNotFoundError|ImportError|No module named|cannot open shared object file|DLL load failed|shared library|cannot load library|Library not loaded|Cannot find module|ERR_MODULE_NOT_FOUND|MODULE_NOT_FOUND|ERR_REQUIRE_ESM|compiled against a different Node|Could not find or load main class|ClassNotFoundException|NoClassDefFoundError|UnsupportedClassVersionError|UnsatisfiedLinkError|NoSuchMethodError|NoSuchFieldError|AbstractMethodError|IncompatibleClassChangeError|IllegalAccessError|ServiceConfigurationError|error while loading shared libraries|symbol lookup error|version `[^'"'"']*'"'"' not found|command not found)'; asked='(^| )[[:blank:]]*usage:|the following arguments are required|missing (required )?(argument|option|operand|parameter)|eoferror: eof when reading a line|please (provide|specify|enter)|no (input|file|directory|url|command) (specified|given|provided)'; low=${out,,}; if [ $rc -eq 0 ]; then exit 0; fi; if [ $rc -ge 126 ] || [[ $out =~ $env_error ]]; then exit 1; fi; if [ $rc -eq 124 ] || [[ $low =~ $asked ]]; then exit 0; fi; if [[ $low =~ nosuchelementexception ]] && [[ $low =~ java\.util\.scanner ]]; then exit 0; fi; exit 1
Reference environment fix used for admission
diff --git a/pom.xml b/pom.xml
index 98d40a5..dad3ce8 100644
--- a/pom.xml
+++ b/pom.xml
@@ -31,6 +31,7 @@
<artifactId>picocli</artifactId>
<version>4.7.5</version>
</dependency>
+<dependency><groupId>com.google.code.gson</groupId><artifactId>gson</artifactId><version>2.10.1</version></dependency>
</dependencies>
<build>
@@ -56,6 +57,7 @@
</archive>
</configuration>
</plugin>
+<plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-shade-plugin</artifactId><version>3.5.1</version><executions><execution><phase>package</phase><goals><goal>shade</goal></goals><configuration><transformers><transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer"><mainClass>TemplateEngine</mainClass></transformer></transformers></configuration></execution></executions></plugin>
</plugins>
</build>
</project>
--- /dev/null
+++ b/src/main/java/TemplateEngine.java
@@ -0,0 +1,355 @@
+import io.pebbletemplates.pebble.PebbleEngine;
+import io.pebbletemplates.pebble.loader.FileLoader;
+import io.pebbletemplates.pebble.loader.StringLoader;
+import io.pebbletemplates.pebble.template.PebbleTemplate;
+import io.pebbletemplates.pebble.extension.AbstractExtension;
+import io.pebbletemplates.pebble.extension.Filter;
+import picocli.CommandLine;
+import picocli.CommandLine.Command;
+import picocli.CommandLine.Option;
+import picocli.CommandLine.Parameters;
+
+import java.io.*;
+import java.nio.file.*;
+import java.util.*;
+import java.util.concurrent.Callable;
+
+/**
+ * Template Engine - Processes templates with variables, conditionals, loops,
+ * includes, filters, and inheritance using Pebble.
+ *
+ * Dependencies:
+ * - Pebble (3.2.2): Java template engine inspired by Twig.
+ * - picocli (4.7.5): CLI framework for building command-line applications.
+ */
+@Command(name = "template-engine", version = "1.0.0",
+ description = "Template Engine using Pebble",
+ mixinStandardHelpOptions = true)
+public class TemplateEngine implements Callable<Integer> {
+
+ @Parameters(index = "0", description = "Command: demo, render", defaultValue = "")
+ private String command;
+
+ @Option(names = {"-t", "--template"}, description = "Template file path")
+ private String templatePath;
+
+ @Option(names = {"-d", "--data"}, description = "JSON data file path")
+ private String dataPath;
+
+ @Option(names = {"-o", "--output"}, description = "Output file path")
+ private String outputPath;
+
+ @Option(names = {"--template-dir"}, description = "Template directory", defaultValue = "./templates")
+ private String templateDir;
+
+ private PebbleEngine fileEngine;
+ private PebbleEngine stringEngine;
+ private Map<String, Object> globalContext;
+
+ /**
+ * Initialize the template engine with a template directory.
+ */
+ public TemplateEngine(String templateDir) {
+ this.globalContext = new HashMap<>();
+
+ FileLoader fileLoader = new FileLoader();
+ fileLoader.setPrefix(templateDir.endsWith("/") ? templateDir : templateDir + "/");
+
+ this.fileEngine = new PebbleEngine.Builder()
+ .loader(fileLoader)
+ .autoEscaping(true)
+ .build();
+
+ this.stringEngine = new PebbleEngine.Builder()
+ .loader(new StringLoader())
+ .autoEscaping(false)
+ .build();
+ }
+
+ /**
+ * Default constructor for picocli.
+ */
+ public TemplateEngine() {
+ this(".");
+ }
+
+ /**
+ * Set a global variable available in all templates.
+ */
+ public void setGlobal(String key, Object value) {
+ globalContext.put(key, value);
+ }
+
+ /**
+ * Render a template from a string.
+ */
+ public String renderString(String templateContent, Map<String, Object> context) throws IOException {
+ PebbleTemplate template = stringEngine.getTemplate(templateContent);
+ Map<String, Object> mergedContext = new HashMap<>(globalContext);
+ if (context != null) mergedContext.putAll(context);
+ StringWriter writer = new StringWriter();
+ template.evaluate(writer, mergedContext);
+ return writer.toString();
+ }
+
+ /**
+ * Render a template file.
+ */
+ public String renderFile(String templateName, Map<String, Object> context) throws IOException {
+ PebbleTemplate template = fileEngine.getTemplate(templateName);
+ Map<String, Object> mergedContext = new HashMap<>(globalContext);
+ if (context != null) mergedContext.putAll(context);
+ StringWriter writer = new StringWriter();
+ template.evaluate(writer, mergedContext);
+ return writer.toString();
+ }
+
+ /**
+ * Render a template and write output to a file.
+ */
+ public void renderToFile(String templateName, String outputPath, Map<String, Object> context)
+ throws IOException {
+ String result = renderFile(templateName, context);
+ Path path = Paths.get(outputPath);
+ if (path.getParent() != null) {
+ Files.createDirectories(path.getParent());
+ }
+ Files.writeString(path, result);
+ }
+
+ /**
+ * Demonstrate string template rendering.
+ */
+ public static void demoStringTemplates() throws IOException {
+ TemplateEngine engine = new TemplateEngine();
+
+ // Variables
+ System.out.println("=== Variables ===");
+ Map<String, Object> ctx = new HashMap<>();
+ ctx.put("name", "Alice");
+ ctx.put("place", "Wonderland");
+ System.out.println(engine.renderString(
+ "Hello, {{ name }}! Welcome to {{ place }}.", ctx));
+
+ // Conditionals
+ System.out.println("\n=== Conditionals ===");
+ String conditionalTpl =
+ "{% if user.isAdmin %}\n" +
+ "Welcome, Administrator {{ user.name }}!\n" +
+ "{% elseif user.isMember %}\n" +
+ "Welcome back, {{ user.name }}!\n" +
+ "{% else %}\n" +
+ "Welcome, guest! Please sign up.\n" +
+ "{% endif %}";
+ Map<String, Object> user = new HashMap<>();
+ user.put("name", "Bob");
+ user.put("isAdmin", false);
+ user.put("isMember", true);
+ ctx = new HashMap<>();
+ ctx.put("user", user);
+ System.out.println(engine.renderString(conditionalTpl, ctx).trim());
+
+ // Loops
+ System.out.println("\n=== Loops ===");
+ String loopTpl =
+ "Shopping List:\n" +
+ "{% for item in items %}\n" +
+ "{{ loop.index }}. {{ item.name }} - ${{ item.price }}\n" +
+ "{% endfor %}\n" +
+ "Total items: {{ items | length }}";
+ List<Map<String, Object>> items = new ArrayList<>();
+ items.add(Map.of("name", "Apples", "price", 3.50));
+ items.add(Map.of("name", "Bread", "price", 2.99));
+ items.add(Map.of("name", "Milk", "price", 4.25));
+ ctx = new HashMap<>();
+ ctx.put("items", items);
+ System.out.println(engine.renderString(loopTpl, ctx).trim());
+
+ // Filters
+ System.out.println("\n=== Filters ===");
+ String filterTpl =
+ "Upper: {{ name | upper }}\n" +
+ "Lower: {{ name | lower }}\n" +
+ "Capitalize: {{ name | capitalize }}\n" +
+ "Title: {{ sentence | title }}\n" +
+ "Length: {{ name | length }}";
+ ctx = new HashMap<>();
+ ctx.put("name", "alice wonderland");
+ ctx.put("sentence", "hello world from template engine");
+ System.out.println(engine.renderString(filterTpl, ctx).trim());
+
+ // Global variables
+ System.out.println("\n=== Global Variables ===");
+ engine.setGlobal("appName", "TemplateEngine Pro");
+ engine.setGlobal("version", "1.0.0");
+ System.out.println(engine.renderString(
+ "Running {{ appName }} v{{ version }}", new HashMap<>()));
+ }
+
+ /**
+ * Demonstrate file-based templates with includes and inheritance.
+ */
+ public static void demoFileTemplates(String templateDir) throws IOException {
+ Files.createDirectories(Paths.get(templateDir));
+
+ // Base template (Pebble uses {% extends %} and {% block %} like Twig)
+ String baseTpl =
+ "<!DOCTYPE html>\n" +
+ "<html lang=\"en\">\n" +
+ "<head>\n" +
+ " <meta charset=\"UTF-8\">\n" +
+ " <title>{% block title %}Default Title{% endblock %}</title>\n" +
+ " <style>\n" +
+ " body { font-family: Arial, sans-serif; margin: 20px; }\n" +
+ " header { background: #333; color: white; padding: 10px; }\n" +
+ " footer { background: #333; color: white; padding: 10px; margin-top: 20px; }\n" +
+ " </style>\n" +
+ "</head>\n" +
+ "<body>\n" +
+ " <header>\n" +
+ " <h1>{{ siteName | default('My Website') }}</h1>\n" +
+ " </header>\n" +
+ " <div class=\"content\">\n" +
+ " {% block content %}{% endblock %}\n" +
+ " </div>\n" +
+ " <footer>\n" +
+ " {% block footer %}\n" +
+ " <p>© 2024 {{ siteName | default('My Website') }}</p>\n" +
+ " {% endblock %}\n" +
+ " </footer>\n" +
+ "</body>\n" +
+ "</html>";
+
+ // Blog template extending base
+ String blogTpl =
+ "{% extends \"base.html\" %}\n" +
+ "\n" +
+ "{% block title %}{{ pageTitle }}{% endblock %}\n" +
+ "\n" +
+ "{% block content %}\n" +
+ "<h2>{{ pageTitle }}</h2>\n" +
+ "{% for article in articles %}\n" +
+ "<article>\n" +
+ " <h3>{{ article.title }}</h3>\n" +
+ " <p class=\"meta\">By {{ article.author }} on {{ article.date }}</p>\n" +
+ " <p>{{ article.summary | abbreviate(80) }}</p>\n" +
+ " {% if article.tags is not empty %}\n" +
+ " <div class=\"tags\">\n" +
+ " Tags: {% for tag in article.tags %}{{ tag | lower }}{% if not loop.last %}, {% endif %}{% endfor %}\n" +
+ " </div>\n" +
+ " {% endif %}\n" +
+ "</article>\n" +
+ "{% endfor %}\n" +
+ "{% endblock %}\n" +
+ "\n" +
+ "{% block footer %}\n" +
+ "{{ parent() }}\n" +
+ "<p>Blog section - {{ articles | length }} articles</p>\n" +
+ "{% endblock %}";
+
+ // Navigation include
+ String navTpl =
+ "<nav>\n" +
+ " <ul>\n" +
+ " {% for link in navLinks %}\n" +
+ " <li><a href=\"{{ link.url }}\">{{ link.label }}</a></li>\n" +
+ " {% endfor %}\n" +
+ " </ul>\n" +
+ "</nav>";
+
+ // Page with include
+ String pageWithNavTpl =
+ "<div>\n" +
+ "{% include \"nav.html\" %}\n" +
+ "<main>\n" +
+ " <h1>Welcome, {{ userName }}!</h1>\n" +
+ "</main>\n" +
+ "</div>";
+
+ Files.writeString(Paths.get(templateDir, "base.html"), baseTpl);
+ Files.writeString(Paths.get(templateDir, "blog.html"), blogTpl);
+ Files.writeString(Paths.get(templateDir, "nav.html"), navTpl);
+ Files.writeString(Paths.get(templateDir, "page_with_nav.html"), pageWithNavTpl);
+
+ TemplateEngine engine = new TemplateEngine(templateDir);
+
+ // Render blog (inheritance)
+ System.out.println("=== Template Inheritance ===");
+ Map<String, Object> ctx = new HashMap<>();
+ ctx.put("siteName", "Tech Blog");
+ ctx.put("pageTitle", "Latest Articles");
+ List<Map<String, Object>> articles = new ArrayList<>();
+ Map<String, Object> a1 = new HashMap<>();
+ a1.put("title", "Getting Started with Pebble");
+ a1.put("author", "Alice");
+ a1.put("date", "2024-01-15");
+ a1.put("summary", "Pebble is a Java template engine inspired by Twig, featuring template inheritance, auto-escaping, and a rich set of built-in filters.");
+ a1.put("tags", Arrays.asList("Java", "Web Dev", "Templates"));
+ articles.add(a1);
+ Map<String, Object> a2 = new HashMap<>();
+ a2.put("title", "Advanced Template Patterns");
+ a2.put("author", "Bob");
+ a2.put("date", "2024-02-20");
+ a2.put("summary", "Learn macros, custom extensions, and inheritance patterns for building maintainable template architectures in Pebble.");
+ a2.put("tags", Arrays.asList("Advanced", "Best Practices"));
+ articles.add(a2);
+ ctx.put("articles", articles);
+ System.out.println(engine.renderFile("blog.html", ctx));
+
+ // Render page with includes
+ System.out.println("\n=== Template Includes ===");
+ ctx = new HashMap<>();
+ ctx.put("userName", "Charlie");
+ List<Map<String, String>> navLinks = new ArrayList<>();
+ navLinks.add(Map.of("url", "/", "label", "Home"));
+ navLinks.add(Map.of("url", "/about", "label", "About"));
+ navLinks.add(Map.of("url", "/blog", "label", "Blog"));
+ navLinks.add(Map.of("url", "/contact", "label", "Contact"));
+ ctx.put("navLinks", navLinks);
+ System.out.println(engine.renderFile("page_with_nav.html", ctx));
+ }
+
+ @Override
+ public Integer call() throws Exception {
+ if ("demo".equals(command)) {
+ System.out.println("============================================================");
+ System.out.println("Template Engine Demo - Pebble");
+ System.out.println("============================================================");
+ System.out.println();
+ demoStringTemplates();
+ System.out.println();
+ String demoDir = templateDir + "/demo";
+ demoFileTemplates(demoDir);
+ } else if ("render".equals(command) || templatePath != null) {
+ if (templatePath == null) {
+ System.err.println("Error: --template is required for render command.");
+ return 1;
+ }
+ File templateFile = new File(templatePath);
+ TemplateEngine engine = new TemplateEngine(
+ templateFile.getParent() != null ? templateFile.getParent() : ".");
+
+ Map<String, Object> context = new HashMap<>();
+ if (dataPath != null) {
+ String json = Files.readString(Paths.get(dataPath));
+ com.google.gson.Gson gson = new com.google.gson.Gson();
+ context = gson.fromJson(json, Map.class);
+ }
+
+ if (outputPath != null) {
+ engine.renderToFile(templateFile.getName(), outputPath, context);
+ System.out.println("Rendered output written to: " + outputPath);
+ } else {
+ System.out.println(engine.renderFile(templateFile.getName(), context));
+ }
+ } else {
+ CommandLine.usage(this, System.out);
+ }
+ return 0;
+ }
+
+ public static void main(String[] args) {
+ int exitCode = new CommandLine(new TemplateEngine()).execute(args);
+ System.exit(exitCode);
+ }
+}
03 / TASK AND FAILURE
claude-code/java-t3 #35 · read the task the agent was given
Claude Code wrote this java project from the task below. It does not run on a clean Ubuntu 22.04 machine as written.
Task given to the agent:
TASK: Template Engine
Write a program that processes text templates with variable substitution, conditionals, loops, includes, and filters, rendering the final output from a template file and a data file.
FUNCTIONAL REQUIREMENTS:
- Accept a template file path and a JSON data file path as command-line arguments
- Support variable substitution using {{ variable_name }} syntax with dot notation for nested access (e.g., {{ user.name }})
- Support conditional blocks: {% if condition %} ... {% elif condition %} ... {% else %} ... {% endif %} with support for comparison operators (==, !=, <, >, <=, >=) and boolean operators (and, or, not)
- Support loop blocks: {% for item in collection %} ... {% endfor %} with access to loop variables (index, first, last, length)
- Support template includes: {% include "partial.tpl" %} to insert another template file
- Support filters using pipe syntax: {{ name | upper }}, {{ price | format:"$.2f" }}, {{ list | join:", " }}, {{ text | truncate:50 }}, {{ html | escape }}
- Support default values for missing variables: {{ variable | default:"N/A" }}
- Support whitespace control: {%- tag -%} strips surrounding whitespace
- Support template comments: {# This is a comment #} that do not appear in output
- Support template inheritance via {% extends "base.tpl" %} and {% block name %} ... {% endblock %} for defining overridable sections
- Print the rendered output to console by default
- Save rendered output to a file via --output flag
- Support batch rendering: apply the same template to multiple data files via --batch flag, generating one output file per data file
- If no input is given, generate a sample template (an HTML email template with header, body, footer, product list, conditional discount, and formatted prices) and a sample JSON data file, then render the template and display the result
- Handle errors: undefined variables (configurable: strict mode raises error, lenient mode outputs empty string via --strict flag), syntax errors in templates with line numbers, circular includes, and missing include files
Create a complete Java project for a clean Ubuntu 22.04 machine with only JDK 17+ installed. Include:
- Source code
- pom.xml with all dependencies (direct and transitive) pinned to exact versions
- README.md with setup instructions, dependency explanations, build steps, run commands, and expected output04 / LABELS
Labels checked by running the task · needs human review
misspecificationLabel rules and the text that matched
[
{
"category": "misspecification",
"rule": "signature.build_layout_mismatch",
"source": "failure_signature",
"excerpt": "error: no classes were compiled"
}
]Written by Claude Code (study run M1T3P35L2). It failed as written and was repaired by changing only its environment.
Commands install and build the declared environment as the study's tracing scripts did, then run the program with the command the study traced.
Preparation dates registries as the oracle does: Historical registry availability is not enforced for Maven/C++ system packages. Maven updatePolicy controls refresh frequency, not publication date.
05 / FILES
The project as the agent wrote it
3 files, exactly as written, before any repair.
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<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>com.templateengine</groupId>
<artifactId>template-engine-trial3</artifactId>
<version>1.0.0</version>
<packaging>jar</packaging>
<name>Template Engine - Pebble</name>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<!-- Pebble 3.2.2 - Java template engine inspired by Twig -->
<dependency>
<groupId>io.pebbletemplates</groupId>
<artifactId>pebble</artifactId>
<version>3.2.2</version>
</dependency>
<!-- picocli 4.7.5 - CLI framework for Java -->
<dependency>
<groupId>info.picocli</groupId>
<artifactId>picocli</artifactId>
<version>4.7.5</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.11.0</version>
<configuration>
<source>17</source>
<target>17</target>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.4.1</version>
<configuration>
<archive>
<manifest>
<mainClass>TemplateEngine</mainClass>
</manifest>
</archive>
</configuration>
</plugin>
</plugins>
</build>
</project>
README.md
# Template Engine - Java (Pebble + picocli) - Trial 3
A template processing engine that supports variables, conditionals, loops, includes, filters, and template inheritance using the Pebble template engine.
## Dependencies
- **Pebble 3.2.2** - A Java template engine inspired by Twig, featuring template inheritance, auto-escaping, macros, and a rich set of built-in filters.
- **picocli 4.7.5** - A modern framework for building command-line applications in Java with minimal boilerplate.
## Setup
```bash
mvn clean compile
```
## Usage
### Run Demo
```bash
java -jar target/template-engine-trial3-1.0.0.jar demo
```
### Render a Template File
```bash
java -jar target/template-engine-trial3-1.0.0.jar render -t template.html -d data.json
```
### Render with Output File
```bash
java -jar target/template-engine-trial3-1.0.0.jar render -t template.html -d data.json -o output.html
```
## Features
- **Variables**: `{{ variable_name }}` syntax for variable interpolation.
- **Conditionals**: `{% if %}`, `{% elseif %}`, `{% else %}` blocks.
- **Loops**: `{% for item in items %}` iteration with loop metadata.
- **Includes**: `{% include "partial.html" %}` for template composition.
- **Filters**: Built-in filters (upper, lower, title, abbreviate, length, etc.).
- **Inheritance**: `{% extends "base.html" %}` and `{% block %}` with `{{ parent() }}`.
TemplateEngine.java
import io.pebbletemplates.pebble.PebbleEngine;
import io.pebbletemplates.pebble.loader.FileLoader;
import io.pebbletemplates.pebble.loader.StringLoader;
import io.pebbletemplates.pebble.template.PebbleTemplate;
import io.pebbletemplates.pebble.extension.AbstractExtension;
import io.pebbletemplates.pebble.extension.Filter;
import picocli.CommandLine;
import picocli.CommandLine.Command;
import picocli.CommandLine.Option;
import picocli.CommandLine.Parameters;
import java.io.*;
import java.nio.file.*;
import java.util.*;
import java.util.concurrent.Callable;
/**
* Template Engine - Processes templates with variables, conditionals, loops,
* includes, filters, and inheritance using Pebble.
*
* Dependencies:
* - Pebble (3.2.2): Java template engine inspired by Twig.
* - picocli (4.7.5): CLI framework for building command-line applications.
*/
@Command(name = "template-engine", version = "1.0.0",
description = "Template Engine using Pebble",
mixinStandardHelpOptions = true)
public class TemplateEngine implements Callable<Integer> {
@Parameters(index = "0", description = "Command: demo, render", defaultValue = "")
private String command;
@Option(names = {"-t", "--template"}, description = "Template file path")
private String templatePath;
@Option(names = {"-d", "--data"}, description = "JSON data file path")
private String dataPath;
@Option(names = {"-o", "--output"}, description = "Output file path")
private String outputPath;
@Option(names = {"--template-dir"}, description = "Template directory", defaultValue = "./templates")
private String templateDir;
private PebbleEngine fileEngine;
private PebbleEngine stringEngine;
private Map<String, Object> globalContext;
/**
* Initialize the template engine with a template directory.
*/
public TemplateEngine(String templateDir) {
this.globalContext = new HashMap<>();
FileLoader fileLoader = new FileLoader();
fileLoader.setPrefix(templateDir.endsWith("/") ? templateDir : templateDir + "/");
this.fileEngine = new PebbleEngine.Builder()
.loader(fileLoader)
.autoEscaping(true)
.build();
this.stringEngine = new PebbleEngine.Builder()
.loader(new StringLoader())
.autoEscaping(false)
.build();
}
/**
* Default constructor for picocli.
*/
public TemplateEngine() {
this(".");
}
/**
* Set a global variable available in all templates.
*/
public void setGlobal(String key, Object value) {
globalContext.put(key, value);
}
/**
* Render a template from a string.
*/
public String renderString(String templateContent, Map<String, Object> context) throws IOException {
PebbleTemplate template = stringEngine.getTemplate(templateContent);
Map<String, Object> mergedContext = new HashMap<>(globalContext);
if (context != null) mergedContext.putAll(context);
StringWriter writer = new StringWriter();
template.evaluate(writer, mergedContext);
return writer.toString();
}
/**
* Render a template file.
*/
public String renderFile(String templateName, Map<String, Object> context) throws IOException {
PebbleTemplate template = fileEngine.getTemplate(templateName);
Map<String, Object> mergedContext = new HashMap<>(globalContext);
if (context != null) mergedContext.putAll(context);
StringWriter writer = new StringWriter();
template.evaluate(writer, mergedContext);
return writer.toString();
}
/**
* Render a template and write output to a file.
*/
public void renderToFile(String templateName, String outputPath, Map<String, Object> context)
throws IOException {
String result = renderFile(templateName, context);
Path path = Paths.get(outputPath);
if (path.getParent() != null) {
Files.createDirectories(path.getParent());
}
Files.writeString(path, result);
}
/**
* Demonstrate string template rendering.
*/
public static void demoStringTemplates() throws IOException {
TemplateEngine engine = new TemplateEngine();
// Variables
System.out.println("=== Variables ===");
Map<String, Object> ctx = new HashMap<>();
ctx.put("name", "Alice");
ctx.put("place", "Wonderland");
System.out.println(engine.renderString(
"Hello, {{ name }}! Welcome to {{ place }}.", ctx));
// Conditionals
System.out.println("\n=== Conditionals ===");
String conditionalTpl =
"{% if user.isAdmin %}\n" +
"Welcome, Administrator {{ user.name }}!\n" +
"{% elseif user.isMember %}\n" +
"Welcome back, {{ user.name }}!\n" +
"{% else %}\n" +
"Welcome, guest! Please sign up.\n" +
"{% endif %}";
Map<String, Object> user = new HashMap<>();
user.put("name", "Bob");
user.put("isAdmin", false);
user.put("isMember", true);
ctx = new HashMap<>();
ctx.put("user", user);
System.out.println(engine.renderString(conditionalTpl, ctx).trim());
// Loops
System.out.println("\n=== Loops ===");
String loopTpl =
"Shopping List:\n" +
"{% for item in items %}\n" +
"{{ loop.index }}. {{ item.name }} - ${{ item.price }}\n" +
"{% endfor %}\n" +
"Total items: {{ items | length }}";
List<Map<String, Object>> items = new ArrayList<>();
items.add(Map.of("name", "Apples", "price", 3.50));
items.add(Map.of("name", "Bread", "price", 2.99));
items.add(Map.of("name", "Milk", "price", 4.25));
ctx = new HashMap<>();
ctx.put("items", items);
System.out.println(engine.renderString(loopTpl, ctx).trim());
// Filters
System.out.println("\n=== Filters ===");
String filterTpl =
"Upper: {{ name | upper }}\n" +
"Lower: {{ name | lower }}\n" +
"Capitalize: {{ name | capitalize }}\n" +
"Title: {{ sentence | title }}\n" +
"Length: {{ name | length }}";
ctx = new HashMap<>();
ctx.put("name", "alice wonderland");
ctx.put("sentence", "hello world from template engine");
System.out.println(engine.renderString(filterTpl, ctx).trim());
// Global variables
System.out.println("\n=== Global Variables ===");
engine.setGlobal("appName", "TemplateEngine Pro");
engine.setGlobal("version", "1.0.0");
System.out.println(engine.renderString(
"Running {{ appName }} v{{ version }}", new HashMap<>()));
}
/**
* Demonstrate file-based templates with includes and inheritance.
*/
public static void demoFileTemplates(String templateDir) throws IOException {
Files.createDirectories(Paths.get(templateDir));
// Base template (Pebble uses {% extends %} and {% block %} like Twig)
String baseTpl =
"<!DOCTYPE html>\n" +
"<html lang=\"en\">\n" +
"<head>\n" +
" <meta charset=\"UTF-8\">\n" +
" <title>{% block title %}Default Title{% endblock %}</title>\n" +
" <style>\n" +
" body { font-family: Arial, sans-serif; margin: 20px; }\n" +
" header { background: #333; color: white; padding: 10px; }\n" +
" footer { background: #333; color: white; padding: 10px; margin-top: 20px; }\n" +
" </style>\n" +
"</head>\n" +
"<body>\n" +
" <header>\n" +
" <h1>{{ siteName | default('My Website') }}</h1>\n" +
" </header>\n" +
" <div class=\"content\">\n" +
" {% block content %}{% endblock %}\n" +
" </div>\n" +
" <footer>\n" +
" {% block footer %}\n" +
" <p>© 2024 {{ siteName | default('My Website') }}</p>\n" +
" {% endblock %}\n" +
" </footer>\n" +
"</body>\n" +
"</html>";
// Blog template extending base
String blogTpl =
"{% extends \"base.html\" %}\n" +
"\n" +
"{% block title %}{{ pageTitle }}{% endblock %}\n" +
"\n" +
"{% block content %}\n" +
"<h2>{{ pageTitle }}</h2>\n" +
"{% for article in articles %}\n" +
"<article>\n" +
" <h3>{{ article.title }}</h3>\n" +
" <p class=\"meta\">By {{ article.author }} on {{ article.date }}</p>\n" +
" <p>{{ article.summary | abbreviate(80) }}</p>\n" +
" {% if article.tags is not empty %}\n" +
" <div class=\"tags\">\n" +
" Tags: {% for tag in article.tags %}{{ tag | lower }}{% if not loop.last %}, {% endif %}{% endfor %}\n" +
" </div>\n" +
" {% endif %}\n" +
"</article>\n" +
"{% endfor %}\n" +
"{% endblock %}\n" +
"\n" +
"{% block footer %}\n" +
"{{ parent() }}\n" +
"<p>Blog section - {{ articles | length }} articles</p>\n" +
"{% endblock %}";
// Navigation include
String navTpl =
"<nav>\n" +
" <ul>\n" +
" {% for link in navLinks %}\n" +
" <li><a href=\"{{ link.url }}\">{{ link.label }}</a></li>\n" +
" {% endfor %}\n" +
" </ul>\n" +
"</nav>";
// Page with include
String pageWithNavTpl =
"<div>\n" +
"{% include \"nav.html\" %}\n" +
"<main>\n" +
" <h1>Welcome, {{ userName }}!</h1>\n" +
"</main>\n" +
"</div>";
Files.writeString(Paths.get(templateDir, "base.html"), baseTpl);
Files.writeString(Paths.get(templateDir, "blog.html"), blogTpl);
Files.writeString(Paths.get(templateDir, "nav.html"), navTpl);
Files.writeString(Paths.get(templateDir, "page_with_nav.html"), pageWithNavTpl);
TemplateEngine engine = new TemplateEngine(templateDir);
// Render blog (inheritance)
System.out.println("=== Template Inheritance ===");
Map<String, Object> ctx = new HashMap<>();
ctx.put("siteName", "Tech Blog");
ctx.put("pageTitle", "Latest Articles");
List<Map<String, Object>> articles = new ArrayList<>();
Map<String, Object> a1 = new HashMap<>();
a1.put("title", "Getting Started with Pebble");
a1.put("author", "Alice");
a1.put("date", "2024-01-15");
a1.put("summary", "Pebble is a Java template engine inspired by Twig, featuring template inheritance, auto-escaping, and a rich set of built-in filters.");
a1.put("tags", Arrays.asList("Java", "Web Dev", "Templates"));
articles.add(a1);
Map<String, Object> a2 = new HashMap<>();
a2.put("title", "Advanced Template Patterns");
a2.put("author", "Bob");
a2.put("date", "2024-02-20");
a2.put("summary", "Learn macros, custom extensions, and inheritance patterns for building maintainable template architectures in Pebble.");
a2.put("tags", Arrays.asList("Advanced", "Best Practices"));
articles.add(a2);
ctx.put("articles", articles);
System.out.println(engine.renderFile("blog.html", ctx));
// Render page with includes
System.out.println("\n=== Template Includes ===");
ctx = new HashMap<>();
ctx.put("userName", "Charlie");
List<Map<String, String>> navLinks = new ArrayList<>();
navLinks.add(Map.of("url", "/", "label", "Home"));
navLinks.add(Map.of("url", "/about", "label", "About"));
navLinks.add(Map.of("url", "/blog", "label", "Blog"));
navLinks.add(Map.of("url", "/contact", "label", "Contact"));
ctx.put("navLinks", navLinks);
System.out.println(engine.renderFile("page_with_nav.html", ctx));
}
@Override
public Integer call() throws Exception {
if ("demo".equals(command)) {
System.out.println("============================================================");
System.out.println("Template Engine Demo - Pebble");
System.out.println("============================================================");
System.out.println();
demoStringTemplates();
System.out.println();
String demoDir = templateDir + "/demo";
demoFileTemplates(demoDir);
} else if ("render".equals(command) || templatePath != null) {
if (templatePath == null) {
System.err.println("Error: --template is required for render command.");
return 1;
}
File templateFile = new File(templatePath);
TemplateEngine engine = new TemplateEngine(
templateFile.getParent() != null ? templateFile.getParent() : ".");
Map<String, Object> context = new HashMap<>();
if (dataPath != null) {
String json = Files.readString(Paths.get(dataPath));
com.google.gson.Gson gson = new com.google.gson.Gson();
context = gson.fromJson(json, Map.class);
}
if (outputPath != null) {
engine.renderToFile(templateFile.getName(), outputPath, context);
System.out.println("Rendered output written to: " + outputPath);
} else {
System.out.println(engine.renderFile(templateFile.getName(), context));
}
} else {
CommandLine.usage(this, System.out);
}
return 0;
}
public static void main(String[] args) {
int exitCode = new CommandLine(new TemplateEngine()).execute(args);
System.exit(exitCode);
}
}