This is an automated email from the ASF dual-hosted git repository. davsclaus pushed a commit to branch CAMEL-25922-nested-unmatched-suffix in repository https://gitbox.apache.org/repos/asf/camel.git
commit cdca36e7cef215f6503f714dfb383071de996982 Author: Claus Ibsen <[email protected]> AuthorDate: Wed Nov 4 21:57:16 2026 +0100 CAMEL-25922: camel-core - a placeholder value with }} but no {{ (such as JSON) is text, not a missing {{ The resolved value of a placeholder is parsed again for nested placeholders, and a }} without a {{ failed with "Missing {{ from the text". JSON that ends with a nested object or a Simple expression always ends with }}, so it failed as a route template or Kamelet parameter, or as a property value. A resolved value without a {{ that can start a placeholder (not escaped or quoted) is now returned as it is, as text without {{ at the top level is. This is the option to relax the check on nested values from CAMEL-18576. Co-Authored-By: Claude Opus 5.5 <[email protected]> Signed-off-by: Claus Ibsen <[email protected]> --- .../properties/DefaultPropertiesParser.java | 5 ++ .../PropertiesComponentNestedJsonValueTest.java | 83 ++++++++++++++++++++++ .../processor/ExpressionPlaceholderNestedTest.java | 29 ++++---- .../ROOT/pages/using-propertyplaceholder.adoc | 18 +++-- 4 files changed, 115 insertions(+), 20 deletions(-) diff --git a/core/camel-base/src/main/java/org/apache/camel/component/properties/DefaultPropertiesParser.java b/core/camel-base/src/main/java/org/apache/camel/component/properties/DefaultPropertiesParser.java index 3bb4eb201663..408e0c5e817f 100644 --- a/core/camel-base/src/main/java/org/apache/camel/component/properties/DefaultPropertiesParser.java +++ b/core/camel-base/src/main/java/org/apache/camel/component/properties/DefaultPropertiesParser.java @@ -184,6 +184,11 @@ public class DefaultPropertiesParser implements PropertiesParser { if (input == null) { return null; } + if (prevKey != null && getMatchingPrefixIndex(input, input.length()) == -1) { + // a resolved value without a {{ (that is not escaped or quoted) has no placeholders, so a }} in it + // (such as JSON) is text, as it is in text without {{ at the top level + return input; + } String answer = input; Property property; // the index to continue scanning from (after the resolved value of the previous placeholder, as the diff --git a/core/camel-core/src/test/java/org/apache/camel/component/properties/PropertiesComponentNestedJsonValueTest.java b/core/camel-core/src/test/java/org/apache/camel/component/properties/PropertiesComponentNestedJsonValueTest.java new file mode 100644 index 000000000000..0ee76f723f52 --- /dev/null +++ b/core/camel-core/src/test/java/org/apache/camel/component/properties/PropertiesComponentNestedJsonValueTest.java @@ -0,0 +1,83 @@ +/* + * 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.properties; + +import java.util.Map; +import java.util.Properties; + +import org.apache.camel.ContextTestSupport; +import org.apache.camel.builder.RouteBuilder; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * A property value without {{ that contains }} (such as JSON that ends with a nested object) is text. + */ +public class PropertiesComponentNestedJsonValueTest extends ContextTestSupport { + + @Test + public void testJsonValue() { + Properties props = new Properties(); + props.put("json", "{\"a\":{\"b\":1}}"); + props.put("ref", "{{json}}"); + props.put("mixed", "{\"a\":{{json}}}"); + context.getPropertiesComponent().setInitialProperties(props); + + assertThat(context.resolvePropertyPlaceholders("{{json}}")).isEqualTo("{\"a\":{\"b\":1}}"); + assertThat(context.resolvePropertyPlaceholders("{{ref}}")).isEqualTo("{\"a\":{\"b\":1}}"); + assertThat(context.resolvePropertyPlaceholders("{{mixed}}")).isEqualTo("{\"a\":{\"a\":{\"b\":1}}}"); + } + + @Test + public void testJsonValueWithDoubleCurlyBrackets() { + // a value with {{ that is not a Camel placeholder needs nested=false or escaping + Properties props = new Properties(); + props.put("template", "{\"query\":{\"match\":{\"body\":\"Hello {{name}}\"}}}"); + props.put("escaped", "{\"query\":{\"match\":{\"body\":\"Hello \\{{name\\}}\"}}}"); + context.getPropertiesComponent().setInitialProperties(props); + + assertThat(context.resolvePropertyPlaceholders("{{template?nested=false}}")) + .isEqualTo("{\"query\":{\"match\":{\"body\":\"Hello {{name}}\"}}}"); + assertThat(context.resolvePropertyPlaceholders("{{escaped}}")) + .isEqualTo("{\"query\":{\"match\":{\"body\":\"Hello {{name}}\"}}}"); + } + + @Test + public void testRouteTemplateParameter() throws Exception { + context.addRoutes(new RouteBuilder() { + @Override + public void configure() { + routeTemplate("setBodyTemplate") + .templateParameter("expression") + .from("direct:{{routeId}}") + .setBody().simple("{{expression}}"); + } + }); + context.addRouteFromTemplate("templated", "setBodyTemplate", + Map.of("routeId", "templated", "expression", "{\"title\":${header.title}}")); + context.start(); + + Object out = template.requestBodyAndHeader("direct:templated", null, "title", "\"Hello\""); + assertThat(out).isEqualTo("{\"title\":\"Hello\"}"); + } + + @Override + public boolean isUseRouteBuilder() { + return false; + } +} diff --git a/core/camel-core/src/test/java/org/apache/camel/processor/ExpressionPlaceholderNestedTest.java b/core/camel-core/src/test/java/org/apache/camel/processor/ExpressionPlaceholderNestedTest.java index 05a45ffd8078..e91d04c11f32 100644 --- a/core/camel-core/src/test/java/org/apache/camel/processor/ExpressionPlaceholderNestedTest.java +++ b/core/camel-core/src/test/java/org/apache/camel/processor/ExpressionPlaceholderNestedTest.java @@ -20,13 +20,10 @@ import java.util.Properties; import org.apache.camel.CamelContext; import org.apache.camel.ContextTestSupport; -import org.apache.camel.FailedToCreateRouteException; import org.apache.camel.builder.RouteBuilder; import org.apache.camel.component.mock.MockEndpoint; import org.junit.jupiter.api.Test; -import static org.junit.jupiter.api.Assertions.assertThrows; - public class ExpressionPlaceholderNestedTest extends ContextTestSupport { @Override @@ -54,16 +51,22 @@ public class ExpressionPlaceholderNestedTest extends ContextTestSupport { @Test public void testPlaceholderOn() throws Exception { - FailedToCreateRouteException exception - = assertThrows(FailedToCreateRouteException.class, () -> context.addRoutes(new RouteBuilder() { - @Override - public void configure() { - from("direct:on") - .setBody().constant("{{query?nested=true}}") - .to("mock:result"); - } - })); - assertIsInstanceOf(IllegalArgumentException.class, exception.getCause()); + // the value has no {{, so the }} at its end is text + context.addRoutes(new RouteBuilder() { + @Override + public void configure() { + from("direct:on") + .setBody().constant("{{query?nested=true}}") + .to("mock:result"); + } + }); + + MockEndpoint mock = getMockEndpoint("mock:result"); + mock.expectedBodiesReceived("{\"query\":{\"match_all\":{}}}"); + + template.sendBody("direct:on", "Hello World"); + + assertMockEndpointsSatisfied(); } @Test diff --git a/docs/user-manual/modules/ROOT/pages/using-propertyplaceholder.adoc b/docs/user-manual/modules/ROOT/pages/using-propertyplaceholder.adoc index 276667af91df..c8166bedc2cc 100644 --- a/docs/user-manual/modules/ROOT/pages/using-propertyplaceholder.adoc +++ b/docs/user-manual/modules/ROOT/pages/using-propertyplaceholder.adoc @@ -368,7 +368,11 @@ YAML:: ==== Turning off nested placeholders (advanced) -If the placeholder value contains data that interfere with the property placeholder syntax `{{` and `}}` (such as JSon data), +The value of a placeholder is parsed for nested placeholders only when it contains `{{`. +A value without `{{` is used as it is, even when it contains `}}`, such as JSON that ends with a nested object +(`{"query":{"match_all":{}}}`). + +If the placeholder value contains `{{` that is not a Camel placeholder (such as JSON with a Mustache template), you can be then explicit turn off nested placeholder by `?nested=false` in the key name, such as shown: [tabs] @@ -417,10 +421,10 @@ In the example above the query of the search is the body, and the _myQuery_ plac [source,properties] ---- -myQuery = {"query":{"match_all":{}}} +myQuery = {"query":{"match":{"body":"Hello {{name}}"}}} ---- -Notice how the JSon query ends with `}}` which interfere with the Camel property placeholder syntax. +Notice how the JSON query searches for the text `{{name}}`, which interferes with the Camel property placeholder syntax. Nested placeholders can also be turned off globally on the xref:components::properties-component.adoc[Properties] component, such as: @@ -430,20 +434,20 @@ CamelContext context = ... context.getPropertiesComponent().setNestedPlaceholder(false); ---- -In the situation with the JSon query ending with `}}` which clashes with Camels property placeholder syntax, then its also possible +In the situation with the JSON query with `{{` which clashes with Camels property placeholder syntax, then its also possible to use escaping as explained in the following section. ==== Escaping a property placeholder -The property placeholder can be problematic if the double curly brackets are used by a third party library like for example a query in ElasticSearch of type `{"query":{"match_all":{}}}`. +The property placeholder can be problematic if the double curly brackets are used by a third party library like for example a Mustache template, such as `Hello {{name}}`. To work around that it is possible to escape the double curly brackets with a backslash character like for example `\{{ property-name \}}`. This way, it won't be interpreted as a property placeholder to resolve and will be resolved as `{{ property-name }}`. -The previous example can then use escaping (see how the value ends with `\\}}`) +The previous example can then use escaping (see how `{{name}}` is written as `\\{{name\\}}`) [source,properties] ---- -myQuery = {"query":{"match_all":{}\\}} +myQuery = {"query":{"match":{"body":"Hello \\{{name\\}}"}}} ---- If for some reason, the backslash character before the double curly brackets must not be interpreted as an escape character, it is possible to add another backslash in front of it to escape it, it will then be seen as a backslash.
