You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@camel.apache.org by gg...@apache.org on 2023/06/07 10:49:20 UTC
[camel] branch main updated: [CAMEL-18189] Read from XML files (camel-xml-io) and populate Camel registry
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...@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());