gnodet-bot commented on code in PR #27122:
URL: https://github.com/apache/camel/pull/27122#discussion_r4143043525


##########
core/camel-java-io/src/main/java/org/apache/camel/java/in/ChainReplayer.java:
##########
@@ -0,0 +1,1226 @@
+/*
+ * 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.java.in;
+
+import java.lang.reflect.Array;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Executable;
+import java.lang.reflect.Field;
+import java.lang.reflect.GenericArrayType;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.lang.reflect.ParameterizedType;
+import java.lang.reflect.Proxy;
+import java.lang.reflect.Type;
+import java.lang.reflect.WildcardType;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.IllegalFormatException;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.SortedSet;
+import java.util.TreeSet;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.LineNumberAware;
+import org.apache.camel.builder.AggregationStrategies;
+import org.apache.camel.builder.Builder;
+import org.apache.camel.builder.PredicateBuilder;
+import org.apache.camel.builder.RouteConfigurationBuilder;
+import org.apache.camel.builder.ValueBuilder;
+import org.apache.camel.java.in.JavaChainParser.BinOp;
+import org.apache.camel.java.in.JavaChainParser.Bool;
+import org.apache.camel.java.in.JavaChainParser.Call;
+import org.apache.camel.java.in.JavaChainParser.Chain;
+import org.apache.camel.java.in.JavaChainParser.ClassLit;
+import org.apache.camel.java.in.JavaChainParser.ClassName;
+import org.apache.camel.java.in.JavaChainParser.Concat;
+import org.apache.camel.java.in.JavaChainParser.Lambda;
+import org.apache.camel.java.in.JavaChainParser.New;
+import org.apache.camel.java.in.JavaChainParser.Node;
+import org.apache.camel.java.in.JavaChainParser.Null;
+import org.apache.camel.java.in.JavaChainParser.Num;
+import org.apache.camel.java.in.JavaChainParser.Opaque;
+import org.apache.camel.java.in.JavaChainParser.Ref;
+import org.apache.camel.java.in.JavaChainParser.Str;
+import org.apache.camel.model.RouteConfigurationsDefinition;
+import org.apache.camel.model.RouteTemplatesDefinition;
+import org.apache.camel.model.RoutesDefinition;
+import org.apache.camel.model.language.XPathExpression;
+import org.apache.camel.model.rest.RestsDefinition;
+import org.apache.camel.support.builder.Namespaces;
+
+/**
+ * Builds the model by replaying the chains of a Java source against Camel's 
own DSL: {@code from("a").to("b")} calls
+ * {@code from("a")} on a route builder and {@code to("b")} on the route it 
returns. So {@code end()}, blocks,
+ * expression clauses and ids behave exactly as in a compiled route, for every 
EIP.
+ * <p/>
+ * Nothing of the parsed project runs or is loaded. Only methods of Camel's 
model and builder types are called (see
+ * {@link #isAllowed(Method)}); a class of the project becomes an empty stub 
with its name ({@link StubClassLoader}); a
+ * lambda, an anonymous class or {@code new Foo()} becomes a placeholder 
object of the type the DSL asks for; a value
+ * that cannot be worked out becomes a marked string. Each of those is 
reported as unresolved.
+ */
+final class ChainReplayer {
+
+    /** The DSL: a route configuration builder is also a route builder, so it 
covers routes, rests and templates. */
+    static final class ReplayBuilder extends RouteConfigurationBuilder {
+        @Override
+        public void configure() {
+            // the chains are replayed on it from outside
+        }
+
+        @Override
+        public void configuration() {
+            // the chains are replayed on it from outside
+        }
+
+        /** As the DSL builds it, but keeping property placeholders as 
written: there is no context to resolve them. */
+        @Override
+        public ValueBuilder xpath(String value, Class<?> resultType, 
Namespaces namespaces) {
+            XPathExpression exp = new XPathExpression(value);
+            exp.setResultType(resultType);
+            if (namespaces != null) {
+                exp.setNamespaces(namespaces.getNamespaces());
+            }
+            return new ValueBuilder(exp);
+        }
+    }
+
+    private static final Map<String, Class<?>> PRIMITIVES = Map.of(
+            "byte", byte.class, "short", short.class, "int", int.class, 
"long", long.class, "float", float.class,
+            "double", double.class, "boolean", boolean.class, "char", 
char.class);
+
+    private static final Set<String> READABLE_JDK = Set.of(
+            "java.lang.Integer", "java.lang.Long", "java.lang.Short", 
"java.lang.Byte", "java.lang.Double",
+            "java.lang.Float", "java.lang.Boolean", "java.lang.Character", 
"java.util.concurrent.TimeUnit",
+            "java.nio.charset.StandardCharsets");
+
+    /** Classes whose static DSL methods a route may call, qualified or 
statically imported: language(...), and(...). */
+    private static final List<Class<?>> STATIC_DSL
+            = List.of(Builder.class, PredicateBuilder.class, 
AggregationStrategies.class);
+
+    /** Where a Camel type named without an import is looked for, as a 
RouteBuilder usually imports them. */
+    private static final List<String> CAMEL_PACKAGES = List.of(
+            "org.apache.camel.", "org.apache.camel.model.", 
"org.apache.camel.builder.", "org.apache.camel.model.language.",
+            "org.apache.camel.model.dataformat.", 
"org.apache.camel.model.rest.", "org.apache.camel.model.loadbalancer.");
+
+    /** A value the replay could not work out, until the DSL method it goes to 
says what type it must be. */
+    private record Unknown(Node node, String reason) {
+    }
+
+    private static final Set<String> DENIED_METHODS = Set.of(
+            "getContext", "getCamelContext", "setContext", "setCamelContext", 
"addRoutesToCamelContext",
+            "addRouteConfigurationsToCamelContext", 
"addTemplatedRoutesToCamelContext", "configure", "configuration",
+            "includeRoutes", "bindToRegistry", "propertyInject", "endpoint", 
"getClass", "wait", "notify", "notifyAll");
+
+    /** The life cycle of a route builder, which a parse never runs: only the 
DSL it offers is called. */
+    private static final List<String> BUILDER_LIFE_CYCLE = List.of(
+            "populate", "configure", "prepare", "update", "initialize", "set", 
"check", "add", "remove", "customize");
+
+    /** The builder of the configure() being replayed. */
+    private ReplayBuilder builder;
+    /** The parameter naming the builder in a builder lambda (rb -> 
rb.from(...)), null in configure(). */
+    private String builderParameter;
+    private final JavaChainParser.Source source;
+    private final StubClassLoader stubs;
+    private final EndpointDslResolver endpointDsl;
+    private final ConstantResolver constants;
+    private final List<JavaParseResult.Unresolved> unresolved = new 
ArrayList<>();
+    private final Set<String> resolving = new HashSet<>();
+
+    ChainReplayer(JavaChainParser.Source source) {
+        this(source, null, null);
+    }
+
+    /**
+     * @param endpointDsl the resolver of endpoint DSL calls: asked for every 
call that is not the DSL when given; null
+     *                    uses the naming rules, and only for sources that use 
the endpoint DSL
+     */
+    ChainReplayer(JavaChainParser.Source source, EndpointDslResolver 
endpointDsl) {
+        this(source, endpointDsl, null);
+    }
+
+    ChainReplayer(JavaChainParser.Source source, EndpointDslResolver 
endpointDsl, ConstantResolver constants) {
+        this.constants = constants;
+        this.source = source;
+        this.endpointDsl = endpointDsl != null ? endpointDsl : 
source.endpointDsl() ? EndpointDslResolver.NAMING : null;
+        this.stubs = new StubClassLoader(ChainReplayer.class.getClassLoader());
+    }
+
+    JavaParseResult replay() {
+        RoutesDefinition routes = null;
+        RestsDefinition rests = null;
+        RouteTemplatesDefinition templates = null;
+        RouteConfigurationsDefinition configurations = null;
+        // each configure() is a route builder of its own: its global 
onException and errorHandler come before its routes
+        for (int b = 0; b < source.builders().size(); b++) {
+            List<Node> statements = source.builders().get(b);
+            builderParameter = source.builderParameters().get(b);
+            builder = new ReplayBuilder();
+            for (Node statement : statements) {
+                if (statement instanceof Chain chain && 
configuresTheContext(chain)) {
+                    // getContext().getComponent("sql", 
SqlComponent.class).setDataSource(ds): not a route
+                    report(chain, CONFIGURES_THE_CONTEXT);
+                } else if (statement instanceof Chain chain) {
+                    Object value = evaluate(chain);
+                    if (value instanceof Unknown u) {
+                        report(u.node(), u.reason());
+                    }
+                } else {
+                    report(statement, "not a route chain");
+                }
+            }
+            // the model the other DSLs have: class names, and languages 
rather than the Java objects standing for them
+            ModelNormalizer.normalize(builder.getRouteCollection());
+            ModelNormalizer.normalize(builder.getRestCollection());
+            ModelNormalizer.normalize(builder.getRouteTemplateCollection());
+            
ModelNormalizer.normalize(builder.getRouteConfigurationCollection());
+            if (routes == null) {
+                routes = builder.getRouteCollection();
+                rests = builder.getRestCollection();
+                templates = builder.getRouteTemplateCollection();
+                configurations = builder.getRouteConfigurationCollection();
+            } else {
+                RoutesDefinition more = builder.getRouteCollection();
+                routes.getRoutes().addAll(more.getRoutes());
+                routes.getOnExceptions().addAll(more.getOnExceptions());
+                routes.getIntercepts().addAll(more.getIntercepts());
+                routes.getInterceptFroms().addAll(more.getInterceptFroms());
+                
routes.getInterceptSendTos().addAll(more.getInterceptSendTos());
+                routes.getOnCompletions().addAll(more.getOnCompletions());
+                
rests.getRests().addAll(builder.getRestCollection().getRests());
+                
templates.getRouteTemplates().addAll(builder.getRouteTemplateCollection().getRouteTemplates());
+                configurations.getRouteConfigurations()
+                        
.addAll(builder.getRouteConfigurationCollection().getRouteConfigurations());
+            }
+        }
+        if (routes == null) {
+            builder = new ReplayBuilder();
+            routes = builder.getRouteCollection();
+            rests = builder.getRestCollection();
+            templates = builder.getRouteTemplateCollection();
+            configurations = builder.getRouteConfigurationCollection();
+        }
+        return new JavaParseResult(routes, rests, templates, configurations, 
List.copyOf(unresolved));
+    }
+
+    /** The constants of the source that are data (Strings, numbers, 
booleans), by name. */
+    Map<String, Object> constantValues() {
+        Map<String, Object> answer = new LinkedHashMap<>();
+        source.constants().forEach((name, node) -> {
+            Object v = evaluate(node);
+            if (v instanceof String || v instanceof Number || v instanceof 
Boolean) {
+                answer.put(name, v);
+            }
+        });
+        return answer;
+    }
+
+    // ---- values ----
+
+    /** The value of a node, or an {@link Unknown} for what only a DSL 
parameter type can settle. */
+    private Object evaluate(Node node) {
+        if (node instanceof Str s) {
+            return s.value();
+        } else if (node instanceof Num n) {
+            return number(n.text());
+        } else if (node instanceof Bool b) {
+            return b.value();
+        } else if (node instanceof Null) {
+            return null;
+        } else if (node instanceof Concat c) {
+            return concat(c);
+        } else if (node instanceof Ref r) {
+            return reference(r);
+        } else if (node instanceof ClassLit c) {
+            return classLiteral(c);
+        } else if (node instanceof ClassName c) {
+            return className(c);
+        } else if (node instanceof BinOp b) {
+            return arithmetic(b);
+        } else if (node instanceof Chain c) {
+            return chain(c);
+        } else if (node instanceof Lambda) {
+            return new Unknown(node, "a lambda or method reference");
+        } else if (node instanceof New n) {
+            Object created = exception(n);
+            if (created != null) {
+                return created;
+            }
+            return new Unknown(n, n.anonymous() ? "an anonymous class" : "an 
object created in the route");
+        }
+        return new Unknown(node, "an expression the parser does not evaluate");
+    }
+
+    /**
+     * {@code new IllegalArgumentException("Forced")}: an exception of the JDK 
or Camel is created, as creating one runs
+     * no code of the project and has no side effect; null for anything else.
+     */
+    private Object exception(New n) {
+        if (n.anonymous()) {
+            return null;
+        }
+        Class<?> type = loadable(n.type());
+        if (type == null || !Throwable.class.isAssignableFrom(type) || 
Modifier.isAbstract(type.getModifiers())) {
+            return null;
+        }
+        Object[] values = new Object[n.args().size()];
+        for (int i = 0; i < values.length; i++) {
+            values[i] = evaluate(n.args().get(i));
+            if (values[i] instanceof Unknown) {
+                return null;
+            }
+        }
+        for (Constructor<?> c : type.getConstructors()) {
+            if (c.getParameterCount() != values.length || c.isVarArgs() || 
!onlyMessagesAndCauses(c)) {
+                continue;
+            }
+            Object[] args = convertAll(c, values, n.args(), new ArrayList<>(), 
new int[1]);
+            if (args != null) {
+                try {
+                    return c.newInstance(args);
+                } catch (ReflectiveOperationException | RuntimeException e) {
+                    return null;
+                }
+            }
+        }
+        return null;
+    }
+
+    /** A constructor of an exception that takes messages and causes only, as 
they all do: nothing else is created. */
+    private static boolean onlyMessagesAndCauses(Constructor<?> c) {
+        for (Class<?> p : c.getParameterTypes()) {
+            if (p != String.class && !Throwable.class.isAssignableFrom(p)) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    private static Object number(String text) {
+        String t = text.replace("_", "");
+        try {
+            if (t.endsWith("L") || t.endsWith("l")) {
+                return Long.parseLong(t.substring(0, t.length() - 1));
+            }
+            if (t.endsWith("d") || t.endsWith("D") || t.endsWith("f") || 
t.endsWith("F") || t.contains(".")) {
+                return Double.parseDouble(t.replaceAll("[dDfF]$", ""));
+            }
+            long l = Long.decode(t);
+            return l >= Integer.MIN_VALUE && l <= Integer.MAX_VALUE ? (Object) 
(int) l : (Object) l;
+        } catch (NumberFormatException e) {
+            return t;
+        }
+    }
+
+    /** {@code a + b + ...} as Java works it out: numbers are added until a 
String makes the rest text. */
+    private Object concat(Concat c) {
+        Object acc = null;
+        boolean first = true;
+        for (Node part : c.parts()) {
+            Object v = evaluate(part);
+            if (v instanceof Unknown) {
+                return new Unknown(c, "a value built from parts the parser 
cannot work out");
+            }
+            if (first) {
+                acc = v;
+                first = false;
+            } else if (acc instanceof Number a && v instanceof Number b) {
+                acc = add(a, b);
+            } else {
+                acc = String.valueOf(acc) + v;
+            }
+        }
+        return acc instanceof String ? acc : acc instanceof Number ? acc : 
String.valueOf(acc);
+    }
+
+    private static Number add(Number a, Number b) {
+        if (a instanceof Double || b instanceof Double) {
+            return a.doubleValue() + b.doubleValue();
+        }
+        long sum = a.longValue() + b.longValue();
+        return a instanceof Long || b instanceof Long ? (Number) sum : 
narrow(sum);
+    }
+
+    /** An int when it fits and both sides were ints, as Java types it. */
+    private static Number narrow(long value) {
+        return value >= Integer.MIN_VALUE && value <= Integer.MAX_VALUE ? 
(Number) (int) value : (Number) value;
+    }
+
+    /** {@code 5 * 1000L}, {@code DELAY / 2}: arithmetic on numbers the parser 
knows. */
+    private Object arithmetic(BinOp b) {
+        Object l = evaluate(b.left());
+        Object r = evaluate(b.right());
+        if (!(l instanceof Number a) || !(r instanceof Number c)) {
+            return new Unknown(b, "an expression the parser does not 
evaluate");
+        }
+        boolean floating = a instanceof Double || c instanceof Double;
+        boolean wide = a instanceof Long || c instanceof Long;
+        try {
+            if (floating) {
+                double x = a.doubleValue();
+                double y = c.doubleValue();
+                return switch (b.op()) {
+                    case "*" -> x * y;
+                    case "/" -> x / y;
+                    case "%" -> x % y;
+                    default -> x - y;
+                };

Review Comment:
   💡 **Minor clarity:** The `default` arm in both switch expressions maps to 
subtraction. This is correct because `BinOp.op` is only ever `"-"`, `"*"`, 
`"/"`, or `"%"` (the parser creates BinOp for `-` and for `product()` 
operators, while `+` becomes `Concat`). A brief comment would make this clearer 
to future readers.
   
   ```suggestion
                       default -> x - y;  // only '-' reaches here: '+' is a 
Concat, not a BinOp
   ```



##########
core/camel-java-io/src/main/java/org/apache/camel/java/in/ModelNormalizer.java:
##########
@@ -0,0 +1,188 @@
+/*
+ * 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.java.in;
+
+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 org.apache.camel.builder.ExpressionClause;
+import org.apache.camel.model.language.ConstantExpression;
+import org.apache.camel.model.language.ExpressionDefinition;
+import org.apache.camel.support.builder.ValueBuilder;
+
+/**
+ * Makes a replayed model what the other DSLs have, so tools see one model 
whatever the DSL.
+ * <p/>
+ * Classes: the Java DSL keeps a class as a {@link Class} ({@code 
throwException(Foo.class, "msg")},
+ * {@code convertBodyTo(byte[].class)}), where XML and YAML keep its name in a 
String option next to it
+ * ({@code exceptionType}, {@code type}); the name is filled in.
+ * <p/>
+ * Expressions: they are made plain languages where they can be. The Java DSL 
keeps an expression clause
+ * ({@code setHeader("x").constant("y")}) or a value builder ({@code 
header("x")}) as an object wrapped in an
+ * {@link ExpressionDefinition}, which no DSL can write back; the language 
inside it ({@code constant}, {@code header},
+ * {@code simple}) is what XML and YAML have. A predicate built in Java 
({@code header("x").isEqualTo("y")}) has no
+ * language and stays as it is.
+ */
+final class ModelNormalizer {
+
+    private ModelNormalizer() {
+    }
+
+    static void normalize(Object root) {
+        walk(root, Collections.newSetFromMap(new IdentityHashMap<>()));

Review Comment:
   ✅ **Cycle handling verified:** The `IdentityHashMap`-based `seen` set at 
line 53 correctly prevents infinite loops when the model graph has back-edges 
(e.g., `parent` references). The explicit skip of 
`f.getName().equals("parent")` at line 83 is an additional safeguard. The 
combination handles cycles correctly.



##########
dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ProjectConstantResolver.java:
##########
@@ -0,0 +1,139 @@
+/*
+ * 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.ai;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.function.Supplier;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.apache.camel.catalog.CamelCatalog;
+import org.apache.camel.java.in.ConstantResolver;
+import org.apache.camel.java.in.LwJavaParser;
+import org.apache.camel.tooling.model.BaseOptionModel;
+import org.apache.camel.tooling.model.ComponentModel;
+
+/**
+ * The constants a Java route refers to that the parser cannot see 
(CAMEL-25148): those of the other source files of the
+ * project ({@code Application.QUEUE}), and the header constants of Camel's 
components, which the catalog records
+ * ({@code KafkaConstants.KEY} is {@code kafka.KEY}), and the values of their 
enums ({@code InfinispanOperation.PUT}),
+ * so no component needs to be on the class path. The files are read, and the 
catalog's headers indexed, only when a
+ * constant is asked for.
+ */
+final class ProjectConstantResolver implements ConstantResolver {
+
+    private static final Pattern PACKAGE = 
Pattern.compile("(?m)^\\s*package\\s+([\\w.]+)\\s*;");
+    private static volatile Map<String, String> headerConstants;

Review Comment:
   🔍 **Thread safety:** This is the racy single-check idiom (Effective Java, 
Item 83). It's safe here because:
   1. The `volatile` write of `headerConstants` happens only after the 
`HashMap` is fully populated
   2. The `volatile` provides happens-before: all `put()` calls are visible to 
subsequent readers of the reference
   3. Concurrent builders are wasteful but not harmful (the result is 
idempotent)
   4. The map is never modified after the `volatile` write
   
   If this class were ever to support invalidation (clearing and rebuilding the 
cache), the pattern would need `synchronized`. As-is, it's correct.



##########
core/camel-java-io/src/main/java/org/apache/camel/java/in/ChainReplayer.java:
##########
@@ -0,0 +1,1226 @@
+/*
+ * 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.java.in;
+
+import java.lang.reflect.Array;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Executable;
+import java.lang.reflect.Field;
+import java.lang.reflect.GenericArrayType;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.lang.reflect.ParameterizedType;
+import java.lang.reflect.Proxy;
+import java.lang.reflect.Type;
+import java.lang.reflect.WildcardType;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.IllegalFormatException;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.SortedSet;
+import java.util.TreeSet;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.LineNumberAware;
+import org.apache.camel.builder.AggregationStrategies;
+import org.apache.camel.builder.Builder;
+import org.apache.camel.builder.PredicateBuilder;
+import org.apache.camel.builder.RouteConfigurationBuilder;
+import org.apache.camel.builder.ValueBuilder;
+import org.apache.camel.java.in.JavaChainParser.BinOp;
+import org.apache.camel.java.in.JavaChainParser.Bool;
+import org.apache.camel.java.in.JavaChainParser.Call;
+import org.apache.camel.java.in.JavaChainParser.Chain;
+import org.apache.camel.java.in.JavaChainParser.ClassLit;
+import org.apache.camel.java.in.JavaChainParser.ClassName;
+import org.apache.camel.java.in.JavaChainParser.Concat;
+import org.apache.camel.java.in.JavaChainParser.Lambda;
+import org.apache.camel.java.in.JavaChainParser.New;
+import org.apache.camel.java.in.JavaChainParser.Node;
+import org.apache.camel.java.in.JavaChainParser.Null;
+import org.apache.camel.java.in.JavaChainParser.Num;
+import org.apache.camel.java.in.JavaChainParser.Opaque;
+import org.apache.camel.java.in.JavaChainParser.Ref;
+import org.apache.camel.java.in.JavaChainParser.Str;
+import org.apache.camel.model.RouteConfigurationsDefinition;
+import org.apache.camel.model.RouteTemplatesDefinition;
+import org.apache.camel.model.RoutesDefinition;
+import org.apache.camel.model.language.XPathExpression;
+import org.apache.camel.model.rest.RestsDefinition;
+import org.apache.camel.support.builder.Namespaces;
+
+/**
+ * Builds the model by replaying the chains of a Java source against Camel's 
own DSL: {@code from("a").to("b")} calls
+ * {@code from("a")} on a route builder and {@code to("b")} on the route it 
returns. So {@code end()}, blocks,
+ * expression clauses and ids behave exactly as in a compiled route, for every 
EIP.
+ * <p/>
+ * Nothing of the parsed project runs or is loaded. Only methods of Camel's 
model and builder types are called (see
+ * {@link #isAllowed(Method)}); a class of the project becomes an empty stub 
with its name ({@link StubClassLoader}); a
+ * lambda, an anonymous class or {@code new Foo()} becomes a placeholder 
object of the type the DSL asks for; a value
+ * that cannot be worked out becomes a marked string. Each of those is 
reported as unresolved.
+ */
+final class ChainReplayer {
+
+    /** The DSL: a route configuration builder is also a route builder, so it 
covers routes, rests and templates. */
+    static final class ReplayBuilder extends RouteConfigurationBuilder {
+        @Override
+        public void configure() {
+            // the chains are replayed on it from outside
+        }
+
+        @Override
+        public void configuration() {
+            // the chains are replayed on it from outside
+        }
+
+        /** As the DSL builds it, but keeping property placeholders as 
written: there is no context to resolve them. */
+        @Override
+        public ValueBuilder xpath(String value, Class<?> resultType, 
Namespaces namespaces) {
+            XPathExpression exp = new XPathExpression(value);
+            exp.setResultType(resultType);
+            if (namespaces != null) {
+                exp.setNamespaces(namespaces.getNamespaces());
+            }
+            return new ValueBuilder(exp);
+        }
+    }
+
+    private static final Map<String, Class<?>> PRIMITIVES = Map.of(
+            "byte", byte.class, "short", short.class, "int", int.class, 
"long", long.class, "float", float.class,
+            "double", double.class, "boolean", boolean.class, "char", 
char.class);
+
+    private static final Set<String> READABLE_JDK = Set.of(
+            "java.lang.Integer", "java.lang.Long", "java.lang.Short", 
"java.lang.Byte", "java.lang.Double",
+            "java.lang.Float", "java.lang.Boolean", "java.lang.Character", 
"java.util.concurrent.TimeUnit",
+            "java.nio.charset.StandardCharsets");
+
+    /** Classes whose static DSL methods a route may call, qualified or 
statically imported: language(...), and(...). */
+    private static final List<Class<?>> STATIC_DSL
+            = List.of(Builder.class, PredicateBuilder.class, 
AggregationStrategies.class);
+
+    /** Where a Camel type named without an import is looked for, as a 
RouteBuilder usually imports them. */
+    private static final List<String> CAMEL_PACKAGES = List.of(
+            "org.apache.camel.", "org.apache.camel.model.", 
"org.apache.camel.builder.", "org.apache.camel.model.language.",
+            "org.apache.camel.model.dataformat.", 
"org.apache.camel.model.rest.", "org.apache.camel.model.loadbalancer.");
+
+    /** A value the replay could not work out, until the DSL method it goes to 
says what type it must be. */
+    private record Unknown(Node node, String reason) {
+    }
+
+    private static final Set<String> DENIED_METHODS = Set.of(
+            "getContext", "getCamelContext", "setContext", "setCamelContext", 
"addRoutesToCamelContext",
+            "addRouteConfigurationsToCamelContext", 
"addTemplatedRoutesToCamelContext", "configure", "configuration",
+            "includeRoutes", "bindToRegistry", "propertyInject", "endpoint", 
"getClass", "wait", "notify", "notifyAll");
+
+    /** The life cycle of a route builder, which a parse never runs: only the 
DSL it offers is called. */
+    private static final List<String> BUILDER_LIFE_CYCLE = List.of(
+            "populate", "configure", "prepare", "update", "initialize", "set", 
"check", "add", "remove", "customize");
+
+    /** The builder of the configure() being replayed. */
+    private ReplayBuilder builder;
+    /** The parameter naming the builder in a builder lambda (rb -> 
rb.from(...)), null in configure(). */
+    private String builderParameter;
+    private final JavaChainParser.Source source;
+    private final StubClassLoader stubs;
+    private final EndpointDslResolver endpointDsl;
+    private final ConstantResolver constants;
+    private final List<JavaParseResult.Unresolved> unresolved = new 
ArrayList<>();
+    private final Set<String> resolving = new HashSet<>();
+
+    ChainReplayer(JavaChainParser.Source source) {
+        this(source, null, null);
+    }
+
+    /**
+     * @param endpointDsl the resolver of endpoint DSL calls: asked for every 
call that is not the DSL when given; null
+     *                    uses the naming rules, and only for sources that use 
the endpoint DSL
+     */
+    ChainReplayer(JavaChainParser.Source source, EndpointDslResolver 
endpointDsl) {
+        this(source, endpointDsl, null);
+    }
+
+    ChainReplayer(JavaChainParser.Source source, EndpointDslResolver 
endpointDsl, ConstantResolver constants) {
+        this.constants = constants;
+        this.source = source;
+        this.endpointDsl = endpointDsl != null ? endpointDsl : 
source.endpointDsl() ? EndpointDslResolver.NAMING : null;
+        this.stubs = new StubClassLoader(ChainReplayer.class.getClassLoader());
+    }
+
+    JavaParseResult replay() {
+        RoutesDefinition routes = null;
+        RestsDefinition rests = null;
+        RouteTemplatesDefinition templates = null;
+        RouteConfigurationsDefinition configurations = null;
+        // each configure() is a route builder of its own: its global 
onException and errorHandler come before its routes
+        for (int b = 0; b < source.builders().size(); b++) {
+            List<Node> statements = source.builders().get(b);
+            builderParameter = source.builderParameters().get(b);
+            builder = new ReplayBuilder();
+            for (Node statement : statements) {
+                if (statement instanceof Chain chain && 
configuresTheContext(chain)) {
+                    // getContext().getComponent("sql", 
SqlComponent.class).setDataSource(ds): not a route
+                    report(chain, CONFIGURES_THE_CONTEXT);
+                } else if (statement instanceof Chain chain) {
+                    Object value = evaluate(chain);
+                    if (value instanceof Unknown u) {
+                        report(u.node(), u.reason());
+                    }
+                } else {
+                    report(statement, "not a route chain");
+                }
+            }
+            // the model the other DSLs have: class names, and languages 
rather than the Java objects standing for them
+            ModelNormalizer.normalize(builder.getRouteCollection());
+            ModelNormalizer.normalize(builder.getRestCollection());
+            ModelNormalizer.normalize(builder.getRouteTemplateCollection());
+            
ModelNormalizer.normalize(builder.getRouteConfigurationCollection());
+            if (routes == null) {
+                routes = builder.getRouteCollection();
+                rests = builder.getRestCollection();
+                templates = builder.getRouteTemplateCollection();
+                configurations = builder.getRouteConfigurationCollection();
+            } else {
+                RoutesDefinition more = builder.getRouteCollection();
+                routes.getRoutes().addAll(more.getRoutes());
+                routes.getOnExceptions().addAll(more.getOnExceptions());
+                routes.getIntercepts().addAll(more.getIntercepts());
+                routes.getInterceptFroms().addAll(more.getInterceptFroms());
+                
routes.getInterceptSendTos().addAll(more.getInterceptSendTos());
+                routes.getOnCompletions().addAll(more.getOnCompletions());
+                
rests.getRests().addAll(builder.getRestCollection().getRests());
+                
templates.getRouteTemplates().addAll(builder.getRouteTemplateCollection().getRouteTemplates());
+                configurations.getRouteConfigurations()
+                        
.addAll(builder.getRouteConfigurationCollection().getRouteConfigurations());
+            }
+        }
+        if (routes == null) {
+            builder = new ReplayBuilder();
+            routes = builder.getRouteCollection();
+            rests = builder.getRestCollection();
+            templates = builder.getRouteTemplateCollection();
+            configurations = builder.getRouteConfigurationCollection();
+        }
+        return new JavaParseResult(routes, rests, templates, configurations, 
List.copyOf(unresolved));
+    }
+
+    /** The constants of the source that are data (Strings, numbers, 
booleans), by name. */
+    Map<String, Object> constantValues() {
+        Map<String, Object> answer = new LinkedHashMap<>();
+        source.constants().forEach((name, node) -> {
+            Object v = evaluate(node);
+            if (v instanceof String || v instanceof Number || v instanceof 
Boolean) {
+                answer.put(name, v);
+            }
+        });
+        return answer;
+    }
+
+    // ---- values ----
+
+    /** The value of a node, or an {@link Unknown} for what only a DSL 
parameter type can settle. */
+    private Object evaluate(Node node) {
+        if (node instanceof Str s) {
+            return s.value();
+        } else if (node instanceof Num n) {
+            return number(n.text());
+        } else if (node instanceof Bool b) {
+            return b.value();
+        } else if (node instanceof Null) {
+            return null;
+        } else if (node instanceof Concat c) {
+            return concat(c);
+        } else if (node instanceof Ref r) {
+            return reference(r);
+        } else if (node instanceof ClassLit c) {
+            return classLiteral(c);
+        } else if (node instanceof ClassName c) {
+            return className(c);
+        } else if (node instanceof BinOp b) {
+            return arithmetic(b);
+        } else if (node instanceof Chain c) {
+            return chain(c);
+        } else if (node instanceof Lambda) {
+            return new Unknown(node, "a lambda or method reference");
+        } else if (node instanceof New n) {
+            Object created = exception(n);
+            if (created != null) {
+                return created;
+            }
+            return new Unknown(n, n.anonymous() ? "an anonymous class" : "an 
object created in the route");
+        }
+        return new Unknown(node, "an expression the parser does not evaluate");
+    }
+
+    /**
+     * {@code new IllegalArgumentException("Forced")}: an exception of the JDK 
or Camel is created, as creating one runs
+     * no code of the project and has no side effect; null for anything else.
+     */
+    private Object exception(New n) {
+        if (n.anonymous()) {
+            return null;
+        }
+        Class<?> type = loadable(n.type());
+        if (type == null || !Throwable.class.isAssignableFrom(type) || 
Modifier.isAbstract(type.getModifiers())) {
+            return null;
+        }
+        Object[] values = new Object[n.args().size()];
+        for (int i = 0; i < values.length; i++) {
+            values[i] = evaluate(n.args().get(i));
+            if (values[i] instanceof Unknown) {
+                return null;
+            }
+        }
+        for (Constructor<?> c : type.getConstructors()) {
+            if (c.getParameterCount() != values.length || c.isVarArgs() || 
!onlyMessagesAndCauses(c)) {
+                continue;
+            }
+            Object[] args = convertAll(c, values, n.args(), new ArrayList<>(), 
new int[1]);
+            if (args != null) {
+                try {
+                    return c.newInstance(args);
+                } catch (ReflectiveOperationException | RuntimeException e) {
+                    return null;
+                }
+            }
+        }
+        return null;
+    }
+
+    /** A constructor of an exception that takes messages and causes only, as 
they all do: nothing else is created. */
+    private static boolean onlyMessagesAndCauses(Constructor<?> c) {
+        for (Class<?> p : c.getParameterTypes()) {
+            if (p != String.class && !Throwable.class.isAssignableFrom(p)) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    private static Object number(String text) {
+        String t = text.replace("_", "");
+        try {
+            if (t.endsWith("L") || t.endsWith("l")) {
+                return Long.parseLong(t.substring(0, t.length() - 1));
+            }
+            if (t.endsWith("d") || t.endsWith("D") || t.endsWith("f") || 
t.endsWith("F") || t.contains(".")) {
+                return Double.parseDouble(t.replaceAll("[dDfF]$", ""));
+            }
+            long l = Long.decode(t);
+            return l >= Integer.MIN_VALUE && l <= Integer.MAX_VALUE ? (Object) 
(int) l : (Object) l;
+        } catch (NumberFormatException e) {
+            return t;
+        }
+    }
+
+    /** {@code a + b + ...} as Java works it out: numbers are added until a 
String makes the rest text. */
+    private Object concat(Concat c) {
+        Object acc = null;
+        boolean first = true;
+        for (Node part : c.parts()) {
+            Object v = evaluate(part);
+            if (v instanceof Unknown) {
+                return new Unknown(c, "a value built from parts the parser 
cannot work out");
+            }
+            if (first) {
+                acc = v;
+                first = false;
+            } else if (acc instanceof Number a && v instanceof Number b) {
+                acc = add(a, b);
+            } else {
+                acc = String.valueOf(acc) + v;
+            }
+        }
+        return acc instanceof String ? acc : acc instanceof Number ? acc : 
String.valueOf(acc);
+    }
+
+    private static Number add(Number a, Number b) {
+        if (a instanceof Double || b instanceof Double) {
+            return a.doubleValue() + b.doubleValue();
+        }
+        long sum = a.longValue() + b.longValue();
+        return a instanceof Long || b instanceof Long ? (Number) sum : 
narrow(sum);
+    }
+
+    /** An int when it fits and both sides were ints, as Java types it. */
+    private static Number narrow(long value) {
+        return value >= Integer.MIN_VALUE && value <= Integer.MAX_VALUE ? 
(Number) (int) value : (Number) value;
+    }
+
+    /** {@code 5 * 1000L}, {@code DELAY / 2}: arithmetic on numbers the parser 
knows. */
+    private Object arithmetic(BinOp b) {
+        Object l = evaluate(b.left());
+        Object r = evaluate(b.right());
+        if (!(l instanceof Number a) || !(r instanceof Number c)) {
+            return new Unknown(b, "an expression the parser does not 
evaluate");
+        }
+        boolean floating = a instanceof Double || c instanceof Double;
+        boolean wide = a instanceof Long || c instanceof Long;
+        try {
+            if (floating) {
+                double x = a.doubleValue();
+                double y = c.doubleValue();
+                return switch (b.op()) {
+                    case "*" -> x * y;
+                    case "/" -> x / y;
+                    case "%" -> x % y;
+                    default -> x - y;
+                };
+            }
+            long x = a.longValue();
+            long y = c.longValue();
+            long v = switch (b.op()) {
+                case "*" -> x * y;
+                case "/" -> x / y;
+                case "%" -> x % y;
+                default -> x - y;
+            };
+            return wide ? (Number) v : narrow(v);
+        } catch (ArithmeticException e) {
+            return new Unknown(b, "an expression the parser does not 
evaluate");
+        }
+    }
+
+    /** {@code Foo.class.getName()}: the name of the class, from the imports, 
without loading it. */
+    private Object className(ClassName c) {
+        Class<?> type = loadable(c.type());
+        String qualified = type != null ? type.getName() : 
binaryName(qualified(c.type()));
+        return switch (c.method()) {
+            case "getSimpleName" -> 
qualified.substring(Math.max(qualified.lastIndexOf('.'), 
qualified.lastIndexOf('$')) + 1);
+            case "getName" -> qualified;
+            default -> qualified.replace('$', '.');
+        };
+    }
+
+    /** A constant of the source, or a static field of a JDK or Camel class 
such as {@code LoggingLevel.INFO}. */
+    private Object reference(Ref r) {
+        String name = r.name();
+        String simple = name.substring(name.lastIndexOf('.') + 1);
+        Node constant = source.constants().get(name);
+        if (constant == null && !name.contains(".")) {
+            // a statically imported constant, or one inherited from a 
superclass
+            String owner = source.staticImports().get(name);
+            List<String> owners = new ArrayList<>();
+            if (owner != null) {
+                owners.add(owner);
+            } else {
+                owners.addAll(source.staticWildcards());
+                source.superclasses().forEach(sup -> 
owners.add(qualified(sup)));
+            }
+            for (String o : owners) {
+                Object v = resolved(o, name);
+                if (v != null) {
+                    return v;
+                }
+            }
+        }
+        if (constant == null && name.contains(".")) {
+            String ownerName = name.substring(0, name.lastIndexOf('.'));
+            if (source.classes().contains(ownerName)) {
+                // MyRoutes.URI in MyRoutes
+                constant = source.constants().get(simple);
+            } else if (loadable(ownerName) == null) {
+                // a class not on the class path: a component's header 
constant, another source file of the project
+                Object v = resolved(qualified(ownerName), simple);
+                if (v != null) {
+                    return v;
+                }
+                return new Unknown(r, "a constant of a class the parser cannot 
see");
+            }
+        }
+        if (constant == null && name.contains(".")) {
+            Class<?> owner = loadable(name.substring(0, 
name.lastIndexOf('.')));
+            if (owner != null) {
+                if (!constantsReadable(owner)) {
+                    return new Unknown(r, "a field of a class whose constants 
the parser does not read");
+                }
+                try {
+                    Field f = owner.getField(simple);
+                    if (isConstant(f)) {
+                        return f.get(null);
+                    }
+                    return new Unknown(r, "not a constant");
+                } catch (ReflectiveOperationException | LinkageError e) {
+                    return new Unknown(r, "no such constant");
+                }
+            }
+        }
+        if (constant != null && resolving.add(name)) {
+            try {
+                Object v = evaluate(constant);
+                return v instanceof Unknown ? new Unknown(r, "a constant the 
parser cannot work out") : v;
+            } finally {
+                resolving.remove(name);
+            }
+        }
+        return new Unknown(r, "a variable or field the parser cannot see");
+    }
+
+    /** A constant the plugged resolver knows, when it is data (a String, a 
number, a boolean); else null. */
+    private Object resolved(String className, String field) {
+        if (constants == null) {
+            return null;
+        }
+        try {
+            Object v = constants.constant(className, field);
+            return v instanceof String || v instanceof Number || v instanceof 
Boolean ? v : null;
+        } catch (RuntimeException e) {
+            return null;
+        }
+    }
+
+    /**
+     * Reading a static field initializes its class, so only classes whose 
initialization is known to be harmless:
+     * Camel's, and a few of the JDK's.
+     */
+    static boolean constantsReadable(Class<?> owner) {
+        String n = owner.getName();
+        return n.startsWith("org.apache.camel.") || READABLE_JDK.contains(n);
+    }
+
+    /** A static final String, primitive, boxed primitive or enum constant: 
data, never an object with behaviour. */
+    static boolean isConstant(Field f) {
+        int m = f.getModifiers();
+        Class<?> t = f.getType();
+        return Modifier.isStatic(m) && Modifier.isFinal(m) && 
Modifier.isPublic(m)
+                && (t.isPrimitive() || t == String.class || t.isEnum() || 
Number.class.isAssignableFrom(t)
+                        && t.getName().startsWith("java.lang.") || t == 
Boolean.class || t == Character.class);
+    }
+
+    private Object classLiteral(ClassLit c) {
+        Class<?> type = loadable(c.type());
+        return type != null ? type : new Unknown(c, "a class of the project");
+    }
+
+    /**
+     * A JDK or Camel class by its name in the source, resolved with the 
imports; null for anything else, which is never
+     * loaded.
+     */
+    private Class<?> loadable(String name) {
+        if (name.endsWith("[]")) {
+            Class<?> component = loadable(name.substring(0, name.length() - 
2));
+            return component != null ? Array.newInstance(component, 
0).getClass() : null;
+        }
+        Class<?> primitive = PRIMITIVES.get(name);
+        if (primitive != null) {
+            return primitive;
+        }
+        List<String> candidates = new ArrayList<>();
+        if (name.contains(".") && Character.isLowerCase(name.charAt(0))) {
+            candidates.add(name);
+        }
+        String first = name.contains(".") ? name.substring(0, 
name.indexOf('.')) : name;
+        String imported = source.imports().get(first);
+        if (imported != null) {
+            candidates.add(imported + name.substring(first.length()));
+        }
+        candidates.add("java.lang." + name);
+        if (imported == null && !Character.isLowerCase(name.charAt(0))) {
+            for (String pkg : CAMEL_PACKAGES) {
+                candidates.add(pkg + name);
+            }
+        }
+        for (String candidate : candidates) {
+            String binary = candidate;
+            // Outer.Inner as a binary name: try the dots from the right as $
+            for (int i = 0; i < 3; i++) {
+                if (binary.startsWith("java.") || 
binary.startsWith("org.apache.camel.")) {
+                    try {
+                        return Class.forName(binary, false, 
ChainReplayer.class.getClassLoader());
+                    } catch (ClassNotFoundException | LinkageError e) {
+                        // try the next form
+                    }
+                }
+                int dot = binary.lastIndexOf('.');
+                if (dot < 0) {
+                    break;
+                }
+                binary = binary.substring(0, dot) + "$" + binary.substring(dot 
+ 1);
+            }
+        }
+        return null;
+    }
+
+    /**
+     * {@code com.acme.Outer.Inner} as the JVM names it, {@code 
com.acme.Outer$Inner}: the segments after the first
+     * class name (the first starting with a capital) are nested classes.
+     */
+    static String binaryName(String name) {
+        String[] parts = name.split("\\.");
+        StringBuilder sb = new StringBuilder();
+        boolean inClass = false;
+        for (String part : parts) {
+            if (!sb.isEmpty()) {
+                sb.append(inClass ? '$' : '.');
+            }
+            sb.append(part);
+            inClass |= !part.isEmpty() && 
Character.isUpperCase(part.charAt(0));
+        }
+        return sb.toString();
+    }
+
+    /** The fully qualified name of a class of the project, from the imports 
or the package of the source. */
+    private String qualified(String name) {
+        if (name.contains(".") && Character.isLowerCase(name.charAt(0))) {
+            return name;
+        }
+        String first = name.contains(".") ? name.substring(0, 
name.indexOf('.')) : name;
+        String imported = source.imports().get(first);
+        if (imported != null) {
+            return imported + name.substring(first.length());
+        }
+        return source.packageName() != null ? source.packageName() + "." + 
name : name;
+    }
+
+    // ---- calls ----
+
+    private Object chain(Chain c) {
+        Object target;
+        Class<?> staticType = null;
+        if (("String".equals(c.qualifier()) || 
"java.lang.String".equals(c.qualifier())) && c.calls().size() == 1
+                && c.calls().get(0).name().equals("format") && 
!c.calls().get(0).args().isEmpty()) {
+            // String.format("netty:tcp://localhost:%d", PORT)
+            String text = formatText(c.calls().get(0).args());
+            return text != null ? text : new Unknown(c, "a format the parser 
cannot work out");
+        }
+        if (c.qualifier() != null && c.qualifier().equals(builderParameter)) {
+            // rb.simple(...) in a builder lambda: the builder
+            target = builder;
+        } else if (c.qualifier() != null) {
+            Class<?> type = loadable(c.qualifier());
+            if (type != null && STATIC_DSL.contains(type)) {
+                staticType = type;
+                target = null;
+            } else {
+                Object q = reference(new Ref(c.qualifier(), c.line()));
+                if (q instanceof Unknown || q == null) {
+                    return new Unknown(c, "a call on something the parser 
cannot see");
+                }
+                target = q;
+            }
+        } else {
+            target = builder;
+        }
+        for (int i = 0; i < c.calls().size(); i++) {
+            Call call = formatted(c.calls().get(i));
+            Object result;
+            if (staticType != null) {
+                result = invokeStatic(List.of(staticType), call);
+                staticType = null;
+            } else {
+                result = invoke(target, call);
+                if (target == builder && result instanceof Unknown && i == 0) {
+                    if (endpointDsl != null && 
call.name().equals("endpoints")) {
+                        // endpoints(mock("a"), direct("b")) for a routing 
slip: the URIs, comma separated
+                        result = endpoints(call);
+                    } else {
+                        // kafka("orders").brokers("b:9092"): the whole chain 
is one endpoint; as in Java, the
+                        // builder's own methods (the endpoint DSL) come 
before statically imported ones
+                        String uri = endpointDsl != null ? endpoint(c) : null;
+                        if (uri != null) {
+                            return uri;
+                        }
+                        // a statically imported DSL method: 
language("groovy", "..."), and(...)
+                        Object viaStatic = 
invokeStatic(unqualifiedStaticDsl(), call);
+                        if (!(viaStatic instanceof Unknown)) {
+                            result = viaStatic;
+                        }
+                    }
+                }
+            }
+            if (result instanceof Unknown u) {
+                // the rest of the chain has nothing to be called on
+                return u;
+            }
+            if (result == null) {
+                // a void method: the rest of the chain, if any, is on the 
builder again (it rarely is)
+                result = builder;
+            }
+            target = result;
+        }
+        return target;
+    }
+
+    /** Why a statement that sets up the CamelContext (components, beans, 
properties) is not read: it is not a route. */
+    static final String CONFIGURES_THE_CONTEXT = "configures the CamelContext, 
not a route";
+
+    /**
+     * Whether a statement starts on the CamelContext rather than the DSL: 
getContext()..., context.addComponent(...).
+     */
+    private boolean configuresTheContext(Chain chain) {
+        if (chain.qualifier() != null) {
+            String q = chain.qualifier();
+            return q.equals("context") || q.equals("camelContext") || 
q.startsWith("context.")
+                    || q.startsWith("camelContext.");
+        }
+        String first = chain.calls().isEmpty() ? "" : 
chain.calls().get(0).name();
+        return first.equals("getContext") || first.equals("getCamelContext");
+    }
+
+    /** The format variants of the DSL, and the call they format for. */
+    private static final Map<String, String> FORMATTED = Map.of("fromF", 
"from", "toF", "to", "simpleF", "simple");
+
+    private static final Pattern FORMAT_SPECIFIER = 
Pattern.compile("%(\\d+\\$)?[-#+ 0,(]*\\d*(\\.\\d+)?[a-zA-Z%]");
+
+    /**
+     * {@code toF("seda:%s?size=%d", QUEUE, 10)} as {@code 
to("seda:orders?size=10")}: formatted as the DSL does when
+     * every argument is known; else each unknown one is a marked placeholder 
in the text (reported).
+     */
+    private Call formatted(Call call) {
+        String plain = FORMATTED.get(call.name());
+        if (plain == null || call.args().isEmpty()) {
+            return call;
+        }
+        String text = formatText(call.args());
+        return text != null ? new Call(plain, List.of(new Str(text, 
call.line())), call.line()) : call;
+    }
+
+    /**
+     * The text of a format and its arguments, as {@code String.format} gives 
it when every argument is known; else with
+     * each unknown one as a marked placeholder (reported). Null when the 
format is not a known String, or an argument
+     * is a class (simpleF(format, resultType, ...)).
+     */
+    private String formatText(List<Node> args) {
+        Object format = evaluate(args.get(0));
+        if (!(format instanceof String fmt)) {
+            return null;
+        }
+        List<Node> argNodes = args.subList(1, args.size());
+        List<Object> values = new ArrayList<>();
+        boolean known = true;
+        for (Node arg : argNodes) {
+            Object v = evaluate(arg);
+            if (v instanceof Class<?>) {
+                return null;
+            }
+            known &= !(v instanceof Unknown);
+            values.add(v);
+        }
+        if (known) {
+            try {
+                return String.format(fmt, values.toArray());
+            } catch (IllegalFormatException e) {
+                // fill in what is known below
+            }
+        }
+        StringBuilder sb = new StringBuilder();
+        Matcher m = FORMAT_SPECIFIER.matcher(fmt);
+        int next = 0;
+        while (m.find()) {
+            String spec = m.group();
+            String replacement;
+            if (spec.equals("%%")) {
+                replacement = "%";
+            } else if (spec.equals("%n")) {
+                replacement = System.lineSeparator();
+            } else {
+                // %2$s names its argument, the others take the next one
+                Matcher indexed = Pattern.compile("^%(\\d+)\\$").matcher(spec);
+                int index = indexed.find() ? 
Integer.parseInt(indexed.group(1)) - 1 : next++;
+                if (index >= 0 && index < values.size()) {
+                    Object v = values.get(index);
+                    Node node = argNodes.get(index);
+                    if (v instanceof Unknown u) {
+                        report(node, u.reason());
+                        replacement = LwJavaParser.UNRESOLVED_PREFIX + 
text(node) + "}";
+                    } else {
+                        replacement = String.valueOf(v);
+                    }
+                } else {
+                    replacement = spec;
+                }
+            }
+            m.appendReplacement(sb, Matcher.quoteReplacement(replacement));
+        }
+        m.appendTail(sb);
+        return sb.toString();
+    }
+
+    /** {@code endpoints(...)} of the endpoint DSL as a constant of their 
URIs, comma separated. */
+    private Object endpoints(Call call) {
+        List<String> uris = new ArrayList<>();
+        for (Node arg : call.args()) {
+            String uri = arg instanceof Chain chain ? endpoint(chain) : null;
+            if (uri == null) {
+                Object v = evaluate(arg);
+                if (!(v instanceof String s)) {
+                    return new Unknown(call, "an endpoint the parser cannot 
work out");
+                }
+                uri = s;
+            }
+            uris.add(uri);
+        }
+        return builder.constant(String.join(",", uris));
+    }
+
+    /**
+     * The classes whose static DSL a call without a qualifier may be: Builder 
and PredicateBuilder always (the Java the
+     * dumper writes calls language(...) so), AggregationStrategies only when 
the source imports it statically.
+     */
+    private List<Class<?>> unqualifiedStaticDsl() {
+        String aggregation = AggregationStrategies.class.getName();
+        boolean imported = source.staticWildcards().contains(aggregation)
+                || source.staticImports().containsValue(aggregation);
+        return imported ? STATIC_DSL : List.of(Builder.class, 
PredicateBuilder.class);
+    }
+
+    /** The URI of an endpoint DSL chain, or null when the resolver does not 
know its factory. */
+    private String endpoint(Chain c) {
+        Call factory = c.calls().get(0);
+        List<String> args = new ArrayList<>();
+        for (Node arg : factory.args()) {
+            args.add(uriValue(arg));
+        }
+        List<EndpointDslResolver.Option> options = new ArrayList<>();
+        for (int i = 1; i < c.calls().size(); i++) {
+            Call call = c.calls().get(i);
+            List<String> values = new ArrayList<>();
+            for (Node arg : call.args()) {
+                values.add(uriValue(arg));
+            }
+            options.add(new EndpointDslResolver.Option(call.name(), values));
+        }
+        EndpointDslResolver.Endpoint endpoint = 
endpointDsl.endpoint(factory.name(), args, options);
+        if (endpoint == null) {
+            return null;
+        }
+        for (String problem : endpoint.problems()) {
+            report(c, problem);
+        }
+        return endpoint.uri();
+    }
+
+    /** A value in an endpoint URI as the endpoint DSL writes it; a marked 
placeholder when unknown (reported). */
+    private String uriValue(Node node) {
+        Object v = evaluate(node);
+        if (v instanceof Unknown u) {
+            report(node, u.reason());
+            return LwJavaParser.UNRESOLVED_PREFIX + text(node) + "}";
+        }
+        if (v instanceof Enum<?> e) {
+            return e.name();
+        }
+        if (v instanceof Class<?> type) {
+            return type.getName();
+        }
+        return String.valueOf(v);
+    }
+
+    private Object invokeStatic(List<Class<?>> types, Call call) {
+        List<Method> candidates = new ArrayList<>();
+        for (Class<?> type : types) {
+            for (Method m : type.getMethods()) {
+                if (m.getName().equals(call.name()) && 
Modifier.isStatic(m.getModifiers())
+                        && (m.getParameterCount() == call.args().size()
+                                || m.isVarArgs() && call.args().size() >= 
m.getParameterCount() - 1)) {
+                    candidates.add(m);
+                }
+            }
+        }
+        if (candidates.isEmpty()) {
+            return new Unknown(call, "not a DSL method (a helper method, or 
the endpoint DSL)");
+        }
+        return call(null, candidates, call);
+    }
+
+    private Object invoke(Object target, Call call) {
+        List<Method> candidates = candidates(target.getClass(), call.name(), 
call.args().size());
+        if (candidates.isEmpty()) {
+            String reason = target == builder
+                    ? "not a DSL method (a helper method, or the endpoint DSL)"
+                    : "no such DSL method on " + 
target.getClass().getSimpleName();
+            return new Unknown(call, reason);
+        }
+        return call(target, candidates, call);
+    }
+
+    /** Calls the overload the arguments fit best; a static one when {@code 
target} is null. */
+    private Object call(Object target, List<Method> candidates, Call call) {
+        Object[] values = new Object[call.args().size()];
+        for (int i = 0; i < values.length; i++) {
+            values[i] = evaluate(call.args().get(i));
+        }
+        Method best = null;
+        Object[] bestArgs = null;
+        int bestCost = Integer.MAX_VALUE;
+        int bestSpecific = -1;
+        List<JavaParseResult.Unresolved> bestReports = null;
+        // in a stable order, as reflection gives methods in none
+        List<Method> ordered = new ArrayList<>(candidates);
+        ordered.sort((a, b) -> signature(a).compareTo(signature(b)));
+        for (Method m : ordered) {
+            List<JavaParseResult.Unresolved> reports = new ArrayList<>();
+            int[] cost = { 0 };
+            Object[] args = convertAll(m, values, call.args(), reports, cost);
+            if (args == null) {
+                continue;
+            }
+            // as Java does, of the overloads that fit the most specific one: 
method(String, String) before
+            // method(Object, String) for method("waiter", "checkOrder")
+            int specific = specificity(m, values);
+            if (cost[0] < bestCost || cost[0] == bestCost && specific > 
bestSpecific) {
+                best = m;
+                bestArgs = args;
+                bestCost = cost[0];
+                bestSpecific = specific;
+                bestReports = reports;
+            }
+        }
+        if (best == null) {
+            return new Unknown(call, "arguments the DSL method does not take");
+        }
+        unresolved.addAll(bestReports);
+        try {
+            best.setAccessible(true);

Review Comment:
   🔍 **Observation:** `setAccessible(true)` on Camel's DSL methods is needed 
because `candidates()` collects protected methods. On newer JDKs (16+) this 
only works if the module opens the package. Since `camel-java-io` and the 
builder/model modules are in the same classloader and Camel's modules use open 
packages, this should be fine, but it's worth noting that a strong module 
boundary on `org.apache.camel.builder` or `org.apache.camel.model` would break 
this. The design doc might want to note this assumption.



##########
core/camel-java-io/src/main/java/org/apache/camel/java/in/JavaTokenizer.java:
##########
@@ -0,0 +1,260 @@
+/*
+ * 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.java.in;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Splits Java source into the tokens the route parser needs: identifiers, 
literals and punctuation, with their line
+ * numbers. Comments and whitespace are dropped. It is not a full Java lexer: 
it knows enough to find the fluent chains
+ * of a RouteBuilder and to skip over what it does not read (lambda bodies, 
anonymous classes, generics).
+ */
+final class JavaTokenizer {
+
+    enum Kind {
+        IDENT,
+        STRING,
+        CHAR,
+        NUMBER,
+        PUNCT,
+        EOF
+    }
+
+    /**
+     * A token. For a string it holds the value with escapes and text blocks 
decoded; for other kinds the source text.
+     */
+    record Token(Kind kind, String text, int line) {
+
+        boolean is(String punct) {
+            return kind == Kind.PUNCT && text.equals(punct);
+        }
+
+        boolean isIdent(String name) {
+            return kind == Kind.IDENT && text.equals(name);
+        }
+    }
+
+    private final String src;
+    private int pos;
+    private int line = 1;
+
+    private JavaTokenizer(String src) {
+        this.src = src;
+    }
+
+    static List<Token> tokenize(String source) {
+        return new JavaTokenizer(source).run();
+    }
+
+    private List<Token> run() {
+        List<Token> tokens = new ArrayList<>();
+        while (true) {
+            skipSpaceAndComments();
+            if (pos >= src.length()) {
+                tokens.add(new Token(Kind.EOF, "", line));
+                return tokens;
+            }
+            char c = src.charAt(pos);
+            int startLine = line;
+            if (src.startsWith("\"\"\"", pos)) {
+                tokens.add(new Token(Kind.STRING, textBlock(), startLine));
+            } else if (c == '"') {
+                tokens.add(new Token(Kind.STRING, quoted('"'), startLine));
+            } else if (c == '\'') {
+                tokens.add(new Token(Kind.CHAR, quoted('\''), startLine));
+            } else if (Character.isJavaIdentifierStart(c)) {
+                int start = pos;
+                while (pos < src.length() && 
Character.isJavaIdentifierPart(src.charAt(pos))) {
+                    pos++;
+                }
+                tokens.add(new Token(Kind.IDENT, src.substring(start, pos), 
startLine));
+            } else if (Character.isDigit(c) || c == '.' && pos + 1 < 
src.length() && Character.isDigit(src.charAt(pos + 1))) {
+                tokens.add(new Token(Kind.NUMBER, number(), startLine));
+            } else {
+                tokens.add(new Token(Kind.PUNCT, punct(), startLine));
+            }
+        }
+    }
+
+    private void skipSpaceAndComments() {
+        while (pos < src.length()) {
+            char c = src.charAt(pos);
+            if (c == '\n') {
+                line++;
+                pos++;
+            } else if (Character.isWhitespace(c)) {
+                pos++;
+            } else if (src.startsWith("//", pos)) {
+                while (pos < src.length() && src.charAt(pos) != '\n') {
+                    pos++;
+                }
+            } else if (src.startsWith("/*", pos)) {
+                int end = src.indexOf("*/", pos + 2);
+                end = end < 0 ? src.length() : end + 2;
+                countLines(pos, end);
+                pos = end;
+            } else {
+                return;
+            }
+        }
+    }
+
+    private String quoted(char quote) {
+        StringBuilder sb = new StringBuilder();
+        pos++;
+        while (pos < src.length() && src.charAt(pos) != quote) {
+            char c = src.charAt(pos);
+            if (c == '\\' && pos + 1 < src.length()) {
+                pos = escape(sb, pos + 1);
+            } else {
+                if (c == '\n') {
+                    // unterminated literal: stop at the end of the line
+                    break;
+                }
+                sb.append(c);
+                pos++;
+            }
+        }
+        pos = Math.min(src.length(), pos + 1);
+        return sb.toString();
+    }
+
+    /** A text block: the content lines with their common indentation removed, 
as javac does. */
+    private String textBlock() {
+        int start = src.indexOf('\n', pos + 3);
+        if (start < 0) {
+            pos = src.length();
+            return "";
+        }
+        int end = start + 1;
+        while (end < src.length() && !src.startsWith("\"\"\"", end)) {
+            end += src.charAt(end) == '\\' ? 2 : 1;

Review Comment:
   🔍 **Edge case:** In `textBlock()`, the scan for the closing `"""` at line 
145 skips two characters after a backslash (`end += 2`). If the source ends 
with a single backslash right at `src.length() - 1`, the next iteration reads 
`end` which is now `src.length() + 1` — but the while condition `end < 
src.length()` prevents that. The `Math.min(end, src.length())` on line 148 also 
guards it. Safe.



##########
dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/JavaRouteReader.java:
##########
@@ -0,0 +1,253 @@
+/*
+ * 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.ai;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.IdentityHashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+
+import org.apache.camel.ErrorHandlerFactory;
+import org.apache.camel.catalog.CamelCatalog;
+import org.apache.camel.dsl.jbang.core.commands.ai.ProjectRoutes.Endpoint;
+import org.apache.camel.dsl.jbang.core.commands.ai.ProjectRoutes.Route;
+import org.apache.camel.java.in.ConstantResolver;
+import org.apache.camel.java.in.JavaParseResult;
+import org.apache.camel.java.in.LwJavaParser;
+import org.apache.camel.model.CatchDefinition;
+import org.apache.camel.model.DynamicRouterDefinition;
+import org.apache.camel.model.EnrichDefinition;
+import org.apache.camel.model.FinallyDefinition;
+import org.apache.camel.model.KameletDefinition;
+import org.apache.camel.model.LogDefinition;
+import org.apache.camel.model.OnExceptionDefinition;
+import org.apache.camel.model.PollDefinition;
+import org.apache.camel.model.PollEnrichDefinition;
+import org.apache.camel.model.ProcessorDefinition;
+import org.apache.camel.model.RecipientListDefinition;
+import org.apache.camel.model.RouteDefinition;
+import org.apache.camel.model.RouteTemplateDefinition;
+import org.apache.camel.model.RoutingSlipDefinition;
+import org.apache.camel.model.SendDefinition;
+import org.apache.camel.model.ToDynamicDefinition;
+import org.apache.camel.model.TryDefinition;
+import org.apache.camel.model.errorhandler.DeadLetterChannelDefinition;
+import org.apache.camel.model.language.ConstantExpression;
+import org.apache.camel.model.language.ExpressionDefinition;
+import org.apache.camel.model.rest.RestDefinition;
+import org.apache.camel.model.rest.VerbDefinition;
+
+/**
+ * The routes of a Java RouteBuilder for the project overview, read into the 
Camel model by the Java DSL parser of
+ * camel-java-io (CAMEL-25148) without compiling it: nesting, error paths, 
constants and the endpoint DSL are seen as in
+ * a compiled route. A route with a part the parser could not work out (a 
lambda, a value from a helper method) is
+ * marked heuristic, as its endpoints may be incomplete.
+ */
+final class JavaRouteReader {
+
+    private static final int MAX_DEPTH = 50;
+
+    private final String file;
+    private final CamelCatalog catalog;
+    private final List<Route> routes = new ArrayList<>();
+
+    private JavaRouteReader(String file, CamelCatalog catalog) {
+        this.file = file;
+        this.catalog = catalog;
+    }
+
+    /** The routes of the source, or an empty list when the parser finds none. 
*/
+    static List<Route> read(String file, String content, CamelCatalog catalog, 
ConstantResolver constants) {
+        LwJavaParser parser = new LwJavaParser();
+        if (catalog != null) {
+            parser.setEndpointDslResolver(new 
CatalogEndpointDslResolver(catalog));
+        }
+        parser.setConstantResolver(constants != null
+                ? constants : new ProjectConstantResolver(Map.of(), catalog));
+        JavaParseResult result = parser.parse(content);
+        JavaRouteReader reader = new JavaRouteReader(file, catalog);
+        List<Integer> starts = new ArrayList<>();
+        result.routes().getRoutes().forEach(r -> 
starts.add(r.getLineNumber()));
+        for (RouteDefinition r : result.routes().getRoutes()) {
+            reader.route(r, null, partial(result, r.getLineNumber(), starts));
+        }
+        for (RouteTemplateDefinition t : 
result.routeTemplates().getRouteTemplates()) {
+            if (t.getRoute() != null) {
+                reader.route(t.getRoute(), t, false);
+            }
+        }
+        for (RestDefinition rest : result.rests().getRests()) {
+            reader.rest(rest);
+        }
+        reader.errorHandlers(result);
+        return reader.routes;
+    }
+
+    /** Whether something the parser did not work out lies between this 
route's line and the next route's. */
+    private static boolean partial(JavaParseResult result, int line, 
List<Integer> starts) {

Review Comment:
   🔍 **Observation:** The `partial()` method determines if a route is heuristic 
by checking if any unresolved item falls between this route's line and the next 
route's line. For the last route, `end` stays at `Integer.MAX_VALUE`, so all 
unresolved items after the last route's start line are attributed to it. This 
is correct — any unresolved item in the source after the last `from()` belongs 
to that route's configure block.



##########
core/camel-java-io/src/main/java/org/apache/camel/java/in/StubClassLoader.java:
##########
@@ -0,0 +1,126 @@
+/*
+ * 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.java.in;
+
+import java.io.ByteArrayOutputStream;
+import java.io.DataOutputStream;
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.regex.Pattern;
+
+/**
+ * Stands in for the classes of the parsed project, which are never loaded: 
{@code onException(MyException.class)} needs
+ * a {@link Class}, and the model only keeps its name. A stub is an empty 
abstract class with the same name that extends
+ * or implements what the DSL method asks for. It has no code, so nothing of 
the project runs, and it lives in a class
+ * loader of its own, for one parse.
+ */
+final class StubClassLoader extends ClassLoader {
+
+    private static final Pattern JAVA_NAME
+            = 
Pattern.compile("[\\p{javaJavaIdentifierStart}][\\p{javaJavaIdentifierPart}]*"
+                              + 
"(\\.[\\p{javaJavaIdentifierStart}][\\p{javaJavaIdentifierPart}]*)*");
+
+    /** At most this many stubs a parse. */
+    static final int MAX_STUBS = 1000;
+
+    private final Map<String, Class<?>> stubs = new HashMap<>();
+
+    StubClassLoader(ClassLoader parent) {
+        super(parent);
+    }
+
+    /**
+     * An empty abstract class with the given name, extending {@code 
superType} when it is a class or implementing it
+     * when it is an interface; null when the name cannot be used. It is only 
asked for a name that is not on the class
+     * path, so a stub never hides a real class, a Camel one included.
+     */
+    Class<?> stub(String name, Class<?> superType) {
+        if (!JAVA_NAME.matcher(name).matches() || name.startsWith("java.") || 
name.startsWith("javax.")

Review Comment:
   ✅ **Security check confirmed:** The guard on line 53-55 correctly rejects 
`java.*`, `javax.*`, `jdk.*`, `sun.*` prefixes, and validates the name against 
`JAVA_NAME`. This prevents a crafted class name from shadowing JDK classes or 
injecting invalid bytecode through the name. The `MAX_STUBS` bound on line 61 
limits metaspace consumption. Both verified by `LwJavaParserSecurityTest`.



##########
dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/CatalogEndpointDslResolver.java:
##########
@@ -0,0 +1,83 @@
+/*
+ * 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.ai;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.apache.camel.catalog.CamelCatalog;
+import org.apache.camel.java.in.EndpointDslResolver;
+import org.apache.camel.tooling.model.BaseOptionModel;
+import org.apache.camel.tooling.model.ComponentModel;
+
+/**
+ * The endpoint DSL with the catalog to hand (CAMEL-25148): a factory is an 
endpoint only when the catalog has the
+ * component, so a helper method of the same shape is not taken for one, even 
in a snippet without imports; a
+ * multi-value option gets the prefix the catalog gives it ({@code 
schedulerProperties("delay", 10)} is
+ * {@code scheduler.delay=10}); an option the component does not have is 
reported.
+ */
+final class CatalogEndpointDslResolver implements EndpointDslResolver {
+
+    private final CamelCatalog catalog;
+
+    CatalogEndpointDslResolver(CamelCatalog catalog) {
+        this.catalog = catalog;
+    }
+
+    @Override
+    public Endpoint endpoint(String factory, List<String> args, List<Option> 
options) {
+        Endpoint named = NAMING.endpoint(factory, args, List.of());
+        if (named == null) {
+            return null;
+        }
+        String scheme = named.uri().substring(0, named.uri().indexOf("://"));
+        // kafka("myKafka", "orders"): a component under another name, the 
catalog cannot say what it is
+        ComponentModel model = args.size() == 2 ? null : 
catalog.componentModel(scheme);
+        if (args.size() != 2 && model == null) {
+            return null;
+        }
+        List<Option> resolved = new ArrayList<>();
+        List<String> problems = new ArrayList<>();
+        for (Option o : options) {
+            BaseOptionModel option = model != null ? option(model, o.name()) : 
null;
+            if (model != null && option == null && !o.values().isEmpty()) {
+                problems.add("no option " + o.name() + " on " + scheme);

Review Comment:
   💡 **Minor:** When `model` is null (args.size() == 2, or component not 
found), the loop still iterates over `options` but `option` will always be 
null. The options are then forwarded to `NAMING.endpoint()` unchanged, which is 
the correct behavior (pass-through to the naming resolver). However, the 
`problems.add("no option " + o.name())` branch at line 60 would fire for every 
option when the component is unknown, producing potentially misleading problem 
messages. Consider skipping the option validation loop entirely when `model == 
null`.
   
   ```suggestion
               BaseOptionModel option = model != null ? option(model, o.name()) 
: null;
               if (model != null && option == null && !o.values().isEmpty()) {
   ```
   
   (This is already the code as written — the `model != null` guard is present. 
On re-read, the logic is correct. Ignore this comment.)



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to