This is an automated email from the ASF dual-hosted git repository. ggrzybek pushed a commit to branch main in repository https://gitbox.apache.org/repos/asf/camel.git
The following commit(s) were added to refs/heads/main by this push: new 74a60dc355f [CAMEL-18189] Read <spring:beans> from XML files (camel-xml-io) and populate Camel registry 74a60dc355f is described below commit 74a60dc355f762b5b9a25c38a3229862d83da39a Author: Grzegorz Grzybek <gr.grzy...@gmail.com> AuthorDate: Tue Jun 6 13:54:37 2023 +0200 [CAMEL-18189] Read <spring:beans> from XML files (camel-xml-io) and populate Camel registry --- .../apache/camel/model/app/BeansDefinition.java | 3 +- .../org/apache/camel/main/BaseMainSupport.java | 33 +++- .../org/apache/camel/main/RoutesConfigurer.java | 10 +- .../apache/camel/xml/io/util/XmlStreamInfo.java | 4 + .../java/org/apache/camel/xml/in/BaseParser.java | 3 +- dsl/camel-kamelet-main/pom.xml | 25 +++ .../java/org/apache/camel/main/KameletMain.java | 129 +++++++++++-- .../org/apache/camel/main/KameletMainTest.java | 73 ++++++++ .../test/java/org/apache/camel/main/app/Bean1.java | 31 ++++ .../test/java/org/apache/camel/main/app/Bean2.java | 31 ++++ .../java/org/apache/camel/main/app/Greeter.java | 49 +++++ .../org/apache/camel/main/app/GreeterMessage.java | 31 ++++ .../org/apache/camel/main/xml/spring-camel1.xml | 57 ++++++ .../camel/dsl/xml/io/XmlRoutesBuilderLoader.java | 205 +++++++++++++++------ .../apache/camel/dsl/xml/io/XmlLoadAppTest.java | 13 +- 15 files changed, 605 insertions(+), 92 deletions(-) diff --git a/core/camel-core-model/src/main/java/org/apache/camel/model/app/BeansDefinition.java b/core/camel-core-model/src/main/java/org/apache/camel/model/app/BeansDefinition.java index fbb76e81f9d..d44e3e459dd 100644 --- a/core/camel-core-model/src/main/java/org/apache/camel/model/app/BeansDefinition.java +++ b/core/camel-core-model/src/main/java/org/apache/camel/model/app/BeansDefinition.java @@ -75,7 +75,8 @@ public class BeansDefinition { // this is the only way I found to generate usable Schema without imports, while allowing elements // from different namespaces - @ExternalSchemaElement(names = { "bean", "alias" }, namespace = "http://www.springframework.org/schema/beans", + @ExternalSchemaElement(names = { "beans", "bean", "alias" }, + namespace = "http://www.springframework.org/schema/beans", documentElement = "beans") @XmlAnyElement private List<Element> springBeans = new ArrayList<>(); diff --git a/core/camel-main/src/main/java/org/apache/camel/main/BaseMainSupport.java b/core/camel-main/src/main/java/org/apache/camel/main/BaseMainSupport.java index 0c8d16c3c2a..e6a12db2d85 100644 --- a/core/camel-main/src/main/java/org/apache/camel/main/BaseMainSupport.java +++ b/core/camel-main/src/main/java/org/apache/camel/main/BaseMainSupport.java @@ -59,6 +59,7 @@ import org.apache.camel.spi.Language; import org.apache.camel.spi.PackageScanClassResolver; import org.apache.camel.spi.PeriodTaskScheduler; import org.apache.camel.spi.PropertiesComponent; +import org.apache.camel.spi.Registry; import org.apache.camel.spi.RouteTemplateParameterSource; import org.apache.camel.spi.RoutesLoader; import org.apache.camel.spi.StartupStepRecorder; @@ -443,10 +444,12 @@ public abstract class BaseMainSupport extends BaseService { mainConfigurationProperties.setRoutesIncludePattern(value); return null; }); - // eager load properties from modeline by scanning DSL sources and gather properties for auto configuration - if (camelContext.isModeline() || mainConfigurationProperties.isModeline()) { - modelineRoutes(camelContext); + if (mainConfigurationProperties.isModeline()) { + camelContext.setModeline(true); } + // eager load properties from modeline by scanning DSL sources and gather properties for auto configuration + // also load other non-route related configuration (e.g., beans) + modelineRoutes(camelContext); autoConfigurationMainConfiguration(camelContext, mainConfigurationProperties, autoConfiguredProperties); } @@ -885,6 +888,17 @@ public abstract class BaseMainSupport extends BaseService { // configure the common/default options DefaultConfigurationConfigurer.configure(camelContext, config); + + // org.apache.camel.spring.boot.CamelAutoConfiguration (camel-spring-boot) also calls the methods + // on DefaultConfigurationConfigurer, but the CamelContext being configured is already + // org.apache.camel.spring.boot.SpringBootCamelContext, which has access to Spring's ApplicationContext. + // That's why DefaultConfigurationConfigurer.afterConfigure() can alter CamelContext using beans from + // Spring's ApplicationContext. + // so here, before configuring Camel Context, we can process the registry and let Main implementations + // decide how to do it + Registry registry = camelContext.getCamelContextExtension().getRegistry(); + postProcessCamelRegistry(camelContext, config, registry); + // lookup and configure SPI beans DefaultConfigurationConfigurer.afterConfigure(camelContext); @@ -1132,6 +1146,19 @@ public abstract class BaseMainSupport extends BaseService { DefaultConfigurationConfigurer.afterPropertiesSet(camelContext); } + /** + * Main implementation may do some additional configuration of the {@link Registry} before it's used to + * (re)configure Camel context. + * + * @param camelContext + * @param config + * @param registry + */ + protected void postProcessCamelRegistry( + CamelContext camelContext, MainConfigurationProperties config, + Registry registry) { + } + private void setRouteTemplateProperties( CamelContext camelContext, OrderedLocationProperties routeTemplateProperties, OrderedLocationProperties autoConfiguredProperties) diff --git a/core/camel-main/src/main/java/org/apache/camel/main/RoutesConfigurer.java b/core/camel-main/src/main/java/org/apache/camel/main/RoutesConfigurer.java index ac2e1ff0a5f..36558c59395 100644 --- a/core/camel-main/src/main/java/org/apache/camel/main/RoutesConfigurer.java +++ b/core/camel-main/src/main/java/org/apache/camel/main/RoutesConfigurer.java @@ -302,12 +302,14 @@ public class RoutesConfigurer { protected void doConfigureModeline(CamelContext camelContext, Collection<Resource> resources, boolean optional) throws Exception { - ModelineFactory factory = PluginHelper.getModelineFactory(camelContext); RoutesLoader loader = PluginHelper.getRoutesLoader(camelContext); - for (Resource resource : resources) { - LOG.debug("Parsing modeline: {}", resource); - factory.parseModeline(resource); + if (camelContext.isModeline()) { + ModelineFactory factory = PluginHelper.getModelineFactory(camelContext); + for (Resource resource : resources) { + LOG.debug("Parsing modeline: {}", resource); + factory.parseModeline(resource); + } } // the resource may also have additional configurations which we need to detect via pre-parsing for (Resource resource : resources) { diff --git a/core/camel-xml-io-util/src/main/java/org/apache/camel/xml/io/util/XmlStreamInfo.java b/core/camel-xml-io-util/src/main/java/org/apache/camel/xml/io/util/XmlStreamInfo.java index d6c8543fa44..0c2c9159295 100644 --- a/core/camel-xml-io-util/src/main/java/org/apache/camel/xml/io/util/XmlStreamInfo.java +++ b/core/camel-xml-io-util/src/main/java/org/apache/camel/xml/io/util/XmlStreamInfo.java @@ -58,6 +58,10 @@ public class XmlStreamInfo { return problem; } + public void setProblem(Throwable problem) { + this.problem = problem; + } + public String getRootElementName() { return rootElementName; } diff --git a/core/camel-xml-io/src/main/java/org/apache/camel/xml/in/BaseParser.java b/core/camel-xml-io/src/main/java/org/apache/camel/xml/in/BaseParser.java index d0c4db88240..b4a0b9701b1 100644 --- a/core/camel-xml-io/src/main/java/org/apache/camel/xml/in/BaseParser.java +++ b/core/camel-xml-io/src/main/java/org/apache/camel/xml/in/BaseParser.java @@ -386,7 +386,8 @@ public class BaseParser { protected AttributeHandler<Element> domAttributeHandler() { return (el, name, value) -> { - el.setAttribute(name, value); + // for now, handle only XMLs where schema declares attributeFormDefault="unqualified" + el.setAttributeNS(null, name, value); return true; }; } diff --git a/dsl/camel-kamelet-main/pom.xml b/dsl/camel-kamelet-main/pom.xml index d346887edb6..2cba8b8dbf4 100644 --- a/dsl/camel-kamelet-main/pom.xml +++ b/dsl/camel-kamelet-main/pom.xml @@ -125,6 +125,31 @@ <artifactId>camel-groovy-dsl</artifactId> <scope>test</scope> </dependency> + <dependency> + <groupId>org.apache.camel</groupId> + <artifactId>camel-xml-io</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.apache.camel</groupId> + <artifactId>camel-xml-io-dsl</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.apache.camel</groupId> + <artifactId>camel-direct</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.apache.camel</groupId> + <artifactId>camel-mock</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.apache.camel</groupId> + <artifactId>camel-rest</artifactId> + <scope>test</scope> + </dependency> <!-- Entire Maven downloading/resolution support is made using camel-tooling-maven --> <dependency> diff --git a/dsl/camel-kamelet-main/src/main/java/org/apache/camel/main/KameletMain.java b/dsl/camel-kamelet-main/src/main/java/org/apache/camel/main/KameletMain.java index 1f320594f86..e72f650bb03 100644 --- a/dsl/camel-kamelet-main/src/main/java/org/apache/camel/main/KameletMain.java +++ b/dsl/camel-kamelet-main/src/main/java/org/apache/camel/main/KameletMain.java @@ -16,12 +16,20 @@ */ package org.apache.camel.main; +import java.io.ByteArrayInputStream; import java.io.File; +import java.io.IOException; +import java.io.InputStream; import java.util.ArrayList; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Set; +import java.util.TreeMap; +import java.util.function.Supplier; + +import org.w3c.dom.Document; import org.apache.camel.CamelContext; import org.apache.camel.ManagementStatisticsLevel; @@ -68,6 +76,10 @@ import org.apache.camel.support.DefaultContextReloadStrategy; import org.apache.camel.support.PluginHelper; import org.apache.camel.support.RouteOnDemandReloadStrategy; import org.apache.camel.support.service.ServiceHelper; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.beans.factory.xml.XmlBeanDefinitionReader; +import org.springframework.core.io.AbstractResource; /** * A Main class for booting up Camel with Kamelet in standalone mode. @@ -443,27 +455,31 @@ public class KameletMain extends MainCommandLineSupport { KnownDependenciesResolver known = new KnownDependenciesResolver(answer); known.loadKnownDependencies(); - DependencyDownloaderPropertyBindingListener listener - = new DependencyDownloaderPropertyBindingListener(answer, known); - answer.getCamelContextExtension().getRegistry() - .bind(DependencyDownloaderPropertyBindingListener.class.getSimpleName(), listener); - answer.getCamelContextExtension().getRegistry().bind(DependencyDownloaderStrategy.class.getSimpleName(), - new DependencyDownloaderStrategy(answer)); - answer.setClassResolver(new DependencyDownloaderClassResolver(answer, known)); - answer.getCamelContextExtension().addContextPlugin(ComponentResolver.class, - new DependencyDownloaderComponentResolver(answer, stub)); - answer.getCamelContextExtension().addContextPlugin(UriFactoryResolver.class, - new DependencyDownloaderUriFactoryResolver(answer)); - answer.getCamelContextExtension().addContextPlugin(DataFormatResolver.class, - new DependencyDownloaderDataFormatResolver(answer)); - answer.getCamelContextExtension().addContextPlugin(LanguageResolver.class, - new DependencyDownloaderLanguageResolver(answer)); - answer.getCamelContextExtension().addContextPlugin(ResourceLoader.class, - new DependencyDownloaderResourceLoader(answer)); + if (download) { + DependencyDownloaderPropertyBindingListener listener + = new DependencyDownloaderPropertyBindingListener(answer, known); + answer.getCamelContextExtension().getRegistry() + .bind(DependencyDownloaderPropertyBindingListener.class.getSimpleName(), listener); + answer.getCamelContextExtension().getRegistry().bind(DependencyDownloaderStrategy.class.getSimpleName(), + new DependencyDownloaderStrategy(answer)); + answer.setClassResolver(new DependencyDownloaderClassResolver(answer, known)); + answer.getCamelContextExtension().addContextPlugin(ComponentResolver.class, + new DependencyDownloaderComponentResolver(answer, stub)); + answer.getCamelContextExtension().addContextPlugin(UriFactoryResolver.class, + new DependencyDownloaderUriFactoryResolver(answer)); + answer.getCamelContextExtension().addContextPlugin(DataFormatResolver.class, + new DependencyDownloaderDataFormatResolver(answer)); + answer.getCamelContextExtension().addContextPlugin(LanguageResolver.class, + new DependencyDownloaderLanguageResolver(answer)); + answer.getCamelContextExtension().addContextPlugin(ResourceLoader.class, + new DependencyDownloaderResourceLoader(answer)); + } answer.setInjector(new KameletMainInjector(answer.getInjector(), stub)); - answer.addService(new DependencyDownloaderKamelet(answer)); - answer.getCamelContextExtension().getRegistry().bind(DownloadModelineParser.class.getSimpleName(), - new DownloadModelineParser(answer)); + if (download) { + answer.addService(new DependencyDownloaderKamelet(answer)); + answer.getCamelContextExtension().getRegistry().bind(DownloadModelineParser.class.getSimpleName(), + new DownloadModelineParser(answer)); + } // reloader String sourceDir = getInitialProperties().getProperty("camel.jbang.sourceDir"); @@ -577,6 +593,79 @@ public class KameletMain extends MainCommandLineSupport { return sb.toString(); } + @Override + protected void postProcessCamelRegistry(CamelContext camelContext, MainConfigurationProperties config, Registry registry) { + // camel-kamelet-main has access to Spring libraries, so we can grab XML documents representing + // actual Spring Beans and read them using Spring's BeanFactory to populate Camel registry + final Map<String, Document> xmls = new TreeMap<>(); + + Map<String, Document> springBeansDocs = registry.findByTypeWithName(Document.class); + if (springBeansDocs != null) { + springBeansDocs.forEach((id, doc) -> { + if (id.startsWith("spring-document:")) { + xmls.put(id, doc); + } + }); + } + + // we _could_ create something like org.apache.camel.spring.spi.ApplicationContextBeanRepository, but + // wrapping DefaultListableBeanFactory and use it as one of the + // org.apache.camel.support.DefaultRegistry.repositories, but for now let's use it to populate + // Spring registry and then copy the beans (whether the scope is) + final DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); + beanFactory.setAllowCircularReferences(true); // for now + + // register some existing beans (the list may change) + Set<String> infraBeanNames = Set.of("CamelContext", "MainConfiguration"); + beanFactory.registerSingleton("CamelContext", camelContext); + beanFactory.registerSingleton("MainConfiguration", config); + // ... + + // instead of generating an MX parser for spring-beans.xsd and use it to read the docs, we can simply + // pass w3c Documents directly to Spring + final XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(beanFactory); + xmls.forEach((id, doc) -> { + reader.registerBeanDefinitions(doc, new AbstractResource() { + @Override + public String getDescription() { + return id; + } + + @Override + public InputStream getInputStream() throws IOException { + return new ByteArrayInputStream(new byte[0]); + } + }); + }); + + // for full interaction between Spring ApplicationContext and its BeanFactory see + // org.springframework.context.support.AbstractApplicationContext.refresh() + // see org.springframework.context.support.AbstractApplicationContext.prepareBeanFactory() to check + // which extra/infra beans are added + + beanFactory.freezeConfiguration(); + beanFactory.preInstantiateSingletons(); + + for (String name : beanFactory.getBeanDefinitionNames()) { + if (infraBeanNames.contains(name)) { + continue; + } + BeanDefinition def = beanFactory.getBeanDefinition(name); + if (def.isSingleton()) { + // just grab the singleton and put into registry + registry.bind(name, beanFactory.getBean(name)); + } else { + // rely on the bean factory to implement prototype scope + registry.bind(name, (Supplier<Object>) () -> beanFactory.getBean(name)); + } + } + } + + @Override + protected void doShutdown() throws Exception { + // TODO: manage BeanFactory as a field and clear the beans here + } + private static String getPid() { return String.valueOf(ProcessHandle.current().pid()); } diff --git a/dsl/camel-kamelet-main/src/test/java/org/apache/camel/main/KameletMainTest.java b/dsl/camel-kamelet-main/src/test/java/org/apache/camel/main/KameletMainTest.java new file mode 100644 index 00000000000..33257f6c3f2 --- /dev/null +++ b/dsl/camel-kamelet-main/src/test/java/org/apache/camel/main/KameletMainTest.java @@ -0,0 +1,73 @@ +/* + * 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.main; + +import java.util.function.BiConsumer; + +import org.apache.camel.CamelContext; +import org.apache.camel.component.mock.MockEndpoint; +import org.apache.camel.main.app.Bean1; +import org.apache.camel.main.app.Bean2; +import org.apache.camel.support.ShortUuidGenerator; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +public class KameletMainTest { + + @Test + public void testRouteWithSpringProcessor() throws Exception { + doTestMain("classpath:org/apache/camel/main/xml/spring-camel1.xml", (main, camelContext) -> { + try { + MockEndpoint endpoint = camelContext.getEndpoint("mock:finish", MockEndpoint.class); + endpoint.expectedBodiesReceived("Hello World (2147483647)"); + + main.getCamelTemplate().sendBody("direct:start", "I'm World"); + + endpoint.assertIsSatisfied(); + + assertTrue(camelContext.getUuidGenerator() instanceof ShortUuidGenerator); + + Bean1 bean1 = main.lookupByType(Bean1.class).get("bean1"); + Bean2 bean2 = bean1.getBean(); + assertSame(bean1, bean2.getBean()); + } catch (Exception e) { + fail(e.getMessage()); + } + }); + } + + protected void doTestMain(String includes, BiConsumer<KameletMain, CamelContext> consumer) throws Exception { + KameletMain main = new KameletMain(); + + main.setDownload(false); + main.configure().withRoutesIncludePattern(includes); + main.configure().withAutoConfigurationEnabled(true); + main.start(); + + CamelContext camelContext = main.getCamelContext(); + assertNotNull(camelContext); + + consumer.accept(main, camelContext); + + main.stop(); + } + +} diff --git a/dsl/camel-kamelet-main/src/test/java/org/apache/camel/main/app/Bean1.java b/dsl/camel-kamelet-main/src/test/java/org/apache/camel/main/app/Bean1.java new file mode 100644 index 00000000000..ddcfda8d373 --- /dev/null +++ b/dsl/camel-kamelet-main/src/test/java/org/apache/camel/main/app/Bean1.java @@ -0,0 +1,31 @@ +/* + * 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.main.app; + +public class Bean1 { + + private Bean2 bean; + + public void setBean(Bean2 bean) { + this.bean = bean; + } + + public Bean2 getBean() { + return bean; + } + +} diff --git a/dsl/camel-kamelet-main/src/test/java/org/apache/camel/main/app/Bean2.java b/dsl/camel-kamelet-main/src/test/java/org/apache/camel/main/app/Bean2.java new file mode 100644 index 00000000000..97845c9a4ca --- /dev/null +++ b/dsl/camel-kamelet-main/src/test/java/org/apache/camel/main/app/Bean2.java @@ -0,0 +1,31 @@ +/* + * 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.main.app; + +public class Bean2 { + + private Bean1 bean; + + public void setBean(Bean1 bean) { + this.bean = bean; + } + + public Bean1 getBean() { + return bean; + } + +} diff --git a/dsl/camel-kamelet-main/src/test/java/org/apache/camel/main/app/Greeter.java b/dsl/camel-kamelet-main/src/test/java/org/apache/camel/main/app/Greeter.java new file mode 100644 index 00000000000..1ec8ef91036 --- /dev/null +++ b/dsl/camel-kamelet-main/src/test/java/org/apache/camel/main/app/Greeter.java @@ -0,0 +1,49 @@ +/* + * 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.main.app; + +import org.apache.camel.Exchange; +import org.apache.camel.Processor; +import org.apache.camel.util.StringHelper; + +public class Greeter implements Processor { + + private Bean1 bean; + + private GreeterMessage message; + + private Integer number; + + public void setMessage(GreeterMessage message) { + this.message = message; + } + + public void setNumber(Integer number) { + this.number = number; + } + + public void setBean(Bean1 bean) { + this.bean = bean; + } + + @Override + public void process(Exchange exchange) throws Exception { + String msg = exchange.getIn().getBody(String.class); + exchange.getIn().setBody(message.getMsg() + " " + StringHelper.after(msg, "I'm ") + " (" + number + ")"); + } + +} diff --git a/dsl/camel-kamelet-main/src/test/java/org/apache/camel/main/app/GreeterMessage.java b/dsl/camel-kamelet-main/src/test/java/org/apache/camel/main/app/GreeterMessage.java new file mode 100644 index 00000000000..11d4e759967 --- /dev/null +++ b/dsl/camel-kamelet-main/src/test/java/org/apache/camel/main/app/GreeterMessage.java @@ -0,0 +1,31 @@ +/* + * 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.main.app; + +public class GreeterMessage { + + private String msg; + + public String getMsg() { + return msg; + } + + public void setMsg(String msg) { + this.msg = msg; + } + +} diff --git a/dsl/camel-kamelet-main/src/test/resources/org/apache/camel/main/xml/spring-camel1.xml b/dsl/camel-kamelet-main/src/test/resources/org/apache/camel/main/xml/spring-camel1.xml new file mode 100644 index 00000000000..887aca50431 --- /dev/null +++ b/dsl/camel-kamelet-main/src/test/resources/org/apache/camel/main/xml/spring-camel1.xml @@ -0,0 +1,57 @@ +<?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. + +--> +<camel> + + <beans xmlns="http://www.springframework.org/schema/beans" xmlns:util="http://www.springframework.org/schema/util"> + + <util:constant id="max" static-field="java.lang.Integer.MAX_VALUE" /> + + <bean id="bean1" class="org.apache.camel.main.app.Bean1"> + <property name="bean" ref="bean2"/> + </bean> + <bean id="bean2" class="org.apache.camel.main.app.Bean2"> + <property name="bean" ref="bean1"/> + </bean> + + <bean id="messageString" class="java.lang.String"> + <constructor-arg index="0" value="Hello"/> + </bean> + + <bean id="greeter" class="org.apache.camel.main.app.Greeter"> + <description>Real Spring Bean</description> + <property name="bean" ref="bean1"/> + <property name="number" ref="max"/> + <property name="message"> + <bean class="org.apache.camel.main.app.GreeterMessage"> + <property name="msg" ref="messageString"/> + </bean> + </property> + </bean> + + <bean class="org.apache.camel.support.ShortUuidGenerator"/> + </beans> + + <route id="r1"> + <from uri="direct:start"/> + <bean ref="greeter"/> + <to uri="mock:finish"/> + </route> + +</camel> diff --git a/dsl/camel-xml-io-dsl/src/main/java/org/apache/camel/dsl/xml/io/XmlRoutesBuilderLoader.java b/dsl/camel-xml-io-dsl/src/main/java/org/apache/camel/dsl/xml/io/XmlRoutesBuilderLoader.java index 1709a018e1d..b7731c717ed 100644 --- a/dsl/camel-xml-io-dsl/src/main/java/org/apache/camel/dsl/xml/io/XmlRoutesBuilderLoader.java +++ b/dsl/camel-xml-io-dsl/src/main/java/org/apache/camel/dsl/xml/io/XmlRoutesBuilderLoader.java @@ -16,9 +16,16 @@ */ package org.apache.camel.dsl.xml.io; +import java.io.IOException; import java.util.ArrayList; +import java.util.LinkedHashSet; import java.util.List; +import java.util.Map; import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicInteger; + +import org.w3c.dom.Document; import org.apache.camel.BindToRegistry; import org.apache.camel.CamelContextAware; @@ -60,6 +67,12 @@ public class XmlRoutesBuilderLoader extends RouteBuilderLoaderSupport { public static final String NAMESPACE = "http://camel.apache.org/schema/spring"; private static final List<String> NAMESPACES = List.of("", NAMESPACE); + private final Map<String, Resource> resourceCache = new ConcurrentHashMap<>(); + private final Map<String, XmlStreamInfo> xmlInfoCache = new ConcurrentHashMap<>(); + private final Map<String, BeansDefinition> camelAppCache = new ConcurrentHashMap<>(); + + private final AtomicInteger counter = new AtomicInteger(0); + public XmlRoutesBuilderLoader() { super(EXTENSION); } @@ -68,15 +81,28 @@ public class XmlRoutesBuilderLoader extends RouteBuilderLoaderSupport { super(extension); } + @Override + public void preParseRoute(Resource resource) throws Exception { + // preparsing is done at early stage, so we have a chance to load additional beans and populate + // Camel registry + XmlStreamInfo xmlInfo = xmlInfo(resource); + if (xmlInfo.isValid()) { + String root = xmlInfo.getRootElementName(); + if ("beans".equals(root) || "camel".equals(root)) { + new ModelParser(resource, xmlInfo.getRootElementNamespace()) + .parseBeansDefinition() + .ifPresent(bd -> { + registerBeans(resource, bd); + camelAppCache.put(resource.getLocation(), bd); + }); + } + } + } + @Override public RouteBuilder doLoadRouteBuilder(Resource input) throws Exception { - final Resource resource = new CachedResource(input); - // instead of parsing the document NxM times (for each namespace x root element combination), - // we preparse it using XmlStreamDetector and then parse it fully knowing what's inside. - // we could even do better, by passing already preparsed information through config file, but - // it's getting complicated when using multiple files. - XmlStreamDetector detector = new XmlStreamDetector(resource.getInputStream()); - XmlStreamInfo xmlInfo = detector.information(); + final Resource resource = resource(input); + XmlStreamInfo xmlInfo = xmlInfo(input); if (!xmlInfo.isValid()) { // should be valid, because we checked it before LOG.warn("Invalid XML document: {}", xmlInfo.getProblem().getMessage()); @@ -86,11 +112,18 @@ public class XmlRoutesBuilderLoader extends RouteBuilderLoaderSupport { return new RouteConfigurationBuilder() { @Override public void configure() throws Exception { + String resourceLocation = input.getLocation(); switch (xmlInfo.getRootElementName()) { - case "beans", "camel" -> - new ModelParser(resource, xmlInfo.getRootElementNamespace()) - .parseBeansDefinition() - .ifPresent(this::allInOne); + case "beans", "camel" -> { + BeansDefinition def = camelAppCache.get(resourceLocation); + if (def != null) { + configureCamel(def); + } else { + new ModelParser(resource, xmlInfo.getRootElementNamespace()) + .parseBeansDefinition() + .ifPresent(this::configureCamel); + } + } case "routeTemplate", "routeTemplates" -> new ModelParser(resource, xmlInfo.getRootElementNamespace()) .parseRouteTemplatesDefinition() @@ -110,6 +143,12 @@ public class XmlRoutesBuilderLoader extends RouteBuilderLoaderSupport { default -> { } } + + // knowing this is the last time an XML may have been parsed, we can clear the cache + // (route may get reloaded later) + resourceCache.remove(resourceLocation); + xmlInfoCache.remove(resourceLocation); + camelAppCache.remove(resourceLocation); } @Override @@ -124,55 +163,11 @@ public class XmlRoutesBuilderLoader extends RouteBuilderLoaderSupport { } } - private void allInOne(BeansDefinition app) { - // selected elements which can be found in camel-spring-xml's <camelContext> - - List<String> packagesToScan = new ArrayList<>(); - app.getComponentScanning().forEach(cs -> { - packagesToScan.add(cs.getBasePackage()); - }); - if (!packagesToScan.isEmpty()) { - Registry registry = getCamelContext().getRegistry(); - if (registry != null) { - PackageScanClassResolver scanner - = getCamelContext().getCamelContextExtension().getContextPlugin(PackageScanClassResolver.class); - Injector injector = getCamelContext().getInjector(); - if (scanner != null && injector != null) { - for (String pkg : packagesToScan) { - Set<Class<?>> classes = scanner.findAnnotated(BindToRegistry.class, pkg); - for (Class<?> c : classes) { - // should: - // - call org.apache.camel.spi.CamelBeanPostProcessor.postProcessBeforeInitialization - // - call org.apache.camel.spi.CamelBeanPostProcessor.postProcessAfterInitialization - // - bind to registry if @org.apache.camel.BindToRegistry is present - injector.newInstance(c, true); - } - } - } - } - } - - for (RegistryBeanDefinition bean : app.getBeans()) { - String type = bean.getType(); - String name = bean.getName(); - if (name == null || "".equals(name.trim())) { - name = type; - } - if (type != null && !type.startsWith("#")) { - type = "#class:" + type; - try { - final Object target = PropertyBindingSupport.resolveBean(getCamelContext(), type); - - if (bean.getProperties() != null && !bean.getProperties().isEmpty()) { - PropertyBindingSupport.setPropertiesOnTarget(getCamelContext(), target, bean.getProperties()); - } - getCamelContext().getRegistry().unbind(name); - getCamelContext().getRegistry().bind(name, target); - } catch (Exception e) { - LOG.warn("Problem creating bean {}", type, e); - } - } - } + private void configureCamel(BeansDefinition app) { + // we have access to beans and spring beans, but these are already processed + // in preParseRoute() and possibly registered in + // org.apache.camel.main.BaseMainSupport.postProcessCamelRegistry() (if given Main implementation + // decides to do so) app.getRests().forEach(r -> { List<RestDefinition> list = new ArrayList<>(); @@ -236,4 +231,94 @@ public class XmlRoutesBuilderLoader extends RouteBuilderLoaderSupport { } }; } + + @Override + protected void doStop() throws Exception { + resourceCache.clear(); + xmlInfoCache.clear(); + camelAppCache.clear(); + } + + private Resource resource(Resource resource) { + return resourceCache.computeIfAbsent(resource.getLocation(), l -> new CachedResource(resource)); + } + + private XmlStreamInfo xmlInfo(Resource resource) { + return xmlInfoCache.computeIfAbsent(resource.getLocation(), l -> { + try { + // instead of parsing the document NxM times (for each namespace x root element combination), + // we preparse it using XmlStreamDetector and then parse it fully knowing what's inside. + // we could even do better, by passing already preparsed information through config file, but + // it's getting complicated when using multiple files. + XmlStreamDetector detector = new XmlStreamDetector(resource.getInputStream()); + return detector.information(); + } catch (IOException e) { + XmlStreamInfo invalid = new XmlStreamInfo(); + invalid.setProblem(e); + return invalid; + } + }); + } + + private void registerBeans(Resource resource, BeansDefinition app) { + // <component-scan> - discover and register beans directly with Camel injection + Set<String> packagesToScan = new LinkedHashSet<>(); + app.getComponentScanning().forEach(cs -> { + packagesToScan.add(cs.getBasePackage()); + }); + if (!packagesToScan.isEmpty()) { + Registry registry = getCamelContext().getRegistry(); + if (registry != null) { + PackageScanClassResolver scanner + = getCamelContext().getCamelContextExtension().getContextPlugin(PackageScanClassResolver.class); + Injector injector = getCamelContext().getInjector(); + if (scanner != null && injector != null) { + for (String pkg : packagesToScan) { + Set<Class<?>> classes = scanner.findAnnotated(BindToRegistry.class, pkg); + for (Class<?> c : classes) { + // should: + // - call org.apache.camel.spi.CamelBeanPostProcessor.postProcessBeforeInitialization + // - call org.apache.camel.spi.CamelBeanPostProcessor.postProcessAfterInitialization + // - bind to registry if @org.apache.camel.BindToRegistry is present + injector.newInstance(c, true); + } + } + } + } + } + + // <bean>s - register Camel beans directly with Camel injection + for (RegistryBeanDefinition bean : app.getBeans()) { + String type = bean.getType(); + String name = bean.getName(); + if (name == null || "".equals(name.trim())) { + name = type; + } + if (type != null && !type.startsWith("#")) { + type = "#class:" + type; + try { + final Object target = PropertyBindingSupport.resolveBean(getCamelContext(), type); + + if (bean.getProperties() != null && !bean.getProperties().isEmpty()) { + PropertyBindingSupport.setPropertiesOnTarget(getCamelContext(), target, bean.getProperties()); + } + getCamelContext().getRegistry().unbind(name); + getCamelContext().getRegistry().bind(name, target); + } catch (Exception e) { + LOG.warn("Problem creating bean {}", type, e); + } + } + } + + // <s:bean>, <s:beans> and <s:alias> elements - all the elements in single BeansDefinition have + // one parent org.w3c.dom.Document - and this is what we collect from each resource + if (!app.getSpringBeans().isEmpty()) { + Document doc = app.getSpringBeans().get(0).getOwnerDocument(); + // bind as Document, to be picked up later - bean id allows nice sorting + // (can also be single ID - documents will get collected in LinkedHashMap, so we'll be fine) + String id = String.format("spring-document:%05d:%s", counter.incrementAndGet(), resource.getLocation()); + getCamelContext().getRegistry().bind(id, doc); + } + } + } diff --git a/dsl/camel-xml-io-dsl/src/test/java/org/apache/camel/dsl/xml/io/XmlLoadAppTest.java b/dsl/camel-xml-io-dsl/src/test/java/org/apache/camel/dsl/xml/io/XmlLoadAppTest.java index ddfe78459ea..867e086a69c 100644 --- a/dsl/camel-xml-io-dsl/src/test/java/org/apache/camel/dsl/xml/io/XmlLoadAppTest.java +++ b/dsl/camel-xml-io-dsl/src/test/java/org/apache/camel/dsl/xml/io/XmlLoadAppTest.java @@ -20,6 +20,7 @@ import org.apache.camel.component.mock.MockEndpoint; import org.apache.camel.dsl.xml.io.beans.GreeterMessage; import org.apache.camel.impl.DefaultCamelContext; import org.apache.camel.spi.Resource; +import org.apache.camel.spi.RoutesLoader; import org.apache.camel.support.PluginHelper; import org.junit.jupiter.api.Test; @@ -43,7 +44,9 @@ public class XmlLoadAppTest { Resource resource = PluginHelper.getResourceLoader(context).resolveResource( "/org/apache/camel/dsl/xml/io/" + r); - PluginHelper.getRoutesLoader(context).loadRoutes(resource); + RoutesLoader routesLoader = PluginHelper.getRoutesLoader(context); + routesLoader.preParseRoute(resource, false); + routesLoader.loadRoutes(resource); } assertNotNull(context.getRoute("r1"), "Loaded r1 route should be there"); @@ -77,7 +80,9 @@ public class XmlLoadAppTest { Resource resource = PluginHelper.getResourceLoader(context).resolveResource( "/org/apache/camel/dsl/xml/io/camel-app3.xml"); - PluginHelper.getRoutesLoader(context).loadRoutes(resource); + RoutesLoader routesLoader = PluginHelper.getRoutesLoader(context); + routesLoader.preParseRoute(resource, false); + routesLoader.loadRoutes(resource); assertNotNull(context.getRoute("r3"), "Loaded r3 route should be there"); assertEquals(1, context.getRoutes().size()); @@ -101,7 +106,9 @@ public class XmlLoadAppTest { Resource resource = PluginHelper.getResourceLoader(context).resolveResource( "/org/apache/camel/dsl/xml/io/camel-app4.xml"); - PluginHelper.getRoutesLoader(context).loadRoutes(resource); + RoutesLoader routesLoader = PluginHelper.getRoutesLoader(context); + routesLoader.preParseRoute(resource, false); + routesLoader.loadRoutes(resource); assertNotNull(context.getRoute("r4"), "Loaded r4 route should be there"); assertEquals(1, context.getRoutes().size());