Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
import org.apache.camel.spi.ExceptionHandler;
import org.apache.camel.spi.HeadersMapFactory;
import org.apache.camel.spi.IdAware;
import org.apache.camel.spi.NormalizedEndpointUri;
import org.apache.camel.spi.OptimisedComponentResolver;
import org.apache.camel.spi.PollDynamicAware;
import org.apache.camel.spi.RouteIdAware;
Expand All @@ -49,6 +50,7 @@
import org.apache.camel.support.EndpointHelper;
import org.apache.camel.support.EventDrivenPollingConsumer;
import org.apache.camel.support.ExchangeHelper;
import org.apache.camel.support.NormalizedUri;
import org.apache.camel.support.cache.DefaultConsumerCache;
import org.apache.camel.support.cache.EmptyConsumerCache;
import org.apache.camel.support.service.ServiceHelper;
Expand Down Expand Up @@ -491,7 +493,27 @@ private static boolean isBridgeErrorHandler(PollingConsumer consumer) {
}

protected static Object prepareRecipient(Exchange exchange, Object recipient) throws NoTypeConversionAvailableException {
return ProcessorHelper.prepareRecipient(exchange, recipient);
if (recipient instanceof Endpoint || recipient instanceof NormalizedEndpointUri) {
return recipient;
} else if (recipient instanceof String string) {
// trim strings as end users might have added spaces between separators
recipient = string.trim();
}
if (recipient != null) {
CamelContext ecc = exchange.getContext();
String uri;
if (recipient instanceof String string) {
uri = string;
} else {
// convert to a string type we can work with
uri = ecc.getTypeConverter().mandatoryConvertTo(String.class, exchange, recipient);
}
// optimize and normalize endpoint without re-resolving property placeholders on the
// per-message evaluated recipient, matching toD and enrich (CAMEL-24282 / CAMEL-24414).
// pollEnrich resolves its static uri at build time, so the placeholder belongs there.
return NormalizedUri.newNormalizedUri(uri, false);
}
return null;
}

protected static Endpoint getExistingEndpoint(Exchange exchange, Object recipient) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,10 @@
* only appears in the per-message evaluated recipient (e.g. from a header) is therefore treated as a literal endpoint
* uri and not re-expanded. See CAMEL-24282.
* <p/>
* {@code recipientList} / {@code routingSlip} / {@code dynamicRouter} compute their recipients entirely from a runtime
* expression (no static template resolved at build time), so they continue to resolve {@code {{...}}} in those
* recipients - this is relied upon e.g. by routes carrying a {@code {{port}}}-style placeholder in the recipient
* header, and must be preserved. {@code pollEnrich} does resolve its static uri at build time, but its per-message
* recipient goes through the same shared resolution path, so it too still resolves {@code {{...}}} (left unchanged in
* this PR).
* CAMEL-24414 extends the same treatment to {@code recipientList}, {@code routingSlip}, {@code dynamicRouter} and
* {@code pollEnrich}, which share {@code ProcessorHelper.prepareRecipient}. A placeholder written in the route is still
* resolved for those EIPs, at build time, by the model - see {@link #recipientListPlaceholderInRouteTextIsResolved()},
* which is why removing the per-message expansion does not take the legitimate pattern away.
*/
class DynamicEndpointMessagePlaceholderTest extends ContextTestSupport {

Expand Down Expand Up @@ -75,7 +73,8 @@ void enrichPlaceholderInHeaderNotResolved() throws Exception {

@Test
void recipientListPlaceholderInHeaderStillResolved() throws Exception {
// recipientList has no build-time template, so {{...}} in the recipient header is still resolved
// unchanged: a recipient supplied at runtime may legitimately carry a placeholder that comes from
// configuration (camel-ftp's FtpProducerRecipientListIT is an in-tree example)
getMockEndpoint("mock:resolved").expectedMessageCount(1);
getMockEndpoint("mock:done").expectedMessageCount(1);

Expand All @@ -84,6 +83,17 @@ void recipientListPlaceholderInHeaderStillResolved() throws Exception {
assertMockEndpointsSatisfied();
}

@Test
void recipientListPlaceholderInRouteTextIsResolved() throws Exception {
// the legitimate pattern: a placeholder written in the route is resolved at build time by the model,
// independently of the per-message path, so aligning recipientList does not remove it
getMockEndpoint("mock:resolved").expectedMessageCount(1);

template.sendBody("direct:rlConstant", "Hello");

assertMockEndpointsSatisfied();
}

@Test
void routingSlipPlaceholderInHeaderStillResolved() throws Exception {
getMockEndpoint("mock:resolved").expectedMessageCount(1);
Expand All @@ -105,17 +115,17 @@ void dynamicRouterPlaceholderInHeaderStillResolved() throws Exception {
}

@Test
void pollEnrichPlaceholderInHeaderStillResolved() throws Exception {
// pollEnrich's per-message recipient goes through the shared resolution path, so {{...}} is still resolved
void pollEnrichPlaceholderInHeaderNotResolved() throws Exception {
// CAMEL-24282 deferred aligning pollEnrich to a follow-up; this is that follow-up
template.sendBody("seda:resolved", "SEED");
getMockEndpoint("mock:done").expectedMessageCount(1);

template.sendBodyAndHeader("direct:pe", "trigger", "target", "seda:{{secretTarget}}");

assertMockEndpointsSatisfied();
// resolved -> pollEnrich drained the seed from seda:resolved (not from the literal seda:{{secretTarget}})
// not resolved -> pollEnrich polled the literal seda:{{secretTarget}} and left the seed untouched
SedaEndpoint seda = context.getEndpoint("seda:resolved", SedaEndpoint.class);
assertThat(seda.getQueue()).isEmpty();
assertThat(seda.getQueue()).hasSize(1);
}

@Test
Expand Down Expand Up @@ -144,6 +154,7 @@ public void configure() {
from("direct:en").enrich().simple("${header.target}")
.aggregationStrategy(AggregationStrategies.useOriginal()).to("mock:done");
from("direct:rl").recipientList(header("target")).to("mock:done");
from("direct:rlConstant").recipientList(constant("mock:{{secretTarget}}"));
from("direct:rs").routingSlip(header("target")).to("mock:done");
from("direct:dr").dynamicRouter(method(DynamicEndpointMessagePlaceholderTest.this, "route"));
from("direct:pe").pollEnrich().simple("${header.target}").timeout(2000).end().to("mock:done");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,26 @@ an `AGENTS.md` file with guidance for AI coding assistants, pointing at the Apac
The `camel-archetype-api-component` archetype also generates its readme again: the file was declared
in the wrong file set and was therefore silently skipped.

=== camel-core - property placeholders in pollEnrich

Camel 4.22 stopped resolving property placeholders (`{{...}}`) on the _per-message evaluated_
recipient for `toD` and `enrich`, and said that aligning `pollEnrich` was deferred to a follow-up.
This is that follow-up: a `{{...}}` token that appears only in the value produced at runtime by the
`pollEnrich` expression is now treated as a literal part of the endpoint URI instead of being
expanded.

Like `toD` and `enrich`, `pollEnrich` resolves its static endpoint URI at build time, so a
placeholder belongs there:

[source,java]
----
.pollEnrich("file:{{inbox}}", 5000)
----

`recipientList`, `routingSlip` and `dynamicRouter` are unchanged. Their recipient is supplied
entirely at runtime and may legitimately carry a placeholder that comes from configuration, so they
continue to resolve `{{...}}` in the computed recipient.

=== camel-hazelcast

`ReplicatedHazelcastAggregationRepository` now applies the same default
Expand Down
Loading