This is an automated email from the ASF dual-hosted git repository.
davsclaus pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git
The following commit(s) were added to refs/heads/main by this push:
new 9fa6919de995 CAMEL-25565: camel-jbang - route-diagram, route-structure
and route-topology read Java source files with the Java DSL parser instead of
compiling them (#27715)
9fa6919de995 is described below
commit 9fa6919de9956050f26338d97544e0e6f249f783
Author: Claus Ibsen <[email protected]>
AuthorDate: Thu Oct 15 21:50:33 2026 +0200
CAMEL-25565: camel-jbang - route-diagram, route-structure and
route-topology read Java source files with the Java DSL parser instead of
compiling them (#27715)
Given Java source files, the three commands (and the
camel_render_route_diagram
MCP tool) booted a short-lived Camel to compile the routes and dump the
model,
which took seconds and needed the project's classes and dependencies. They
now
read Java sources with the Java DSL parser, as camel transform route and
camel
overview already do: the routes are added to a CamelContext that is never
started and dumped with the same DefaultDumpRoutesStrategy. Parts the parser
cannot read are listed (as an unread array with --json). Routes and steps
get
their ids as when Camel starts, so a route without routeId is shown. A new
--compile option restores the old behaviour. YAML and XML sources are
unchanged.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Signed-off-by: Claus Ibsen <[email protected]>
---
design/java-dsl-parser.adoc | 5 +
.../ROOT/pages/camel-4x-upgrade-guide-4_24.adoc | 9 +
.../ROOT/pages/camel-jbang-diagnostics.adoc | 3 +-
.../camel-jbang-cmd-route-diagram.adoc | 1 +
.../camel-jbang-cmd-route-structure.adoc | 1 +
.../camel-jbang-cmd-route-topology.adoc | 1 +
.../modules/ROOT/pages/route-diagram.adoc | 5 +
.../META-INF/camel-jbang-commands-metadata.json | 2 +-
.../jbang/core/commands/TransformJavaRoutes.java | 114 +++++++++--
.../core/commands/action/ActionBaseCommand.java | 67 ++++++-
.../commands/action/CamelRouteDiagramAction.java | 14 +-
.../commands/action/CamelRouteStructureAction.java | 14 ++
.../commands/action/CamelRouteTopologyAction.java | 12 +-
.../commands/action/RouteActionJavaSourceTest.java | 210 +++++++++++++++++++++
.../jbang/core/commands/mcp/RouteDiagramTools.java | 16 +-
15 files changed, 442 insertions(+), 32 deletions(-)
diff --git a/design/java-dsl-parser.adoc b/design/java-dsl-parser.adoc
index 80ee524bbfd2..8c021b46e5ac 100644
--- a/design/java-dsl-parser.adoc
+++ b/design/java-dsl-parser.adoc
@@ -108,6 +108,11 @@ camel-jbang reads Java routes with the parser only; the
pattern matching on stri
running Camel, in a CamelContext that is never started (CAMEL-25202); it
compiles the routes only when one cannot be
read completely, or with `--compile`.
+`camel cmd route-diagram`, `route-topology` and `route-structure` (and the
`camel_render_route_diagram` MCP tool) read
+Java source files the same way, and dump the route structure and topology JSON
from that CamelContext (CAMEL-25565). A
+route read only partly is still shown: a lambda or a processor is the
placeholder step, and the parts the parser could
+not read are listed. They compile the routes only with `--compile`.
+
== Security
The parser reads sources nobody vetted: a cloned example opened in the TUI,
documentation, a project an AI agent
diff --git
a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
index 1f85081cbd9a..23ffb50c014e 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
@@ -841,6 +841,15 @@ failures of the route go. A highlighted path that steps
into an `onException` cl
The `<camel-topology-diagram>` web component no longer draws the error paths
of the route topology as calls between
routes. With its new `errors` attribute it draws the routes reached only on
error in a frame below the other routes.
+=== camel-jbang - route-diagram, route-structure and route-topology read Java
sources without compiling them
+
+Given Java source files, `camel cmd route-diagram`, `camel cmd
route-structure` and `camel cmd route-topology` (and the
+`camel_render_route_diagram` MCP tool) now read the routes with the Java DSL
parser, as `camel transform route` and
+`camel overview` do, instead of compiling the sources and loading their routes
in a short-lived Camel. A step the parser
+cannot work out, such as a lambda or a processor, is shown as a placeholder,
and the parts it could not read are
+listed (with `--json`, as an `unread` array in the JSON). Use the new
`--compile` option to compile the sources and
+load their routes as before.
+
=== camel-jbang - export to Quarkus keeps the names in `quarkus.*` keys
`camel export --runtime=quarkus` wrote every `quarkus.*` key except
`quarkus.hawtio.*` in dash case: an upper-case
diff --git a/docs/user-manual/modules/ROOT/pages/camel-jbang-diagnostics.adoc
b/docs/user-manual/modules/ROOT/pages/camel-jbang-diagnostics.adoc
index 8ed30eb3de6b..1613e5130bb7 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-jbang-diagnostics.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-jbang-diagnostics.adoc
@@ -180,7 +180,8 @@ camel cmd route-structure myApp --filter=myRoute --brief
camel cmd route-structure myApp --description
----
-Give a source file instead of a running integration to see the structure
without running it.
+Give a source file instead of a running integration to see the structure
without running it. A Java source file is read
+by the Java DSL parser, without compiling it; use `--compile` to compile it
instead.
`--brief` shortens each node, and `--description` shows the description of
each node instead of its code.
=== Route dump
diff --git
a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-cmd-route-diagram.adoc
b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-cmd-route-diagram.adoc
index 960f11cdfe34..a1014d6a478c 100644
---
a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-cmd-route-diagram.adoc
+++
b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-cmd-route-diagram.adoc
@@ -20,6 +20,7 @@ camel cmd route-diagram [options]
|===
| Option | Description | Default | Type
| `--box-width` | Node box width in logical pixels | 180 | int
+| `--compile` | Whether to compile Java source files and load their routes. By
default Java routes are read by the Java DSL parser, without compiling them |
false | boolean
| `--errors` | Draw the error handling of each route in a frame below its
steps: its onException clauses, where its failures go and where it is reached
on error from. Without it only the steps of the route are drawn | | boolean
| `--filter` | Filter route by filename or route id | | String
| `--font-size` | Font size in logical pixels for node text | 12 | int
diff --git
a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-cmd-route-structure.adoc
b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-cmd-route-structure.adoc
index 8ce1fdceb2b5..187e045a7618 100644
---
a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-cmd-route-structure.adoc
+++
b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-cmd-route-structure.adoc
@@ -20,6 +20,7 @@ camel cmd route-structure [options]
|===
| Option | Description | Default | Type
| `--brief` | To show less detailed route structure | | boolean
+| `--compile` | Whether to compile Java source files and load their routes. By
default Java routes are read by the Java DSL parser, without compiling them |
false | boolean
| `--description` | To show description instead of code | | boolean
| `--filter` | Filter route by filename or route id (multiple names can be
separated by comma) | | String
| `--ignore-loading-error` | Whether to ignore route loading and compilation
errors (use this with care!) | false | boolean
diff --git
a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-cmd-route-topology.adoc
b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-cmd-route-topology.adoc
index 53a58591d1f0..190a9cd947b2 100644
---
a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-cmd-route-topology.adoc
+++
b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-cmd-route-topology.adoc
@@ -20,6 +20,7 @@ camel cmd route-topology [options]
|===
| Option | Description | Default | Type
| `--box-width` | Width of diagram node boxes | 180 | int
+| `--compile` | Whether to compile Java source files and load their routes. By
default Java routes are read by the Java DSL parser, without compiling them |
false | boolean
| `--description` | Prefer route description over route id in node labels | |
boolean
| `--errors` | Draw the error handling: the routes reached only on error in a
frame of their own below the routes, with where their failures come from (with
--theme) | | boolean
| `--external` | Include external systems (consumers at top, producers at
bottom) | | boolean
diff --git a/docs/user-manual/modules/ROOT/pages/route-diagram.adoc
b/docs/user-manual/modules/ROOT/pages/route-diagram.adoc
index bca05e6582bd..b461d2b20438 100644
--- a/docs/user-manual/modules/ROOT/pages/route-diagram.adoc
+++ b/docs/user-manual/modules/ROOT/pages/route-diagram.adoc
@@ -27,6 +27,11 @@ It also works with Java source files:
camel cmd route-diagram MyRoute.java
----
+Java routes are read by the Java DSL parser, without compiling them, so the
diagram is ready in milliseconds and the
+source does not need its dependencies. A step the parser cannot work out, such
as a lambda or a processor, is shown as a
+placeholder, and the parts it could not read are listed. Use `--compile` to
compile the source and load its routes
+instead. The same applies to `camel cmd route-topology` and `camel cmd
route-structure`.
+
And you can include multiple files:
[source,bash]
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/generated/resources/META-INF/camel-jbang-commands-metadata.json
b/dsl/camel-jbang/camel-jbang-core/src/generated/resources/META-INF/camel-jbang-commands-metadata.json
index 55f88b0342ce..5355a0ee5470 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/generated/resources/META-INF/camel-jbang-commands-metadata.json
+++
b/dsl/camel-jbang/camel-jbang-core/src/generated/resources/META-INF/camel-jbang-commands-metadata.json
@@ -3,7 +3,7 @@
{ "name": "ask", "fullName": "ask", "description": "Ask a question about a
running Camel application using AI", "sourceClass":
"org.apache.camel.dsl.jbang.core.commands.Ask", "options": [ { "names":
"--api-key", "description": "API key. Also reads ANTHROPIC_API_KEY,
OPENAI_API_KEY, WATSONX_APIKEY, or LLM_API_KEY env vars", "javaType":
"java.lang.String", "type": "string" }, { "names": "--api-type", "description":
"API type: 'ollama', 'openai', 'anthropic', or 'watsonx'", "javaType": [...]
{ "name": "bind", "fullName": "bind", "description": "DEPRECATED: Bind
source and sink Kamelets as a new Camel integration", "deprecated": true,
"sourceClass": "org.apache.camel.dsl.jbang.core.commands.bind.Bind", "options":
[ { "names": "--error-handler", "description": "Add error handler
(none|log|sink:<endpoint>). Sink endpoints are expected in the format
[[apigroup\/]version:]kind:[namespace\/]name, plain Camel URIs or Kamelet
name.", "javaType": "java.lang.String", "type": "stri [...]
{ "name": "catalog", "fullName": "catalog", "description": "List artifacts
from Camel Catalog", "sourceClass":
"org.apache.camel.dsl.jbang.core.commands.catalog.CatalogCommand", "options": [
{ "names": "-h,--help", "description": "Display the help and sub-commands",
"javaType": "boolean", "type": "boolean" } ], "subcommands": [ { "name":
"component", "fullName": "catalog component", "description": "List components
from the Camel Catalog", "sourceClass": "org.apache.camel.dsl.jbang.co [...]
- { "name": "cmd", "fullName": "cmd", "description": "Performs commands in
the running Camel integrations, such as start\/stop route, or change logging
levels.", "sourceClass":
"org.apache.camel.dsl.jbang.core.commands.action.CamelAction", "options": [ {
"names": "-h,--help", "description": "Display the help and sub-commands",
"javaType": "boolean", "type": "boolean" } ], "subcommands": [ { "name":
"browse", "fullName": "cmd browse", "description": "Browse pending messages on
endpoints [...]
+ { "name": "cmd", "fullName": "cmd", "description": "Performs commands in
the running Camel integrations, such as start\/stop route, or change logging
levels.", "sourceClass":
"org.apache.camel.dsl.jbang.core.commands.action.CamelAction", "options": [ {
"names": "-h,--help", "description": "Display the help and sub-commands",
"javaType": "boolean", "type": "boolean" } ], "subcommands": [ { "name":
"browse", "fullName": "cmd browse", "description": "Browse pending messages on
endpoints [...]
{ "name": "completion", "fullName": "completion", "description": "Generate
completion script for bash\/zsh", "sourceClass":
"org.apache.camel.dsl.jbang.core.commands.Complete", "options": [ { "names":
"-h,--help", "description": "Display the help and sub-commands", "javaType":
"boolean", "type": "boolean" } ] },
{ "name": "config", "fullName": "config", "description": "Get and set user
configuration values", "sourceClass":
"org.apache.camel.dsl.jbang.core.commands.config.ConfigCommand", "options": [ {
"names": "-h,--help", "description": "Display the help and sub-commands",
"javaType": "boolean", "type": "boolean" } ], "subcommands": [ { "name": "get",
"fullName": "config get", "description": "Display user configuration value",
"sourceClass": "org.apache.camel.dsl.jbang.core.commands.config. [...]
{ "name": "debug", "fullName": "debug", "description": "Debug local Camel
integration", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.Debug",
"options": [ { "names": "--ago", "description": "Use ago instead of yyyy-MM-dd
HH:mm:ss in timestamp.", "javaType": "boolean", "type": "boolean" }, { "names":
"--background", "description": "Run in the background", "defaultValue":
"false", "javaType": "boolean", "type": "boolean" }, { "names":
"--background-wait", "description": "To [...]
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/TransformJavaRoutes.java
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/TransformJavaRoutes.java
index 1172cf28159d..331a9fc698c4 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/TransformJavaRoutes.java
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/TransformJavaRoutes.java
@@ -27,32 +27,44 @@ import java.util.Map;
import java.util.function.Supplier;
import java.util.stream.Stream;
+import org.apache.camel.NamedNode;
import org.apache.camel.catalog.CamelCatalog;
import org.apache.camel.catalog.DefaultCamelCatalog;
import org.apache.camel.dsl.jbang.core.commands.ai.ProjectRoutes;
import org.apache.camel.impl.DefaultCamelContext;
import org.apache.camel.impl.DefaultDumpRoutesStrategy;
import org.apache.camel.java.in.JavaParseResult;
+import org.apache.camel.model.RouteDefinition;
+import org.apache.camel.model.RouteDefinitionHelper;
import org.apache.camel.spi.Resource;
import org.apache.camel.support.ResourceHelper;
/**
- * {@code camel transform route} for Java routes without compiling them
(CAMEL-25202): the sources are read into the
- * model by the Java DSL parser of camel-java-io, and dumped as YAML or XML by
the same dump strategy a running Camel
- * uses ({@code camel.main.dumpRoutes}), in a CamelContext that is never
started. Nothing of the project is compiled,
- * loaded or run.
+ * Java routes read without compiling them: the sources are read into the
model by the Java DSL parser of camel-java-io,
+ * and dumped by the same dump strategy a running Camel uses ({@code
camel.main.dumpRoutes}), in a CamelContext that is
+ * never started. Nothing of the project is compiled, loaded or run.
+ * <p>
+ * Used by {@code camel transform route} (as YAML or XML, CAMEL-25202), and by
{@code camel cmd route-diagram},
+ * {@code route-structure} and {@code route-topology} (as the route structure
and topology JSON, CAMEL-25565).
*/
-final class TransformJavaRoutes {
+public final class TransformJavaRoutes {
- /** Whether the routes were transformed, and why not: the first part the
parser could not read. */
- record Result(boolean transformed, String reason) {
+ /**
+ * Whether the routes were dumped, and why not; and the parts the parser
could not read (empty when every route was
+ * read completely).
+ */
+ public record Result(boolean transformed, String reason, List<String>
unread) {
+ }
+
+ /** The routes read from the files, and the parts of them the parser could
not read. */
+ private record Read(List<JavaParseResult> results, List<Resource>
resources, List<String> unread, String failure) {
}
private TransformJavaRoutes() {
}
/** Whether the files are all Java sources, which the parser may read. */
- static boolean applies(List<String> files) {
+ public static boolean applies(List<String> files) {
return !files.isEmpty()
&& files.stream().allMatch(f -> f.endsWith(".java") &&
Files.isRegularFile(Path.of(f)));
}
@@ -67,33 +79,86 @@ final class TransformJavaRoutes {
*/
static Result transform(List<String> files, String format, String output,
boolean uriAsParameters)
throws Exception {
+ Read read = read(files, true);
+ if (read.failure() != null) {
+ return new Result(false, read.failure(), read.unread());
+ }
+ dump(read, format, output, uriAsParameters);
+ return new Result(true, null, List.of());
+ }
+
+ /**
+ * Dumps the route structure and the route topology of the Java routes of
the files as JSON, as
+ * {@code camel.main.dumpRoutes=json} does: one route structure file per
source file, and
+ * {@code route-topology.json}. A route the parser reads only partly is
dumped too: a step it cannot work out (a
+ * lambda, a processor) is the placeholder the parser puts in the model,
and the parts it could not read are
+ * returned.
+ *
+ * @param output the directory (or file) to write to
+ * @return whether they were dumped; else why not (no file has a
route the parser can read)
+ */
+ public static Result dumpStructure(List<String> files, String output)
throws Exception {
+ Read read = read(files, false);
+ if (read.failure() != null) {
+ return new Result(false, read.failure(), read.unread());
+ }
+ dump(read, "json", output, false);
+ return new Result(true, null, read.unread());
+ }
+
+ /**
+ * Reads the routes of the files.
+ *
+ * @param complete whether every route must be read completely: else the
first part the parser could not read is the
+ * failure; when not, a file without routes is skipped,
and the failure is that no file has one
+ */
+ private static Read read(List<String> files, boolean complete) throws
Exception {
CamelCatalog catalog = new DefaultCamelCatalog();
Map<String, Supplier<String>> sources = javaSources(files);
List<JavaParseResult> results = new ArrayList<>();
List<Resource> resources = new ArrayList<>();
+ List<String> unread = new ArrayList<>();
for (String f : files) {
String content = Files.readString(Path.of(f),
StandardCharsets.UTF_8);
JavaParseResult result = ProjectRoutes.parseJava(content, sources,
catalog);
for (JavaParseResult.Unresolved u : result.unresolved()) {
if (!JavaParseResult.configuresTheContext(u)) {
- return new Result(false, f + ":" + u.line() + " " +
u.text() + " (" + u.reason() + ")");
+ String part = f + ":" + u.line() + " " + u.text() + " (" +
u.reason() + ")";
+ if (complete) {
+ return new Read(List.of(), List.of(), List.of(part),
part);
+ }
+ unread.add(part);
}
}
if (result.routes().getRoutes().isEmpty() &&
result.rests().getRests().isEmpty()
&& result.routeTemplates().getRouteTemplates().isEmpty()
&&
result.routeConfigurations().getRouteConfigurations().isEmpty()) {
- return new Result(false, f + " has no routes the parser can
read");
+ if (complete) {
+ return new Read(List.of(), List.of(), List.of(), f + " has
no routes the parser can read");
+ }
+ continue;
}
results.add(result);
// the source file, so each is dumped on its own and named after
it, as when the routes run
resources.add(ResourceHelper.fromString("file:" +
Path.of(f).getFileName(), content));
}
+ if (results.isEmpty()) {
+ return new Read(List.of(), List.of(), unread, String.join(", ",
files) + " has no routes the parser can read");
+ }
+ return new Read(results, resources, unread, null);
+ }
+
+ private static void dump(Read read, String format, String output, boolean
uriAsParameters) throws Exception {
try (DefaultCamelContext context = new DefaultCamelContext()) {
// never started: adding the definitions only registers them
- for (int i = 0; i < results.size(); i++) {
- JavaParseResult r = results.get(i);
- Resource resource = resources.get(i);
+ for (int i = 0; i < read.results().size(); i++) {
+ JavaParseResult r = read.results().get(i);
+ Resource resource = read.resources().get(i);
r.routes().getRoutes().forEach(d -> d.setResource(resource));
+ if ("json".equals(format)) {
+ // the source of each step, as a routes loader records it
(the route structure shows it)
+ r.routes().getRoutes().forEach(d -> locate(d,
resource.getLocation()));
+ }
r.rests().getRests().forEach(d -> d.setResource(resource));
r.routeTemplates().getRouteTemplates().forEach(d ->
d.setResource(resource));
r.routeConfigurations().getRouteConfigurations().forEach(d ->
d.setResource(resource));
@@ -102,6 +167,14 @@ final class TransformJavaRoutes {
context.addRouteTemplateDefinitions(r.routeTemplates().getRouteTemplates());
context.addRouteDefinitions(r.routes().getRoutes());
}
+ if ("json".equals(format)) {
+ // the ids a start assigns: the route structure looks each
route up by its id, and shows the step ids
+ // (a route id that is a property placeholder is kept as it
is, as there are no properties)
+
context.getPropertiesComponent().setIgnoreMissingProperty(true);
+ List<RouteDefinition> routes = context.getRouteDefinitions();
+ RouteDefinitionHelper.forceAssignIds(context, routes);
+ routes.forEach(route ->
RouteDefinitionHelper.forceAssignIds(context, route));
+ }
DefaultDumpRoutesStrategy dumper = new DefaultDumpRoutesStrategy();
dumper.setCamelContext(context);
dumper.setInclude("routes,rests,routeConfigurations,beans,dataFormats");
@@ -111,7 +184,20 @@ final class TransformJavaRoutes {
dumper.setOutput(output);
dumper.dumpRoutes(format);
}
- return new Result(true, null);
+ }
+
+ /** Sets the source location of the node and its children (the line
numbers the parser has set already). */
+ private static void locate(NamedNode node, String location) {
+ if (node.getLocation() == null) {
+ node.setLocation(location);
+ }
+ if (node instanceof RouteDefinition route && route.getInput() != null
&& route.getInput().getLocation() == null) {
+ route.getInput().setLocation(location);
+ }
+ List<? extends NamedNode> children = node.getChildren();
+ if (children != null) {
+ children.forEach(c -> locate(c, location));
+ }
}
/** The Java sources of the files' folders, for the constants a route
takes from another class. */
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/ActionBaseCommand.java
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/ActionBaseCommand.java
index cf9bcea3b3c0..029e2982f3c6 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/ActionBaseCommand.java
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/ActionBaseCommand.java
@@ -28,6 +28,7 @@ import java.util.regex.Pattern;
import org.apache.camel.dsl.jbang.core.commands.CamelCommand;
import org.apache.camel.dsl.jbang.core.commands.CamelJBangMain;
import org.apache.camel.dsl.jbang.core.commands.Run;
+import org.apache.camel.dsl.jbang.core.commands.TransformJavaRoutes;
import org.apache.camel.dsl.jbang.core.common.CamelJBangConstants;
import org.apache.camel.dsl.jbang.core.common.CommandLineHelper;
import org.apache.camel.dsl.jbang.core.common.PathUtils;
@@ -36,11 +37,15 @@ import org.apache.camel.main.KameletMain;
import org.apache.camel.support.PatternHelper;
import org.apache.camel.util.FileUtil;
import org.apache.camel.util.StopWatch;
+import org.apache.camel.util.json.JsonArray;
import org.apache.camel.util.json.JsonObject;
import org.apache.camel.util.json.Jsoner;
abstract class ActionBaseCommand extends CamelCommand {
+ /** The parts of Java sources the parser could not read, when the routes
were read from Java sources. */
+ private List<String> unreadSourceParts;
+
protected ActionBaseCommand(CamelJBangMain main) {
super(main);
}
@@ -189,18 +194,22 @@ abstract class ActionBaseCommand extends CamelCommand {
}
/**
- * Boots the given source files through a transient Camel run (see {@link
Run#runTransform(boolean)}) that dumps the
- * route structure as JSON into a temporary work directory, so
route-diagram / route-topology can render from source
- * files without a running integration. The caller reads the dumped JSON
from {@link SourceDump#workDir()} and is
- * responsible for deleting that directory when done.
+ * Dumps the route structure of the given source files as JSON into a
temporary work directory, so route-diagram /
+ * route-topology can render from source files without a running
integration. Java sources are read by the Java DSL
+ * parser (see {@link TransformJavaRoutes#dumpStructure(List, String)})
without compiling them, unless
+ * {@code compile} is set; other sources, or Java sources with {@code
compile}, are booted through a transient Camel
+ * run (see {@link Run#runTransform(boolean)}). The caller reads the
dumped JSON from {@link SourceDump#workDir()}
+ * and is responsible for deleting that directory when done.
*
* @param sourceFiles the source files to load
* @param workDirPrefix prefix for the (pid-suffixed) temporary work
directory name
* @param ignoreLoadingError whether to ignore route loading and
compilation errors
- * @return the dump location and the instant the boot
started, or {@code null} if a file does not
- * exist or the boot failed (an error is
already printed to the user in that case)
+ * @param compile whether to compile Java sources and boot
them, instead of reading them with the parser
+ * @return the dump location and the instant the dump
started, or {@code null} if a file does not
+ * exist or the dump failed (an error is
already printed to the user in that case)
*/
- protected SourceDump dumpRoutesFromSource(List<String> sourceFiles, String
workDirPrefix, boolean ignoreLoadingError)
+ protected SourceDump dumpRoutesFromSource(
+ List<String> sourceFiles, String workDirPrefix, boolean
ignoreLoadingError, boolean compile)
throws Exception {
for (String name : sourceFiles) {
File f = new File(name);
@@ -217,6 +226,10 @@ abstract class ActionBaseCommand extends CamelCommand {
// previous --watch iteration whose cleanup above raced with (or lost
against) a slow filesystem
Instant bootStart = Instant.now();
+ if (!compile && TransformJavaRoutes.applies(sourceFiles)) {
+ return dumpJavaRoutes(sourceFiles, target) ? new
SourceDump(workDir, bootStart) : null;
+ }
+
Run run = new Run(getMain()) {
@Override
protected void doAddInitialProperty(KameletMain main) {
@@ -242,6 +255,46 @@ abstract class ActionBaseCommand extends CamelCommand {
return new SourceDump(workDir, bootStart);
}
+ /**
+ * Dumps the route structure and topology JSON of Java sources read by the
Java DSL parser, and prints what the
+ * parser could not read.
+ *
+ * @return whether the routes were dumped (else an error is already
printed)
+ */
+ protected boolean dumpJavaRoutes(List<String> sourceFiles, String target)
throws Exception {
+ TransformJavaRoutes.Result result =
TransformJavaRoutes.dumpStructure(sourceFiles, target);
+ if (!result.transformed()) {
+ printer().printErr("No routes found: " + result.reason()
+ + " (use --compile to compile the source and
load its routes)");
+ return false;
+ }
+ unreadSourceParts = result.unread();
+ if (!unreadSourceParts.isEmpty() && printSourceNotes()) {
+ printer().println("Routes read without compiling them; the parser
could not work out these parts "
+ + "(use --compile to compile the source
instead):");
+ unreadSourceParts.forEach(u -> printer().println(" " + u));
+ }
+ return true;
+ }
+
+ /**
+ * Whether to print the parts of Java sources the parser could not read:
not when the output is JSON, which lists
+ * them instead (see {@link #addUnreadSourceParts(JsonObject)}).
+ */
+ protected boolean printSourceNotes() {
+ return true;
+ }
+
+ /**
+ * Adds the parts of Java sources the parser could not read to the JSON
output, as an {@code unread} array, so a
+ * tool reading the JSON can tell that routes may be missing from it.
+ */
+ protected void addUnreadSourceParts(JsonObject jo) {
+ if (unreadSourceParts != null && !unreadSourceParts.isEmpty()) {
+ jo.put("unread", new JsonArray(unreadSourceParts));
+ }
+ }
+
/**
* Result of {@link #dumpRoutesFromSource}: the temporary directory the
route JSON was dumped into, and the instant
* the transient boot started (used to tell a fresh dump apart from a
stale one left by an earlier run).
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelRouteDiagramAction.java
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelRouteDiagramAction.java
index 161e8e8e89ae..c9e4eb4416ca 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelRouteDiagramAction.java
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelRouteDiagramAction.java
@@ -130,6 +130,11 @@ public class CamelRouteDiagramAction extends
ActionWatchCommand {
description = "Whether to ignore route loading and
compilation errors (use this with care!)")
boolean ignoreLoadingError;
+ @CommandLine.Option(names = { "--compile" }, defaultValue = "false",
+ description = "Whether to compile Java source files
and load their routes. By default Java "
+ + "routes are read by the Java DSL
parser, without compiling them")
+ boolean compile;
+
@CommandLine.Option(names = { "--errors" },
description = "Draw the error handling of each route
in a frame below its steps: its onException clauses,"
+ " where its failures go and where it
is reached on error from. Without it only the steps"
@@ -396,13 +401,12 @@ public class CamelRouteDiagramAction extends
ActionWatchCommand {
}
/**
- * Loads the given source files via a transient Camel boot (see
- * {@link #dumpRoutesFromSource(List, String, boolean)}) and returns the
merged route structure across all of them,
- * or {@code null} if a file does not exist or loading failed (an error is
already printed to the user in that
- * case). Package visible for testing.
+ * Loads the given source files (see {@link #dumpRoutesFromSource(List,
String, boolean, boolean)}) and returns the
+ * merged route structure across all of them, or {@code null} if a file
does not exist or loading failed (an error
+ * is already printed to the user in that case). Package visible for
testing.
*/
List<RouteInfo> doCallSource(List<String> sourceFiles) throws Exception {
- SourceDump dump = dumpRoutesFromSource(sourceFiles,
"route-diagram-source-", ignoreLoadingError);
+ SourceDump dump = dumpRoutesFromSource(sourceFiles,
"route-diagram-source-", ignoreLoadingError, compile);
if (dump == null) {
return null;
}
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelRouteStructureAction.java
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelRouteStructureAction.java
index fc8fcc793a05..08a5ef1cfad9 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelRouteStructureAction.java
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelRouteStructureAction.java
@@ -26,6 +26,7 @@ import java.util.Objects;
import org.apache.camel.dsl.jbang.core.commands.CamelJBangMain;
import org.apache.camel.dsl.jbang.core.commands.Run;
+import org.apache.camel.dsl.jbang.core.commands.TransformJavaRoutes;
import org.apache.camel.dsl.jbang.core.common.CamelJBangConstants;
import org.apache.camel.dsl.jbang.core.common.CommandLineHelper;
import org.apache.camel.dsl.jbang.core.common.PathUtils;
@@ -88,6 +89,11 @@ public class CamelRouteStructureAction extends
ActionBaseCommand {
description = "Whether to ignore route loading and
compilation errors (use this with care!)")
boolean ignoreLoadingError;
+ @CommandLine.Option(names = { "--compile" }, defaultValue = "false",
+ description = "Whether to compile Java source files
and load their routes. By default Java "
+ + "routes are read by the Java DSL
parser, without compiling them")
+ boolean compile;
+
@CommandLine.Option(names = { "--sort" }, completionCandidates =
NameIdCompletionCandidates.class,
description = "Sort route by name or id", defaultValue
= "name")
String sort;
@@ -128,6 +134,7 @@ public class CamelRouteStructureAction extends
ActionBaseCommand {
JsonObject jo = waitForOutputFile(outputFile);
if (jo != null) {
if (jsonOutput) {
+ addUnreadSourceParts(jo);
String dump = Jsoner.prettyPrint(jo.toJson(), 2);
printer().println(dump);
} else {
@@ -224,6 +231,9 @@ public class CamelRouteStructureAction extends
ActionBaseCommand {
}
final String target = CommandLineHelper.CAMEL_JBANG_WORK_DIR +
"/structure-output.json";
+ if (!compile && TransformJavaRoutes.applies(List.of(name))) {
+ return dumpJavaRoutes(List.of(name), target) ? 0 : 1;
+ }
Run run = new Run(getMain()) {
@Override
@@ -339,4 +349,8 @@ public class CamelRouteStructureAction extends
ActionBaseCommand {
String description;
}
+ @Override
+ protected boolean printSourceNotes() {
+ return !jsonOutput;
+ }
}
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelRouteTopologyAction.java
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelRouteTopologyAction.java
index e4af2f17c6be..281e23e578f0 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelRouteTopologyAction.java
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelRouteTopologyAction.java
@@ -115,6 +115,11 @@ public class CamelRouteTopologyAction extends
ActionBaseCommand {
description = "Whether to ignore route loading and
compilation errors (use this with care!)")
boolean ignoreLoadingError;
+ @CommandLine.Option(names = { "--compile" }, defaultValue = "false",
+ description = "Whether to compile Java source files
and load their routes. By default Java "
+ + "routes are read by the Java DSL
parser, without compiling them")
+ boolean compile;
+
private long pid;
public CamelRouteTopologyAction(CamelJBangMain main) {
@@ -160,7 +165,7 @@ public class CamelRouteTopologyAction extends
ActionBaseCommand {
}
private int doCallSource(List<String> sourceFiles) throws Exception {
- SourceDump dump = dumpRoutesFromSource(sourceFiles,
"route-topology-source-", ignoreLoadingError);
+ SourceDump dump = dumpRoutesFromSource(sourceFiles,
"route-topology-source-", ignoreLoadingError, compile);
if (dump == null) {
return 1;
}
@@ -193,6 +198,7 @@ public class CamelRouteTopologyAction extends
ActionBaseCommand {
*/
int renderTopology(JsonObject jo) throws Exception {
if (jsonOutput) {
+ addUnreadSourceParts(jo);
String dump = Jsoner.prettyPrint(jo.toJson(), 2);
printer().println(dump);
} else if (theme != null) {
@@ -369,4 +375,8 @@ public class CamelRouteTopologyAction extends
ActionBaseCommand {
}
}
+ @Override
+ protected boolean printSourceNotes() {
+ return !jsonOutput;
+ }
}
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/RouteActionJavaSourceTest.java
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/RouteActionJavaSourceTest.java
new file mode 100644
index 000000000000..40969d254666
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/RouteActionJavaSourceTest.java
@@ -0,0 +1,210 @@
+/*
+ * 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.camel.dsl.jbang.core.commands.action;
+
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.List;
+
+import org.apache.camel.diagram.RouteDiagramLayoutEngine.RouteInfo;
+import org.apache.camel.dsl.jbang.core.commands.CamelJBangMain;
+import org.apache.camel.dsl.jbang.core.common.StringPrinter;
+import org.apache.camel.util.json.JsonArray;
+import org.apache.camel.util.json.JsonObject;
+import org.apache.camel.util.json.Jsoner;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * route-diagram, route-topology and route-structure read Java source files
with the Java DSL parser (CAMEL-25565): the
+ * routes are dumped from a CamelContext that is never started, so nothing is
compiled and a source that does not
+ * compile (here: a processor class that does not exist) still gets its
diagram.
+ */
+class RouteActionJavaSourceTest {
+
+ private static final String ORDERS = """
+ import org.apache.camel.builder.RouteBuilder;
+
+ public class Orders extends RouteBuilder {
+ @Override
+ public void configure() {
+ from("timer:orders").routeId("intake")
+ .setBody(constant("order"))
+ .process(exchange ->
exchange.getMessage().setHeader("checked", true))
+ .to("direct:billing");
+ }
+ }
+ """;
+
+ private static final String BILLING = """
+ import org.apache.camel.builder.RouteBuilder;
+
+ public class Billing extends RouteBuilder {
+ @Override
+ public void configure() {
+ from("direct:billing").routeId("billing")
+ .process(new NoSuchProcessor())
+ .log("${body}");
+ }
+ }
+ """;
+
+ private static final String MIXED = """
+ import org.apache.camel.builder.RouteBuilder;
+
+ public class Mixed extends RouteBuilder {
+ @Override
+ public void configure() {
+ from("timer:tick").routeId("named")
+ .to("direct:next");
+
+ from("direct:next")
+ .log("${body}");
+ }
+ }
+ """;
+
+ @Test
+ void diagramReadsJavaRoutesWithoutCompilingThem(@TempDir Path dir) throws
Exception {
+ Path orders = write(dir, "Orders.java", ORDERS);
+ Path billing = write(dir, "Billing.java", BILLING);
+ StringPrinter printer = new StringPrinter();
+ CamelRouteDiagramAction command = new CamelRouteDiagramAction(new
CamelJBangMain().withPrinter(printer));
+
+ List<RouteInfo> routes =
command.doCallSource(List.of(orders.toString(), billing.toString()));
+
+ assertThat(routes).extracting(r ->
r.routeId).containsExactlyInAnyOrder("intake", "billing");
+ RouteInfo intake = routes.stream().filter(r ->
"intake".equals(r.routeId)).findFirst().orElseThrow();
+ // the lambda is a placeholder step, so the route keeps all its steps
+ assertThat(intake.nodes).extracting(n -> n.type).contains("from",
"setBody", "process", "to");
+ assertThat(printer.getOutput())
+ .contains("Routes read without compiling them")
+ .contains("Orders.java:")
+ .contains("a lambda or method reference");
+ }
+
+ @Test
+ void diagramCompilesWithCompileOption(@TempDir Path dir) throws Exception {
+ Path missing = dir.resolve("Missing.java");
+ StringPrinter printer = new StringPrinter();
+ CamelRouteDiagramAction command = new CamelRouteDiagramAction(new
CamelJBangMain().withPrinter(printer));
+ command.compile = true;
+
+ // the existence check comes first, also when compiling
+ assertThat(command.doCallSource(List.of(missing.toString()))).isNull();
+ assertThat(printer.getOutput()).contains("File does not exist: " +
missing);
+ }
+
+ @Test
+ void diagramReportsJavaSourceWithoutRoutes(@TempDir Path dir) throws
Exception {
+ Path helper = write(dir, "Helper.java", """
+ public class Helper {
+ public static String name() {
+ return "helper";
+ }
+ }
+ """);
+ StringPrinter printer = new StringPrinter();
+ CamelRouteDiagramAction command = new CamelRouteDiagramAction(new
CamelJBangMain().withPrinter(printer));
+
+ assertThat(command.doCallSource(List.of(helper.toString()))).isNull();
+ assertThat(printer.getOutput())
+ .contains("No routes found")
+ .contains("Helper.java has no routes the parser can read")
+ .contains("--compile");
+ }
+
+ @Test
+ void topologyLinksJavaRoutesOfSeveralFiles(@TempDir Path dir) throws
Exception {
+ Path orders = write(dir, "Orders.java", ORDERS);
+ Path billing = write(dir, "Billing.java", BILLING);
+ StringPrinter printer = new StringPrinter();
+ CamelRouteTopologyAction command = new CamelRouteTopologyAction(new
CamelJBangMain().withPrinter(printer));
+ command.files = List.of(orders.toString(), billing.toString());
+ command.jsonOutput = true;
+
+ int exit = command.doCall();
+
+ assertThat(exit).isZero();
+ // with --json only the JSON is printed, and the parts the parser
could not read are listed in it
+ JsonObject topology = (JsonObject)
Jsoner.deserialize(printer.getOutput());
+ JsonArray edges = (JsonArray) topology.get("edges");
+ assertThat(edges).hasSize(1);
+ JsonObject edge = (JsonObject) edges.get(0);
+ assertThat(edge.getString("fromRouteId")).isEqualTo("intake");
+ assertThat(edge.getString("toRouteId")).isEqualTo("billing");
+ JsonArray unread = (JsonArray) topology.get("unread");
+ assertThat(unread).isNotEmpty();
+ assertThat(unread.get(0).toString()).contains("Orders.java:");
+ }
+
+ @Test
+ void structureReadsJavaRoutesWithoutCompilingThem(@TempDir Path dir)
throws Exception {
+ Path billing = write(dir, "Billing.java", BILLING);
+ StringPrinter printer = new StringPrinter();
+ CamelRouteStructureAction command = new CamelRouteStructureAction(new
CamelJBangMain().withPrinter(printer));
+ command.name = billing.toString();
+
+ int exit = command.doCall();
+
+ assertThat(exit).isZero();
+ assertThat(printer.getOutput())
+ .contains("Source: Billing.java")
+ .contains("billing")
+ .contains("direct:billing")
+ .contains("an object created in the route");
+ }
+
+ @Test
+ void diagramReadsJavaRoutesWithoutRouteId(@TempDir Path dir) throws
Exception {
+ Path mixed = write(dir, "Mixed.java", MIXED);
+ StringPrinter printer = new StringPrinter();
+ CamelRouteDiagramAction command = new CamelRouteDiagramAction(new
CamelJBangMain().withPrinter(printer));
+
+ List<RouteInfo> routes =
command.doCallSource(List.of(mixed.toString()));
+
+ // the route without an id gets one, as when Camel starts, and the
named route is kept
+ assertThat(routes).extracting(r ->
r.routeId).hasSize(2).contains("named").doesNotContainNull();
+ }
+
+ @Test
+ void structureReadsJavaRouteWithoutRouteId(@TempDir Path dir) throws
Exception {
+ Path mixed = write(dir, "Mixed.java", MIXED);
+ StringPrinter printer = new StringPrinter();
+ CamelRouteStructureAction command = new CamelRouteStructureAction(new
CamelJBangMain().withPrinter(printer));
+ command.name = mixed.toString();
+ // the default of --sort (two routes are sorted)
+ command.sort = "name";
+
+ int exit = command.doCall();
+
+ assertThat(exit).isZero();
+ assertThat(printer.getOutput())
+ .contains("Source: Mixed.java")
+ .contains("named")
+ .contains("timer:tick")
+ .contains("direct:next");
+ }
+
+ private static Path write(Path dir, String name, String content) throws
Exception {
+ Path file = dir.resolve(name);
+ Files.writeString(file, content);
+ return file;
+ }
+}
diff --git
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/RouteDiagramTools.java
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/RouteDiagramTools.java
index 9d4e5a56491d..302f287edbfb 100644
---
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/RouteDiagramTools.java
+++
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/RouteDiagramTools.java
@@ -42,7 +42,9 @@ public class RouteDiagramTools {
@Tool(annotations = @Tool.Annotations(readOnlyHint = false,
destructiveHint = false, openWorldHint = false),
description = "Generate a diagram of Camel routes from a route
source file (YAML, XML, Java, ...). "
- + "The source file is parsed without running it. "
+ + "Java routes are read by the Java DSL parser,
without compiling them; a step it cannot "
+ + "work out (a lambda, a processor) is shown as a
placeholder, and the parts it could not read "
+ + "are listed in the message. "
+ "For image themes (dark, light, transparent) the PNG
is written to disk and the absolute path "
+ "is returned. For text themes (ascii, unicode) the
diagram is returned directly as text so "
+ "it can be read and understood by the caller. "
@@ -120,13 +122,13 @@ public class RouteDiagramTools {
if (textMode) {
String asciiContent = success ? Files.readString(out.toPath())
: null;
String message = success
- ? "ASCII diagram generated (" + size + " bytes)"
+ ? withNotes("ASCII diagram generated (" + size + "
bytes)", capturingPrinter)
: "Failed to render diagram (exit code " + exit + "):
" + capturingPrinter.getOutput();
return new RouteDiagramResult(success, resolvedOutput, size,
message, asciiContent);
}
String message = success
- ? "Diagram saved to: " + resolvedOutput
+ ? withNotes("Diagram saved to: " + resolvedOutput,
capturingPrinter)
: "Failed to render diagram (exit code " + exit + "): " +
capturingPrinter.getOutput();
return new RouteDiagramResult(success, resolvedOutput, size,
message, null);
} catch (Throwable e) {
@@ -135,6 +137,14 @@ public class RouteDiagramTools {
}
}
+ /**
+ * The message, followed by what the command printed, such as the parts of
a Java route the parser could not read.
+ */
+ private static String withNotes(String message, CapturingPrinter printer) {
+ String notes = printer.getOutput();
+ return notes.isEmpty() ? message : message + "\n" + notes;
+ }
+
/**
* @param asciiDiagram plain-text diagram content for ascii/unicode
themes; null for image themes
*/