davsclaus commented on code in PR #27122:
URL: https://github.com/apache/camel/pull/27122#discussion_r4143112870


##########
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:
   Thanks, added the comment to both switch expressions (the floating point and 
the integer one).
   
   _Claude Code on behalf of davsclaus_



##########
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:
   Good point. `setAccessible(true)` is now only called for non-public methods, 
which are the route builder's protected ones, called as a `RouteBuilder` 
subclass would. Public DSL methods are invoked as they are. The design document 
(`design/java-dsl-parser.adoc`, Security) now states the assumption: Camel's 
builder and model packages must be open to camel-java-io, as they are on the 
class path and in Camel's module layout. A strong module boundary on 
`org.apache.camel.builder`/`org.apache.camel.model` would need them opened to 
camel-java-io.
   
   _Claude Code on behalf of davsclaus_



-- 
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