This is an automated email from the ASF dual-hosted git repository. davsclaus pushed a commit to branch backport/CAMEL-26282-camel-4.22.x in repository https://gitbox.apache.org/repos/asf/camel.git
commit 10e38e05d86c402620cc4ebf947f3e6c0f909381 Author: Claus Ibsen <[email protected]> AuthorDate: Wed Nov 4 12:04:23 2026 +0100 CAMEL-26282: camel-flowable - xml inbound channels parse events with the Camel XML settings (#29150) An inbound channel of type camel with the xml deserializer type and no deserializerDelegateExpression now parses the event with CamelStringToXmlDocumentDeserializer instead of the Flowable 7 StringToXmlDocumentDeserializer, which uses the JDK default parser settings. The Camel deserializer does not load external DTDs, does not include external general or parameter entities, keeps entity references as nodes and does not process XInclude, as Flowable 8 does, and turns on secure processing. A custom deserializer is kept as it is. Adds a section to the component documentation and a note to the 4.22.3 upgrade guide. Co-Authored-By: Claude Opus 5.5 <[email protected]> Signed-off-by: Claus Ibsen <[email protected]> (cherry picked from commit 03ed6ff1a3a19d98384ba95b2b30ac488e84fb51) --- .../camel/catalog/docs/flowable-component.adoc | 10 ++ .../src/main/docs/flowable-component.adoc | 10 ++ .../CamelStringToXmlDocumentDeserializer.java | 87 ++++++++++++++++ .../camel/component/flowable/FlowableProducer.java | 22 ++++ .../CamelStringToXmlDocumentDeserializerTest.java | 64 ++++++++++++ .../flowable/FlowableInboundXmlChannelTest.java | 114 +++++++++++++++++++++ .../test/resources/channel/userXmlChannel.channel | 11 ++ .../src/test/resources/event/userXmlEvent.event | 14 +++ .../src/test/resources/process/startXml.bpmn20.xml | 44 ++++++++ .../ROOT/pages/camel-4x-upgrade-guide-4_22.adoc | 9 ++ 10 files changed, 385 insertions(+) diff --git a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/flowable-component.adoc b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/flowable-component.adoc index 55ecbd0c62bb..d228fbf43133 100644 --- a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/flowable-component.adoc +++ b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/flowable-component.adoc @@ -35,6 +35,16 @@ include::partial$component-endpoint-headers.adoc[] The body content is defined by the event model deployed to the Flowable Event Registry. The event message can then be mapped to process or case variables in the process BPMN or case CMMN model. +=== XML events + +For an inbound channel of type `camel` with `deserializerType` set to `xml`, the producer parses the event with the XML +parser settings that Camel uses, which are also the settings of Flowable 8: external DTDs are not loaded, external +general and parameter entities are not included, entity references are kept as entity reference nodes, and XInclude +is not processed. + +To parse the event in another way, set `deserializerDelegateExpression` in the channel model to your own +`InboundEventDeserializer`. The producer keeps a custom deserializer as it is. + === Sending to a channel that is not deployed yet A route that sends to `flowable:channelKey` can start before the inbound channel model with that key is deployed diff --git a/components/camel-flowable/src/main/docs/flowable-component.adoc b/components/camel-flowable/src/main/docs/flowable-component.adoc index 55ecbd0c62bb..d228fbf43133 100644 --- a/components/camel-flowable/src/main/docs/flowable-component.adoc +++ b/components/camel-flowable/src/main/docs/flowable-component.adoc @@ -35,6 +35,16 @@ include::partial$component-endpoint-headers.adoc[] The body content is defined by the event model deployed to the Flowable Event Registry. The event message can then be mapped to process or case variables in the process BPMN or case CMMN model. +=== XML events + +For an inbound channel of type `camel` with `deserializerType` set to `xml`, the producer parses the event with the XML +parser settings that Camel uses, which are also the settings of Flowable 8: external DTDs are not loaded, external +general and parameter entities are not included, entity references are kept as entity reference nodes, and XInclude +is not processed. + +To parse the event in another way, set `deserializerDelegateExpression` in the channel model to your own +`InboundEventDeserializer`. The producer keeps a custom deserializer as it is. + === Sending to a channel that is not deployed yet A route that sends to `flowable:channelKey` can start before the inbound channel model with that key is deployed diff --git a/components/camel-flowable/src/main/java/org/apache/camel/component/flowable/CamelStringToXmlDocumentDeserializer.java b/components/camel-flowable/src/main/java/org/apache/camel/component/flowable/CamelStringToXmlDocumentDeserializer.java new file mode 100644 index 000000000000..375aecc61b85 --- /dev/null +++ b/components/camel-flowable/src/main/java/org/apache/camel/component/flowable/CamelStringToXmlDocumentDeserializer.java @@ -0,0 +1,87 @@ +/* + * 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.component.flowable; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; + +import javax.xml.XMLConstants; +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; + +import org.w3c.dom.Document; + +import org.flowable.common.engine.api.FlowableException; +import org.flowable.eventregistry.impl.serialization.StringToXmlDocumentDeserializer; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Deserializes the event of an inbound channel with the xml deserializer type into a {@link Document}. + * <p/> + * The parser does not load external DTDs, does not include external general or parameter entities, does not expand + * entity references and does not process XInclude, which are the settings Flowable 8 uses for this deserializer (the + * Flowable 7 deserializer leaves them at the JDK defaults). Secure processing is turned on, and access to external DTDs + * and schemas is restricted, as in the other XML parsing of Camel. + */ +public class CamelStringToXmlDocumentDeserializer extends StringToXmlDocumentDeserializer { + + private static final Logger LOG = LoggerFactory.getLogger(CamelStringToXmlDocumentDeserializer.class); + + @Override + public Document deserialize(Object rawEvent) { + try { + DocumentBuilder documentBuilder = createDocumentBuilderFactory().newDocumentBuilder(); + try (InputStream inputStream = new ByteArrayInputStream(convertEventToBytes(rawEvent))) { + return documentBuilder.parse(inputStream); + } + } catch (Exception e) { + throw new FlowableException("Could not deserialize event to xml", e); + } + } + + protected DocumentBuilderFactory createDocumentBuilderFactory() { + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + factory.setExpandEntityReferences(false); + factory.setXIncludeAware(false); + setFeature(factory, XMLConstants.FEATURE_SECURE_PROCESSING, true); + setFeature(factory, "http://xml.org/sax/features/external-general-entities", false); + setFeature(factory, "http://xml.org/sax/features/external-parameter-entities", false); + setFeature(factory, "http://apache.org/xml/features/nonvalidating/load-external-dtd", false); + setFeature(factory, "http://apache.org/xml/features/nonvalidating/load-dtd-grammar", false); + setAttribute(factory, XMLConstants.ACCESS_EXTERNAL_DTD, ""); + setAttribute(factory, XMLConstants.ACCESS_EXTERNAL_SCHEMA, ""); + return factory; + } + + private static void setFeature(DocumentBuilderFactory factory, String feature, boolean value) { + try { + factory.setFeature(feature, value); + } catch (ParserConfigurationException e) { + LOG.debug("DocumentBuilderFactory does not support the feature {} with value {}", feature, value, e); + } + } + + private static void setAttribute(DocumentBuilderFactory factory, String attribute, String value) { + try { + factory.setAttribute(attribute, value); + } catch (IllegalArgumentException e) { + LOG.debug("DocumentBuilderFactory does not support the attribute {}", attribute, e); + } + } +} diff --git a/components/camel-flowable/src/main/java/org/apache/camel/component/flowable/FlowableProducer.java b/components/camel-flowable/src/main/java/org/apache/camel/component/flowable/FlowableProducer.java index d73d7e8d98f2..2ef90844e529 100644 --- a/components/camel-flowable/src/main/java/org/apache/camel/component/flowable/FlowableProducer.java +++ b/components/camel-flowable/src/main/java/org/apache/camel/component/flowable/FlowableProducer.java @@ -16,6 +16,8 @@ */ package org.apache.camel.component.flowable; +import org.w3c.dom.Document; + import org.apache.camel.Endpoint; import org.apache.camel.Exchange; import org.apache.camel.Message; @@ -23,10 +25,14 @@ import org.apache.camel.support.DefaultProducer; import org.flowable.common.engine.api.FlowableException; import org.flowable.eventregistry.impl.DefaultInboundEvent; import org.flowable.eventregistry.impl.EventRegistryEngineConfiguration; +import org.flowable.eventregistry.impl.pipeline.DefaultInboundEventProcessingPipeline; +import org.flowable.eventregistry.impl.serialization.StringToXmlDocumentDeserializer; import org.flowable.eventregistry.model.CamelInboundChannelModel; public class FlowableProducer extends DefaultProducer { + private static final CamelStringToXmlDocumentDeserializer XML_DESERIALIZER = new CamelStringToXmlDocumentDeserializer(); + protected CamelInboundChannelModel camelInboundChannel; protected EventRegistryEngineConfiguration eventRegistryEngineConfiguration; @@ -50,11 +56,27 @@ public class FlowableProducer extends DefaultProducer { configuration = deployed.getEventRegistryEngineConfiguration(); } + useCamelXmlDeserializer(channel); + Message message = exchange.getMessage(); DefaultInboundEvent inboundEvent = new DefaultInboundEvent(message.getBody(), message.getHeaders()); configuration.getEventRegistry().eventReceived(channel, inboundEvent); } + /** + * An inbound channel with the xml deserializer type and no deserializerDelegateExpression gets the default xml + * deserializer of Flowable. Use the Camel one instead, which parses with the XML settings Camel uses. A custom + * deserializer is kept as it is. + */ + @SuppressWarnings("unchecked") + private static void useCamelXmlDeserializer(CamelInboundChannelModel channel) { + if (channel.getInboundEventProcessingPipeline() instanceof DefaultInboundEventProcessingPipeline<?> pipeline + && pipeline.getInboundEventDeserializer() != null + && pipeline.getInboundEventDeserializer().getClass() == StringToXmlDocumentDeserializer.class) { + ((DefaultInboundEventProcessingPipeline<Document>) pipeline).setInboundEventDeserializer(XML_DESERIALIZER); + } + } + private FlowableEndpoint lookupDeployedEndpoint() { Endpoint endpoint = getEndpoint().getCamelContext().hasEndpoint(getEndpoint().getEndpointUri()); if (endpoint instanceof FlowableEndpoint flowableEndpoint && flowableEndpoint.getCamelInboundChannel() != null diff --git a/components/camel-flowable/src/test/java/org/apache/camel/component/flowable/CamelStringToXmlDocumentDeserializerTest.java b/components/camel-flowable/src/test/java/org/apache/camel/component/flowable/CamelStringToXmlDocumentDeserializerTest.java new file mode 100644 index 000000000000..b3a05a31eace --- /dev/null +++ b/components/camel-flowable/src/test/java/org/apache/camel/component/flowable/CamelStringToXmlDocumentDeserializerTest.java @@ -0,0 +1,64 @@ +/* + * 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.component.flowable; + +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; + +import org.w3c.dom.Document; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; + +public class CamelStringToXmlDocumentDeserializerTest { + + private final CamelStringToXmlDocumentDeserializer deserializer = new CamelStringToXmlDocumentDeserializer(); + + @Test + public void xmlEventIsDeserialized() { + Document document = deserializer.deserialize("<user><name>John Doe</name><age>23</age></user>"); + + assertEquals("user", document.getDocumentElement().getNodeName()); + assertEquals("John Doe", document.getElementsByTagName("name").item(0).getTextContent()); + } + + @Test + public void entityDeclaredInDoctypeIsNotExpandedFromFile() throws Exception { + Path file = Path.of("target", "flowable-xml-deserializer", "entity.txt").toAbsolutePath(); + Files.createDirectories(file.getParent()); + Files.writeString(file, "content-of-the-entity-file", StandardCharsets.UTF_8); + + Document document = deserializer.deserialize( + "<?xml version=\"1.0\"?><!DOCTYPE user [<!ENTITY content SYSTEM \"" + file.toUri() + "\">]>" + + "<user><name>&content;</name></user>"); + + assertFalse(document.getDocumentElement().getTextContent().contains("content-of-the-entity-file")); + } + + @Test + public void externalDtdIsNotLoaded() { + Path missing = Path.of("target", "flowable-xml-deserializer", "missing.dtd").toAbsolutePath(); + + Document document = deserializer.deserialize( + "<?xml version=\"1.0\"?><!DOCTYPE user SYSTEM \"" + missing.toUri() + "\"><user><name>John Doe</name></user>"); + + assertEquals("John Doe", document.getElementsByTagName("name").item(0).getTextContent()); + } +} diff --git a/components/camel-flowable/src/test/java/org/apache/camel/component/flowable/FlowableInboundXmlChannelTest.java b/components/camel-flowable/src/test/java/org/apache/camel/component/flowable/FlowableInboundXmlChannelTest.java new file mode 100644 index 000000000000..063e86092b01 --- /dev/null +++ b/components/camel-flowable/src/test/java/org/apache/camel/component/flowable/FlowableInboundXmlChannelTest.java @@ -0,0 +1,114 @@ +/* + * 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.component.flowable; + +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; + +import org.w3c.dom.Document; + +import org.flowable.engine.runtime.ProcessInstance; +import org.flowable.eventregistry.api.InboundEventDeserializer; +import org.flowable.eventregistry.impl.pipeline.DefaultInboundEventProcessingPipeline; +import org.flowable.eventregistry.impl.serialization.StringToXmlDocumentDeserializer; +import org.flowable.eventregistry.model.CamelInboundChannelModel; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertSame; + +public class FlowableInboundXmlChannelTest extends CamelFlowableTestCase { + + private String processDeploymentId; + + @BeforeEach + public void deployEventRegistryModels() { + eventRegistryEngineConfiguration.getEventRepositoryService().createDeployment() + .addClasspathResource("channel/userXmlChannel.channel") + .addClasspathResource("event/userXmlEvent.event") + .deploy(); + processDeploymentId = deployProcessDefinition("process/startXml.bpmn20.xml"); + } + + @AfterEach + public void deleteProcessDeployment() { + repositoryService.deleteDeployment(processDeploymentId, true); + } + + @Test + public void xmlEventStartsProcessWithMappedVariables() { + template.sendBody("direct:startXml", "<user><name>John Doe</name><age>23</age></user>"); + + ProcessInstance processInstance = startedProcessInstance(); + assertEquals("John Doe", runtimeService.getVariable(processInstance.getId(), "name")); + assertEquals(23, runtimeService.getVariable(processInstance.getId(), "age")); + } + + @Test + public void xmlChannelUsesCamelXmlDeserializer() { + template.sendBody("direct:startXml", "<user><name>John Doe</name><age>23</age></user>"); + + assertInstanceOf(CamelStringToXmlDocumentDeserializer.class, xmlPipeline().getInboundEventDeserializer()); + } + + @Test + public void customXmlDeserializerIsKept() { + InboundEventDeserializer<Document> custom = new StringToXmlDocumentDeserializer() { + }; + xmlPipeline().setInboundEventDeserializer(custom); + + template.sendBody("direct:startXml", "<user><name>John Doe</name><age>23</age></user>"); + + assertSame(custom, xmlPipeline().getInboundEventDeserializer()); + assertEquals("John Doe", runtimeService.getVariable(startedProcessInstance().getId(), "name")); + } + + @Test + public void entityDeclaredInDoctypeIsNotExpandedFromFile() throws Exception { + Path file = Path.of("target", "flowable-xml-channel", "entity.txt").toAbsolutePath(); + Files.createDirectories(file.getParent()); + Files.writeString(file, "content-of-the-entity-file", StandardCharsets.UTF_8); + + template.sendBody("direct:startXml", + "<?xml version=\"1.0\"?><!DOCTYPE event [<!ENTITY content SYSTEM \"" + file.toUri() + "\">]>" + + "<name>&content;</name>"); + + Object name = runtimeService.getVariable(startedProcessInstance().getId(), "name"); + assertFalse(name != null && name.toString().contains("content-of-the-entity-file"), + "The entity declared in the DOCTYPE must not be expanded from the file, but the variable is: " + name); + } + + private ProcessInstance startedProcessInstance() { + ProcessInstance processInstance + = runtimeService.createProcessInstanceQuery().processDefinitionKey("camelXmlProcess").singleResult(); + assertNotNull(processInstance); + return processInstance; + } + + @SuppressWarnings("unchecked") + private DefaultInboundEventProcessingPipeline<Document> xmlPipeline() { + CamelInboundChannelModel channel + = context.getEndpoint("flowable:userXmlChannel", FlowableEndpoint.class).getCamelInboundChannel(); + return (DefaultInboundEventProcessingPipeline<Document>) channel.getInboundEventProcessingPipeline(); + } +} diff --git a/components/camel-flowable/src/test/resources/channel/userXmlChannel.channel b/components/camel-flowable/src/test/resources/channel/userXmlChannel.channel new file mode 100644 index 000000000000..242b6cd3b301 --- /dev/null +++ b/components/camel-flowable/src/test/resources/channel/userXmlChannel.channel @@ -0,0 +1,11 @@ +{ + "name": "Camel user xml channel", + "key": "userXmlChannel", + "channelType": "inbound", + "type": "camel", + "sourceUri": "direct:startXml", + "deserializerType": "xml", + "channelEventKeyDetection": { + "fixedValue": "userXmlEvent" + } +} diff --git a/components/camel-flowable/src/test/resources/event/userXmlEvent.event b/components/camel-flowable/src/test/resources/event/userXmlEvent.event new file mode 100644 index 000000000000..53ed6ae3518b --- /dev/null +++ b/components/camel-flowable/src/test/resources/event/userXmlEvent.event @@ -0,0 +1,14 @@ +{ + "key": "userXmlEvent", + "name": "User Xml Event", + "payload": [ + { + "name": "name", + "type": "string" + }, + { + "name": "age", + "type": "integer" + } + ] +} diff --git a/components/camel-flowable/src/test/resources/process/startXml.bpmn20.xml b/components/camel-flowable/src/test/resources/process/startXml.bpmn20.xml new file mode 100644 index 000000000000..0b0054983673 --- /dev/null +++ b/components/camel-flowable/src/test/resources/process/startXml.bpmn20.xml @@ -0,0 +1,44 @@ +<?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. + +--> +<definitions xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL" + xmlns:flowable="http://flowable.org/bpmn" + targetNamespace="Examples" + xmlns:tns="Examples"> + + <process id="camelXmlProcess"> + + <startEvent id="theStart"> + <extensionElements> + <flowable:eventType>userXmlEvent</flowable:eventType> + <flowable:eventOutParameter source="name" target="name" /> + <flowable:eventOutParameter source="age" target="age" /> + </extensionElements> + </startEvent> + + <sequenceFlow id="flow1" sourceRef="theStart" targetRef="userTask"/> + + <userTask id="userTask" /> + <sequenceFlow id="flow2" sourceRef="userTask" targetRef="end" /> + + <endEvent id="end" /> + + </process> + +</definitions> \ No newline at end of file diff --git a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_22.adoc b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_22.adoc index 219fcef617cb..4ffee56c75ca 100644 --- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_22.adoc +++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_22.adoc @@ -197,6 +197,15 @@ a header in the transferred exchange is no longer used, also when the receiving the `CamelNettySSLClientCert*` headers on a connection without a client certificate). This also applies to the reply the producer receives. Send any such value of the sender under a header name of your own. +=== camel-flowable - xml inbound channels parse the event with the Camel XML settings + +The `flowable` producer now parses the event of an inbound channel of type `camel` with `deserializerType` set to +`xml` with the XML parser settings that Camel uses, which are also the settings of Flowable 8: external DTDs are not +loaded, external general and parameter entities are not included, entity references are kept as entity reference +nodes, and XInclude is not processed. Before, the Flowable 7 deserializer parsed the event with the JDK defaults. +An event that relies on an external DTD or an external entity no longer gets their content. A channel that sets +`deserializerDelegateExpression` keeps its own deserializer, which can parse such events. + == Upgrading from 4.22.1 to 4.22.2 === camel-management - Route group exchange counters count once per entry into the group again
