This is an automated email from the ASF dual-hosted git repository.

davsclaus pushed a commit to branch fix/CAMEL-24107
in repository https://gitbox.apache.org/repos/asf/camel.git

commit 3c8c413aed5cab3ba823d487e0f20a38d6ea05a2
Author: Claus Ibsen <[email protected]>
AuthorDate: Thu Jul 16 11:48:20 2026 +0200

    CAMEL-24107: camel-xslt - stylesheet reload is not thread-safe, stale 
pooled transformers keep serving the old stylesheet
    
    Co-Authored-By: Claude Opus 4.6 <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
---
 .../apache/camel/component/xslt/XsltBuilder.java   |  67 +++++----
 .../apache/camel/component/xslt/XsltEndpoint.java  |  12 +-
 .../camel/builder/xml/XsltBuilderReloadTest.java   | 161 +++++++++++++++++++++
 .../org/apache/camel/builder/xml/example2.xsl      |  28 ++++
 4 files changed, 239 insertions(+), 29 deletions(-)

diff --git 
a/components/camel-xslt/src/main/java/org/apache/camel/component/xslt/XsltBuilder.java
 
b/components/camel-xslt/src/main/java/org/apache/camel/component/xslt/XsltBuilder.java
index 30fc027fde20..3874c86e8df0 100644
--- 
a/components/camel-xslt/src/main/java/org/apache/camel/component/xslt/XsltBuilder.java
+++ 
b/components/camel-xslt/src/main/java/org/apache/camel/component/xslt/XsltBuilder.java
@@ -25,6 +25,7 @@ import java.util.Map;
 import java.util.Set;
 import java.util.concurrent.ArrayBlockingQueue;
 import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.atomic.AtomicLong;
 import java.util.concurrent.locks.Lock;
 import java.util.concurrent.locks.ReentrantLock;
 
