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

davsclaus pushed a commit to branch fix/CAMEL-25591
in repository https://gitbox.apache.org/repos/asf/camel.git

commit c1af970191c7bc565c4765fad7907fdd2ce55d7e
Author: Claus Ibsen <[email protected]>
AuthorDate: Mon Oct 12 15:45:52 2026 +0200

    CAMEL-25591: camel-report-maven-plugin - read Java routes with the Java DSL 
parser instead of camel-route-parser
    
    JavaRouteModelReader reads a Java source into the Camel model with the Java
    DSL parser of camel-java-io and gives the details the goals work with: the
    node tree of each route for route-coverage (built with the rules of the XML
    tree), and the endpoints, simple expressions and route ids for validate. As
    camel-route-parser did, a part of a uri only known at runtime is validated 
as
    a placeholder, and endpoints injected with @EndpointInject and String field
    initializers are read. XML routes stay on camel-route-parser.
    
    Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
---
 catalog/camel-report-maven-plugin/pom.xml          |   9 +-
 .../src/main/docs/camel-report-maven-plugin.adoc   |   5 +
 .../apache/camel/maven/JavaRouteModelReader.java   | 534 +++++++++++++++++++++
 .../org/apache/camel/maven/RouteCoverageMojo.java  |  30 +-
 .../java/org/apache/camel/maven/ValidateMojo.java  |  45 +-
 .../camel/maven/JavaRouteModelReaderTest.java      | 243 ++++++++++
 .../ROOT/pages/camel-4x-upgrade-guide-4_24.adoc    |   8 +
 .../ROOT/pages/camel-report-maven-plugin.adoc      |   5 +
 8 files changed, 840 insertions(+), 39 deletions(-)

diff --git a/catalog/camel-report-maven-plugin/pom.xml 
b/catalog/camel-report-maven-plugin/pom.xml
index fac7aeda25cc..e83857b793a6 100644
--- a/catalog/camel-report-maven-plugin/pom.xml
+++ b/catalog/camel-report-maven-plugin/pom.xml
@@ -117,7 +117,14 @@
             </exclusions>
         </dependency>
 
-        <!-- camel route parser -->
+        <!-- the Java DSL parser, which reads Java routes into the Camel model 
without compiling them -->
+        <dependency>
+            <groupId>org.apache.camel</groupId>
+            <artifactId>camel-java-io</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+
+        <!-- camel route parser (XML routes) -->
         <dependency>
             <groupId>org.apache.camel</groupId>
             <artifactId>camel-route-parser</artifactId>
diff --git 
a/catalog/camel-report-maven-plugin/src/main/docs/camel-report-maven-plugin.adoc
 
b/catalog/camel-report-maven-plugin/src/main/docs/camel-report-maven-plugin.adoc
index 4db61c3f05d7..488086d31933 100644
--- 
a/catalog/camel-report-maven-plugin/src/main/docs/camel-report-maven-plugin.adoc
+++ 
b/catalog/camel-report-maven-plugin/src/main/docs/camel-report-maven-plugin.adoc
@@ -15,6 +15,11 @@ For validating the source code for mis-configured Camel:
 - seda/direct endpoint pairs
 - configuration in properties files
 
+Java routes are read with the Java DSL parser of camel-java-io, and XML routes 
with camel-route-parser, without
+compiling or running them. In a Java route, a value only known at runtime, 
such as a port held in a field, is
+validated as a property placeholder (`+http://localhost:{{port}}/orders+`); an 
endpoint injected into a field with
+`@EndpointInject`, and a field with a string initializer, are read as well.
+
 Then you can run the `validate` goal from the command line or from within your 
Java editor such as IDEA or Eclipse.
 
 [source,bash]
diff --git 
a/catalog/camel-report-maven-plugin/src/main/java/org/apache/camel/maven/JavaRouteModelReader.java
 
