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


##########
core/camel-java-io/src/main/java/org/apache/camel/java/in/ChainReplayer.java:
##########
@@ -0,0 +1,1232 @@
+/*
+ * 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;
+                    // only '-' reaches here: '+' is a Concat, not a BinOp
+                    // only '-' reaches here: '+' is a Concat, not a BinOp

Review Comment:
   💡 **Nit:** The comment is duplicated (copy-paste). Also, the integer switch 
at line 397 (`default -> x - y`) is still uncommented — the response said 
"added the comment to both switch expressions" but it only landed (twice) in 
the floating-point one.
   
   ```suggestion
                       // only '-' reaches here: '+' is a Concat, not a BinOp
   ```



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