This is an automated email from the ASF dual-hosted git repository. davsclaus pushed a commit to branch av in repository https://gitbox.apache.org/repos/asf/camel.git
commit 0eedf3f2fbe996b08c2e2ab89a8eeafea0d9ea3c Author: Claus Ibsen <[email protected]> AuthorDate: Wed Jan 7 15:28:04 2026 +0100 CAMEL-22818: camel-core - AdviceWith should pin point to same resource so route coverage can correlate --- .../camel-spring-parent/camel-spring-xml/pom.xml | 4 + .../spring/DumpModelAsXmlPlaceholdersTest.java | 2 +- .../SpringDumpRouteCoverageAdviceWithTest.java | 84 ++++++++++++++++ .../XmlIoDumpRouteCoverageAdviceWithTest.java | 109 +++++++++++++++++++++ .../SpringDumpRouteCoverageAdviceWithTest.xml | 36 +++++++ .../XmlIoDumpRouteCoverageAdviceWithTest.xml | 27 +++++ .../java/org/apache/camel/builder/AdviceWith.java | 8 ++ .../org/apache/camel/builder/AdviceWithTasks.java | 27 ++--- .../camel/issues/AdviceWithWeaveByIdTest.java | 56 +++++++++++ .../management/mbean/ManagedCamelContext.java | 2 +- .../org/apache/camel/xml/LwModelToXMLDumper.java | 4 +- 11 files changed, 345 insertions(+), 14 deletions(-) diff --git a/components/camel-spring-parent/camel-spring-xml/pom.xml b/components/camel-spring-parent/camel-spring-xml/pom.xml index b538df65a13d..d680e7e818fa 100644 --- a/components/camel-spring-parent/camel-spring-xml/pom.xml +++ b/components/camel-spring-parent/camel-spring-xml/pom.xml @@ -48,6 +48,10 @@ <groupId>org.apache.camel</groupId> <artifactId>camel-spring</artifactId> </dependency> + <dependency> + <groupId>org.apache.camel</groupId> + <artifactId>camel-xml-io</artifactId> + </dependency> <dependency> <groupId>org.apache.camel</groupId> <artifactId>camel-xml-jaxb</artifactId> diff --git a/components/camel-spring-parent/camel-spring-xml/src/test/java/org/apache/camel/spring/DumpModelAsXmlPlaceholdersTest.java b/components/camel-spring-parent/camel-spring-xml/src/test/java/org/apache/camel/spring/DumpModelAsXmlPlaceholdersTest.java index 8234b73068f5..94c1a60fa2f1 100644 --- a/components/camel-spring-parent/camel-spring-xml/src/test/java/org/apache/camel/spring/DumpModelAsXmlPlaceholdersTest.java +++ b/components/camel-spring-parent/camel-spring-xml/src/test/java/org/apache/camel/spring/DumpModelAsXmlPlaceholdersTest.java @@ -42,7 +42,7 @@ public class DumpModelAsXmlPlaceholdersTest extends SpringTestSupport { assertNotNull(xml); log.info(xml); - assertTrue(xml.contains("<route xmlns=\"http://camel.apache.org/schema/spring\" id=\"Gouda\">")); + assertTrue(xml.contains("id=\"Gouda\">")); assertTrue(xml.contains("\"direct:start-{{cheese.type}}\"")); assertTrue(xml.contains("\"direct:end-{{cheese.type}}\"")); } diff --git a/components/camel-spring-parent/camel-spring-xml/src/test/java/org/apache/camel/spring/management/SpringDumpRouteCoverageAdviceWithTest.java b/components/camel-spring-parent/camel-spring-xml/src/test/java/org/apache/camel/spring/management/SpringDumpRouteCoverageAdviceWithTest.java new file mode 100644 index 000000000000..887f45e527ee --- /dev/null +++ b/components/camel-spring-parent/camel-spring-xml/src/test/java/org/apache/camel/spring/management/SpringDumpRouteCoverageAdviceWithTest.java @@ -0,0 +1,84 @@ +/* + * 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.spring.management; + +import javax.management.MBeanServer; +import javax.management.ObjectName; + +import org.w3c.dom.Document; + +import org.apache.camel.builder.AdviceWith; +import org.apache.camel.spring.SpringTestSupport; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.DisabledOnOs; +import org.junit.jupiter.api.condition.OS; +import org.springframework.context.support.AbstractXmlApplicationContext; +import org.springframework.context.support.ClassPathXmlApplicationContext; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@DisabledOnOs(OS.AIX) +public class SpringDumpRouteCoverageAdviceWithTest extends SpringTestSupport { + + @Override + protected boolean useJmx() { + return true; + } + + @Override + public boolean isUseAdviceWith() { + return true; + } + + @Override + protected AbstractXmlApplicationContext createApplicationContext() { + return new ClassPathXmlApplicationContext( + "org/apache/camel/spring/management/SpringDumpRouteCoverageAdviceWithTest.xml"); + } + + protected MBeanServer getMBeanServer() { + return context.getManagementStrategy().getManagementAgent().getMBeanServer(); + } + + @Test + public void testDumpRouteCoverage() throws Exception { + AdviceWith.adviceWith(context, "hello-process-pipeline", advice -> { + advice.weaveById("target-id").replace().to("mock:hello").id("target-id"); + }); + + context.start(); + + // get the stats for the route + MBeanServer mbeanServer = getMBeanServer(); + ObjectName on = getContextObjectName(); + + getMockEndpoint("mock:hello").expectedMessageCount(1); + template.sendBody("direct:hello-process", "Hello World"); + assertMockEndpointsSatisfied(); + + String xml = (String) mbeanServer.invoke(on, "dumpRoutesCoverageAsXml", null, null); + log.info(xml); + + assertTrue(xml.contains("exchangesTotal=\"1\"")); + + // should be valid XML + Document doc = context.getTypeConverter().convertTo(Document.class, xml); + assertNotNull(doc); + } + +} diff --git a/components/camel-spring-parent/camel-spring-xml/src/test/java/org/apache/camel/spring/management/XmlIoDumpRouteCoverageAdviceWithTest.java b/components/camel-spring-parent/camel-spring-xml/src/test/java/org/apache/camel/spring/management/XmlIoDumpRouteCoverageAdviceWithTest.java new file mode 100644 index 000000000000..839976cc3ec4 --- /dev/null +++ b/components/camel-spring-parent/camel-spring-xml/src/test/java/org/apache/camel/spring/management/XmlIoDumpRouteCoverageAdviceWithTest.java @@ -0,0 +1,109 @@ +/* + * 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.spring.management; + +import java.io.InputStream; + +import javax.management.MBeanServer; +import javax.management.MalformedObjectNameException; +import javax.management.ObjectName; + +import org.w3c.dom.Document; + +import org.apache.camel.ContextTestSupport; +import org.apache.camel.builder.AdviceWith; +import org.apache.camel.model.RoutesDefinition; +import org.apache.camel.util.IOHelper; +import org.apache.camel.xml.LwModelHelper; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.DisabledOnOs; +import org.junit.jupiter.api.condition.OS; + +import static org.apache.camel.management.DefaultManagementAgent.DEFAULT_DOMAIN; +import static org.apache.camel.management.DefaultManagementObjectNameStrategy.KEY_CONTEXT; +import static org.apache.camel.management.DefaultManagementObjectNameStrategy.KEY_NAME; +import static org.apache.camel.management.DefaultManagementObjectNameStrategy.KEY_TYPE; +import static org.apache.camel.management.DefaultManagementObjectNameStrategy.TYPE_CONTEXT; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@DisabledOnOs(OS.AIX) +public class XmlIoDumpRouteCoverageAdviceWithTest extends ContextTestSupport { + + @Override + protected boolean useJmx() { + return true; + } + + @Override + public boolean isUseAdviceWith() { + return true; + } + + protected MBeanServer getMBeanServer() { + return context.getManagementStrategy().getManagementAgent().getMBeanServer(); + } + + @Test + public void testDumpRouteCoverage() throws Exception { + InputStream is = this.getClass() + .getResourceAsStream("XmlIoDumpRouteCoverageAdviceWithTest.xml"); + RoutesDefinition routes = LwModelHelper.loadRoutesDefinition(is); + context.addRouteDefinition(routes.getRoutes().get(0)); + IOHelper.close(is); + + AdviceWith.adviceWith(context, "hello-process-pipeline", advice -> { + advice.weaveById("target-id").replace().to("mock:hello"); + }); + + context.start(); + + // get the stats for the route + MBeanServer mbeanServer = getMBeanServer(); + ObjectName on = getContextObjectName(); + + getMockEndpoint("mock:hello").expectedMessageCount(1); + template.sendBody("direct:hello-process", "Hello World"); + assertMockEndpointsSatisfied(); + + String xml = (String) mbeanServer.invoke(on, "dumpRoutesCoverageAsXml", null, null); + log.info(xml); + + // advice replaced <log> -> <to> which we should find in the XML dump + assertTrue(xml.contains("<to exchangesTotal=\"1\"")); + assertTrue(xml.contains("uri=\"mock:hello\"")); + assertTrue(xml.contains("sourceLineNumber=\"24\"")); + + // should be valid XML + Document doc = context.getTypeConverter().convertTo(Document.class, xml); + assertNotNull(doc); + } + + private ObjectName getContextObjectName() throws MalformedObjectNameException { + return getCamelObjectName(TYPE_CONTEXT, context.getName()); + } + + private ObjectName getCamelObjectName(String type, String name) throws MalformedObjectNameException { + String quote = "\""; + String on = DEFAULT_DOMAIN + ":" + + KEY_CONTEXT + "=" + context.getManagementName() + "," + + KEY_TYPE + "=" + type + "," + + KEY_NAME + "=" + quote + name + quote; + return ObjectName.getInstance(on); + } + +} diff --git a/components/camel-spring-parent/camel-spring-xml/src/test/resources/org/apache/camel/spring/management/SpringDumpRouteCoverageAdviceWithTest.xml b/components/camel-spring-parent/camel-spring-xml/src/test/resources/org/apache/camel/spring/management/SpringDumpRouteCoverageAdviceWithTest.xml new file mode 100644 index 000000000000..339b3b2ad7c5 --- /dev/null +++ b/components/camel-spring-parent/camel-spring-xml/src/test/resources/org/apache/camel/spring/management/SpringDumpRouteCoverageAdviceWithTest.xml @@ -0,0 +1,36 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + + 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. + +--> +<beans xmlns="http://www.springframework.org/schema/beans" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation=" + http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd + http://camel.apache.org/schema/spring http://camel.apache.org/schema/spring/camel-spring.xsd + "> + + <camelContext xmlns="http://camel.apache.org/schema/spring"> + <route id="hello-process-pipeline"> + <from uri="direct:hello-process"/> + <log message="hello-process Start"/> + <log message="some message" id="target-id"/> + <log message="hello-process Done"/> + </route> + </camelContext> + +</beans> diff --git a/components/camel-spring-parent/camel-spring-xml/src/test/resources/org/apache/camel/spring/management/XmlIoDumpRouteCoverageAdviceWithTest.xml b/components/camel-spring-parent/camel-spring-xml/src/test/resources/org/apache/camel/spring/management/XmlIoDumpRouteCoverageAdviceWithTest.xml new file mode 100644 index 000000000000..d0aa2e3475f7 --- /dev/null +++ b/components/camel-spring-parent/camel-spring-xml/src/test/resources/org/apache/camel/spring/management/XmlIoDumpRouteCoverageAdviceWithTest.xml @@ -0,0 +1,27 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + + 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. + +--> +<routes xmlns="http://camel.apache.org/schema/xml-io"> + <route id="hello-process-pipeline"> + <from uri="direct:hello-process"/> + <log message="hello-process Start"/> + <log message="some message" id="target-id"/> + <log message="hello-process Done"/> + </route> +</routes> diff --git a/core/camel-core-model/src/main/java/org/apache/camel/builder/AdviceWith.java b/core/camel-core-model/src/main/java/org/apache/camel/builder/AdviceWith.java index d21ae501fc8e..4dd8af70b4b1 100644 --- a/core/camel-core-model/src/main/java/org/apache/camel/builder/AdviceWith.java +++ b/core/camel-core-model/src/main/java/org/apache/camel/builder/AdviceWith.java @@ -202,6 +202,14 @@ public final class AdviceWith { ExtendedCamelContext ecc = camelContext.getCamelContextExtension(); Model model = camelContext.getCamelContextExtension().getContextPlugin(Model.class); + // before we can advice then the input route must be prepared + if (!definition.isPrepared()) { + RoutesDefinition routes = new RoutesDefinition(); + routes.setCamelContext(camelContext); + routes.setResource(definition.getResource()); + routes.prepareRoute(definition); + } + // inject this route into the advice route builder so it can access this route // and offer features to manipulate the route directly if (builder instanceof AdviceWithRouteBuilder arb) { diff --git a/core/camel-core-model/src/main/java/org/apache/camel/builder/AdviceWithTasks.java b/core/camel-core-model/src/main/java/org/apache/camel/builder/AdviceWithTasks.java index b7a00aa91094..5aaff2c3a22f 100644 --- a/core/camel-core-model/src/main/java/org/apache/camel/builder/AdviceWithTasks.java +++ b/core/camel-core-model/src/main/java/org/apache/camel/builder/AdviceWithTasks.java @@ -22,6 +22,7 @@ import java.util.Iterator; import java.util.List; import org.apache.camel.Endpoint; +import org.apache.camel.LineNumberAware; import org.apache.camel.model.AdviceWithDefinition; import org.apache.camel.model.ChoiceDefinition; import org.apache.camel.model.EndpointRequiredDefinition; @@ -223,14 +224,14 @@ public final class AdviceWithTasks { int index = outputs.indexOf(output); if (index != -1) { match = true; + ProcessorDefinition existing = outputs.remove(index); // flattern as replace uses a pipeline as temporary holder - ProcessorDefinition<?> flattern = flatternOutput(replace); - outputs.add(index + 1, flattern); - Object old = outputs.remove(index); + ProcessorDefinition<?> flattern = flatternOutput(replace, existing); + outputs.add(index, flattern); // must set parent on the node we added in the route ProcessorDefinition<?> parent = output.getParent() != null ? output.getParent() : route; flattern.setParent(parent); - LOG.info("AdviceWith ({}) : [{}] --> replace [{}]", matchBy.getId(), old, flattern); + LOG.info("AdviceWith ({}) : [{}] --> replace [{}]", matchBy.getId(), existing, flattern); } } } @@ -355,9 +356,9 @@ public final class AdviceWithTasks { int index = outputs.indexOf(output); if (index != -1) { match = true; + ProcessorDefinition existing = outputs.get(index); // flattern as before uses a pipeline as temporary holder - ProcessorDefinition<?> flattern = flatternOutput(before); - Object existing = outputs.get(index); + ProcessorDefinition<?> flattern = flatternOutput(before, existing); outputs.add(index, flattern); // must set parent on the node we added in the route ProcessorDefinition<?> parent = output.getParent() != null ? output.getParent() : route; @@ -425,9 +426,9 @@ public final class AdviceWithTasks { int index = outputs.indexOf(output); if (index != -1) { match = true; + ProcessorDefinition existing = outputs.get(index); // flattern as after uses a pipeline as temporary holder - ProcessorDefinition<?> flattern = flatternOutput(after); - Object existing = outputs.get(index); + ProcessorDefinition<?> flattern = flatternOutput(after, existing); outputs.add(index + 1, flattern); // must set parent on the node we added in the route ProcessorDefinition<?> parent = output.getParent() != null ? output.getParent() : route; @@ -635,17 +636,21 @@ public final class AdviceWithTasks { }; } - private static ProcessorDefinition<?> flatternOutput(ProcessorDefinition<?> output) { + private static ProcessorDefinition<?> flatternOutput(ProcessorDefinition<?> output, LineNumberAware source) { if (output instanceof AdviceWithDefinition advice) { + // copy over location from source so the advised nodes also have same location + advice.getOutputs().forEach(o -> LineNumberAware.trySetLineNumberAware(o, source)); if (advice.getOutputs().size() == 1) { - return advice.getOutputs().get(0); + output = advice.getOutputs().get(0); } else { // it should be a pipeline PipelineDefinition pipe = new PipelineDefinition(); pipe.setOutputs(advice.getOutputs()); - return pipe; + output = pipe; } } + // copy over location from source so the advised nodes also have same location + LineNumberAware.trySetLineNumberAware(output, source); return output; } diff --git a/core/camel-core/src/test/java/org/apache/camel/issues/AdviceWithWeaveByIdTest.java b/core/camel-core/src/test/java/org/apache/camel/issues/AdviceWithWeaveByIdTest.java new file mode 100644 index 000000000000..6b4c822cccbd --- /dev/null +++ b/core/camel-core/src/test/java/org/apache/camel/issues/AdviceWithWeaveByIdTest.java @@ -0,0 +1,56 @@ +/* + * 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.issues; + +import org.apache.camel.ContextTestSupport; +import org.apache.camel.builder.AdviceWith; +import org.apache.camel.builder.AdviceWithRouteBuilder; +import org.apache.camel.builder.RouteBuilder; +import org.junit.jupiter.api.Test; + +public class AdviceWithWeaveByIdTest extends ContextTestSupport { + + @Test + public void testWeaveByType() throws Exception { + AdviceWith.adviceWith(context.getRouteDefinitions().get(0), context, new AdviceWithRouteBuilder() { + @Override + public void configure() { + weaveById("target-id").replace().to("mock:baz"); + } + }); + + getMockEndpoint("mock:baz").expectedMessageCount(1); + + template.sendBody("direct:start", "World"); + + assertMockEndpointsSatisfied(); + } + + @Override + protected RouteBuilder createRouteBuilder() { + return new RouteBuilder() { + @Override + public void configure() { + from("direct:start") + .log("a") + .log("b").id("target-id") + .log("c"); + } + }; + } + +} diff --git a/core/camel-management/src/main/java/org/apache/camel/management/mbean/ManagedCamelContext.java b/core/camel-management/src/main/java/org/apache/camel/management/mbean/ManagedCamelContext.java index 6b88d05205a0..cfd2bb3d7693 100644 --- a/core/camel-management/src/main/java/org/apache/camel/management/mbean/ManagedCamelContext.java +++ b/core/camel-management/src/main/java/org/apache/camel/management/mbean/ManagedCamelContext.java @@ -894,7 +894,7 @@ public class ManagedCamelContext extends ManagedPerformanceCounter implements Ti getExchangesTotal(), getTotalProcessingTime())) .append(">\n"); - String xml = dumpRoutesAsXml(false, true); + String xml = dumpRoutesAsXml(false, true, true); if (xml != null) { // use the coverage xml parser to dump the routes and enrich with coverage stats Document dom = RouteCoverageXmlParser.parseXml(context, new ByteArrayInputStream(xml.getBytes())); diff --git a/core/camel-xml-io/src/main/java/org/apache/camel/xml/LwModelToXMLDumper.java b/core/camel-xml-io/src/main/java/org/apache/camel/xml/LwModelToXMLDumper.java index f009c7803570..eb0edb17e547 100644 --- a/core/camel-xml-io/src/main/java/org/apache/camel/xml/LwModelToXMLDumper.java +++ b/core/camel-xml-io/src/main/java/org/apache/camel/xml/LwModelToXMLDumper.java @@ -104,10 +104,12 @@ public class LwModelToXMLDumper implements ModelToXMLDumper { } // write location information if (sourceLocation || context.isDebugging()) { - String loc = (def instanceof RouteDefinition ? ((RouteDefinition) def).getInput() : def).getLocation(); int line = (def instanceof RouteDefinition ? ((RouteDefinition) def).getInput() : def).getLineNumber(); if (line != -1) { writer.addAttribute("sourceLineNumber", Integer.toString(line)); + } + String loc = (def instanceof RouteDefinition ? ((RouteDefinition) def).getInput() : def).getLocation(); + if (loc != null) { writer.addAttribute("sourceLocation", loc); } }
