This is an automated email from the ASF dual-hosted git repository. davsclaus 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 c51b05200fe Blueprint (#11240) c51b05200fe is described below commit c51b05200feb5bac00cb0d22716062bbe9746208 Author: Claus Ibsen <claus.ib...@gmail.com> AuthorDate: Wed Aug 30 19:43:46 2023 +0200 Blueprint (#11240) CAMEL-19806: camel-jbang - Allow to load OSGi <blueprint> with embedded <camelContext> CAMEL-19807: Move spring XML <beans> logic to its own class --- .../apache/camel/catalog/schemas/camel-spring.xsd | 1 + .../apache/camel/model/app/BeansDefinition.java | 14 + .../java/org/apache/camel/xml/in/ModelParser.java | 14 +- .../java/org/apache/camel/xml/out/ModelWriter.java | 3 +- .../java/org/apache/camel/xml/in/BaseParser.java | 70 +++-- .../org/apache/camel/yaml/out/ModelWriter.java | 3 +- .../apache/camel/dsl/jbang/core/commands/Run.java | 2 +- .../java/org/apache/camel/main/KameletMain.java | 293 ++----------------- .../java/org/apache/camel/main/util/XmlHelper.java | 12 + .../xml/blueprint/BlueprintXmlBeansHandler.java | 224 +++++++++++++++ .../main/xml/spring/SpringXmlBeansHandler.java | 316 +++++++++++++++++++++ .../apache/camel/dsl/xml/io/XmlModelParser.java | 20 +- .../camel/dsl/xml/io/XmlRoutesBuilderLoader.java | 15 +- ...eansLoadTest.java => XmlBlueprintLoadTest.java} | 13 +- .../camel/dsl/xml/io/XmlSpringBeansLoadTest.java | 8 +- .../apache/camel/dsl/xml/io/blueprintRoutes.xml | 50 ++++ .../packaging/ModelXmlParserGeneratorMojo.java | 4 +- 17 files changed, 742 insertions(+), 320 deletions(-) diff --git a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/schemas/camel-spring.xsd b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/schemas/camel-spring.xsd index b4bdabc4b57..4572bd2431d 100644 --- a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/schemas/camel-spring.xsd +++ b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/schemas/camel-spring.xsd @@ -13583,6 +13583,7 @@ org.apache.camel.builder.RouteBuilder. </xs:element> <xs:element maxOccurs="unbounded" minOccurs="0" name="bean" type="tns:registryBeanDefinition"/> <xs:any maxOccurs="unbounded" minOccurs="0" namespace="##other" processContents="skip"/> + <xs:any maxOccurs="unbounded" minOccurs="0" namespace="##other" processContents="skip"/> <xs:element maxOccurs="unbounded" minOccurs="0" ref="tns:restConfiguration"/> <xs:element maxOccurs="unbounded" minOccurs="0" ref="tns:rest"/> <xs:element maxOccurs="unbounded" minOccurs="0" ref="tns:routeConfiguration"/> 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 debe7c0a9f0..a8230ccff11 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 @@ -52,6 +52,7 @@ import org.apache.camel.spi.annotations.ExternalSchemaElement; "componentScanning", "beans", "springBeans", + "blueprintBeans", "restConfigurations", "rests", "routeConfigurations", @@ -82,6 +83,11 @@ public class BeansDefinition { documentElement = "beans") @XmlAnyElement private List<Element> springBeans = new ArrayList<>(); + @ExternalSchemaElement(names = { "bean" }, + namespace = "http://www.osgi.org/xmlns/blueprint/v1.0.0", + documentElement = "blueprint") + @XmlAnyElement + private List<Element> blueprintBeans = new ArrayList<>(); // the order comes from <camelContext> (org.apache.camel.spring.xml.CamelContextFactoryBean) // to make things less confusing, as it's not easy to simply tell JAXB to use <xsd:choice maxOccurs="unbounded"> @@ -127,6 +133,14 @@ public class BeansDefinition { this.springBeans = springBeans; } + public List<Element> getBlueprintBeans() { + return blueprintBeans; + } + + public void setBlueprintBeans(List<Element> blueprintBeans) { + this.blueprintBeans = blueprintBeans; + } + public List<RestConfigurationDefinition> getRestConfigurations() { return restConfigurations; } diff --git a/core/camel-xml-io/src/generated/java/org/apache/camel/xml/in/ModelParser.java b/core/camel-xml-io/src/generated/java/org/apache/camel/xml/in/ModelParser.java index 074c655829e..d8890ea2ce6 100644 --- a/core/camel-xml-io/src/generated/java/org/apache/camel/xml/in/ModelParser.java +++ b/core/camel-xml-io/src/generated/java/org/apache/camel/xml/in/ModelParser.java @@ -1568,7 +1568,7 @@ public class ModelParser extends BaseParser { } public Optional<ApplicationDefinition> parseApplicationDefinition() throws IOException, XmlPullParserException { - String tag = getNextTag("beans", "camel"); + String tag = getNextTag("beans", "blueprint", "camel"); if (tag != null) { return Optional.of(doParseApplicationDefinition()); } @@ -1580,7 +1580,7 @@ public class ModelParser extends BaseParser { } public Optional<BeansDefinition> parseBeansDefinition() throws IOException, XmlPullParserException { - String tag = getNextTag("beans", "camel"); + String tag = getNextTag("beans", "blueprint", "camel"); if (tag != null) { return Optional.of(doParseBeansDefinition()); } @@ -1588,7 +1588,15 @@ public class ModelParser extends BaseParser { } protected <T extends BeansDefinition> ElementHandler<T> beansDefinitionElementHandler() { return (def, key) -> { - if ("http://www.springframework.org/schema/beans".equals(parser.getNamespace())) { + if ("http://www.osgi.org/xmlns/blueprint/v1.0.0".equals(parser.getNamespace())) { + Element el = doParseDOMElement("blueprint", "http://www.osgi.org/xmlns/blueprint/v1.0.0", def.getBlueprintBeans()); + if (el != null) { + doAddElement(el, def.getBlueprintBeans(), def::setBlueprintBeans); + return true; + } + return false; + } + else if ("http://www.springframework.org/schema/beans".equals(parser.getNamespace())) { Element el = doParseDOMElement("beans", "http://www.springframework.org/schema/beans", def.getSpringBeans()); if (el != null) { doAddElement(el, def.getSpringBeans(), def::setSpringBeans); diff --git a/core/camel-xml-io/src/generated/java/org/apache/camel/xml/out/ModelWriter.java b/core/camel-xml-io/src/generated/java/org/apache/camel/xml/out/ModelWriter.java index 3b2487ee6d6..2e89e6409c4 100644 --- a/core/camel-xml-io/src/generated/java/org/apache/camel/xml/out/ModelWriter.java +++ b/core/camel-xml-io/src/generated/java/org/apache/camel/xml/out/ModelWriter.java @@ -2519,9 +2519,10 @@ public class ModelWriter extends BaseWriter { throws IOException { doWriteList(null, "route", def.getRoutes(), this::doWriteRouteDefinition); domElements(def.getSpringBeans()); - doWriteList(null, "restConfiguration", def.getRestConfigurations(), this::doWriteRestConfigurationDefinition); + domElements(def.getBlueprintBeans()); doWriteList(null, "component-scan", def.getComponentScanning(), this::doWriteComponentScanDefinition); doWriteList(null, "bean", def.getBeans(), this::doWriteRegistryBeanDefinition); + doWriteList(null, "restConfiguration", def.getRestConfigurations(), this::doWriteRestConfigurationDefinition); doWriteList(null, "rest", def.getRests(), this::doWriteRestDefinition); doWriteList(null, "routeConfiguration", def.getRouteConfigurations(), this::doWriteRouteConfigurationDefinition); doWriteList(null, "routeTemplate", def.getRouteTemplates(), this::doWriteRouteTemplateDefinition); 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 b811d62c339..5d1a2a7bf55 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 @@ -16,12 +16,29 @@ */ package org.apache.camel.xml.in; +import org.apache.camel.LineNumberAware; +import org.apache.camel.model.language.ExpressionDefinition; +import org.apache.camel.spi.NamespaceAware; +import org.apache.camel.spi.Resource; +import org.apache.camel.util.ObjectHelper; +import org.apache.camel.util.URISupport; +import org.apache.camel.xml.io.MXParser; +import org.apache.camel.xml.io.XmlPullParser; +import org.apache.camel.xml.io.XmlPullParserException; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.Text; + +import javax.xml.XMLConstants; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; import java.io.IOException; import java.io.InputStream; import java.io.Reader; import java.util.ArrayList; import java.util.Arrays; import java.util.Base64; +import java.util.HashSet; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; @@ -31,29 +48,11 @@ import java.util.Set; import java.util.function.Consumer; import java.util.function.Supplier; -import javax.xml.XMLConstants; -import javax.xml.parsers.DocumentBuilderFactory; -import javax.xml.parsers.ParserConfigurationException; - -import org.w3c.dom.Document; -import org.w3c.dom.Element; -import org.w3c.dom.Text; - -import org.apache.camel.LineNumberAware; -import org.apache.camel.model.language.ExpressionDefinition; -import org.apache.camel.spi.NamespaceAware; -import org.apache.camel.spi.Resource; -import org.apache.camel.util.ObjectHelper; -import org.apache.camel.util.URISupport; -import org.apache.camel.xml.io.MXParser; -import org.apache.camel.xml.io.XmlPullParser; -import org.apache.camel.xml.io.XmlPullParserException; - public class BaseParser { protected final MXParser parser; protected String namespace; - protected String secondNamespace = ""; + protected Set<String> secondaryNamespaces = new HashSet<>(); protected Resource resource; public BaseParser(Resource resource) throws IOException, XmlPullParserException { @@ -88,8 +87,8 @@ public class BaseParser { this.namespace = namespace != null ? namespace : ""; } - public void addSecondNamespace(String secondNamespace) { - this.secondNamespace = secondNamespace; + public void addSecondaryNamespace(String namespace) { + this.secondaryNamespaces.add(namespace); } protected <T> T doParse( @@ -368,6 +367,20 @@ public class BaseParser { return pn; } + protected String getNextTag(String name, String name2, String name3) throws XmlPullParserException, IOException { + if (parser.nextTag() != XmlPullParser.START_TAG) { + throw new XmlPullParserException("Expected starting tag"); + } + + String pn = parser.getName(); + boolean match = Objects.equals(name, pn) || Objects.equals(name2, pn) || Objects.equals(name3, pn); + if (!match || !matchNamespace(namespace, parser.getNamespace(), null, false)) { + return ""; // empty tag + } + + return pn; + } + protected void handleOtherAttribute(Object definition, String name, String ns, String val) throws XmlPullParserException { // Ignore if ("http://www.w3.org/2001/XMLSchema-instance".equals(ns)) { @@ -465,15 +478,22 @@ public class BaseParser { } protected boolean matchNamespace(String ns, boolean optional) { - return matchNamespace(ns, namespace, secondNamespace, optional); + return matchNamespace(ns, namespace, secondaryNamespaces, optional); } - protected static boolean matchNamespace(String ns, String namespace, String namespace2, boolean optional) { + protected static boolean matchNamespace(String ns, String namespace, Set<String> secondaryNamespaces, boolean optional) { if (optional && ns.isEmpty()) { return true; } - - return Objects.equals(ns, namespace) || Objects.equals(ns, namespace2); + if (Objects.equals(ns, namespace)) { + return true; + } + for (String second : secondaryNamespaces) { + if (Objects.equals(ns, second)) { + return true; + } + } + return false; } } diff --git a/core/camel-yaml-io/src/generated/java/org/apache/camel/yaml/out/ModelWriter.java b/core/camel-yaml-io/src/generated/java/org/apache/camel/yaml/out/ModelWriter.java index 3ab7d56d7d0..3010257af60 100644 --- a/core/camel-yaml-io/src/generated/java/org/apache/camel/yaml/out/ModelWriter.java +++ b/core/camel-yaml-io/src/generated/java/org/apache/camel/yaml/out/ModelWriter.java @@ -2519,9 +2519,10 @@ public class ModelWriter extends BaseWriter { throws IOException { doWriteList(null, "route", def.getRoutes(), this::doWriteRouteDefinition); domElements(def.getSpringBeans()); - doWriteList(null, "restConfiguration", def.getRestConfigurations(), this::doWriteRestConfigurationDefinition); + domElements(def.getBlueprintBeans()); doWriteList(null, "component-scan", def.getComponentScanning(), this::doWriteComponentScanDefinition); doWriteList(null, "bean", def.getBeans(), this::doWriteRegistryBeanDefinition); + doWriteList(null, "restConfiguration", def.getRestConfigurations(), this::doWriteRestConfigurationDefinition); doWriteList(null, "rest", def.getRests(), this::doWriteRestDefinition); doWriteList(null, "routeConfiguration", def.getRouteConfigurations(), this::doWriteRouteConfigurationDefinition); doWriteList(null, "routeTemplate", def.getRouteTemplates(), this::doWriteRouteTemplateDefinition); diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Run.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Run.java index 39d8b128c1f..44e39f5de38 100644 --- a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Run.java +++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Run.java @@ -93,7 +93,7 @@ public class Run extends CamelCommand { "templatedRoute", "templatedRoutes", "rest", "rests", "routeConfiguration", - "beans", "camel" + "beans", "blueprint", "camel" }; private static final Set<String> ACCEPTED_XML_ROOT_ELEMENTS 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 7cc52d1045f..c436be5703f 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,24 +16,13 @@ */ 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.Arrays; -import java.util.Iterator; -import java.util.LinkedHashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Objects; -import java.util.Set; -import java.util.StringJoiner; import java.util.TreeMap; -import java.util.function.Supplier; -import java.util.regex.Matcher; -import java.util.regex.Pattern; import org.w3c.dom.Document; @@ -70,8 +59,8 @@ import org.apache.camel.main.download.PackageNameSourceLoader; import org.apache.camel.main.download.TypeConverterLoaderDownloadListener; import org.apache.camel.main.injection.AnnotationDependencyInjection; import org.apache.camel.main.util.ExtraFilesClassLoader; -import org.apache.camel.model.Model; -import org.apache.camel.model.app.RegistryBeanDefinition; +import org.apache.camel.main.xml.blueprint.BlueprintXmlBeansHandler; +import org.apache.camel.main.xml.spring.SpringXmlBeansHandler; import org.apache.camel.spi.ClassResolver; import org.apache.camel.spi.CliConnector; import org.apache.camel.spi.CliConnectorFactory; @@ -87,29 +76,10 @@ import org.apache.camel.spi.RoutesLoader; import org.apache.camel.spi.UriFactoryResolver; import org.apache.camel.startup.jfr.FlightRecorderStartupStepRecorder; import org.apache.camel.support.DefaultContextReloadStrategy; -import org.apache.camel.support.ObjectHelper; import org.apache.camel.support.PluginHelper; import org.apache.camel.support.RouteOnDemandReloadStrategy; import org.apache.camel.support.service.ServiceHelper; import org.apache.camel.tooling.maven.MavenGav; -import org.apache.camel.util.StringHelper; -import org.springframework.beans.MutablePropertyValues; -import org.springframework.beans.PropertyValue; -import org.springframework.beans.factory.BeanFactory; -import org.springframework.beans.factory.CannotLoadBeanClassException; -import org.springframework.beans.factory.SmartFactoryBean; -import org.springframework.beans.factory.SmartInitializingSingleton; -import org.springframework.beans.factory.config.BeanDefinition; -import org.springframework.beans.factory.config.BeanReference; -import org.springframework.beans.factory.config.ConstructorArgumentValues; -import org.springframework.beans.factory.config.TypedStringValue; -import org.springframework.beans.factory.support.AbstractBeanDefinition; -import org.springframework.beans.factory.support.DefaultListableBeanFactory; -import org.springframework.beans.factory.support.GenericBeanDefinition; -import org.springframework.beans.factory.xml.XmlBeanDefinitionReader; -import org.springframework.core.io.AbstractResource; -import org.springframework.core.io.Resource; -import org.springframework.core.metrics.StartupStep; /** * A Main class for booting up Camel with Kamelet in standalone mode. @@ -118,8 +88,6 @@ public class KameletMain extends MainCommandLineSupport { public static final String DEFAULT_KAMELETS_LOCATION = "classpath:/kamelets,github:apache:camel-kamelets/kamelets"; - private static final Pattern SPRING_PATTERN = Pattern.compile("(\\$\\{.*?})"); // non-greedy mode - protected final MainRegistry registry = new MainRegistry(); private boolean download = true; private String repos; @@ -132,10 +100,8 @@ public class KameletMain extends MainCommandLineSupport { private DownloadListener downloadListener; private DependencyDownloaderClassLoader classLoader; - // when preparing spring-based beans, we may have problems loading classes which are provided with Java DSL - // that's why some beans should be processed later - private final List<String> delayedBeans = new LinkedList<>(); - private Set<String> infraBeanNames; + private final SpringXmlBeansHandler springXmlBeansHandler = new SpringXmlBeansHandler(); + private final BlueprintXmlBeansHandler blueprintXmlBeansHandler = new BlueprintXmlBeansHandler(); public KameletMain() { configureInitialProperties(DEFAULT_KAMELETS_LOCATION); @@ -693,250 +659,33 @@ public class KameletMain extends MainCommandLineSupport { @Override protected void preProcessCamelRegistry(CamelContext camelContext, MainConfigurationProperties config) { - // 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<>(); + final Map<String, Document> springXmls = new TreeMap<>(); + final Map<String, Document> blueprintXmls = new TreeMap<>(); - Map<String, Document> springBeansDocs = registry.findByTypeWithName(Document.class); - if (springBeansDocs != null) { - springBeansDocs.forEach((id, doc) -> { + Map<String, Document> xmlDocs = registry.findByTypeWithName(Document.class); + if (xmlDocs != null) { + xmlDocs.forEach((id, doc) -> { if (id.startsWith("camel-xml-io-dsl-spring-xml:")) { - xmls.put(id, doc); + springXmls.put(id, doc); + } else if (id.startsWith("camel-xml-io-dsl-blueprint-xml:")) { + blueprintXmls.put(id, doc); } }); } - - if (!xmls.isEmpty()) { - processSpringBeans(camelContext, config, xmls); + if (!springXmls.isEmpty()) { + // 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 + springXmlBeansHandler.processSpringBeans(camelContext, config, springXmls); + } + if (!blueprintXmls.isEmpty()) { + blueprintXmlBeansHandler.processBlueprintBeans(camelContext, config, blueprintXmls); } - } - - private void processSpringBeans( - CamelContext camelContext, MainConfigurationProperties config, final Map<String, Document> xmls) { - // 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 - beanFactory.setBeanClassLoader(classLoader); - beanFactory.setBeanExpressionResolver((value, beanExpressionContext) -> extractValue(value, true)); - registry.bind("SpringBeanFactory", beanFactory); - - // register some existing beans (the list may change) - // would be nice to keep the documentation up to date: docs/user-manual/modules/ROOT/pages/camel-jbang.adoc - 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 getFilename() { - if (id.startsWith("camel-xml-io-dsl-spring-xml:")) { - // this is a camel bean via camel-xml-io-dsl - return StringHelper.afterLast(id, ":"); - } - return null; - } - - @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(); - - List<String> beanNames = Arrays.asList(beanFactory.getBeanDefinitionNames()); - - // Trigger initialization of all non-lazy singleton beans... - instantiateAndRegisterBeans(beanFactory, beanNames); } @Override protected void postProcessCamelRegistry(CamelContext camelContext, MainConfigurationProperties config) { - if (delayedBeans.isEmpty()) { - return; - } - - DefaultListableBeanFactory beanFactory - = registry.lookupByNameAndType("SpringBeanFactory", DefaultListableBeanFactory.class); - - // we have some beans with classes that we couldn't load before. now, after loading the routes - // we may have the needed class definitions - for (String beanName : delayedBeans) { - BeanDefinition bd = beanFactory.getMergedBeanDefinition(beanName); - if (bd instanceof AbstractBeanDefinition abd) { - if (!abd.hasBeanClass()) { - Class<?> c = camelContext.getClassResolver().resolveClass(abd.getBeanClassName()); - abd.setBeanClass(c); - } - } - } - - instantiateAndRegisterBeans(beanFactory, delayedBeans); - } - - private void instantiateAndRegisterBeans(DefaultListableBeanFactory beanFactory, List<String> beanNames) { - List<String> instantiatedBeanNames = new LinkedList<>(); - - for (String beanName : beanNames) { - BeanDefinition bd = beanFactory.getMergedBeanDefinition(beanName); - if (!bd.isAbstract() && bd.isSingleton() && !bd.isLazyInit()) { - try { - if (beanFactory.isFactoryBean(beanName)) { - Object bean = beanFactory.getBean(BeanFactory.FACTORY_BEAN_PREFIX + beanName); - if (bean instanceof SmartFactoryBean<?> smartFactoryBean && smartFactoryBean.isEagerInit()) { - beanFactory.getBean(beanName); - instantiatedBeanNames.add(beanName); - } - } else { - beanFactory.getBean(beanName); - instantiatedBeanNames.add(beanName); - } - } catch (CannotLoadBeanClassException ignored) { - // we'll try to resolve later - delayedBeans.add(beanName); - } - } - } - - // Trigger post-initialization callback for all applicable beans... - for (String beanName : instantiatedBeanNames) { - Object singletonInstance = beanFactory.getSingleton(beanName); - if (singletonInstance instanceof SmartInitializingSingleton smartSingleton) { - StartupStep smartInitialize = beanFactory.getApplicationStartup() - .start("spring.beans.smart-initialize") - .tag("beanName", beanName); - smartSingleton.afterSingletonsInstantiated(); - smartInitialize.end(); - } - } - - for (String name : instantiatedBeanNames) { - 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)); - } - - // register bean into model (as a BeanRegistry that allows Camel DSL to know about these beans) - Model model = camelContext.getCamelContextExtension().getContextPlugin(Model.class); - if (model != null) { - RegistryBeanDefinition rrd = new RegistryBeanDefinition(); - if (def instanceof GenericBeanDefinition gbd) { - // set camel resource to refer to the source file - Resource res = gbd.getResource(); - if (res != null) { - String fn = res.getFilename(); - if (fn != null) { - rrd.setResource(camelContext.getCamelContextExtension().getContextPlugin(ResourceLoader.class) - .resolveResource("file:" + fn)); - } - } - } - rrd.setType(def.getBeanClassName()); - rrd.setName(name); - model.addRegistryBean(rrd); - - // constructor arguments - ConstructorArgumentValues ctr = def.getConstructorArgumentValues(); - StringJoiner sj = new StringJoiner(", "); - for (ConstructorArgumentValues.ValueHolder v : ctr.getIndexedArgumentValues().values()) { - Object val = v.getValue(); - if (val instanceof TypedStringValue tsv) { - sj.add("'" + extractValue(tsv.getValue(), false) + "'"); - } else if (val instanceof BeanReference br) { - sj.add("'#bean:" + br.getBeanName() + "'"); - } - } - if (sj.length() > 0) { - rrd.setType("#class:" + def.getBeanClassName() + "(" + sj + ")"); - } - // property values - if (def.hasPropertyValues()) { - Map<String, Object> properties = new LinkedHashMap<>(); - rrd.setProperties(properties); - - MutablePropertyValues values = def.getPropertyValues(); - for (PropertyValue v : values) { - String key = v.getName(); - PropertyValue src = v.getOriginalPropertyValue(); - Object val = src.getValue(); - if (val instanceof TypedStringValue tsv) { - properties.put(key, extractValue(tsv.getValue(), false)); - } else if (val instanceof BeanReference br) { - properties.put(key, "#bean:" + br.getBeanName()); - } else if (val instanceof List) { - int i = 0; - Iterator<?> it = ObjectHelper.createIterator(val); - while (it.hasNext()) { - String k = key + "[" + i + "]"; - val = it.next(); - if (val instanceof TypedStringValue tsv) { - properties.put(k, extractValue(tsv.getValue(), false)); - } else if (val instanceof BeanReference br) { - properties.put(k, "#bean:" + br.getBeanName()); - } - i++; - } - } else if (val instanceof Map) { - Map<TypedStringValue, Object> map = (Map) val; - for (Map.Entry<TypedStringValue, Object> entry : map.entrySet()) { - String k = key + "[" + entry.getKey().getValue() + "]"; - val = entry.getValue(); - if (val instanceof TypedStringValue tsv) { - properties.put(k, extractValue(tsv.getValue(), false)); - } else if (val instanceof BeanReference br) { - properties.put(k, "#bean:" + br.getBeanName()); - } - } - } - } - } - } - } - } - - protected String extractValue(String val, boolean resolve) { - // spring placeholder prefix - if (val != null && val.contains("${")) { - Matcher matcher = SPRING_PATTERN.matcher(val); - while (matcher.find()) { - String group = matcher.group(1); - String replace = "{{" + group.substring(2, group.length() - 1) + "}}"; - val = matcher.replaceFirst(replace); - // we changed so reset matcher so it can find more - matcher.reset(val); - } - } - - if (resolve && camelContext != null) { - // if running camel then resolve property placeholders from beans - val = camelContext.resolvePropertyPlaceholders(val); - } - return val; + springXmlBeansHandler.createAndRegisterBeans(camelContext); + blueprintXmlBeansHandler.createAndRegisterBeans(camelContext); } @Override diff --git a/dsl/camel-kamelet-main/src/main/java/org/apache/camel/main/util/XmlHelper.java b/dsl/camel-kamelet-main/src/main/java/org/apache/camel/main/util/XmlHelper.java index 52b843abfcf..0a362f18334 100644 --- a/dsl/camel-kamelet-main/src/main/java/org/apache/camel/main/util/XmlHelper.java +++ b/dsl/camel-kamelet-main/src/main/java/org/apache/camel/main/util/XmlHelper.java @@ -20,6 +20,8 @@ import javax.xml.XMLConstants; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; +import org.w3c.dom.Node; + import org.apache.camel.util.ObjectHelper; public final class XmlHelper { @@ -60,4 +62,14 @@ public final class XmlHelper { return factory; } + public static String getAttribute(Node node, String key) { + if (node != null && node.hasAttributes()) { + Node attr = node.getAttributes().getNamedItem(key); + if (attr != null) { + return attr.getNodeValue(); + } + } + return null; + } + } diff --git a/dsl/camel-kamelet-main/src/main/java/org/apache/camel/main/xml/blueprint/BlueprintXmlBeansHandler.java b/dsl/camel-kamelet-main/src/main/java/org/apache/camel/main/xml/blueprint/BlueprintXmlBeansHandler.java new file mode 100644 index 00000000000..bcdf6ab97fd --- /dev/null +++ b/dsl/camel-kamelet-main/src/main/java/org/apache/camel/main/xml/blueprint/BlueprintXmlBeansHandler.java @@ -0,0 +1,224 @@ +/* + * 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.xml.blueprint; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.StringJoiner; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.w3c.dom.Document; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; + +import org.apache.camel.CamelContext; +import org.apache.camel.main.MainConfigurationProperties; +import org.apache.camel.main.util.XmlHelper; +import org.apache.camel.model.Model; +import org.apache.camel.model.app.RegistryBeanDefinition; +import org.apache.camel.spi.Resource; +import org.apache.camel.spi.ResourceLoader; +import org.apache.camel.support.PropertyBindingSupport; +import org.apache.camel.util.StringHelper; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Used for parsing and discovering legacy OSGi <blueprint> XML to make it runnable on camel-jbang, and for tooling to + * migrate this to modern Camel DSL in plain Camel XML or YAML DSL. + */ +public class BlueprintXmlBeansHandler { + + private static final Logger LOG = LoggerFactory.getLogger(BlueprintXmlBeansHandler.class); + private static final Pattern BLUEPRINT_PATTERN = Pattern.compile("\\$\\{(.*?)}"); // non-greedy mode + + // when preparing blueprint-based beans, we may have problems loading classes which are provided with Java DSL + // that's why some beans should be processed later + private final Map<String, Node> delayedBeans = new LinkedHashMap<>(); + private final Map<String, Resource> resources = new LinkedHashMap<>(); + private final List<RegistryBeanDefinition> delayedRegistrations = new ArrayList<>(); + + /** + * Parses the XML documents and discovers blueprint beans, which will be created manually via Camel. + */ + public void processBlueprintBeans( + CamelContext camelContext, MainConfigurationProperties config, final Map<String, Document> xmls) { + + LOG.debug("Loading beans from classic OSGi <blueprint> XML"); + + xmls.forEach((id, doc) -> { + if (id.startsWith("camel-xml-io-dsl-blueprint-xml:")) { + // this is a camel bean via camel-xml-io-dsl + String fileName = StringHelper.afterLast(id, ":"); + discoverBeans(camelContext, fileName, doc); + } + }); + } + + /** + * Invoked at later stage to create and register Blueprint beans into Camel {@link org.apache.camel.spi.Registry}. + */ + public void createAndRegisterBeans(CamelContext camelContext) { + LOG.info("Discovered {} OSGi <blueprint> XML beans", delayedBeans.size()); + + for (Map.Entry<String, Node> entry : delayedBeans.entrySet()) { + String id = entry.getKey(); + Node n = entry.getValue(); + RegistryBeanDefinition def = createBeanModel(camelContext, id, n); + LOG.info("Creating bean: {}", def); + registerBeanDefinition(camelContext, def, true); + } + + if (!delayedRegistrations.isEmpty()) { + // some of the beans were not available yet, so we have to try register them now + for (RegistryBeanDefinition def : delayedRegistrations) { + LOG.info("Creating bean (2nd-try): {}", def); + registerBeanDefinition(camelContext, def, false); + } + delayedRegistrations.clear(); + } + + } + + private RegistryBeanDefinition createBeanModel(CamelContext camelContext, String name, Node node) { + RegistryBeanDefinition rrd = new RegistryBeanDefinition(); + rrd.setResource(resources.get(name)); + rrd.setType(XmlHelper.getAttribute(node, "class")); + rrd.setName(name); + + // constructor arguments + StringJoiner sj = new StringJoiner(", "); + NodeList props = node.getChildNodes(); + for (int i = 0; i < props.getLength(); i++) { + Node child = props.item(i); + // assume the args are in order (1, 2) + if ("argument".equals(child.getNodeName())) { + String val = XmlHelper.getAttribute(child, "value"); + String ref = XmlHelper.getAttribute(child, "ref"); + if (val != null) { + sj.add("'" + extractValue(camelContext, val, false) + "'"); + } else if (ref != null) { + sj.add("'#bean:" + extractValue(camelContext, ref, false) + "'"); + } + } + } + if (sj.length() > 0) { + rrd.setType("#class:" + rrd.getType() + "(" + sj + ")"); + } + + // property values + Map<String, Object> properties = new LinkedHashMap<>(); + props = node.getChildNodes(); + for (int i = 0; i < props.getLength(); i++) { + Node child = props.item(i); + // assume the args are in order (1, 2) + if ("property".equals(child.getNodeName())) { + String key = XmlHelper.getAttribute(child, "name"); + String val = XmlHelper.getAttribute(child, "value"); + String ref = XmlHelper.getAttribute(child, "ref"); + + // TODO: List/Map properties + if (key != null && val != null) { + properties.put(key, extractValue(camelContext, val, false)); + } else if (key != null && ref != null) { + properties.put(key, extractValue(camelContext, "#bean:" + ref, false)); + } + } + } + if (!properties.isEmpty()) { + rrd.setProperties(properties); + } + + return rrd; + } + + private void discoverBeans(CamelContext camelContext, String fileName, Document dom) { + Resource resource = camelContext.getCamelContextExtension().getContextPlugin(ResourceLoader.class) + .resolveResource("file:" + fileName); + + NodeList beans = dom.getElementsByTagName("bean"); + for (int i = 0; i < beans.getLength(); i++) { + Node n = beans.item(i); + if (n.hasAttributes()) { + String id = XmlHelper.getAttribute(n, "id"); + if (id != null) { + delayedBeans.put(id, n); + resources.put(id, resource); + } + } + } + } + + protected String extractValue(CamelContext camelContext, String val, boolean resolve) { + // blueprint placeholder prefix + if (val != null && val.contains("${")) { + Matcher matcher = BLUEPRINT_PATTERN.matcher(val); + while (matcher.find()) { + String replace = "{{" + matcher.group(1) + "}}"; + val = matcher.replaceFirst(replace); + // we changed so reset matcher so it can find more + matcher.reset(val); + } + } + + if (resolve && camelContext != null) { + // if running camel then resolve property placeholders from beans + val = camelContext.resolvePropertyPlaceholders(val); + } + return val; + } + + /** + * Try to instantiate bean from the definition. + */ + private void registerBeanDefinition(CamelContext camelContext, RegistryBeanDefinition def, boolean delayIfFailed) { + String type = def.getType(); + String name = def.getName(); + if (name == null || name.trim().isEmpty()) { + name = type; + } + if (type != null) { + if (!type.startsWith("#")) { + type = "#class:" + type; + } + try { + final Object target = PropertyBindingSupport.resolveBean(camelContext, type); + + if (def.getProperties() != null && !def.getProperties().isEmpty()) { + PropertyBindingSupport.setPropertiesOnTarget(camelContext, target, def.getProperties()); + } + camelContext.getRegistry().unbind(name); + camelContext.getRegistry().bind(name, target); + + // register bean in model + Model model = camelContext.getCamelContextExtension().getContextPlugin(Model.class); + model.addRegistryBean(def); + + } catch (Exception e) { + if (delayIfFailed) { + delayedRegistrations.add(def); + } else { + LOG.warn("Error creating bean: {} due to: {}. This exception is ignored.", type, e.getMessage(), e); + } + } + } + } + +} diff --git a/dsl/camel-kamelet-main/src/main/java/org/apache/camel/main/xml/spring/SpringXmlBeansHandler.java b/dsl/camel-kamelet-main/src/main/java/org/apache/camel/main/xml/spring/SpringXmlBeansHandler.java new file mode 100644 index 00000000000..f6c4d7eba43 --- /dev/null +++ b/dsl/camel-kamelet-main/src/main/java/org/apache/camel/main/xml/spring/SpringXmlBeansHandler.java @@ -0,0 +1,316 @@ +/* + * 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.xml.spring; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.Arrays; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.StringJoiner; +import java.util.function.Supplier; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.w3c.dom.Document; + +import org.apache.camel.CamelContext; +import org.apache.camel.main.MainConfigurationProperties; +import org.apache.camel.model.Model; +import org.apache.camel.model.app.RegistryBeanDefinition; +import org.apache.camel.spi.ResourceLoader; +import org.apache.camel.support.ObjectHelper; +import org.apache.camel.util.StringHelper; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.MutablePropertyValues; +import org.springframework.beans.PropertyValue; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.CannotLoadBeanClassException; +import org.springframework.beans.factory.SmartFactoryBean; +import org.springframework.beans.factory.SmartInitializingSingleton; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.BeanReference; +import org.springframework.beans.factory.config.ConstructorArgumentValues; +import org.springframework.beans.factory.config.TypedStringValue; +import org.springframework.beans.factory.support.AbstractBeanDefinition; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.beans.factory.support.GenericBeanDefinition; +import org.springframework.beans.factory.xml.XmlBeanDefinitionReader; +import org.springframework.core.io.AbstractResource; +import org.springframework.core.io.Resource; +import org.springframework.core.metrics.StartupStep; + +/** + * Used for parsing and discovering legacy Spring XML <beans> to make it runnable on camel-jbang, and for tooling to + * migrate this to modern Camel DSL in plain Camel XML or YAML DSL. + */ +public class SpringXmlBeansHandler { + + private static final Logger LOG = LoggerFactory.getLogger(SpringXmlBeansHandler.class); + private static final Pattern SPRING_PATTERN = Pattern.compile("\\$\\{(.*?)}"); // non-greedy mode + + // when preparing spring-based beans, we may have problems loading classes which are provided with Java DSL + // that's why some beans should be processed later + private final List<String> delayedBeans = new LinkedList<>(); + // register some existing beans (the list may change) + // would be nice to keep the documentation up to date: docs/user-manual/modules/ROOT/pages/camel-jbang.adoc + private final Set<String> infraBeanNames = Set.of("CamelContext", "MainConfiguration"); + + /** + * Parses the XML documents and discovers spring beans, which will be created by Spring {@link BeanFactory}. + */ + public void processSpringBeans( + CamelContext camelContext, MainConfigurationProperties config, final Map<String, Document> xmls) { + + LOG.debug("Loading beans from classic Spring <beans> XML"); + + // 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 + beanFactory.setBeanClassLoader(camelContext.getApplicationContextClassLoader()); + beanFactory.setBeanExpressionResolver((value, beanExpressionContext) -> extractValue(camelContext, value, true)); + camelContext.getRegistry().bind("SpringBeanFactory", beanFactory); + + 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 getFilename() { + if (id.startsWith("camel-xml-io-dsl-spring-xml:")) { + // this is a camel bean via camel-xml-io-dsl + return StringHelper.afterLast(id, ":"); + } + return null; + } + + @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(); + + List<String> beanNames = Arrays.asList(beanFactory.getBeanDefinitionNames()); + + // Trigger initialization of all non-lazy singleton beans... + instantiateAndRegisterBeans(camelContext, beanFactory, beanNames); + } + + /** + * Invoked at later stage to create and register Spring beans into Camel {@link org.apache.camel.spi.Registry}. + */ + public void createAndRegisterBeans(CamelContext camelContext) { + if (delayedBeans.isEmpty()) { + return; + } + + DefaultListableBeanFactory beanFactory + = camelContext.getRegistry().lookupByNameAndType("SpringBeanFactory", DefaultListableBeanFactory.class); + + // we have some beans with classes that we couldn't load before. now, after loading the routes + // we may have the needed class definitions + for (String beanName : delayedBeans) { + BeanDefinition bd = beanFactory.getMergedBeanDefinition(beanName); + if (bd instanceof AbstractBeanDefinition abd) { + if (!abd.hasBeanClass()) { + Class<?> c = camelContext.getClassResolver().resolveClass(abd.getBeanClassName()); + abd.setBeanClass(c); + } + } + } + + instantiateAndRegisterBeans(camelContext, beanFactory, delayedBeans); + } + + private void instantiateAndRegisterBeans( + CamelContext camelContext, DefaultListableBeanFactory beanFactory, List<String> beanNames) { + List<String> instantiatedBeanNames = new LinkedList<>(); + + for (String beanName : beanNames) { + BeanDefinition bd = beanFactory.getMergedBeanDefinition(beanName); + if (!bd.isAbstract() && bd.isSingleton() && !bd.isLazyInit()) { + try { + if (beanFactory.isFactoryBean(beanName)) { + Object bean = beanFactory.getBean(BeanFactory.FACTORY_BEAN_PREFIX + beanName); + if (bean instanceof SmartFactoryBean<?> smartFactoryBean && smartFactoryBean.isEagerInit()) { + beanFactory.getBean(beanName); + instantiatedBeanNames.add(beanName); + } + } else { + beanFactory.getBean(beanName); + instantiatedBeanNames.add(beanName); + } + } catch (CannotLoadBeanClassException ignored) { + // we'll try to resolve later + delayedBeans.add(beanName); + } + } + } + + // Trigger post-initialization callback for all applicable beans... + for (String beanName : instantiatedBeanNames) { + Object singletonInstance = beanFactory.getSingleton(beanName); + if (singletonInstance instanceof SmartInitializingSingleton smartSingleton) { + StartupStep smartInitialize = beanFactory.getApplicationStartup() + .start("spring.beans.smart-initialize") + .tag("beanName", beanName); + smartSingleton.afterSingletonsInstantiated(); + smartInitialize.end(); + } + } + + for (String name : instantiatedBeanNames) { + if (infraBeanNames.contains(name)) { + continue; + } + BeanDefinition def = beanFactory.getBeanDefinition(name); + if (def.isSingleton()) { + // just grab the singleton and put into registry + camelContext.getRegistry().bind(name, beanFactory.getBean(name)); + } else { + // rely on the bean factory to implement prototype scope + camelContext.getRegistry().bind(name, (Supplier<Object>) () -> beanFactory.getBean(name)); + } + + addBeanToCamelModel(camelContext, name, def); + } + } + + private void addBeanToCamelModel(CamelContext camelContext, String name, BeanDefinition def) { + // register bean into model (as a BeanRegistry that allows Camel DSL to know about these beans) + Model model = camelContext.getCamelContextExtension().getContextPlugin(Model.class); + if (model != null) { + RegistryBeanDefinition rrd = new RegistryBeanDefinition(); + if (def instanceof GenericBeanDefinition gbd) { + // set camel resource to refer to the source file + Resource res = gbd.getResource(); + if (res != null) { + String fn = res.getFilename(); + if (fn != null) { + rrd.setResource(camelContext.getCamelContextExtension().getContextPlugin(ResourceLoader.class) + .resolveResource("file:" + fn)); + } + } + } + rrd.setType(def.getBeanClassName()); + rrd.setName(name); + model.addRegistryBean(rrd); + + // constructor arguments + ConstructorArgumentValues ctr = def.getConstructorArgumentValues(); + StringJoiner sj = new StringJoiner(", "); + for (ConstructorArgumentValues.ValueHolder v : ctr.getIndexedArgumentValues().values()) { + Object val = v.getValue(); + if (val instanceof TypedStringValue tsv) { + sj.add("'" + extractValue(camelContext, tsv.getValue(), false) + "'"); + } else if (val instanceof BeanReference br) { + sj.add("'#bean:" + extractValue(camelContext, br.getBeanName(), false) + "'"); + } + } + if (sj.length() > 0) { + rrd.setType("#class:" + def.getBeanClassName() + "(" + sj + ")"); + } + // property values + if (def.hasPropertyValues()) { + Map<String, Object> properties = new LinkedHashMap<>(); + rrd.setProperties(properties); + + MutablePropertyValues values = def.getPropertyValues(); + for (PropertyValue v : values) { + String key = v.getName(); + PropertyValue src = v.getOriginalPropertyValue(); + Object val = src.getValue(); + if (val instanceof TypedStringValue tsv) { + properties.put(key, extractValue(camelContext, tsv.getValue(), false)); + } else if (val instanceof BeanReference br) { + properties.put(key, "#bean:" + extractValue(camelContext, br.getBeanName(), false)); + } else if (val instanceof List) { + int i = 0; + Iterator<?> it = ObjectHelper.createIterator(val); + while (it.hasNext()) { + String k = key + "[" + i + "]"; + val = it.next(); + if (val instanceof TypedStringValue tsv) { + properties.put(k, extractValue(camelContext, tsv.getValue(), false)); + } else if (val instanceof BeanReference br) { + properties.put(k, "#bean:" + extractValue(camelContext, br.getBeanName(), false)); + } + i++; + } + } else if (val instanceof Map) { + Map<TypedStringValue, Object> map = (Map) val; + for (Map.Entry<TypedStringValue, Object> entry : map.entrySet()) { + String k = key + "[" + entry.getKey().getValue() + "]"; + val = entry.getValue(); + if (val instanceof TypedStringValue tsv) { + properties.put(k, extractValue(camelContext, tsv.getValue(), false)); + } else if (val instanceof BeanReference br) { + properties.put(k, "#bean:" + extractValue(camelContext, br.getBeanName(), false)); + } + } + } + } + } + } + } + + protected String extractValue(CamelContext camelContext, String val, boolean resolve) { + // spring placeholder prefix + if (val != null && val.contains("${")) { + Matcher matcher = SPRING_PATTERN.matcher(val); + while (matcher.find()) { + String replace = "{{" + matcher.group(1) + "}}"; + val = matcher.replaceFirst(replace); + // we changed so reset matcher so it can find more + matcher.reset(val); + } + } + + if (resolve && camelContext != null) { + // if running camel then resolve property placeholders from beans + val = camelContext.resolvePropertyPlaceholders(val); + } + return val; + } + +} diff --git a/dsl/camel-xml-io-dsl/src/main/java/org/apache/camel/dsl/xml/io/XmlModelParser.java b/dsl/camel-xml-io-dsl/src/main/java/org/apache/camel/dsl/xml/io/XmlModelParser.java index b440e0a90ce..fde9a848027 100644 --- a/dsl/camel-xml-io-dsl/src/main/java/org/apache/camel/dsl/xml/io/XmlModelParser.java +++ b/dsl/camel-xml-io-dsl/src/main/java/org/apache/camel/dsl/xml/io/XmlModelParser.java @@ -23,23 +23,31 @@ import org.apache.camel.xml.in.ModelParser; import org.apache.camel.xml.io.XmlPullParserException; /** - * XML {@link ModelParser} that supports loading classic Spring XML <beans> with embedded <camelContext>, with limited - * parsing, to discover <routes> inside <camelContext>. + * XML {@link ModelParser} that supports loading: + * <ul> + * <li>Standard Camel XML DSL</li> + * <li>Classic Spring XML <beans> with embedded <camelContext> (limited parsing, to discover <routes> inside + * <camelContext>)</li> + * <li>Legacy OSGi <blueprint> with embedded <camelContext> (limited parsing, to discover <routes> inside + * <camelContext>)</li> + * </ul> */ public class XmlModelParser extends ModelParser { private static final String SPRING_NS = "http://camel.apache.org/schema/spring"; + private static final String BLUEPRINT_NS = "http://camel.apache.org/schema/blueprint"; public XmlModelParser(Resource input, String namespace) throws IOException, XmlPullParserException { super(input, namespace); - addSecondNamespace(SPRING_NS); + addSecondaryNamespace(SPRING_NS); + addSecondaryNamespace(BLUEPRINT_NS); } @Override protected boolean handleUnexpectedElement(String namespace, String name) throws XmlPullParserException { - // accept embedded <camelContext> inside Spring XML <beans> files, so we can discover - // embedded <routes> inside this <camelContext>. - if ("camelContext".equals(name) && SPRING_NS.equals(namespace)) { + // accept embedded <camelContext> inside Spring XML <beans> files or OSGi <blueprint> files, + // so we can discover embedded <routes> inside this <camelContext>. + if ("camelContext".equals(name) && (SPRING_NS.equals(namespace) || BLUEPRINT_NS.equals(namespace))) { return true; } return super.handleUnexpectedElement(namespace, name); 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 bae6983dbac..3dace301432 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 @@ -89,7 +89,7 @@ public class XmlRoutesBuilderLoader extends RouteBuilderLoaderSupport { XmlStreamInfo xmlInfo = xmlInfo(resource); if (xmlInfo.isValid()) { String root = xmlInfo.getRootElementName(); - if ("beans".equals(root) || "camel".equals(root)) { + if ("beans".equals(root) || "blueprint".equals(root) || "camel".equals(root)) { new XmlModelParser(resource, xmlInfo.getRootElementNamespace()) .parseBeansDefinition() .ifPresent(bd -> { @@ -116,7 +116,7 @@ public class XmlRoutesBuilderLoader extends RouteBuilderLoaderSupport { public void configure() throws Exception { String resourceLocation = input.getLocation(); switch (xmlInfo.getRootElementName()) { - case "beans", "camel" -> { + case "beans", "blueprint", "camel" -> { BeansDefinition def = camelAppCache.get(resourceLocation); if (def != null) { configureCamel(def); @@ -298,6 +298,17 @@ public class XmlRoutesBuilderLoader extends RouteBuilderLoaderSupport { String id = String.format("camel-xml-io-dsl-spring-xml:%05d:%s", counter.incrementAndGet(), resource.getLocation()); getCamelContext().getRegistry().bind(id, doc); } + + // <s:bean> 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.getBlueprintBeans().isEmpty()) { + Document doc = app.getBlueprintBeans().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("camel-xml-io-dsl-blueprint-xml:%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/XmlSpringBeansLoadTest.java b/dsl/camel-xml-io-dsl/src/test/java/org/apache/camel/dsl/xml/io/XmlBlueprintLoadTest.java similarity index 72% copy from dsl/camel-xml-io-dsl/src/test/java/org/apache/camel/dsl/xml/io/XmlSpringBeansLoadTest.java copy to dsl/camel-xml-io-dsl/src/test/java/org/apache/camel/dsl/xml/io/XmlBlueprintLoadTest.java index a28258f7d62..ce3f61d0629 100644 --- a/dsl/camel-xml-io-dsl/src/test/java/org/apache/camel/dsl/xml/io/XmlSpringBeansLoadTest.java +++ b/dsl/camel-xml-io-dsl/src/test/java/org/apache/camel/dsl/xml/io/XmlBlueprintLoadTest.java @@ -19,20 +19,23 @@ package org.apache.camel.dsl.xml.io; import org.apache.camel.impl.DefaultCamelContext; import org.apache.camel.spi.Resource; import org.apache.camel.support.PluginHelper; +import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; -public class XmlSpringBeansLoadTest { +public class XmlBlueprintLoadTest { @Test public void testLoadRoutesBuilderFromXml() throws Exception { try (DefaultCamelContext context = new DefaultCamelContext()) { - // load spring XML <beans> with embedded <camelContext> + // load OSGi blueprint XML <blueprint> with embedded <camelContext> Resource resource = PluginHelper.getResourceLoader(context).resolveResource( - "/org/apache/camel/dsl/xml/io/springBeans.xml"); + "/org/apache/camel/dsl/xml/io/blueprintRoutes.xml"); - PluginHelper.getRoutesLoader(context).loadRoutes(resource); + Assertions.assertDoesNotThrow(() -> { + // should be able to parse the file and not fail (camel-jbang supports creating spring beans) + PluginHelper.getRoutesLoader(context).loadRoutes(resource); + }); - // should be able to parse the file and not fail (camel-jbang supports creating spring beans) } } diff --git a/dsl/camel-xml-io-dsl/src/test/java/org/apache/camel/dsl/xml/io/XmlSpringBeansLoadTest.java b/dsl/camel-xml-io-dsl/src/test/java/org/apache/camel/dsl/xml/io/XmlSpringBeansLoadTest.java index a28258f7d62..c5f7ba6b226 100644 --- a/dsl/camel-xml-io-dsl/src/test/java/org/apache/camel/dsl/xml/io/XmlSpringBeansLoadTest.java +++ b/dsl/camel-xml-io-dsl/src/test/java/org/apache/camel/dsl/xml/io/XmlSpringBeansLoadTest.java @@ -19,6 +19,7 @@ package org.apache.camel.dsl.xml.io; import org.apache.camel.impl.DefaultCamelContext; import org.apache.camel.spi.Resource; import org.apache.camel.support.PluginHelper; +import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; public class XmlSpringBeansLoadTest { @@ -30,9 +31,10 @@ public class XmlSpringBeansLoadTest { Resource resource = PluginHelper.getResourceLoader(context).resolveResource( "/org/apache/camel/dsl/xml/io/springBeans.xml"); - PluginHelper.getRoutesLoader(context).loadRoutes(resource); - - // should be able to parse the file and not fail (camel-jbang supports creating spring beans) + Assertions.assertDoesNotThrow(() -> { + // should be able to parse the file and not fail (camel-jbang supports creating spring beans) + PluginHelper.getRoutesLoader(context).loadRoutes(resource); + }); } } diff --git a/dsl/camel-xml-io-dsl/src/test/resources/org/apache/camel/dsl/xml/io/blueprintRoutes.xml b/dsl/camel-xml-io-dsl/src/test/resources/org/apache/camel/dsl/xml/io/blueprintRoutes.xml new file mode 100644 index 00000000000..96515d8c48f --- /dev/null +++ b/dsl/camel-xml-io-dsl/src/test/resources/org/apache/camel/dsl/xml/io/blueprintRoutes.xml @@ -0,0 +1,50 @@ +<?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. + +--> +<blueprint xmlns="http://www.osgi.org/xmlns/blueprint/v1.0.0" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation=" + http://www.osgi.org/xmlns/blueprint/v1.0.0 https://www.osgi.org/xmlns/blueprint/v1.0.0/blueprint.xsd"> + + <!-- spring beans --> + <bean id="orderService" class="com.foo.OrderService"> + <argument index="0" value="true"/> + <argument index="1" ref="office"/> + </bean> + <!-- uses blueprint property placeholder ${xxx} syntax --> + <bean id="office" class="com.foo.Address"> + <property name="zip" value="${zipCode}"/> + <property name="street" value="${streetName}"/> + </bean> + + <!-- embed Camel with routes --> + <camelContext xmlns="http://camel.apache.org/schema/blueprint"> + + <route> + <from uri="timer:xml?period={{time:1000}}"/> + <setBody> + <simple>${random(1000)}</simple> + </setBody> + <bean ref="orderService"/> + <log message="${body}"/> + </route> + + </camelContext> + +</blueprint> diff --git a/tooling/maven/camel-package-maven-plugin/src/main/java/org/apache/camel/maven/packaging/ModelXmlParserGeneratorMojo.java b/tooling/maven/camel-package-maven-plugin/src/main/java/org/apache/camel/maven/packaging/ModelXmlParserGeneratorMojo.java index 36db5315cf6..c2a5c7cb5ed 100644 --- a/tooling/maven/camel-package-maven-plugin/src/main/java/org/apache/camel/maven/packaging/ModelXmlParserGeneratorMojo.java +++ b/tooling/maven/camel-package-maven-plugin/src/main/java/org/apache/camel/maven/packaging/ModelXmlParserGeneratorMojo.java @@ -594,7 +594,9 @@ public class ModelXmlParserGeneratorMojo extends AbstractGeneratorMojo { .setName("parse" + name) .addThrows(IOException.class) .addThrows(XML_PULL_PARSER_EXCEPTION) - .setBody(String.format("String tag = getNextTag(\"%s\", \"%s\");", "beans", "camel"), + .setBody( + String.format("String tag = getNextTag(\"%s\", \"%s\", \"%s\");", "beans", "blueprint", + "camel"), "if (tag != null) {", String.format(" return Optional.of(doParse%s());", name), "}",