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}}";));
+    }
+}

Reply via email to