b/catalog/camel-report-maven-plugin/src/main/java/org/apache/camel/maven/JavaRouteModelReader.java
new file mode 100644
index 000000000000..feffe3ab1c92
--- /dev/null
+++ 
b/catalog/camel-report-maven-plugin/src/main/java/org/apache/camel/maven/JavaRouteModelReader.java
@@ -0,0 +1,534 @@
+/*
+ * 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.maven;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.Modifier;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.IdentityHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.apache.camel.LineNumberAware;
+import org.apache.camel.NamedNode;
+import org.apache.camel.catalog.CamelCatalog;
+import org.apache.camel.java.in.JavaParseResult;
+import org.apache.camel.java.in.LwJavaParser;
+import org.apache.camel.model.ChoiceDefinition;
+import org.apache.camel.model.EnrichDefinition;
+import org.apache.camel.model.ExpressionNode;
+import org.apache.camel.model.ExpressionSubElementDefinition;
+import org.apache.camel.model.FromDefinition;
+import org.apache.camel.model.InterceptFromDefinition;
+import org.apache.camel.model.InterceptSendToEndpointDefinition;
+import org.apache.camel.model.LoopDefinition;
+import org.apache.camel.model.PollDefinition;
+import org.apache.camel.model.PollEnrichDefinition;
+import org.apache.camel.model.ProcessorDefinition;
+import org.apache.camel.model.RouteDefinition;
+import org.apache.camel.model.SendDefinition;
+import org.apache.camel.model.ToDynamicDefinition;
+import org.apache.camel.model.language.ConstantExpression;
+import org.apache.camel.model.language.ExpressionDefinition;
+import org.apache.camel.parser.model.CamelEndpointDetails;
+import org.apache.camel.parser.model.CamelNodeDetails;
+import org.apache.camel.parser.model.CamelNodeDetailsFactory;
+import org.apache.camel.parser.model.CamelRouteDetails;
+import org.apache.camel.parser.model.CamelSimpleExpressionDetails;
+import org.apache.camel.tooling.model.EipModel;
+
+/**
+ * The routes of a Java source read by the Java DSL parser of camel-java-io 
(CAMEL-25591), as the details the report
+ * goals work with: the node tree of each route (route coverage), and the 
endpoints, simple expressions and route ids
+ * (validate). The source is read into the Camel model without compiling it; a 
part the parser cannot work out (a value
+ * computed at runtime, a lambda) is left out, and an endpoint uri it cannot 
work out is returned as unparsable.
+ * <p/>
+ * The node tree follows the rules of the XML DSL tree of camel-route-parser: 
the EIPs with an input or output, without
+ * the route (the tree starts with from) and without when and otherwise (their 
steps are the outputs of the choice).
+ */
+final class JavaRouteModelReader {
+
+    private static final String METHOD_NAME = "configure";
+    private static final Pattern PACKAGE = 
Pattern.compile("(?m)^\\s*package\\s+([\\w.]+)\\s*;");
+    private static final int MAX_DEPTH = 64;
+    /** A part of a value the parser could not work out, as it marks it in the 
model. */
+    private static final Pattern UNRESOLVED = 
Pattern.compile("\\?\\{([^}]*)}");
+    private static final Pattern FIELD_NAME = 
Pattern.compile("[A-Za-z_$][\\w$]*");
+    /** The uri of an endpoint injected into a field, which is not a part of 
configure(). */
+    private static final Pattern ENDPOINT_INJECT
+            = 
Pattern.compile("@EndpointInject\\s*\\(\\s*(?:(?:value|uri)\\s*=\\s*)?\"([^\"]+)\"");
+
+    private final CamelCatalog catalog;
+    private final String fileName;
+    private final String className;
+    private final String content;
+    private final JavaParseResult result;
+
+    private JavaRouteModelReader(CamelCatalog catalog, String fileName, String 
content) {
+        this.catalog = catalog;
+        this.fileName = fileName;
+        this.content = content;
+        this.className = className(fileName, content);
+        this.result = new LwJavaParser().parse(content);
+    }
+
+    /**
+     * Reads a Java source.
+     *
+     * @param catalog  the catalog, to know the EIPs and which expressions are 
predicates
+     * @param fileName the file the details refer to
+     * @param content  the Java source
+     */
+    static JavaRouteModelReader read(CamelCatalog catalog, String fileName, 
String content) {
+        return new JavaRouteModelReader(catalog, fileName, content);
+    }
+
+    /** Whether the source has routes the parser could read. */
+    boolean hasRoutes() {
+        return !result.routes().getRoutes().isEmpty();
+    }
+
+    /** The class the details refer to: the package of the source and the name 
of the file. */
+    String className() {
+        return className;
+    }
+
+    // ---- route coverage ----
+
+    /** The node tree of each route, starting with its from. */
+    List<CamelNodeDetails> routeTrees() {
+        List<CamelNodeDetails> answer = new ArrayList<>();
+        for (RouteDefinition route : result.routes().getRoutes()) {
+            FromDefinition input = route.getInput();
+            if (input == null) {
+                continue;
+            }
+            CamelNodeDetailsFactory factory = 
CamelNodeDetailsFactory.newInstance();
+            CamelNodeDetails from = factory.newNode(null, "from");
+            from.setRouteId(route.getRouteId());
+            int line = line(input, line(route, 0));
+            describe(from, line);
+            answer.add(from);
+            for (ProcessorDefinition<?> p : route.getOutputs()) {
+                tree(factory, p, from, line, 0);
+            }
+        }
+        return answer;
+    }
+
+    private void tree(
+            CamelNodeDetailsFactory factory, ProcessorDefinition<?> p, 
CamelNodeDetails parent, int parentLine,
+            int depth) {
+        if (p == null || depth > MAX_DEPTH) {
+            return;
+        }
+        String name = p.getShortName();
+        int line = line(p, parentLine);
+        CamelNodeDetails node = parent;
+        if (!"when".equals(name) && !"otherwise".equals(name) && isEip(name)) {
+            node = factory.newNode(parent, name);
+            node.setRouteId(parent.getRouteId());
+            if (p.hasCustomIdAssigned()) {
+                node.setNodeId(p.getId());
+            }
+            describe(node, line);
+            parent.addOutput(node);
+        }
+        // the steps of a choice are those of its when and otherwise
+        for (ProcessorDefinition<?> child : p.getOutputs()) {
+            tree(factory, child, node, line, depth + 1);
+        }
+    }
+
+    private void describe(CamelNodeDetails node, int line) {
+        node.setFileName(fileName);
+        node.setClassName(className);
+        node.setMethodName(METHOD_NAME);
+        node.setLineNumber(Integer.toString(line));
+        node.setLineNumberEnd(Integer.toString(line));
+    }
+
+    /** Whether the name is an EIP of the catalog that has an input or output, 
as the XML tree has it. */
+    private boolean isEip(String name) {
+        if (name == null) {
+            return false;
+        }
+        try {
+            EipModel model = catalog.eipModel(name);
+            return model != null && (model.isInput() || model.isOutput());
+        } catch (RuntimeException e) {
+            return false;
+        }
+    }
+
+    // ---- validate ----
+
+    /**
+     * Adds the endpoints of the routes: the inputs and a poll or pollEnrich 
(consumers), and a to, toD, wireTap or
+     * enrich (producers).
+     *
+     * @param unparsable the uris the parser could not work out; may be null
+     */
+    void endpoints(List<CamelEndpointDetails> endpoints, List<String> 
unparsable) {
+        for (Object o : modelObjects()) {
+            String uri = null;
+            boolean consumer = false;
+            if (o instanceof FromDefinition from) {
+                uri = from.getUri();
+                consumer = true;
+            } else if (o instanceof InterceptFromDefinition || o instanceof 
InterceptSendToEndpointDefinition) {
+                // their uri is a pattern of the endpoints they intercept: 
validated when it names one endpoint, and
+                // not a consumer nor a producer of it
+                String pattern = o instanceof InterceptFromDefinition f
+                        ? f.getUri()
+                        : ((InterceptSendToEndpointDefinition) o).getUri();
+                String known = pattern != null && !pattern.contains("*") ? 
known(pattern) : null;
+                if (known != null) {
+                    endpoints.add(endpoint(known, line(o, 0)));
+                }
+                continue;
+            } else if (o instanceof ToDynamicDefinition d) {
+                // toD and wireTap
+                uri = d.getUri();
+            } else if (o instanceof SendDefinition<?> s) {
+                uri = s.getUri();
+            } else if (o instanceof PollDefinition poll) {
+                uri = poll.getUri();
+                consumer = true;
+            } else if ((o instanceof EnrichDefinition || o instanceof 
PollEnrichDefinition)
+                    && ((ExpressionNode) o).getExpression() instanceof 
ConstantExpression c) {
+                uri = c.getExpression();
+                consumer = o instanceof PollEnrichDefinition;
+            }
+            if (uri == null) {
+                continue;
+            }
+            String known = known(uri);
+            if (known == null) {
+                if (unparsable != null) {
+                    unparsable.add(uri);
+                }
+                continue;
+            }
+            CamelEndpointDetails detail = endpoint(known, line(o, 0));
+            detail.setConsumerOnly(consumer);
+            detail.setProducerOnly(!consumer);
+            endpoints.add(detail);
+        }
+        // the endpoints injected into fields: @EndpointInject("mock:result")
+        Matcher m = ENDPOINT_INJECT.matcher(content);
+        while (m.find()) {
+            endpoints.add(endpoint(m.group(1), lineOf(m.start())));
+        }
+    }
+
+    private CamelEndpointDetails endpoint(String uri, int line) {
+        CamelEndpointDetails detail = new CamelEndpointDetails();
+        detail.setFileName(fileName);
+        detail.setClassName(className);
+        detail.setMethodName(METHOD_NAME);
+        detail.setLineNumber(Integer.toString(line));
+        detail.setLineNumberEnd(Integer.toString(line));
+        detail.setEndpointUri(uri);
+        detail.setEndpointComponentName(uri.contains(":") ? uri.substring(0, 
uri.indexOf(':')) : null);
+        return detail;
+    }
+
+    /**
+     * The uri with each part the parser could not work out (a value only 
known at runtime, such as a port in a field)
+     * as a property placeholder, so its component and options can still be 
validated, as camel-route-parser did:
+     * {@code "http://localhost:"; + port + "/x"} is 
http://localhost:{{port}}/x. Null when the component itself is not
+     * known.
+     */
+    static String withPlaceholders(String uri) {
+        String answer = uri;
+        if (uri.startsWith(LwJavaParser.UNRESOLVED_PREFIX) && 
uri.endsWith("}")) {
+            // the whole uri is a value the parser could not work out: build 
it from its string literals
+            answer = 
concatenation(uri.substring(LwJavaParser.UNRESOLVED_PREFIX.length(), 
uri.length() - 1));
+            if (answer == null) {
+                return null;
+            }
+        } else if (uri.contains(LwJavaParser.UNRESOLVED_PREFIX)) {
+            Matcher m = UNRESOLVED.matcher(uri);
+            StringBuilder sb = new StringBuilder();
+            while (m.find()) {
+                m.appendReplacement(sb, 
Matcher.quoteReplacement(placeholder(m.group(1))));
+            }
+            m.appendTail(sb);
+            answer = sb.toString();
+        }
+        int colon = answer.indexOf(':');
+        if (colon <= 0 || answer.contains(LwJavaParser.UNRESOLVED_PREFIX) || 
answer.substring(0, colon).contains("{{")) {
+            return null;
+        }
+        return answer;
+    }
+
+    /**
+     * The value of a string concatenation in Java source, with each term that 
is not a string literal as a placeholder;
+     * null when it has no string literal or is not a concatenation the method 
can read.
+     */
+    private static String concatenation(String expression) {
+        StringBuilder answer = new StringBuilder();
+        StringBuilder term = new StringBuilder();
+        boolean literal = false;
+        int depth = 0;
+        for (int i = 0; i < expression.length(); i++) {
+            char c = expression.charAt(i);
+            if (c == '"' && depth == 0 && term.toString().isBlank()) {
+                // a string literal: up to the closing quote, with its escapes
+                int end = i + 1;
+                StringBuilder text = new StringBuilder();
+                while (end < expression.length() && expression.charAt(end) != 
'"') {
+                    char e = expression.charAt(end);
+                    if (e == '\\' && end + 1 < expression.length()) {
+                        end++;
+                        e = expression.charAt(end);
+                    }
+                    text.append(e);
+                    end++;
+                }
+                if (end >= expression.length()) {
+                    return null;
+                }
+                answer.append(text);
+                literal = true;
+                term.setLength(0);
+                term.append(' ');
+                i = end;
+            } else if (c == '+' && depth == 0) {
+                if (!term.toString().isBlank()) {
+                    answer.append(placeholder(term.toString()));
+                }
+                term.setLength(0);
+            } else {
+                if (c == '(') {
+                    depth++;
+                } else if (c == ')') {
+                    depth--;
+                }
+                if (!Character.isWhitespace(c) || !term.isEmpty()) {
+                    term.append(c);
+                }
+            }
+        }
+        if (!term.toString().isBlank()) {
+            answer.append(placeholder(term.toString()));
+        }
+        return literal ? answer.toString() : null;
+    }
+
+    private static String placeholder(String term) {
+        String name = term.strip().replaceAll("[^\\w.]", "");
+        return "{{" + (name.isEmpty() ? "value" : name) + "}}";
+    }
+
+    /**
+     * The uri as {@link #withPlaceholders(String)}; for a uri that is only 
the name of a field of the class, the
+     * endpoint injected into it or its string initializer, as 
camel-route-parser read them: {@code to(result)} with
+     * {@code @EndpointInject("mock:result") MockEndpoint result}, {@code 
from(ftp)} with
+     * {@code private String ftp = "ftp:localhost/inbox"}.
+     */
+    private String known(String uri) {
+        if (uri.startsWith(LwJavaParser.UNRESOLVED_PREFIX) && 
uri.endsWith("}")) {
+            String name = 
uri.substring(LwJavaParser.UNRESOLVED_PREFIX.length(), uri.length() - 
1).strip();
+            if (FIELD_NAME.matcher(name).matches()) {
+                // an endpoint injected into the field: 
@EndpointInject("mock:result") MockEndpoint result
+                Matcher m = 
Pattern.compile("@EndpointInject\\s*\\(\\s*(?:(?:value|uri)\\s*=\\s*)?\"([^\"]+)\"\\s*\\)[^;]*?\\b"
+                                            + Pattern.quote(name) + "\\s*;")
+                        .matcher(content);
+                if (m.find()) {
+                    return withPlaceholders(m.group(1));
+                }
+                m = Pattern.compile("\\bString\\s+" + Pattern.quote(name) + 
"\\s*=\\s*([^;]+);").matcher(content);
+                String value = m.find() ? concatenation(m.group(1).strip()) : 
null;
+                return value != null ? withPlaceholders(value) : null;
+            }
+        }
+        return withPlaceholders(uri);
+    }
+
+    /** The 1-based line of a position in the source. */
+    private int lineOf(int position) {
+        int line = 1;
+        for (int i = 0; i < position && i < content.length(); i++) {
+            if (content.charAt(i) == '\n') {
+                line++;
+            }
+        }
+        return line;
+    }
+
+    /** Adds the simple expressions and predicates of the routes. */
+    void simpleExpressions(List<CamelSimpleExpressionDetails> expressions) {
+        for (Object o : modelObjects()) {
+            // a step, or a when of a choice (which is no processor of its own)
+            if (!(o instanceof NamedNode p)) {
+                continue;
+            }
+            forEachExpression(p, (option, e) -> {
+                String text = e.getExpression();
+                if (!"simple".equals(e.getLanguage()) || text == null || 
text.contains(LwJavaParser.UNRESOLVED_PREFIX)) {
+                    return;
+                }
+                CamelSimpleExpressionDetails detail = new 
CamelSimpleExpressionDetails();
+                detail.setFileName(fileName);
+                detail.setClassName(className);
+                detail.setMethodName(METHOD_NAME);
+                String line = Integer.toString(line(e, line(p, 0)));
+                detail.setLineNumber(line);
+                detail.setLineNumberEnd(line);
+                detail.setSimple(text);
+                boolean predicate = isPredicate(p, option);
+                detail.setPredicate(predicate);
+                detail.setExpression(!predicate);
+                expressions.add(detail);
+            });
+        }
+    }
+
+    /** Adds the ids of the routes that have one. */
+    void routeIds(List<CamelRouteDetails> routes) {
+        for (RouteDefinition route : result.routes().getRoutes()) {
+            String id = route.getRouteId();
+            if (id == null || id.contains(LwJavaParser.UNRESOLVED_PREFIX)) {
+                continue;
+            }
+            CamelRouteDetails detail = new CamelRouteDetails();
+            detail.setFileName(fileName);
+            detail.setClassName(className);
+            detail.setMethodName(METHOD_NAME);
+            String line = Integer.toString(line(route.getInput(), line(route, 
0)));
+            detail.setLineNumber(line);
+            detail.setLineNumberEnd(line);
+            detail.setRouteId(id);
+            routes.add(detail);
+        }
+    }
+
+    private boolean isPredicate(NamedNode p, String option) {
+        if (p instanceof LoopDefinition loop) {
+            return "true".equals(loop.getDoWhile());
+        }
+        try {
+            EipModel model = catalog.eipModel(p.getShortName());
+            return model != null && model.getOptions().stream()
+                    .anyMatch(o -> option.equals(o.getName()) && 
"expression".equals(o.getKind()) && o.isAsPredicate());
+        } catch (RuntimeException e) {
+            return false;
+        }
+    }
+
+    // ---- the model ----
+
+    /**
+     * The objects of the Camel model the source has: the routes and their 
steps, and the onException, onCompletion and
+     * intercepts of the route builder, at any depth.
+     */
+    private List<Object> modelObjects() {
+        List<Object> answer = new ArrayList<>();
+        Set<Object> seen = Collections.newSetFromMap(new IdentityHashMap<>());
+        for (RouteDefinition route : result.routes().getRoutes()) {
+            collect(route.getInput(), answer, seen, 0);
+            route.getOutputs().forEach(p -> collect(p, answer, seen, 0));
+        }
+        result.routes().getOnExceptions().forEach(p -> collect(p, answer, 
seen, 0));
+        result.routes().getOnCompletions().forEach(p -> collect(p, answer, 
seen, 0));
+        result.routes().getIntercepts().forEach(p -> collect(p, answer, seen, 
0));
+        result.routes().getInterceptFroms().forEach(p -> collect(p, answer, 
seen, 0));
+        result.routes().getInterceptSendTos().forEach(p -> collect(p, answer, 
seen, 0));
+        return answer;
+    }
+
+    private static void collect(Object o, List<Object> answer, Set<Object> 
seen, int depth) {
+        if (o == null || depth > MAX_DEPTH || !seen.add(o)) {
+            return;
+        }
+        answer.add(o);
+        if (o instanceof ChoiceDefinition choice) {
+            // the outputs of a choice are the steps of its when and 
otherwise, not the when and otherwise
+            choice.getWhenClauses().forEach(w -> collect(w, answer, seen, 
depth + 1));
+            collect(choice.getOtherwise(), answer, seen, depth + 1);
+        }
+        if (o instanceof ProcessorDefinition<?> p) {
+            p.getOutputs().forEach(c -> collect(c, answer, seen, depth + 1));
+        }
+    }
+
+    /** Calls the action with each expression of the step and the name of its 
option. */
+    private static void forEachExpression(Object p, ExpressionAction action) {
+        for (Class<?> c = p.getClass(); c != null && c != Object.class; c = 
c.getSuperclass()) {
+            for (Field f : c.getDeclaredFields()) {
+                if (Modifier.isStatic(f.getModifiers()) || 
f.getType().isPrimitive() || f.getType() == String.class) {
+                    continue;
+                }
+                Object v;
+                try {
+                    f.setAccessible(true);
+                    v = f.get(p);
+                } catch (ReflectiveOperationException | RuntimeException e) {
+                    continue;
+                }
+                if (v instanceof ExpressionSubElementDefinition sub) {
+                    v = sub.getExpressionType();
+                }
+                if (v instanceof ExpressionDefinition e) {
+                    action.accept(f.getName(), e);
+                } else if (v instanceof Collection<?> list) {
+                    for (Object item : list) {
+                        if (item instanceof ExpressionDefinition e) {
+                            action.accept(f.getName(), e);
+                        }
+                    }
+                } else if (v instanceof Map<?, ?> map) {
+                    for (Object item : map.values()) {
+                        if (item instanceof ExpressionDefinition e) {
+                            action.accept(f.getName(), e);
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+    @FunctionalInterface
+    private interface ExpressionAction {
+        void accept(String option, ExpressionDefinition expression);
+    }
+
+    private static int line(Object node, int fallback) {
+        if (node instanceof LineNumberAware aware && aware.getLineNumber() > 
0) {
+            return aware.getLineNumber();
+        }
+        return fallback;
+    }
+
+    private static String className(String fileName, String content) {
+        String name = fileName.replace('\\', '/');
+        name = name.substring(name.lastIndexOf('/') + 1);
+        if (name.endsWith(".java")) {
+            name = name.substring(0, name.length() - ".java".length());
+        }
+        Matcher m = PACKAGE.matcher(content);
+        return m.find() ? m.group(1) + "." + name : name;
+    }
+}
diff --git 
a/catalog/camel-report-maven-plugin/src/main/java/org/apache/camel/maven/RouteCoverageMojo.java
 
b/catalog/camel-report-maven-plugin/src/main/java/org/apache/camel/maven/RouteCoverageMojo.java
index b35fc2fe0b02..0c385a303a5f 100644
--- 
a/catalog/camel-report-maven-plugin/src/main/java/org/apache/camel/maven/RouteCoverageMojo.java
+++ 
b/catalog/camel-report-maven-plugin/src/main/java/org/apache/camel/maven/RouteCoverageMojo.java
@@ -21,6 +21,8 @@ import java.io.File;
 import java.io.FileInputStream;
 import java.io.InputStream;
 import java.io.PrintStream;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
 import java.util.ArrayList;
 import java.util.Iterator;
 import java.util.LinkedHashMap;
@@ -47,9 +49,10 @@ import org.w3c.dom.Attr;
 import org.w3c.dom.Document;
 import org.w3c.dom.Element;
 
+import org.apache.camel.catalog.CamelCatalog;
+import org.apache.camel.catalog.DefaultCamelCatalog;
 import org.apache.camel.maven.htmlxlsx.process.CoverageResultsProcessor;
 import org.apache.camel.maven.model.RouteCoverageNode;
-import org.apache.camel.parser.RouteBuilderParser;
 import org.apache.camel.parser.XmlRouteParser;
 import org.apache.camel.parser.helper.RouteCoverageHelper;
 import org.apache.camel.parser.model.CamelNodeDetails;
@@ -60,9 +63,6 @@ import org.apache.maven.plugin.MojoExecutionException;
 import org.apache.maven.plugins.annotations.Mojo;
 import org.apache.maven.plugins.annotations.Parameter;
 import org.apache.maven.project.MavenProject;
-import org.jboss.forge.roaster.Roaster;
-import org.jboss.forge.roaster.model.JavaType;
-import org.jboss.forge.roaster.model.source.JavaClassSource;
 
 import static org.apache.camel.catalog.common.CatalogHelper.asRelativeFile;
 import static 
org.apache.camel.catalog.common.CatalogHelper.findJavaRouteBuilderClasses;
@@ -115,6 +115,8 @@ public class RouteCoverageMojo extends AbstractMojo {
     /**
      * Whether to include test source code
      */
+    private CamelCatalog catalog;
+
     @Parameter(property = "camel.includeTest", defaultValue = "false")
     private boolean includeTest;
 
@@ -469,17 +471,21 @@ public class RouteCoverageMojo extends AbstractMojo {
         }
     }
 
+    private CamelCatalog catalog() {
+        if (catalog == null) {
+            catalog = new DefaultCamelCatalog();
+        }
+        return catalog;
+    }
+
     private void addJavaFiles(File file, List<CamelNodeDetails> routeTrees) {
         if (matchFile(file)) {
             try {
-                // parse the java source code and find Camel RouteBuilder 
classes
-                String fqn = file.getPath();
-                JavaType<?> out = Roaster.parse(file);
-                // we should only parse java classes (not interfaces and enums 
etc)
-                if (out instanceof JavaClassSource clazz) {
-                    List<CamelNodeDetails> result = 
RouteBuilderParser.parseRouteBuilderTree(clazz, fqn, true);
-                    routeTrees.addAll(result);
-                }
+                // read the java source into the Camel model with the Java DSL 
parser, without compiling it, and build
+                // the node tree of its routes (CAMEL-25591)
+                JavaRouteModelReader reader = JavaRouteModelReader.read(
+                        catalog(), file.getPath(), 
Files.readString(file.toPath(), StandardCharsets.UTF_8));
+                routeTrees.addAll(reader.routeTrees());
             } catch (Exception e) {
                 getLog().warn("Error parsing java file " + file + " code due " 
+ e.getMessage(), e);
             }
diff --git 
a/catalog/camel-report-maven-plugin/src/main/java/org/apache/camel/maven/ValidateMojo.java
 
b/catalog/camel-report-maven-plugin/src/main/java/org/apache/camel/maven/ValidateMojo.java
index c95e423cc40f..4c8bed060315 100644
--- 
a/catalog/camel-report-maven-plugin/src/main/java/org/apache/camel/maven/ValidateMojo.java
+++ 
b/catalog/camel-report-maven-plugin/src/main/java/org/apache/camel/maven/ValidateMojo.java
@@ -17,6 +17,7 @@
 package org.apache.camel.maven;
 
 import java.io.*;
+import java.nio.charset.StandardCharsets;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.Paths;
@@ -34,7 +35,6 @@ import org.apache.camel.catalog.EndpointValidationResult;
 import org.apache.camel.catalog.LanguageValidationResult;
 import org.apache.camel.catalog.common.FileUtil;
 import org.apache.camel.catalog.maven.MavenVersionManager;
-import org.apache.camel.parser.RouteBuilderParser;
 import org.apache.camel.parser.XmlRouteParser;
 import org.apache.camel.parser.model.CamelEndpointDetails;
 import org.apache.camel.parser.model.CamelRouteDetails;
@@ -55,9 +55,6 @@ import org.apache.maven.plugins.annotations.Parameter;
 import org.apache.maven.project.MavenProject;
 import org.eclipse.aether.RepositorySystem;
 import org.eclipse.aether.RepositorySystemSession;
-import org.jboss.forge.roaster.Roaster;
-import org.jboss.forge.roaster.model.JavaType;
-import org.jboss.forge.roaster.model.source.JavaClassSource;
 
 import static org.apache.camel.catalog.common.CatalogHelper.asRelativeFile;
 import static 
org.apache.camel.catalog.common.CatalogHelper.findJavaRouteBuilderClasses;
@@ -560,7 +557,7 @@ public class ValidateMojo extends AbstractMojo {
 
         for (File file : javaFiles) {
             if (matchFile(file)) {
-                parseJavaRouteFile(endpoints, simpleExpressions, routeIds, 
file);
+                parseJavaRouteFile(catalog, endpoints, simpleExpressions, 
routeIds, file);
             }
         }
         for (File file : xmlFiles) {
@@ -835,7 +832,7 @@ public class ValidateMojo extends AbstractMojo {
     }
 
     private void parseJavaRouteFile(
-            List<CamelEndpointDetails> endpoints, 
List<CamelSimpleExpressionDetails> simpleExpressions,
+            CamelCatalog catalog, List<CamelEndpointDetails> endpoints, 
List<CamelSimpleExpressionDetails> simpleExpressions,
             List<CamelRouteDetails> routeIds, File file) {
         try {
             List<CamelEndpointDetails> fileEndpoints = new ArrayList<>();
@@ -843,28 +840,24 @@ public class ValidateMojo extends AbstractMojo {
             List<CamelSimpleExpressionDetails> fileSimpleExpressions = new 
ArrayList<>();
             List<String> unparsable = new ArrayList<>();
 
-            // parse the java source code and find Camel RouteBuilder classes
-            String fqn = file.getPath();
-            String baseDir = ".";
-            JavaType<?> out = Roaster.parse(file);
-            // we should only parse java classes (not interfaces and enums 
etc.)
-            if (out instanceof JavaClassSource clazz) {
-                RouteBuilderParser.parseRouteBuilderEndpoints(clazz, baseDir, 
fqn, fileEndpoints, unparsable, includeTest);
-                RouteBuilderParser.parseRouteBuilderSimpleExpressions(clazz, 
baseDir, fqn, fileSimpleExpressions);
-                if (duplicateRouteId) {
-                    RouteBuilderParser.parseRouteBuilderRouteIds(clazz, 
baseDir, fqn, fileRouteIds);
-                }
+            // read the java source into the Camel model with the Java DSL 
parser, without compiling it (CAMEL-25591)
+            JavaRouteModelReader reader = JavaRouteModelReader.read(
+                    catalog, file.getPath(), Files.readString(file.toPath(), 
StandardCharsets.UTF_8));
+            reader.endpoints(fileEndpoints, unparsable);
+            reader.simpleExpressions(fileSimpleExpressions);
+            if (duplicateRouteId) {
+                reader.routeIds(fileRouteIds);
+            }
 
-                // add what we found in this file to the total list
-                endpoints.addAll(fileEndpoints);
-                simpleExpressions.addAll(fileSimpleExpressions);
-                routeIds.addAll(fileRouteIds);
+            // add what we found in this file to the total list
+            endpoints.addAll(fileEndpoints);
+            simpleExpressions.addAll(fileSimpleExpressions);
+            routeIds.addAll(fileRouteIds);
 
-                // was there any unparsable?
-                if (logUnparseable && !unparsable.isEmpty()) {
-                    for (String uri : unparsable) {
-                        getLog().warn("Cannot parse endpoint uri " + uri + " 
in java file " + file);
-                    }
+            // was there any unparsable?
+            if (logUnparseable && !unparsable.isEmpty()) {
+                for (String uri : unparsable) {
+                    getLog().warn("Cannot parse endpoint uri " + uri + " in 
java file " + file);
                 }
             }
         } catch (Exception e) {
diff --git 
a/catalog/camel-report-maven-plugin/src/test/java/org/apache/camel/maven/JavaRouteModelReaderTest.java
 
b/catalog/camel-report-maven-plugin/src/test/java/org/apache/camel/maven/JavaRouteModelReaderTest.java
new file mode 100644
index 000000000000..38706c59ac2e
--- /dev/null
+++ 
b/catalog/camel-report-maven-plugin/src/test/java/org/apache/camel/maven/JavaRouteModelReaderTest.java
@@ -0,0 +1,243 @@
+/*
+ * 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.maven;
+
+import java.io.ByteArrayInputStream;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.apache.camel.catalog.CamelCatalog;
+import org.apache.camel.catalog.DefaultCamelCatalog;
+import org.apache.camel.parser.XmlRouteParser;
+import org.apache.camel.parser.model.CamelEndpointDetails;
+import org.apache.camel.parser.model.CamelNodeDetails;
+import org.apache.camel.parser.model.CamelRouteDetails;
+import org.apache.camel.parser.model.CamelSimpleExpressionDetails;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * CAMEL-25591: the report goals read Java routes with the Java DSL parser, 
into the same details camel-route-parser
+ * gives.
+ */
+class JavaRouteModelReaderTest {
+
+    private static final CamelCatalog CATALOG = new DefaultCamelCatalog();
+
+    private static final String ORDERS = """
+            package com.foo.orders;
+
+            import org.apache.camel.builder.RouteBuilder;
+
+            public class OrderRoute extends RouteBuilder {
+                private static final String AUDIT = "seda:audit";
+
+                @Override
+                public void configure() {
+                    from("direct:orders").routeId("orders")
+                        .filter(simple("${header.amount} > 100"))
+                            .to(AUDIT)
+                        .end()
+                        .choice()
+                            .when(simple("${header.vip}"))
+                                .setBody(simple("vip ${body}"))
+                                .to("seda:vip")
+                            .otherwise()
+                                .log("normal")
+                        .end()
+                        .toD("direct:${header.next}")
+                        .to(helper());
+
+                    from("timer:tick")
+                        .pollEnrich("seda:audit")
+                        .to("log:tick");
+                }
+            }
+            """;
+
+    /** The same routes in the XML DSL, the tree camel-route-parser builds for 
them. */
+    private static final String ORDERS_XML = """
+            <routes xmlns="http://camel.apache.org/schema/spring";>
+              <route id="orders">
+                <from uri="direct:orders"/>
+                <filter>
+                  <simple>${header.amount} &gt; 100</simple>
+                  <to uri="seda:audit"/>
+                </filter>
+                <choice>
+                  <when>
+                    <simple>${header.vip}</simple>
+                    <setBody><simple>vip ${body}</simple></setBody>
+                    <to uri="seda:vip"/>
+                  </when>
+                  <otherwise>
+                    <log message="normal"/>
+                  </otherwise>
+                </choice>
+                <toD uri="direct:${header.next}"/>
+                <to uri="direct:helper"/>
+              </route>
+              <route>
+                <from uri="timer:tick"/>
+                <pollEnrich><constant>seda:audit</constant></pollEnrich>
+                <to uri="log:tick"/>
+              </route>
+            </routes>
+            """;
+
+    private static JavaRouteModelReader reader() {
+        return JavaRouteModelReader.read(CATALOG, 
"src/main/java/com/foo/orders/OrderRoute.java", ORDERS);
+    }
+
+    @Test
+    void routeTreesAreTheTreesOfTheXmlDsl() {
+        List<CamelNodeDetails> java = reader().routeTrees();
+        List<CamelNodeDetails> xml = XmlRouteParser.parseXmlRouteTree(
+                new 
ByteArrayInputStream(ORDERS_XML.getBytes(StandardCharsets.UTF_8)), ".", 
"orders.xml");
+
+        assertEquals(2, java.size());
+        assertEquals(withoutLines(xml.get(0).dump(0)), 
withoutLines(java.get(0).dump(0)));
+        assertEquals(withoutLines(xml.get(1).dump(0)), 
withoutLines(java.get(1).dump(0)));
+
+        CamelNodeDetails from = java.get(0);
+        assertEquals("orders", from.getRouteId());
+        assertEquals("10", from.getLineNumber());
+        assertEquals("com.foo.orders.OrderRoute", from.getClassName());
+        assertEquals("configure", from.getMethodName());
+        // the filter is the first step, on its own line
+        assertEquals("11", from.getOutputs().get(0).getLineNumber());
+        assertNull(java.get(1).getRouteId());
+    }
+
+    @Test
+    void endpointsWithConsumersAndProducers() {
+        List<CamelEndpointDetails> endpoints = new ArrayList<>();
+        List<String> unparsable = new ArrayList<>();
+        reader().endpoints(endpoints, unparsable);
+
+        List<String> uris = 
endpoints.stream().map(CamelEndpointDetails::getEndpointUri).toList();
+        assertTrue(uris.containsAll(List.of(
+                "direct:orders", "seda:audit", "seda:vip", 
"direct:${header.next}", "timer:tick", "log:tick")), uris::toString);
+        CamelEndpointDetails orders = endpoints.stream().filter(e -> 
"direct:orders".equals(e.getEndpointUri()))
+                .findFirst().orElseThrow();
+        assertTrue(orders.isConsumerOnly());
+        assertFalse(orders.isProducerOnly());
+        assertEquals("direct", orders.getEndpointComponentName());
+        assertEquals("10", orders.getLineNumber());
+        // the pollEnrich consumes from seda:audit, the to in the filter 
produces to it (the constant is resolved)
+        assertTrue(endpoints.stream().anyMatch(e -> 
"seda:audit".equals(e.getEndpointUri()) && e.isConsumerOnly()));
+        assertTrue(endpoints.stream().anyMatch(e -> 
"seda:audit".equals(e.getEndpointUri()) && e.isProducerOnly()));
+        // to(helper()) is only known at runtime
+        assertEquals(1, unparsable.size(), unparsable::toString);
+    }
+
+    @Test
+    void simpleExpressionsAndPredicates() {
+        List<CamelSimpleExpressionDetails> expressions = new ArrayList<>();
+        reader().simpleExpressions(expressions);
+
+        CamelSimpleExpressionDetails filter = find(expressions, 
"${header.amount} > 100");
+        assertTrue(filter.isPredicate());
+        assertEquals("11", filter.getLineNumber());
+        assertTrue(find(expressions, "${header.vip}").isPredicate());
+        CamelSimpleExpressionDetails body = find(expressions, "vip ${body}");
+        assertFalse(body.isPredicate());
+        assertTrue(body.isExpression());
+    }
+
+    @Test
+    void routeIds() {
+        List<CamelRouteDetails> routes = new ArrayList<>();
+        reader().routeIds(routes);
+
+        assertEquals(1, routes.size());
+        assertEquals("orders", routes.get(0).getRouteId());
+        assertEquals("10", routes.get(0).getLineNumber());
+        assertEquals("com.foo.orders.OrderRoute", 
routes.get(0).getClassName());
+    }
+
+    @Test
+    void aValueOnlyKnownAtRuntimeIsAPlaceholder() {
+        JavaRouteModelReader reader = JavaRouteModelReader.read(CATALOG, 
"HttpRoute.java", """
+                import org.apache.camel.EndpointInject;
+                import org.apache.camel.builder.RouteBuilder;
+
+                public class HttpRoute extends RouteBuilder {
+                    protected int port;
+
+                    @EndpointInject("mock:result")
+                    protected Object result;
+
+                    private String ftp = "ftp:localhost:21/inbox?delay=" + 
5000 + "&passiveMode=true";
+
+                    @Override
+                    public void configure() {
+                        from("jetty:http://localhost:"; + port + "/sender")
+                            .to("http://localhost:"; + port + 
"/other?bridgeEndpoint=true")
+                            .to(result);
+                        from(ftp).to("log:ftp");
+                    }
+                }
+                """);
+        List<CamelEndpointDetails> endpoints = new ArrayList<>();
+        List<String> unparsable = new ArrayList<>();
+        reader.endpoints(endpoints, unparsable);
+
+        List<String> uris = 
endpoints.stream().map(CamelEndpointDetails::getEndpointUri).toList();
+        // as camel-route-parser did: the component and options of the uri are 
still validated
+        assertTrue(uris.contains("jetty:http://localhost:{{port}}/sender";), 
uris::toString);
+        
assertTrue(uris.contains("http://localhost:{{port}}/other?bridgeEndpoint=true";),
 uris::toString);
+        // the to of the injected endpoint in the route, and its field
+        assertEquals(List.of("16", "7"), endpoints.stream().filter(e -> 
"mock:result".equals(e.getEndpointUri()))
+                .map(CamelEndpointDetails::getLineNumber).toList());
+        // a field with a string initializer
+        
assertTrue(uris.contains("ftp:localhost:21/inbox?delay={{5000}}&passiveMode=true"),
 uris::toString);
+        assertTrue(unparsable.isEmpty(), unparsable::toString);
+
+        assertNull(JavaRouteModelReader.withPlaceholders("?{scheme()}:foo"));
+    }
+
+    @Test
+    void aClassWithoutRoutes() {
+        JavaRouteModelReader reader = JavaRouteModelReader.read(CATALOG, 
"Util.java", """
+                public class Util {
+                    public static String name() {
+                        return "util";
+                    }
+                }
+                """);
+        assertFalse(reader.hasRoutes());
+        assertTrue(reader.routeTrees().isEmpty());
+        assertEquals("Util", reader.className());
+    }
+
+    private static CamelSimpleExpressionDetails 
find(List<CamelSimpleExpressionDetails> list, String simple) {
+        return list.stream().filter(e -> 
simple.equals(e.getSimple())).findFirst()
+                .orElseThrow(() -> new AssertionError(
+                        "no " + simple + " in " + list.stream()
+                                
.map(CamelSimpleExpressionDetails::getSimple).toList()));
+    }
+
+    private static String withoutLines(String dump) {
+        return dump.replaceAll("(?m)^[^\\t]*\\t", "");
+    }
+}
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 5a3c1009e3ab..b767adc91b38 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
@@ -27,3 +27,11 @@ A `hashicorp:` property with a field of a secret that does 
not exist, such as `{
 now resolves to no value, as it already did without a field, so its default 
value or an optional placeholder
 (`{{?hashicorp:secret:missing#field}}`) applies. Before, without a default 
value, it failed with a
 `NullPointerException`.
+
+=== camel-report-maven-plugin - Java routes are read with the Java DSL parser
+
+The `validate` and `route-coverage` goals read Java routes with the Java DSL 
parser of camel-java-io instead of
+camel-route-parser (Roaster). XML routes are read as before. The parser reads 
every `configure()` method of a source
+file, including those of a route builder created inside a test, so `validate` 
can find and validate endpoints it did
+not see before. An endpoint uri that is only a method call, such as 
`from(inboxQueue())`, is not validated (it was
+validated as a property placeholder without a component before); with 
`logUnparseable=true` it is logged.
diff --git a/docs/user-manual/modules/ROOT/pages/camel-report-maven-plugin.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-report-maven-plugin.adoc
index 4db61c3f05d7..488086d31933 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-report-maven-plugin.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-report-maven-plugin.adoc
@@ -15,6 +15,11 @@ For validating the source code for mis-configured Camel:
 - seda/direct endpoint pairs
 - configuration in properties files
 
+Java routes are read with the Java DSL parser of camel-java-io, and XML routes 
with camel-route-parser, without
+compiling or running them. In a Java route, a value only known at runtime, 
such as a port held in a field, is
+validated as a property placeholder (`+http://localhost:{{port}}/orders+`); an 
endpoint injected into a field with
+`@EndpointInject`, and a field with a string initializer, are read as well.
+
 Then you can run the `validate` goal from the command line or from within your 
Java editor such as IDEA or Eclipse.
 
 [source,bash]

Reply via email to