@@ -66,7 +67,8 @@ public class XsltBuilder implements Processor {
 
     protected static final Logger LOG = 
LoggerFactory.getLogger(XsltBuilder.class);
     private Map<String, Object> parameters = new HashMap<>();
-    private Templates template;
+    private volatile Templates template;
+    private final AtomicLong templateGeneration = new AtomicLong();
     private volatile BlockingQueue<Transformer> transformers;
     private volatile SourceHandlerFactory sourceHandlerFactory;
     private ResultHandlerFactory resultHandlerFactory = new 
StringResultHandlerFactory();
@@ -80,6 +82,7 @@ public class XsltBuilder implements Processor {
 
     private final XMLConverterHelper converter = new XMLConverterHelper();
     private final Lock sourceHandlerFactoryLock = new ReentrantLock();
+    private final Lock transformerSourceLock = new ReentrantLock();
 
     public XsltBuilder() {
     }
@@ -103,6 +106,7 @@ public class XsltBuilder implements Processor {
             exchange.getExchangeExtension().addOnCompletion(new 
XsltBuilderOnCompletion(fileName));
         }
 
+        long gen = templateGeneration.get();
         Transformer transformer = getTransformer();
         configureTransformer(transformer, exchange);
 
@@ -128,7 +132,7 @@ public class XsltBuilder implements Processor {
             LOG.trace("Transform complete with result {}", result);
             resultHandler.setBody(out);
         } finally {
-            releaseTransformer(transformer);
+            releaseTransformer(transformer, gen);
             // IOHelper can handle if null
             IOHelper.close(is);
         }
@@ -283,6 +287,7 @@ public class XsltBuilder implements Processor {
 
     public void setTemplate(Templates template) {
         this.template = template;
+        templateGeneration.incrementAndGet();
         if (transformers != null) {
             transformers.clear();
         }
@@ -344,28 +349,33 @@ public class XsltBuilder implements Processor {
      * @throws TransformerConfigurationException is thrown if creating a XSLT 
transformer failed.
      */
     public void setTransformerSource(Source source) throws 
TransformerConfigurationException {
-        TransformerFactory factory = converter.getTransformerFactory();
-        if (errorListener != null) {
-            factory.setErrorListener(errorListener);
-        } else {
-            // use a logger error listener so users can see from the logs what 
the error may be
-            factory.setErrorListener(new XsltErrorListener());
-        }
-        if (getUriResolver() != null) {
-            factory.setURIResolver(getUriResolver());
-        }
+        transformerSourceLock.lock();
+        try {
+            TransformerFactory factory = converter.getTransformerFactory();
+            if (errorListener != null) {
+                factory.setErrorListener(errorListener);
+            } else {
+                // use a logger error listener so users can see from the logs 
what the error may be
+                factory.setErrorListener(new XsltErrorListener());
+            }
+            if (getUriResolver() != null) {
+                factory.setURIResolver(getUriResolver());
+            }
 
-        // Check that the call to createTemplates() returns a valid template 
instance.
-        // In case of a xslt parse error, it will return null, and we should 
stop the
-        // deployment and raise an exception as the route will not be setup 
properly.
-        Templates templates = createTemplates(factory, source);
-        if (templates != null) {
-            setTemplate(templates);
-        } else {
-            throw new TransformerConfigurationException(
-                    "Error creating XSLT template. "
-                                                        + "This is most likely 
be caused by a XML parse error. "
-                                                        + "Please verify your 
XSLT file configured.");
+            // Check that the call to createTemplates() returns a valid 
template instance.
+            // In case of a xslt parse error, it will return null, and we 
should stop the
+            // deployment and raise an exception as the route will not be 
setup properly.
+            Templates templates = createTemplates(factory, source);
+            if (templates != null) {
+                setTemplate(templates);
+            } else {
+                throw new TransformerConfigurationException(
+                        "Error creating XSLT template. "
+                                                            + "This is most 
likely be caused by a XML parse error. "
+                                                            + "Please verify 
your XSLT file configured.");
+            }
+        } finally {
+            transformerSourceLock.unlock();
         }
     }
 
@@ -434,13 +444,14 @@ public class XsltBuilder implements Processor {
         this.xsltMessageLogger = xsltMessageLogger;
     }
 
-    private void releaseTransformer(Transformer transformer) {
+    private void releaseTransformer(Transformer transformer, long generation) {
         if (transformers != null) {
-            transformer.reset();
-            boolean result = transformers.offer(transformer);
-            if (!result) {
-                LOG.error("failed to offer() a transform");
+            if (generation != templateGeneration.get()) {
+                // template was reloaded while this transformer was in use — 
discard it
+                return;
             }
+            transformer.reset();
+            transformers.offer(transformer);
         }
     }
 
diff --git 
a/components/camel-xslt/src/main/java/org/apache/camel/component/xslt/XsltEndpoint.java
 
b/components/camel-xslt/src/main/java/org/apache/camel/component/xslt/XsltEndpoint.java
index f7f6b4f825d4..9e6500882af1 100644
--- 
a/components/camel-xslt/src/main/java/org/apache/camel/component/xslt/XsltEndpoint.java
+++ 
b/components/camel-xslt/src/main/java/org/apache/camel/component/xslt/XsltEndpoint.java
@@ -20,6 +20,8 @@ import java.io.IOException;
 import java.io.InputStream;
 import java.util.HashMap;
 import java.util.Map;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
 
 import javax.xml.transform.ErrorListener;
 import javax.xml.transform.Source;
@@ -63,6 +65,7 @@ public class XsltEndpoint extends ProcessorEndpoint {
 
     private volatile boolean cacheCleared;
     private volatile XsltBuilder xslt;
+    private final Lock reloadLock = new ReentrantLock();
     private Map<String, Object> parameters;
 
     @UriPath
@@ -147,7 +150,14 @@ public class XsltEndpoint extends ProcessorEndpoint {
             }
         }
         if (!contentCache || cacheCleared) {
-            loadResource(resourceUri, xslt);
+            reloadLock.lock();
+            try {
+                if (!contentCache || cacheCleared) {
+                    loadResource(resourceUri, xslt);
+                }
+            } finally {
+                reloadLock.unlock();
+            }
         }
         super.onExchange(exchange);
     }
diff --git 
a/core/camel-core/src/test/java/org/apache/camel/builder/xml/XsltBuilderReloadTest.java
 
b/core/camel-core/src/test/java/org/apache/camel/builder/xml/XsltBuilderReloadTest.java
new file mode 100644
index 000000000000..7df571b763db
--- /dev/null
+++ 
b/core/camel-core/src/test/java/org/apache/camel/builder/xml/XsltBuilderReloadTest.java
@@ -0,0 +1,161 @@
+/*
+ * 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.builder.xml;
+
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+import javax.xml.transform.stream.StreamSource;
+
+import org.apache.camel.ContextTestSupport;
+import org.apache.camel.Exchange;
+import org.apache.camel.component.xslt.XsltBuilder;
+import org.apache.camel.support.DefaultExchange;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class XsltBuilderReloadTest extends ContextTestSupport {
+
+    private static final String INPUT = "<hello>world!</hello>";
+    private static final String EXPECTED_V1 = "<?xml version=\"1.0\" 
encoding=\"UTF-8\"?><goodbye>world!</goodbye>";
+    private static final String EXPECTED_V2 = "<?xml version=\"1.0\" 
encoding=\"UTF-8\"?><farewell>world!</farewell>";
+
+    @Test
+    public void testPooledTransformersInvalidatedOnTemplateChange() throws 
Exception {
+        URL styleSheet1 = getClass().getResource("example.xsl");
+        URL styleSheet2 = getClass().getResource("example2.xsl");
+
+        XsltBuilder builder = XsltBuilder.xslt(styleSheet1);
+        builder.transformerCacheSize(10);
+
+        // process first message — transformer compiled from stylesheet 1 is 
returned to pool
+        Exchange exchange1 = new DefaultExchange(context);
+        exchange1.getIn().setBody(INPUT);
+        builder.process(exchange1);
+        assertEquals(EXPECTED_V1, 
exchange1.getMessage().getBody(String.class));
+
+        // reload stylesheet — pool must be invalidated
+        builder.setTransformerSource(new 
StreamSource(styleSheet2.openStream()));
+
+        // process second message — must use new template, not a stale pooled 
transformer
+        Exchange exchange2 = new DefaultExchange(context);
+        exchange2.getIn().setBody(INPUT);
+        builder.process(exchange2);
+        assertEquals(EXPECTED_V2, 
exchange2.getMessage().getBody(String.class));
+    }
+
+    @Test
+    public void testMultipleReloadsWithTransformerCache() throws Exception {
+        URL styleSheet1 = getClass().getResource("example.xsl");
+        URL styleSheet2 = getClass().getResource("example2.xsl");
+
+        XsltBuilder builder = XsltBuilder.xslt(styleSheet1);
+        builder.transformerCacheSize(5);
+
+        // fill the pool with a few transformers from stylesheet 1
+        for (int i = 0; i < 3; i++) {
+            Exchange exchange = new DefaultExchange(context);
+            exchange.getIn().setBody(INPUT);
+            builder.process(exchange);
+            assertEquals(EXPECTED_V1, 
exchange.getMessage().getBody(String.class));
+        }
+
+        // reload to stylesheet 2
+        builder.setTransformerSource(new 
StreamSource(styleSheet2.openStream()));
+
+        // all subsequent messages must use the new stylesheet
+        for (int i = 0; i < 5; i++) {
+            Exchange exchange = new DefaultExchange(context);
+            exchange.getIn().setBody(INPUT);
+            builder.process(exchange);
+            assertEquals(EXPECTED_V2, 
exchange.getMessage().getBody(String.class),
+                    "Message " + i + " should use the new stylesheet");
+        }
+
+        // reload back to stylesheet 1
+        builder.setTransformerSource(new 
StreamSource(styleSheet1.openStream()));
+
+        for (int i = 0; i < 3; i++) {
+            Exchange exchange = new DefaultExchange(context);
+            exchange.getIn().setBody(INPUT);
+            builder.process(exchange);
+            assertEquals(EXPECTED_V1, 
exchange.getMessage().getBody(String.class));
+        }
+    }
+
+    @Test
+    public void testConcurrentProcessingDuringReload() throws Exception {
+        URL styleSheet1 = getClass().getResource("example.xsl");
+        URL styleSheet2 = getClass().getResource("example2.xsl");
+
+        XsltBuilder builder = XsltBuilder.xslt(styleSheet1);
+        builder.transformerCacheSize(10);
+
+        // warm up the pool
+        Exchange warmup = new DefaultExchange(context);
+        warmup.getIn().setBody(INPUT);
+        builder.process(warmup);
+
+        // reload to stylesheet 2
+        builder.setTransformerSource(new 
StreamSource(styleSheet2.openStream()));
+
+        // launch multiple threads that all process concurrently after the 
reload
+        int threadCount = 8;
+        CountDownLatch startLatch = new CountDownLatch(1);
+        CountDownLatch doneLatch = new CountDownLatch(threadCount);
+        List<String> results = new ArrayList<>();
+        List<Throwable> errors = new ArrayList<>();
+
+        for (int i = 0; i < threadCount; i++) {
+            Thread t = new Thread(() -> {
+                try {
+                    startLatch.await(10, TimeUnit.SECONDS);
+                    Exchange exchange = new DefaultExchange(context);
+                    exchange.getIn().setBody(INPUT);
+                    builder.process(exchange);
+                    synchronized (results) {
+                        
results.add(exchange.getMessage().getBody(String.class));
+                    }
+                } catch (Throwable e) {
+                    synchronized (errors) {
+                        errors.add(e);
+                    }
+                } finally {
+                    doneLatch.countDown();
+                }
+            });
+            t.start();
+        }
+
+        // release all threads at once
+        startLatch.countDown();
+        assertTrue(doneLatch.await(30, TimeUnit.SECONDS), "All threads should 
complete");
+        assertTrue(errors.isEmpty(), "No errors expected: " + errors);
+        assertEquals(threadCount, results.size());
+
+        // all results must use the new stylesheet
+        for (int i = 0; i < results.size(); i++) {
+            assertEquals(EXPECTED_V2, results.get(i),
+                    "Thread " + i + " should use the new stylesheet");
+        }
+    }
+}
diff --git 
a/core/camel-core/src/test/resources/org/apache/camel/builder/xml/example2.xsl 
b/core/camel-core/src/test/resources/org/apache/camel/builder/xml/example2.xsl
new file mode 100644
index 000000000000..b51cd91daea4
--- /dev/null
+++ 
b/core/camel-core/src/test/resources/org/apache/camel/builder/xml/example2.xsl
@@ -0,0 +1,28 @@
+<?xml version = "1.0"?>
+<!--
+
+    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.
+
+-->
+<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform";>
+
+  <xsl:template match="/">
+    <farewell>
+      <xsl:value-of select="/hello"/>
+    </farewell>
+  </xsl:template>
+
+</xsl:stylesheet>

Reply via email to