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 91ecb8cebc30 CAMEL-26426: camel-core - AbstractExchange: a field for 
TO_ENDPOINT and a lazily created internal properties map (#28761)
91ecb8cebc30 is described below

commit 91ecb8cebc309a5e6a64f33812feaa02dbf4d2bb
Author: Adriano Machado <[email protected]>
AuthorDate: Sat Oct 31 05:29:28 2026 -0400

    CAMEL-26426: camel-core - AbstractExchange: a field for TO_ENDPOINT and a 
lazily created internal properties map (#28761)
    
    Co-authored-by: Claude Opus 5.5 <[email protected]>
---
 .../apache/camel/support/AbstractExchangeTest.java | 213 +++++++++++++++++++++
 .../org/apache/camel/support/AbstractExchange.java |  73 +++++--
 .../camel/support/DefaultPooledExchange.java       |   2 +-
 .../camel/support/ExtendedExchangeExtension.java   |   4 +-
 4 files changed, 270 insertions(+), 22 deletions(-)

diff --git 
a/core/camel-core/src/test/java/org/apache/camel/support/AbstractExchangeTest.java
 
b/core/camel-core/src/test/java/org/apache/camel/support/AbstractExchangeTest.java
index 6e9179a909c8..788f705d5aa9 100644
--- 
a/core/camel-core/src/test/java/org/apache/camel/support/AbstractExchangeTest.java
+++ 
b/core/camel-core/src/test/java/org/apache/camel/support/AbstractExchangeTest.java
@@ -16,22 +16,34 @@
  */
 package org.apache.camel.support;
 
+import java.util.Map;
+
 import org.apache.camel.CamelContext;
 import org.apache.camel.Exchange;
+import org.apache.camel.ExchangePropertyKey;
 import org.apache.camel.clock.Clock;
 import org.apache.camel.impl.DefaultCamelContext;
 import org.apache.camel.spi.DataType;
 import org.apache.camel.spi.DataTypeAware;
 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.assertNull;
 import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertTrue;
 
 /**
  * The unit test for the class {@link AbstractExchange}.
  */
 public class AbstractExchangeTest {
 
+    private static final String URI = "mock://foo";
+
+    private final CamelContext context = new DefaultCamelContext();
+
     static class CustomAbstractExchange extends AbstractExchange {
         private final Clock clock = new MonotonicClock();
 
@@ -77,4 +89,205 @@ public class AbstractExchangeTest {
         assertSame(type2, ((DataTypeAware) e2.getMessage()).getDataType());
     }
 
+    @Test
+    void shouldNotCreateInternalPropertiesForToEndpoint() {
+        // every send sets this key, so it must not cost every exchange the 
map sized for all the keys
+        DefaultExchange exchange = new DefaultExchange(context);
+        assertNull(exchange.internalProperties);
+
+        exchange.setProperty(ExchangePropertyKey.TO_ENDPOINT, URI);
+        assertEquals(URI, 
exchange.getProperty(ExchangePropertyKey.TO_ENDPOINT));
+        assertNull(exchange.internalProperties);
+
+        AbstractExchange copy = (AbstractExchange) exchange.copy();
+        assertNull(copy.internalProperties);
+    }
+
+    @Test
+    void shouldKeepToEndpointBesideOtherInternalProperties() {
+        DefaultExchange exchange = new DefaultExchange(context);
+        exchange.setProperty(ExchangePropertyKey.TO_ENDPOINT, URI);
+        exchange.setProperty(ExchangePropertyKey.CHARSET_NAME, "UTF-8");
+
+        assertNotNull(exchange.internalProperties);
+        assertEquals(URI, 
exchange.getProperty(ExchangePropertyKey.TO_ENDPOINT));
+        assertEquals("UTF-8", 
exchange.getProperty(ExchangePropertyKey.CHARSET_NAME));
+
+        assertEquals("UTF-8", 
exchange.removeProperty(ExchangePropertyKey.CHARSET_NAME));
+        assertEquals(URI, 
exchange.getProperty(ExchangePropertyKey.TO_ENDPOINT));
+    }
+
+    @Test
+    void shouldReadOtherInternalPropertiesWithoutTheMap() {
+        DefaultExchange exchange = new DefaultExchange(context);
+
+        assertNull(exchange.getProperty(ExchangePropertyKey.CHARSET_NAME));
+        assertNull(exchange.getProperty(Exchange.CHARSET_NAME));
+        assertNull(exchange.removeProperty(ExchangePropertyKey.CHARSET_NAME));
+        assertTrue(exchange.getInternalProperties().isEmpty());
+        assertTrue(exchange.getAllProperties().isEmpty());
+    }
+
+    @Test
+    void shouldAccessToEndpointByKeyAndByName() {
+        // the name is the public constant that routes and components use
+        DefaultExchange exchange = new DefaultExchange(context);
+
+        exchange.setProperty(Exchange.TO_ENDPOINT, URI);
+        assertEquals(URI, 
exchange.getProperty(ExchangePropertyKey.TO_ENDPOINT));
+        assertEquals(URI, exchange.getProperty(Exchange.TO_ENDPOINT));
+        assertEquals(URI, exchange.getProperty(Exchange.TO_ENDPOINT, 
String.class));
+
+        assertEquals(URI, exchange.removeProperty(Exchange.TO_ENDPOINT));
+        assertNull(exchange.getProperty(ExchangePropertyKey.TO_ENDPOINT));
+
+        exchange.setProperty(ExchangePropertyKey.TO_ENDPOINT, URI);
+        assertEquals(URI, 
exchange.removeProperty(ExchangePropertyKey.TO_ENDPOINT));
+        assertNull(exchange.getProperty(Exchange.TO_ENDPOINT));
+    }
+
+    @Test
+    void shouldListToEndpointWithTheInternalProperties() {
+        // the dev consoles and tracers dump the exchange with these
+        DefaultExchange exchange = new DefaultExchange(context);
+        exchange.setProperty(ExchangePropertyKey.TO_ENDPOINT, URI);
+        exchange.setProperty("foo", "bar");
+
+        assertEquals(Map.of("CamelToEndpoint", URI), 
exchange.getInternalProperties());
+        assertEquals(Map.of("CamelToEndpoint", URI, "foo", "bar"), 
exchange.getAllProperties());
+    }
+
+    @Test
+    void shouldCopyToEndpointIndependently() {
+        DefaultExchange exchange = new DefaultExchange(context);
+        exchange.setProperty(ExchangePropertyKey.TO_ENDPOINT, URI);
+
+        Exchange copy = exchange.copy();
+        assertEquals(URI, copy.getProperty(ExchangePropertyKey.TO_ENDPOINT));
+
+        copy.setProperty(ExchangePropertyKey.TO_ENDPOINT, "mock://bar");
+        assertEquals(URI, 
exchange.getProperty(ExchangePropertyKey.TO_ENDPOINT));
+    }
+
+    @Test
+    void shouldCopyInternalPropertiesIndependently() {
+        DefaultExchange exchange = new DefaultExchange(context);
+        exchange.setProperty(ExchangePropertyKey.CHARSET_NAME, "UTF-8");
+
+        Exchange copy = exchange.copy();
+        assertEquals("UTF-8", 
copy.getProperty(ExchangePropertyKey.CHARSET_NAME));
+
+        copy.setProperty(ExchangePropertyKey.CHARSET_NAME, "ISO-8859-1");
+        assertEquals("UTF-8", 
exchange.getProperty(ExchangePropertyKey.CHARSET_NAME));
+    }
+
+    @Test
+    void shouldCreateCopyWithToEndpoint() {
+        DefaultExchange exchange = new DefaultExchange(context);
+        exchange.setProperty(ExchangePropertyKey.TO_ENDPOINT, URI);
+        exchange.setProperty("foo", "bar");
+
+        Exchange copy = 
exchange.getExchangeExtension().createCopyWithProperties(context);
+
+        assertEquals(URI, copy.getProperty(ExchangePropertyKey.TO_ENDPOINT));
+        assertEquals("bar", copy.getProperty("foo"));
+        assertNull(((AbstractExchange) copy).internalProperties);
+    }
+
+    @Test
+    void shouldCreateCopyWithInternalProperties() {
+        DefaultExchange exchange = new DefaultExchange(context);
+        exchange.setProperty(ExchangePropertyKey.CHARSET_NAME, "UTF-8");
+
+        Exchange copy = 
exchange.getExchangeExtension().createCopyWithProperties(context);
+
+        assertEquals("UTF-8", 
copy.getProperty(ExchangePropertyKey.CHARSET_NAME));
+    }
+
+    @Test
+    void shouldCopyInternalPropertiesToTarget() {
+        DefaultExchange source = new DefaultExchange(context);
+        source.setProperty(ExchangePropertyKey.TO_ENDPOINT, URI);
+        source.setProperty(ExchangePropertyKey.CHARSET_NAME, "UTF-8");
+
+        DefaultExchange target = new DefaultExchange(context);
+        source.getExchangeExtension().copyInternalProperties(target);
+
+        assertEquals(URI, target.getProperty(ExchangePropertyKey.TO_ENDPOINT));
+        assertEquals("UTF-8", 
target.getProperty(ExchangePropertyKey.CHARSET_NAME));
+        // the target has its own map, so it can change without changing the 
source
+        target.setProperty(ExchangePropertyKey.CHARSET_NAME, "ISO-8859-1");
+        assertEquals("UTF-8", 
source.getProperty(ExchangePropertyKey.CHARSET_NAME));
+    }
+
+    @Test
+    void shouldKeepPropertiesOfTargetThatTheSourceDoesNotHave() {
+        DefaultExchange target = new DefaultExchange(context);
+        target.setProperty(ExchangePropertyKey.TO_ENDPOINT, URI);
+        target.setProperty(ExchangePropertyKey.CHARSET_NAME, "UTF-8");
+
+        new 
DefaultExchange(context).getExchangeExtension().copyInternalProperties(target);
+
+        assertEquals(URI, target.getProperty(ExchangePropertyKey.TO_ENDPOINT));
+        assertEquals("UTF-8", 
target.getProperty(ExchangePropertyKey.CHARSET_NAME));
+    }
+
+    @Test
+    void shouldKeepToEndpointOfTargetWhenTheSourceRemovedIt() {
+        // a removed property is not in the source any more, so it must not 
clear the value of the target
+        DefaultExchange source = new DefaultExchange(context);
+        source.setProperty(ExchangePropertyKey.TO_ENDPOINT, "mock://bar");
+        source.removeProperty(ExchangePropertyKey.TO_ENDPOINT);
+
+        DefaultExchange target = new DefaultExchange(context);
+        target.setProperty(ExchangePropertyKey.TO_ENDPOINT, URI);
+
+        source.getExchangeExtension().copyInternalProperties(target);
+
+        assertEquals(URI, target.getProperty(ExchangePropertyKey.TO_ENDPOINT));
+    }
+
+    @Test
+    void shouldRemoveToEndpointWithAllProperties() {
+        DefaultExchange exchange = new DefaultExchange(context);
+        exchange.setProperty(ExchangePropertyKey.TO_ENDPOINT, URI);
+
+        assertTrue(exchange.removeProperties("*"));
+        assertNull(exchange.getProperty(ExchangePropertyKey.TO_ENDPOINT));
+    }
+
+    @Test
+    void shouldRemoveToEndpointByPattern() {
+        DefaultExchange exchange = new DefaultExchange(context);
+        exchange.setProperty(ExchangePropertyKey.TO_ENDPOINT, URI);
+
+        assertTrue(exchange.removeProperties("CamelTo*"));
+        assertNull(exchange.getProperty(ExchangePropertyKey.TO_ENDPOINT));
+    }
+
+    @Test
+    void shouldKeepToEndpointExcludedFromPattern() {
+        DefaultExchange exchange = new DefaultExchange(context);
+        exchange.setProperty(ExchangePropertyKey.TO_ENDPOINT, URI);
+        exchange.setProperty(ExchangePropertyKey.CHARSET_NAME, "UTF-8");
+
+        exchange.removeProperties("Camel*", "CamelToEndpoint");
+
+        assertEquals(URI, 
exchange.getProperty(ExchangePropertyKey.TO_ENDPOINT));
+        assertNull(exchange.getProperty(ExchangePropertyKey.CHARSET_NAME));
+    }
+
+    @Test
+    void shouldClearInternalPropertiesOfPooledExchangeWhenDone() {
+        // a reused exchange must not see the endpoint of its previous use
+        DefaultPooledExchange exchange = new DefaultPooledExchange(context);
+        exchange.setProperty(ExchangePropertyKey.TO_ENDPOINT, URI);
+        exchange.setProperty(ExchangePropertyKey.CHARSET_NAME, "UTF-8");
+        assertFalse(exchange.getInternalProperties().isEmpty());
+
+        exchange.done();
+
+        assertTrue(exchange.getInternalProperties().isEmpty());
+    }
+
 }
diff --git 
a/core/camel-support/src/main/java/org/apache/camel/support/AbstractExchange.java
 
b/core/camel-support/src/main/java/org/apache/camel/support/AbstractExchange.java
index 102bee085166..5680f5945e6d 100644
--- 
a/core/camel-support/src/main/java/org/apache/camel/support/AbstractExchange.java
+++ 
b/core/camel-support/src/main/java/org/apache/camel/support/AbstractExchange.java
@@ -58,7 +58,9 @@ abstract class AbstractExchange implements Exchange, 
ExchangeExtension {
 
     private final CamelContext context;
 
-    protected final EnumMap<ExchangePropertyKey, Object> internalProperties;
+    // created when needed, as the map is sized for all the keys. TO_ENDPOINT 
is set on every send, so it has its own field
+    protected EnumMap<ExchangePropertyKey, Object> internalProperties;
+    private Object toEndpoint;
     protected Map<String, Object> properties;
     protected Map<String, SafeCopyProperty> safeCopyProperties;
     protected ExchangeVariableRepository variableRepository;
@@ -96,7 +98,7 @@ abstract class AbstractExchange implements Exchange, 
ExchangeExtension {
     protected AbstractExchange(CamelContext context, 
EnumMap<ExchangePropertyKey, Object> internalProperties,
                                Map<String, Object> properties) {
         this.context = context;
-        this.internalProperties = new EnumMap<>(internalProperties);
+        this.internalProperties = internalProperties != null ? new 
EnumMap<>(internalProperties) : null;
         this.properties = safeCopyProperties(properties);
     }
 
@@ -107,13 +109,11 @@ abstract class AbstractExchange implements Exchange, 
ExchangeExtension {
     protected AbstractExchange(CamelContext context, ExchangePattern pattern) {
         this.context = context;
         this.pattern = pattern;
-        this.internalProperties = new EnumMap<>(ExchangePropertyKey.class);
     }
 
     protected AbstractExchange(Exchange parent) {
         this.context = parent.getContext();
         this.pattern = parent.getPattern();
-        this.internalProperties = new EnumMap<>(ExchangePropertyKey.class);
         this.fromEndpoint = parent.getFromEndpoint();
         this.fromRouteId = parent.getFromRouteId();
         this.unitOfWork = parent.getUnitOfWork();
@@ -123,7 +123,8 @@ abstract class AbstractExchange implements Exchange, 
ExchangeExtension {
     protected AbstractExchange(AbstractExchange parent) {
         this.context = parent.getContext();
         this.pattern = parent.getPattern();
-        this.internalProperties = new EnumMap<>(parent.internalProperties);
+        this.internalProperties = parent.internalProperties != null ? new 
EnumMap<>(parent.internalProperties) : null;
+        this.toEndpoint = parent.toEndpoint;
         this.fromEndpoint = parent.getFromEndpoint();
         this.fromRouteId = parent.getFromRouteId();
         this.unitOfWork = parent.getUnitOfWork();
@@ -171,13 +172,15 @@ abstract class AbstractExchange implements Exchange, 
ExchangeExtension {
     public Exchange copy() {
         AbstractExchange exchange = newCopy();
 
-        if (getContext().isMessageHistory()) {
-            
exchange.internalProperties.computeIfPresent(ExchangePropertyKey.MESSAGE_HISTORY,
-                    (k, v) -> new 
CopyOnWriteArrayList<>((List<MessageHistory>) v));
+        if (exchange.internalProperties != null) {
+            if (getContext().isMessageHistory()) {
+                
exchange.internalProperties.computeIfPresent(ExchangePropertyKey.MESSAGE_HISTORY,
+                        (k, v) -> new 
CopyOnWriteArrayList<>((List<MessageHistory>) v));
+            }
+            // the claim check repository is scoped per exchange, so the copy 
must not share it
+            
exchange.internalProperties.computeIfPresent(ExchangePropertyKey.CLAIM_CHECK_REPOSITORY,
+                    (k, v) -> v instanceof SafeCopyProperty scp ? 
scp.safeCopy() : v);
         }
-        // the claim check repository is scoped per exchange, so the copy must 
not share it
-        
exchange.internalProperties.computeIfPresent(ExchangePropertyKey.CLAIM_CHECK_REPOSITORY,
-                (k, v) -> v instanceof SafeCopyProperty scp ? scp.safeCopy() : 
v);
 
         return exchange;
     }
@@ -189,7 +192,10 @@ abstract class AbstractExchange implements Exchange, 
ExchangeExtension {
 
     @Override
     public Object getProperty(ExchangePropertyKey key) {
-        return internalProperties.get(key);
+        if (key == ExchangePropertyKey.TO_ENDPOINT) {
+            return toEndpoint;
+        }
+        return internalProperties != null ? internalProperties.get(key) : null;
     }
 
     @Override
@@ -251,12 +257,24 @@ abstract class AbstractExchange implements Exchange, 
ExchangeExtension {
 
     @Override
     public void setProperty(ExchangePropertyKey key, Object value) {
+        if (key == ExchangePropertyKey.TO_ENDPOINT) {
+            toEndpoint = value;
+            return;
+        }
+        if (internalProperties == null) {
+            internalProperties = new EnumMap<>(ExchangePropertyKey.class);
+        }
         internalProperties.put(key, value);
     }
 
     @Override
     public Object removeProperty(ExchangePropertyKey key) {
-        return internalProperties.remove(key);
+        if (key == ExchangePropertyKey.TO_ENDPOINT) {
+            Object answer = toEndpoint;
+            toEndpoint = null;
+            return answer;
+        }
+        return internalProperties != null ? internalProperties.remove(key) : 
null;
     }
 
     @Override
@@ -264,7 +282,7 @@ abstract class AbstractExchange implements Exchange, 
ExchangeExtension {
         Object answer = null;
         ExchangePropertyKey key = 
ExchangePropertyKey.asExchangePropertyKey(name);
         if (key != null) {
-            answer = internalProperties.get(key);
+            answer = getProperty(key);
             // if the property is not an internal then fallback to lookup in 
the properties map
         }
         if (answer == null && properties != null) {
@@ -326,7 +344,7 @@ abstract class AbstractExchange implements Exchange, 
ExchangeExtension {
             if (properties != null) {
                 properties.clear();
             }
-            internalProperties.clear();
+            clearInternalProperties();
             return true;
         }
 
@@ -338,7 +356,7 @@ abstract class AbstractExchange implements Exchange, 
ExchangeExtension {
                     continue;
                 }
                 matches = true;
-                internalProperties.remove(epk);
+                removeProperty(epk);
             }
         }
 
@@ -1012,7 +1030,18 @@ abstract class AbstractExchange implements Exchange, 
ExchangeExtension {
 
     @Override
     public void copyInternalProperties(Exchange target) {
-        ((AbstractExchange) 
target).internalProperties.putAll(internalProperties);
+        AbstractExchange exchange = (AbstractExchange) target;
+        // null means the source has no value, so the target keeps its own, as 
for a key that is not in the map
+        if (toEndpoint != null) {
+            exchange.toEndpoint = toEndpoint;
+        }
+        if (internalProperties != null) {
+            if (exchange.internalProperties == null) {
+                exchange.internalProperties = new 
EnumMap<>(internalProperties);
+            } else {
+                exchange.internalProperties.putAll(internalProperties);
+            }
+        }
     }
 
     @Override
@@ -1029,7 +1058,7 @@ abstract class AbstractExchange implements Exchange, 
ExchangeExtension {
     public Map<String, Object> getInternalProperties() {
         Map<String, Object> map = new HashMap<>();
         for (ExchangePropertyKey key : ExchangePropertyKey.values()) {
-            Object value = internalProperties.get(key);
+            Object value = getProperty(key);
             if (value != null) {
                 map.put(key.getName(), value);
             }
@@ -1040,10 +1069,18 @@ abstract class AbstractExchange implements Exchange, 
ExchangeExtension {
     @Override
     public Exchange createCopyWithProperties(CamelContext context) {
         DefaultExchange answer = new DefaultExchange(context, 
internalProperties, properties);
+        answer.setProperty(ExchangePropertyKey.TO_ENDPOINT, toEndpoint);
         answer.setPattern(pattern);
         return answer;
     }
 
+    void clearInternalProperties() {
+        toEndpoint = null;
+        if (internalProperties != null) {
+            internalProperties.clear();
+        }
+    }
+
     public void resetExtension() {
         if (unitOfWork != null) {
             unitOfWork.reset();
diff --git 
a/core/camel-support/src/main/java/org/apache/camel/support/DefaultPooledExchange.java
 
b/core/camel-support/src/main/java/org/apache/camel/support/DefaultPooledExchange.java
index 37e783558ae9..cfdda01fa15d 100644
--- 
a/core/camel-support/src/main/java/org/apache/camel/support/DefaultPooledExchange.java
+++ 
b/core/camel-support/src/main/java/org/apache/camel/support/DefaultPooledExchange.java
@@ -93,7 +93,7 @@ public final class DefaultPooledExchange extends 
AbstractExchange implements Poo
                     this.properties.clear();
                 }
             }
-            internalProperties.clear();
+            clearInternalProperties();
             if (this.safeCopyProperties != null) {
                 this.safeCopyProperties.clear();
             }
diff --git 
a/core/camel-support/src/main/java/org/apache/camel/support/ExtendedExchangeExtension.java
 
b/core/camel-support/src/main/java/org/apache/camel/support/ExtendedExchangeExtension.java
index 16dad05f146b..0b3442289386 100644
--- 
a/core/camel-support/src/main/java/org/apache/camel/support/ExtendedExchangeExtension.java
+++ 
b/core/camel-support/src/main/java/org/apache/camel/support/ExtendedExchangeExtension.java
@@ -422,8 +422,6 @@ public class ExtendedExchangeExtension implements 
ExchangeExtension {
 
     @Override
     public Exchange createCopyWithProperties(CamelContext context) {
-        DefaultExchange answer = new DefaultExchange(context, 
exchange.internalProperties, exchange.properties);
-        answer.setPattern(exchange.pattern);
-        return answer;
+        return exchange.createCopyWithProperties(context);
     }
 }

Reply via email to