Skip to content

Commit 4b22581

Browse files
gnodetclaude
andauthored
CAMEL-21208: Replace manual string-based code generation with FreeMarker templates (#22200)
* CAMEL-21208: Replace manual string-based code generation with FreeMarker templates in camel-jbang - Add FreeMarker 2.3.34 dependency to camel-jbang-core - Create TemplateHelper utility using square bracket syntax ([=var], [#if]...[/#if]) to avoid conflicts with ${...} (Maven) and <...> (XML) in generated content - Convert all 23 .tmpl template files to .ftl FreeMarker templates - Refactor Export*, Run, and Init commands to use TemplateHelper instead of manual StringBuilder/replaceAll/replaceFirst code generation - Extract shared helpers (buildRepositoryList, buildDependencyList, formatBuildProperties, mavenGavComparator) into ExportBaseCommand - Maintain backward compatibility for catalog-provided templates in ExportSpringBoot - Remove 4 unreferenced templates (main-docker-*.tmpl, main-jkube-pom.tmpl) - Keep deprecated bind templates (.tmpl) used by TemplateProvider Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * CAMEL-21208: Fix review findings - Add backward compat check for old .tmpl catalog template name in ExportSpringBoot - Remove dead model variables (CamelVersion, QuarkusManagementPort) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * CAMEL-21208: Minor cleanup from review - Remove unnecessary bare block in Run.generateOpenApi (leftover from try-with-resources) - Deduplicate model building in Export.copyDockerFiles Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * CAMEL-21208: Add unit test for TemplateHelper Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * CAMEL-21208: Expand TemplateHelper tests to cover all templates Comprehensive test coverage for all 23 FreeMarker templates: - FreeMarker-processed templates (processTemplate): code-java, main, spring-boot-main, Dockerfile21/25, readme, readme.native, rest-dsl.yaml, run-custom-camel-version, main-pom (with deps, repos, jib/jkube), spring-boot-pom, quarkus-pom - Init templates (raw text loading): java, yaml, xml, kamelet source/sink/action, pipe, integration Tests verify: license header stripping, placeholder resolution, conditional rendering, list iteration, Maven ${} passthrough, Kamelet {{}} passthrough, and missing template error handling. Also regenerate jbang command docs after rebase. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent c4d3c50 commit 4b22581

51 files changed

Lines changed: 2180 additions & 886 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

dsl/camel-jbang/camel-jbang-core/pom.xml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,13 @@
152152
<artifactId>plexus-xml</artifactId>
153153
</dependency>
154154

155+
<!-- template engine for code generation -->
156+
<dependency>
157+
<groupId>org.freemarker</groupId>
158+
<artifactId>freemarker</artifactId>
159+
<version>${freemarker-version}</version>
160+
</dependency>
161+
155162
<!-- test dependencies -->
156163
<dependency>
157164
<groupId>org.apache.camel</groupId>

dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Export.java

Lines changed: 21 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -17,24 +17,24 @@
1717
package org.apache.camel.dsl.jbang.core.commands;
1818

1919
import java.io.FileNotFoundException;
20-
import java.io.InputStream;
20+
import java.io.IOException;
2121
import java.nio.file.Files;
2222
import java.nio.file.Path;
2323
import java.nio.file.Paths;
2424
import java.text.SimpleDateFormat;
25-
import java.util.Comparator;
2625
import java.util.Date;
26+
import java.util.HashMap;
27+
import java.util.Map;
2728
import java.util.Properties;
2829

2930
import org.apache.camel.dsl.jbang.core.common.CamelJBangConstants;
3031
import org.apache.camel.dsl.jbang.core.common.PropertyResolver;
3132
import org.apache.camel.dsl.jbang.core.common.RuntimeType;
3233
import org.apache.camel.dsl.jbang.core.common.RuntimeUtil;
3334
import org.apache.camel.dsl.jbang.core.common.SourceScheme;
34-
import org.apache.camel.tooling.maven.MavenGav;
35+
import org.apache.camel.dsl.jbang.core.common.TemplateHelper;
3536
import org.apache.camel.util.CamelCaseOrderedProperties;
3637
import org.apache.camel.util.FileUtil;
37-
import org.apache.camel.util.IOHelper;
3838
import picocli.CommandLine.Command;
3939

4040
import static org.apache.camel.dsl.jbang.core.common.CamelJBangConstants.CAMEL_SPRING_BOOT_VERSION;
@@ -300,72 +300,6 @@ protected String getVersion() {
300300
return "1.0-SNAPSHOT";
301301
}
302302

303-
public Comparator<MavenGav> mavenGavComparator() {
304-
return new Comparator<MavenGav>() {
305-
@Override
306-
public int compare(MavenGav o1, MavenGav o2) {
307-
int r1 = rankGroupId(o1);
308-
int r2 = rankGroupId(o2);
309-
310-
if (r1 > r2) {
311-
return -1;
312-
} else if (r2 > r1) {
313-
return 1;
314-
} else {
315-
return o1.toString().compareTo(o2.toString());
316-
}
317-
}
318-
319-
int rankGroupId(MavenGav o1) {
320-
String g1 = o1.getGroupId();
321-
if (g1 == null) {
322-
return 0;
323-
}
324-
325-
switch (g1) {
326-
case "org.springframework.boot" -> {
327-
return 30;
328-
}
329-
case "io.quarkus" -> {
330-
return 30;
331-
}
332-
case "org.apache.camel.quarkus" -> {
333-
String a1 = o1.getArtifactId();
334-
// main/core/engine first
335-
if ("camel-quarkus-core".equals(a1)) {
336-
return 21;
337-
}
338-
return 20;
339-
}
340-
case "org.apache.camel.springboot" -> {
341-
String a1 = o1.getArtifactId();
342-
// main/core/engine first
343-
if ("camel-spring-boot-starter".equals(a1)) {
344-
return 21;
345-
} else if ("camel-spring-boot-engine-starter".equals(a1)) {
346-
return 22;
347-
}
348-
return 20;
349-
}
350-
case "org.apache.camel" -> {
351-
String a1 = o1.getArtifactId();
352-
// main/core/engine first
353-
if ("camel-main".equals(a1)) {
354-
return 11;
355-
}
356-
return 10;
357-
}
358-
case "org.apache.camel.kamelets" -> {
359-
return 5;
360-
}
361-
default -> {
362-
return 0;
363-
}
364-
}
365-
}
366-
};
367-
}
368-
369303
// Maven reproducible builds: https://maven.apache.org/guides/mini/guide-reproducible-builds.html
370304
protected String getBuildMavenProjectDate() {
371305
// 2024-09-23T10:00:00Z
@@ -378,33 +312,33 @@ protected void copyDockerFiles(String buildDir) throws Exception {
378312
Path docker = Path.of(buildDir).resolve("src/main/docker");
379313
Files.createDirectories(docker);
380314
String[] ids = gav.split(":");
381-
String templateName = "templates/Dockerfile" + javaVersion + ".tmpl";
382-
InputStream is = ExportCamelMain.class.getClassLoader().getResourceAsStream(templateName);
383-
if (is == null) {
315+
316+
Map<String, Object> model = new HashMap<>();
317+
model.put("ArtifactId", ids[1]);
318+
model.put("Version", ids[2]);
319+
model.put("AppJar", ids[1] + "-" + ids[2] + ".jar");
320+
321+
String ftlName = "Dockerfile" + javaVersion + ".ftl";
322+
String context;
323+
try {
324+
context = TemplateHelper.processTemplate(ftlName, model);
325+
} catch (IOException e) {
384326
// fallback to JDK 21 template
385327
printer().printf("No Dockerfile template for Java %s, falling back to Java 21 template%n", javaVersion);
386-
is = ExportCamelMain.class.getClassLoader().getResourceAsStream("templates/Dockerfile21.tmpl");
328+
context = TemplateHelper.processTemplate("Dockerfile21.ftl", model);
387329
}
388-
String context = IOHelper.loadText(is);
389-
IOHelper.close(is);
390-
391-
String appJar = ids[1] + "-" + ids[2] + ".jar";
392-
context = context.replaceAll("\\{\\{ \\.ArtifactId }}", ids[1]);
393-
context = context.replaceAll("\\{\\{ \\.Version }}", ids[2]);
394-
context = context.replaceAll("\\{\\{ \\.AppJar }}", appJar);
395330
Files.writeString(docker.resolve("Dockerfile"), context);
396331
}
397332

398333
// Copy the readme.md into the same Maven project root directory.
399334
protected void copyReadme(String buildDir, String appJar) throws Exception {
400335
String[] ids = gav.split(":");
401-
InputStream is = ExportCamelMain.class.getClassLoader().getResourceAsStream("templates/readme.md.tmpl");
402-
String context = IOHelper.loadText(is);
403-
IOHelper.close(is);
336+
Map<String, Object> model = new HashMap<>();
337+
model.put("ArtifactId", ids[1]);
338+
model.put("Version", ids[2]);
339+
model.put("AppRuntimeJar", appJar);
404340

405-
context = context.replaceAll("\\{\\{ \\.ArtifactId }}", ids[1]);
406-
context = context.replaceAll("\\{\\{ \\.Version }}", ids[2]);
407-
context = context.replaceAll("\\{\\{ \\.AppRuntimeJar }}", appJar);
341+
String context = TemplateHelper.processTemplate("readme.md.ftl", model);
408342
Files.writeString(Path.of(buildDir).resolve("readme.md"), context);
409343
}
410344
}

dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ExportBaseCommand.java

Lines changed: 139 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -408,46 +408,144 @@ private static String humanReadableSize(long bytes) {
408408
}
409409
}
410410

411-
protected static String mavenRepositoriesAsPomXml(String repos) {
412-
StringBuilder sb = new StringBuilder();
413-
int i = 1;
414-
sb.append(" <repositories>\n");
415-
if (!repos.isEmpty()) {
411+
/**
412+
* Builds a list of repository data maps from a comma-separated repos string, for use with FreeMarker templates.
413+
*/
414+
protected static List<Map<String, Object>> buildRepositoryList(String repos) {
415+
List<Map<String, Object>> result = new ArrayList<>();
416+
if (repos != null && !repos.isEmpty()) {
417+
int i = 1;
416418
for (String repo : repos.split(",")) {
417-
sb.append(" <repository>\n");
418-
sb.append(" <id>custom").append(i++).append("</id>\n");
419-
sb.append(" <url>").append(repo).append("</url>\n");
420-
if (repo.contains("snapshots")) {
421-
sb.append(" <releases>\n");
422-
sb.append(" <enabled>false</enabled>\n");
423-
sb.append(" </releases>\n");
424-
sb.append(" <snapshots>\n");
425-
sb.append(" <enabled>true</enabled>\n");
426-
sb.append(" </snapshots>\n");
427-
}
428-
sb.append(" </repository>\n");
419+
Map<String, Object> r = new HashMap<>();
420+
r.put("id", "custom" + i++);
421+
r.put("url", repo);
422+
r.put("isSnapshot", repo.contains("snapshots"));
423+
result.add(r);
429424
}
430425
}
431-
sb.append(" </repositories>\n");
432-
sb.append(" <pluginRepositories>\n");
433-
if (!repos.isEmpty()) {
434-
for (String repo : repos.split(",")) {
435-
sb.append(" <pluginRepository>\n");
436-
sb.append(" <id>custom").append(i++).append("</id>\n");
437-
sb.append(" <url>").append(repo).append("</url>\n");
438-
if (repo.contains("snapshots")) {
439-
sb.append(" <releases>\n");
440-
sb.append(" <enabled>false</enabled>\n");
441-
sb.append(" </releases>\n");
442-
sb.append(" <snapshots>\n");
443-
sb.append(" <enabled>true</enabled>\n");
444-
sb.append(" </snapshots>\n");
426+
return result;
427+
}
428+
429+
/**
430+
* Builds a list of dependency data maps from the deps set, for use with FreeMarker templates.
431+
*/
432+
protected List<Map<String, Object>> buildDependencyList(Set<String> deps) {
433+
List<MavenGav> gavs = new ArrayList<>();
434+
for (String dep : deps) {
435+
MavenGav gav = parseMavenGav(dep);
436+
String gid = gav.getGroupId();
437+
if ("org.apache.camel".equals(gid)) {
438+
// uses BOM so version should not be included
439+
gav.setVersion(null);
440+
}
441+
gavs.add(gav);
442+
}
443+
444+
// sort artifacts
445+
gavs.sort(mavenGavComparator());
446+
447+
List<Map<String, Object>> result = new ArrayList<>();
448+
for (MavenGav gav : gavs) {
449+
Map<String, Object> dep = new HashMap<>();
450+
dep.put("groupId", gav.getGroupId());
451+
dep.put("artifactId", gav.getArtifactId());
452+
dep.put("version", gav.getVersion());
453+
dep.put("scope", gav.getScope());
454+
dep.put("isLib", "lib".equals(gav.getPackaging()));
455+
dep.put("isKameletsUtils", "camel-kamelets-utils".equals(gav.getArtifactId()));
456+
result.add(dep);
457+
}
458+
return result;
459+
}
460+
461+
public Comparator<MavenGav> mavenGavComparator() {
462+
return new Comparator<MavenGav>() {
463+
@Override
464+
public int compare(MavenGav o1, MavenGav o2) {
465+
int r1 = rankGroupId(o1);
466+
int r2 = rankGroupId(o2);
467+
468+
if (r1 > r2) {
469+
return -1;
470+
} else if (r2 > r1) {
471+
return 1;
472+
} else {
473+
return o1.toString().compareTo(o2.toString());
445474
}
446-
sb.append(" </pluginRepository>\n");
475+
}
476+
477+
int rankGroupId(MavenGav o1) {
478+
String g1 = o1.getGroupId();
479+
if (g1 == null) {
480+
return 0;
481+
}
482+
483+
switch (g1) {
484+
case "org.springframework.boot" -> {
485+
return 30;
486+
}
487+
case "io.quarkus" -> {
488+
return 30;
489+
}
490+
case "org.apache.camel.quarkus" -> {
491+
String a1 = o1.getArtifactId();
492+
// main/core/engine first
493+
if ("camel-quarkus-core".equals(a1)) {
494+
return 21;
495+
}
496+
return 20;
497+
}
498+
case "org.apache.camel.springboot" -> {
499+
String a1 = o1.getArtifactId();
500+
// main/core/engine first
501+
if ("camel-spring-boot-starter".equals(a1)) {
502+
return 21;
503+
} else if ("camel-spring-boot-engine-starter".equals(a1)) {
504+
return 22;
505+
}
506+
return 20;
507+
}
508+
case "org.apache.camel" -> {
509+
String a1 = o1.getArtifactId();
510+
// main/core/engine first
511+
if ("camel-main".equals(a1)) {
512+
return 11;
513+
}
514+
return 10;
515+
}
516+
case "org.apache.camel.kamelets" -> {
517+
return 5;
518+
}
519+
default -> {
520+
return 0;
521+
}
522+
}
523+
}
524+
};
525+
}
526+
527+
/**
528+
* Formats build properties as XML property lines for inclusion in POM templates.
529+
*/
530+
protected String formatBuildProperties() {
531+
Properties properties = mapBuildProperties();
532+
533+
if (!skipPlugins) {
534+
Set<PluginExporter> exporters = PluginHelper.getActivePlugins(getMain(), repositories).values()
535+
.stream()
536+
.map(Plugin::getExporter)
537+
.filter(Optional::isPresent)
538+
.map(Optional::get)
539+
.collect(Collectors.toSet());
540+
541+
for (PluginExporter exporter : exporters) {
542+
exporter.getBuildProperties().forEach(properties::putIfAbsent);
447543
}
448544
}
449-
sb.append(" </pluginRepositories>\n");
450-
return sb.toString();
545+
546+
return properties.entrySet().stream()
547+
.map(item -> String.format(" <%s>%s</%s>", item.getKey(), item.getValue(), item.getKey()))
548+
.collect(Collectors.joining(System.lineSeparator()));
451549
}
452550

453551
protected abstract Integer export() throws Exception;
@@ -489,30 +587,15 @@ protected void addDependencies(String... deps) {
489587
dependencies.addAll(Arrays.asList(depsArray));
490588
}
491589

590+
/**
591+
* @deprecated Use {@link #formatBuildProperties()} instead for FreeMarker templates.
592+
*/
593+
@Deprecated
492594
protected String replaceBuildProperties(String context) {
493-
Properties properties = mapBuildProperties();
494-
495-
if (!skipPlugins) {
496-
Set<PluginExporter> exporters = PluginHelper.getActivePlugins(getMain(), repositories).values()
497-
.stream()
498-
.map(Plugin::getExporter)
499-
.filter(Optional::isPresent)
500-
.map(Optional::get)
501-
.collect(Collectors.toSet());
502-
503-
for (PluginExporter exporter : exporters) {
504-
exporter.getBuildProperties().forEach(properties::putIfAbsent);
505-
}
506-
}
507-
508-
String mavenProperties = properties.entrySet().stream()
509-
.map(item -> {
510-
return String.format(" <%s>%s</%s>", item.getKey(), item.getValue(), item.getKey());
511-
})
512-
.collect(Collectors.joining(System.lineSeparator()));
513-
595+
String mavenProperties = formatBuildProperties();
514596
if (!mavenProperties.isEmpty()) {
515-
context = context.replaceFirst(Pattern.quote("{{ .BuildProperties }}"), Matcher.quoteReplacement(mavenProperties));
597+
context = context.replaceFirst(Pattern.quote("{{ .BuildProperties }}"),
598+
Matcher.quoteReplacement(mavenProperties));
516599
} else {
517600
context = context.replaceFirst(Pattern.quote("{{ .BuildProperties }}"), "");
518601
}

0 commit comments

Comments
 (0)