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 9285996c3096 CAMEL-25110: camel-core - Property placeholder parser:
fix bugs found in a deep review (#27016)
9285996c3096 is described below
commit 9285996c309676624af838f3511a76bf9765070f
Author: Claus Ibsen <[email protected]>
AuthorDate: Mon Sep 28 22:44:38 2026 +0200
CAMEL-25110: camel-core - Property placeholder parser: fix bugs found in a
deep review (#27016)
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Signed-off-by: Claus Ibsen <[email protected]>
---
.../properties/DefaultPropertiesParser.java | 58 +++++++++---
.../properties/PropertiesParserEdgeCasesTest.java | 103 +++++++++++++++++++++
2 files changed, 148 insertions(+), 13 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 16e9b394fa73..932936182470 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
@@ -71,7 +71,8 @@ public class DefaultPropertiesParser implements
PropertiesParser {
ParsingContext context
= new ParsingContext(properties, defaultFallbackEnabled,
keepUnresolvedOptional, nestedPlaceholder);
String answer = context.parse(text);
- if (keepUnresolvedOptional && answer != null &&
answer.contains(UNRESOLVED_PREFIX_TOKEN)) {
+ boolean ignoreMissing = propertiesComponent != null &&
propertiesComponent.isIgnoreMissingProperty();
+ if ((keepUnresolvedOptional || ignoreMissing) && answer != null &&
answer.contains(UNRESOLVED_PREFIX_TOKEN)) {
// replace temporary unresolved keys back to with placeholders so
they are kept as-is
answer = answer.replace(UNRESOLVED_PREFIX_TOKEN, PREFIX_TOKEN);
answer = answer.replace(UNRESOLVED_SUFFIX_TOKEN, SUFFIX_TOKEN);
@@ -110,12 +111,13 @@ public class DefaultPropertiesParser implements
PropertiesParser {
public String parse(String input) {
// does the key turn on or off nested?
boolean nested = nestedPlaceholder;
- if (input.contains("?nested=true")) {
+ // the option is in the key of a placeholder (such as
{{foo?nested=false}})
+ if (input.contains("?nested=true" + SUFFIX_TOKEN)) {
nested = true;
- input = input.replace("?nested=true", "");
- } else if (input.contains("?nested=false")) {
+ input = input.replace("?nested=true" + SUFFIX_TOKEN,
SUFFIX_TOKEN);
+ } else if (input.contains("?nested=false" + SUFFIX_TOKEN)) {
nested = false;
- input = input.replace("?nested=false", "");
+ input = input.replace("?nested=false" + SUFFIX_TOKEN,
SUFFIX_TOKEN);
}
if (nested) {
return doParseNested(null, input, new HashSet<>());
@@ -138,19 +140,28 @@ public class DefaultPropertiesParser implements
PropertiesParser {
StringBuilder answer = new StringBuilder(input.length());
Property property;
String prevKey = null;
+ boolean first = true;
while ((property = readProperty(prevKey, input)) != null) {
- String before = input.substring(0, property.getBeginIndex());
+ int beginIndex = property.getBeginIndex();
+ if (beginIndex > 0 && input.charAt(beginIndex - 1) == '\\') {
+ // The escape character has been escaped, so we need to
restore it
+ beginIndex--;
+ }
+ String before = input.substring(0, beginIndex);
String after = input.substring(property.getEndIndex());
String parsed = property.getValue();
- if (parsed != null) {
- answer.append(before);
- answer.append(parsed);
- } else if (property.getBeginIndex() == 0 && input.length() ==
property.getEndIndex()) {
+ if (parsed == null && first && property.getBeginIndex() == 0
+ && input.length() == property.getEndIndex()) {
// its only a single placeholder which is parsed as null
return null;
}
+ answer.append(before);
+ if (parsed != null) {
+ answer.append(parsed);
+ }
input = after;
prevKey = property.getKey();
+ first = false;
}
if (!input.isEmpty()) {
answer.append(input);
@@ -171,7 +182,10 @@ public class DefaultPropertiesParser implements
PropertiesParser {
}
String answer = input;
Property property;
- while ((property = readProperty(prevKey, answer)) != null) {
+ // the index to continue scanning from (after the resolved value
of the previous placeholder, as the
+ // resolved value must not be parsed again, such as a value that
ends with an escape character)
+ int from = 0;
+ while ((property = readProperty(prevKey, answer, from)) != null) {
if (replacedPropertyKeys.contains(property.getKey())) {
// Check for circular references, also for optional keys,
as a circular reference can never be
// resolved (returning the text unresolved would make the
caller parse the same placeholder again,
@@ -190,7 +204,7 @@ public class DefaultPropertiesParser implements
PropertiesParser {
newReplaced.add(property.getKey());
int beginIndex = property.getBeginIndex();
- if (beginIndex > 0 && answer.charAt(beginIndex - 1) == '\\') {
+ if (beginIndex > from && answer.charAt(beginIndex - 1) ==
'\\') {
// The escape character has been escaped, so we need to
restore it
beginIndex--;
}
@@ -199,6 +213,7 @@ public class DefaultPropertiesParser implements
PropertiesParser {
String parsed = doParseNested(property.getKey(),
property.getValue(), newReplaced);
if (parsed != null) {
answer = before + parsed + after;
+ from = before.length() + parsed.length();
} else {
if (beginIndex == 0 && input.length() ==
property.getEndIndex()) {
// its only a single placeholder which is parsed as
null
@@ -206,6 +221,7 @@ public class DefaultPropertiesParser implements
PropertiesParser {
break;
} else {
answer = before + after;
+ from = before.length();
}
}
}
@@ -218,6 +234,18 @@ public class DefaultPropertiesParser implements
PropertiesParser {
* @param input Input string
* @return A property in the given string or {@code null} if not
found
*/
+ private Property readProperty(String prevKey, String input, int from) {
+ if (from <= 0) {
+ return readProperty(prevKey, input);
+ }
+ Property property = readProperty(prevKey, input.substring(from));
+ if (property == null) {
+ return null;
+ }
+ return new Property(
+ property.getBeginIndex() + from, property.getEndIndex() +
from, property.getKey(), property.getValue());
+ }
+
private Property readProperty(String prevKey, String input) {
// Find the index of the first valid suffix token
int suffix = getSuffixIndex(input);
@@ -329,7 +357,8 @@ public class DefaultPropertiesParser implements
PropertiesParser {
PropertiesFunction function =
propertiesComponent.getPropertiesFunction(prefix);
if (function != null) {
String remainder = StringHelper.after(key, ":");
- boolean remainderOptional =
remainder.startsWith(OPTIONAL_TOKEN);
+ // optional can be marked on the key ({{?fn:key}}) or on
the remainder ({{fn:?key}})
+ boolean remainderOptional = optional ||
remainder.startsWith(OPTIONAL_TOKEN);
if (function.lookupFirst(remainder)) {
String value = getPropertyValue(prevKey, remainder,
input);
if (value == null && (remainderOptional ||
function.optional(remainder))) {
@@ -341,6 +370,9 @@ public class DefaultPropertiesParser implements
PropertiesParser {
} else {
remainder = value;
}
+ } else if (remainder.startsWith(OPTIONAL_TOKEN)) {
+ // the function should use the key without the
optional token
+ remainder =
remainder.substring(OPTIONAL_TOKEN.length());
}
log.debug("Property with key [{}] is applied by function
[{}]", key, function.getName());
String value = function.apply(remainder);
diff --git
a/core/camel-core/src/test/java/org/apache/camel/component/properties/PropertiesParserEdgeCasesTest.java
b/core/camel-core/src/test/java/org/apache/camel/component/properties/PropertiesParserEdgeCasesTest.java
new file mode 100644
index 000000000000..bbd87d3b9e37
--- /dev/null
+++
b/core/camel-core/src/test/java/org/apache/camel/component/properties/PropertiesParserEdgeCasesTest.java
@@ -0,0 +1,103 @@
+/*
+ * 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.Properties;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.impl.DefaultCamelContext;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+public class PropertiesParserEdgeCasesTest {
+
+ private CamelContext context;
+
+ private CamelContext createContext(boolean nested) {
+ if (context != null) {
+ context.stop();
+ }
+ context = new DefaultCamelContext();
+ Properties prop = new Properties();
+ prop.setProperty("a", "A");
+ prop.setProperty("dir", "C:\\temp\\");
+ prop.setProperty("file", "f.txt");
+ context.getPropertiesComponent().setInitialProperties(prop);
+ context.getPropertiesComponent().setNestedPlaceholder(nested);
+ context.start();
+ return context;
+ }
+
+ @AfterEach
+ public void stop() {
+ if (context != null) {
+ context.stop();
+ }
+ System.clearProperty("camel.parser.test");
+ }
+
+ @Test
+ public void testOptionalFunction() {
+ System.setProperty("camel.parser.test", "S");
+ createContext(true);
+ assertEquals("S",
context.resolvePropertyPlaceholders("{{sys:?camel.parser.test}}"));
+ assertEquals("S",
context.resolvePropertyPlaceholders("{{?sys:camel.parser.test}}"));
+
assertNull(context.resolvePropertyPlaceholders("{{sys:?camel.parser.nope}}"));
+
assertNull(context.resolvePropertyPlaceholders("{{?sys:camel.parser.nope}}"));
+ }
+
+ @Test
+ public void testIgnoreMissingProperty() {
+ createContext(true);
+ context.getPropertiesComponent().setIgnoreMissingProperty(true);
+ assertEquals("{{nope}}",
context.resolvePropertyPlaceholders("{{nope}}"));
+ assertEquals("x-{{nope}}-A",
context.resolvePropertyPlaceholders("x-{{nope}}-{{a}}"));
+ }
+
+ @Test
+ public void testValueEndingWithBackslash() {
+ createContext(true);
+ assertEquals("C:\\temp\\f.txt",
context.resolvePropertyPlaceholders("{{dir}}{{file}}"));
+ createContext(false);
+ assertEquals("C:\\temp\\f.txt",
context.resolvePropertyPlaceholders("{{dir}}{{file}}"));
+ }
+
+ @Test
+ public void testNotNestedOptional() {
+ createContext(false);
+ assertEquals("abcdef",
context.resolvePropertyPlaceholders("abc{{?x}}def"));
+ assertEquals("abc", context.resolvePropertyPlaceholders("abc{{?x}}"));
+ assertEquals("", context.resolvePropertyPlaceholders("{{?x}}{{?y}}"));
+ }
+
+ @Test
+ public void testEscapedBackslash() {
+ createContext(true);
+ assertEquals("x\\A",
context.resolvePropertyPlaceholders("x\\\\{{a}}"));
+ createContext(false);
+ assertEquals("x\\A",
context.resolvePropertyPlaceholders("x\\\\{{a}}"));
+ }
+
+ @Test
+ public void testNestedOptionOnlyInPlaceholder() {
+ createContext(true);
+ assertEquals("http://x?nested=true&A",
context.resolvePropertyPlaceholders("http://x?nested=true&{{a}}"));
+ }
+}