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]
