This is an automated email from the ASF dual-hosted git repository.

davsclaus pushed a commit to branch fix/CAMEL-24962
in repository https://gitbox.apache.org/repos/asf/camel.git

commit 1746815b241fcb82f3968548584b4f7d7c08d113
Author: Claus Ibsen <[email protected]>
AuthorDate: Wed Sep 23 18:53:41 2026 +0200

    CAMEL-24968: simple - fix string, math, date and file function edge cases
    
    - capitalize of an empty value, and capitalize all after any whitespace 
(camel-util)
    - sum, max, min and average without arguments use the message body, as 
documented
    - date:header.Last-Modified and date:header.my.date read the named header 
(an offset starts with a digit)
    - date-with-timezone accepts a timezone with a colon, such as GMT+02:00
    - safeQuote escapes quotes, backslashes and control characters so the 
result is valid JSON
    - kindOfType: BigDecimal is a number, a Map is an object, any array is an 
array
    - substring with a null number reports it; substringBefore/After/Between 
with no delimiter return null
    - pad with a separator of several characters pads to the exact width; docs 
and catalog had the direction backwards
    - size and length count any kind of array
    - a leading dot is a hidden file, not an extension: .bashrc has no 
extension (camel-util FileUtil)
    
    Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
---
 .../org/apache/camel/language/simple/simple.json   |   2 +-
 .../modules/languages/pages/simple-functions.adoc  |   7 +-
 .../language/simple/DateExpressionBuilder.java     |   9 +-
 .../language/simple/MiscExpressionBuilder.java     |  40 +++---
 .../camel/language/simple/SimpleConstants.java     |  10 +-
 .../language/simple/StringExpressionBuilder.java   |  93 ++++++-------
 .../simple/functions/DateFunctionFactory.java      |  10 ++
 .../simple/functions/MathFunctionFactory.java      |   3 +-
 .../simple/SimpleStringMathDateFunctionsTest.java  | 144 +++++++++++++++++++++
 .../java/org/apache/camel/util/FileUtilTest.java   |  11 ++
 .../org/apache/camel/support/LanguageHelper.java   |   9 +-
 .../main/java/org/apache/camel/util/FileUtil.java  |  10 +-
 .../java/org/apache/camel/util/StringHelper.java   |  10 +-
 .../org/apache/camel/util/StringHelperTest.java    |   5 +
 14 files changed, 274 insertions(+), 89 deletions(-)

diff --git 
a/core/camel-core-languages/src/generated/resources/META-INF/org/apache/camel/language/simple/simple.json
 
b/core/camel-core-languages/src/generated/resources/META-INF/org/apache/camel/language/simple/simple.json
index a1df2fe174f9..dfc3d543b094 100644
--- 
a/core/camel-core-languages/src/generated/resources/META-INF/org/apache/camel/language/simple/simple.json
+++ 
b/core/camel-core-languages/src/generated/resources/META-INF/org/apache/camel/language/simple/simple.json
@@ -131,7 +131,7 @@
     "not": { "index": 102, "kind": "function", "displayName": "Not", "group": 
"condition", "label": "condition", "required": false, "javaType": "boolean", 
"prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, 
"secret": false, "description": "Evaluates the predicate and returns the 
opposite.", "ognl": false, "suffix": "}" },
     "null": { "index": 103, "kind": "function", "displayName": "Null", 
"group": "other", "label": "other", "required": false, "javaType": "Object", 
"prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, 
"secret": false, "description": "Returns a null value", "ognl": false, 
"suffix": "}", "examples": [ "${null} -> null" ] },
     "originalBody": { "index": 104, "kind": "function", "displayName": 
"Original Body", "group": "core", "label": "core", "required": false, 
"javaType": "Object", "prefix": "${", "deprecated": false, "deprecationNote": 
"", "autowired": false, "secret": false, "description": "The original incoming 
body (only available if allowUseOriginalMessage=true).", "ognl": false, 
"suffix": "}" },
-    "pad(exp,width,separator)": { "index": 105, "kind": "function", 
"displayName": "Pad String", "group": "string", "label": "string", "required": 
false, "javaType": "String", "prefix": "${", "deprecated": false, 
"deprecationNote": "", "autowired": false, "secret": false, "description": 
"Pads the expression with extra padding if necessary, according the the total 
width. The separator is by default a space. If the width is negative then 
padding to the right, otherwise to the left.", "ognl [...]
+    "pad(exp,width,separator)": { "index": 105, "kind": "function", 
"displayName": "Pad String", "group": "string", "label": "string", "required": 
false, "javaType": "String", "prefix": "${", "deprecated": false, 
"deprecationNote": "", "autowired": false, "secret": false, "description": 
"Pads the expression with extra padding if necessary, according to the total 
width. The separator is by default a space. A positive width pads to the right 
(after the value), and a negative width pads to  [...]
     "pretty(exp)": { "index": 106, "kind": "function", "displayName": "Pretty 
Print", "group": "xml", "label": "json,xml", "required": false, "javaType": 
"String", "prefix": "${", "deprecated": false, "deprecationNote": "", 
"autowired": false, "secret": false, "description": "Converts the expression to 
a String, and attempts to pretty print if JSon or XML, otherwise the expression 
is returned as the String value.", "ognl": false, "suffix": "}", "params": [ { 
"name": "exp", "javaType": "O [...]
     "prettyBody": { "index": 107, "kind": "function", "displayName": "Pretty 
Body", "group": "xml", "label": "json,xml", "required": false, "javaType": 
"String", "prefix": "${", "deprecated": false, "deprecationNote": "", 
"autowired": false, "secret": false, "description": "Converts the body to a 
String, and attempts to pretty print if JSon or XML; otherwise the body is 
returned as the String value.", "ognl": false, "suffix": "}" },
     "sort(exp,reverse)": { "index": 108, "kind": "function", "displayName": 
"Sort", "group": "collection", "label": "collection", "required": false, 
"javaType": "List", "prefix": "${", "deprecated": false, "deprecationNote": "", 
"autowired": false, "secret": false, "description": "Sorts the message body or 
expression in natural order", "ognl": false, "suffix": "}", "params": [ { 
"name": "exp", "javaType": "Object", "required": false, "defaultValue": "body", 
"description": "The expression [...]
diff --git 
a/core/camel-core-languages/src/main/docs/modules/languages/pages/simple-functions.adoc
 
b/core/camel-core-languages/src/main/docs/modules/languages/pages/simple-functions.adoc
index 525d77ecd2f1..41d80e7bbcf8 100644
--- 
a/core/camel-core-languages/src/main/docs/modules/languages/pages/simple-functions.adoc
+++ 
b/core/camel-core-languages/src/main/docs/modules/languages/pages/simple-functions.adoc
@@ -520,7 +520,7 @@ The string functions is various functions to work with 
values that are String an
 |`lowercase(exp)` | `String` | Lowercases the expression
 |`normalizeWhitespace()` | `String` | Normalizes the whitespace in the message 
body by cleaning up excess whitespaces.
 |`normalizeWhitespace(exp)` | `String` | Normalizes the whitespace in the 
expression by cleaning up excess whitespaces.
-|`pad(exp,width,separator)` | `String` | Pads the expression with extra 
padding if necessary, according the total width The separator is by default a 
space. If the width is negative then padding to the right, otherwise to the 
left.
+|`pad(exp,width,separator)` | `String` | Pads the expression with extra 
padding if necessary, according to the total width. The separator is by default 
a space. A positive width pads to the right (after the value), and a negative 
width pads to the left (before the value).
 |`replace(from,to)` | `String` | Replace all the string values in the message 
body. To make it easier to replace single and double quotes, then you can use 
XML escaped values `\&quot;` as double quote, `\&apos;` as single quote, and 
`\&empty;` as empty value.
 |`replace(from,to,exp)` | `String` | Replace all the string values in the 
given expression. To make it easier to replace single and double quotes, then 
you can use XML escaped values `\&quot;` as double quote, `\&apos;` as single 
quote, and `\&empty;` as empty value.
 |`safeQuote()` | `String` | Returns the message body safely quoted if needed
@@ -636,13 +636,16 @@ If the value is currently in single quote, then this will 
be converted to double
 
 If the message body contains `Hello World` then `${quote()}` returns `"Hello 
World"`.
 And using `${quote('Hi from me')}` then `"Hi from me"` is returned.
+The `quote` function does not escape the value, so a value with double quotes 
or backslashes is not a valid JSON string;
+use `safeQuote` (or `escape(json)`) for JSON.
 
 The `unquote` function removes any surrounding single or double quotes from a 
value.
 If the body contains `'hello'` then `${unquote()}` returns `hello` (without 
the quotes).
 
 The `safeQuote` function is quoting the value depending on the value type
 (uses the same logic as `kindOfType` function). In essence values that are 
null, boolean or numbers are not quoted,
-while everything else is. The `safeQuote` function is useful when working with 
JSON data.
+while everything else is. When quoted, double quotes, backslashes and control 
characters in the value are escaped,
+so the result is a valid JSON string. The `safeQuote` function is useful when 
working with JSON data.
 
 For example when doing JSON to JSON mapping you can extract values form the 
source document,
 to be included in the output, but the values may or may not need to be quoted. 
Then you can use the `~>` chain operator
diff --git 
a/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/DateExpressionBuilder.java
 
b/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/DateExpressionBuilder.java
index fc9a006d0b81..407409fb1de2 100644
--- 
a/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/DateExpressionBuilder.java
+++ 
b/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/DateExpressionBuilder.java
@@ -18,6 +18,7 @@ package org.apache.camel.language.simple;
 
 import java.util.Date;
 import java.util.List;
+import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
 import org.apache.camel.Exchange;
@@ -30,7 +31,9 @@ import org.apache.camel.support.LanguageHelper;
  */
 public final class DateExpressionBuilder {
 
-    private static final Pattern OFFSET_PATTERN = 
Pattern.compile("([+-])([^+-]+)");
+    // an offset starts with a digit, so a name such as header.Last-Modified 
is not an offset
+    private static final Pattern OFFSET_PATTERN = 
Pattern.compile("([+-])\\s*(\\d[^+-]*)");
+    private static final Pattern OFFSET_START = Pattern.compile("[+-]\\s*\\d");
 
     private DateExpressionBuilder() {
     }
@@ -46,7 +49,9 @@ public final class DateExpressionBuilder {
     public static Expression dateExpression(
             final String commandWithOffsets, final String timezone,
             final String pattern) {
-        final String command = commandWithOffsets.split("[+-]", 2)[0].trim();
+        final Matcher offsetStart = OFFSET_START.matcher(commandWithOffsets);
+        final String command
+                = (offsetStart.find() ? commandWithOffsets.substring(0, 
offsetStart.start()) : commandWithOffsets).trim();
         final List<Long> offsets = 
LanguageHelper.captureOffsets(commandWithOffsets, OFFSET_PATTERN);
 
         return new ExpressionAdapter() {
diff --git 
a/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/MiscExpressionBuilder.java
 
b/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/MiscExpressionBuilder.java
index f1a7c23e0a1d..5adb2c781128 100644
--- 
a/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/MiscExpressionBuilder.java
+++ 
b/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/MiscExpressionBuilder.java
@@ -454,22 +454,7 @@ public final class MiscExpressionBuilder {
                 } else {
                     value = exchange.getMessage().getBody();
                 }
-                if (value != null) {
-                    Class<?> type = value.getClass();
-                    if (ObjectHelper.isNumericType(type)) {
-                        return "number";
-                    } else if (boolean.class == type || Boolean.class == type) 
{
-                        return "boolean";
-                    } else if (value instanceof CharSequence) {
-                        return "string";
-                    } else if (ObjectHelper.isPrimitiveArrayType(type) || 
value instanceof Collection
-                            || value instanceof Map<?, ?>) {
-                        return "array";
-                    } else {
-                        return "object";
-                    }
-                }
-                return "null";
+                return kindOfType(value);
             }
 
             @Override
@@ -1113,4 +1098,27 @@ public final class MiscExpressionBuilder {
             }
         };
     }
+
+    /**
+     * What kind of type is the value in JSON terms (null, number, string, 
boolean, array or object)
+     */
+    static String kindOfType(Object value) {
+        if (value == null) {
+            return "null";
+        }
+        Class<?> type = value.getClass();
+        if (value instanceof Number) {
+            // also BigDecimal and BigInteger, such as numbers from a JSON 
document
+            return "number";
+        } else if (Boolean.class == type) {
+            return "boolean";
+        } else if (value instanceof CharSequence) {
+            return "string";
+        } else if (type.isArray() || value instanceof Collection) {
+            return "array";
+        } else {
+            // a Map is a JSON object
+            return "object";
+        }
+    }
 }
diff --git 
a/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/SimpleConstants.java
 
b/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/SimpleConstants.java
index 992ea86f809e..7e676b064f05 100644
--- 
a/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/SimpleConstants.java
+++ 
b/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/SimpleConstants.java
@@ -656,15 +656,15 @@ public final class SimpleConstants {
               label = "core")
     public static final String ORIGINAL_BODY = "originalBody";
 
-    @Metadata(description = "Pads the expression with extra padding if 
necessary, according the the total width. The separator is by default a space. 
If the width is negative then padding to the right, otherwise to the left.",
+    @Metadata(description = "Pads the expression with extra padding if 
necessary, according to the total width. The separator is by default a space. A 
positive width pads to the right (after the value), and a negative width pads 
to the left (before the value).",
               label = "string", javaType = "String", displayName = "Pad 
String",
               examples = {
-                      "${pad('Hi',10)} ->         Hi // left-padded with 
spaces to width 10",
-                      "${pad('Hi',-10)} -> Hi         // right-padded with 
spaces",
-                      "${pad('42',5,'0')} -> 00042 // left-padded with zeros" 
},
+                      "${pad('Hi',10)} -> Hi         // padded to the right 
with spaces to width 10",
+                      "${pad('Hi',-10)} ->         Hi // padded to the left 
with spaces",
+                      "${pad('42',-5,'0')} -> 00042 // padded to the left with 
zeros" },
               annotations = {
                       "param=exp:Object:required::The expression to pad",
-                      "param=width:int:required::The target width. Negative 
for right-padding",
+                      "param=width:int:required::The target width. Positive 
pads to the right, negative pads to the left",
                       "param=separator:String:optional: :The padding character 
(default is space)" })
     public static final String PAD = "pad(exp,width,separator)";
 
diff --git 
a/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/StringExpressionBuilder.java
 
b/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/StringExpressionBuilder.java
index a14654126b55..2c66feb6d0eb 100644
--- 
a/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/StringExpressionBuilder.java
+++ 
b/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/StringExpressionBuilder.java
@@ -17,6 +17,7 @@
 package org.apache.camel.language.simple;
 
 import java.io.InputStream;
+import java.lang.reflect.Array;
 import java.util.Collection;
 import java.util.Locale;
 import java.util.Map;
@@ -66,30 +67,22 @@ public final class StringExpressionBuilder {
                     value = exchange.getMessage().getBody(Object.class);
                 }
                 if (value != null) {
-                    String type = kindOfType(value);
+                    String type = MiscExpressionBuilder.kindOfType(value);
                     if ("string".equals(type) || "array".equals(type) || 
"object".equals(type)) {
                         String body = 
exchange.getContext().getTypeConverter().tryConvertTo(String.class, exchange, 
value);
-                        body = StringHelper.removeLeadingAndEndingQuotes(body);
-                        value = StringQuoteHelper.doubleQuote(body);
+                        // a value that is already one quoted string is not 
quoted twice
+                        if (isQuotedOnce(body, '"') || isQuotedOnce(body, 
'\'')) {
+                            body = body.substring(1, body.length() - 1);
+                        }
+                        // escape quotes, backslashes and control characters 
so the result is a valid JSON string
+                        value = "\"" + 
EscapeHelper.escape(EscapeHelper.Kind.JSON, body) + "\"";
                     }
                 }
                 return value;
             }
 
-            private String kindOfType(Object value) {
-                Class<?> type = value.getClass();
-                if (ObjectHelper.isNumericType(type)) {
-                    return "number";
-                } else if (boolean.class == type || Boolean.class == type) {
-                    return "boolean";
-                } else if (value instanceof CharSequence) {
-                    return "string";
-                } else if (ObjectHelper.isPrimitiveArrayType(type) || value 
instanceof Collection
-                        || value instanceof Map<?, ?>) {
-                    return "array";
-                } else {
-                    return "object";
-                }
+            private static boolean isQuotedOnce(String text, char quote) {
+                return text.length() >= 2 && text.charAt(0) == quote && 
text.indexOf(quote, 1) == text.length() - 1;
             }
 
             @Override
@@ -296,14 +289,16 @@ public final class StringExpressionBuilder {
                 }
 
                 int max = Math.abs(width);
-                while (max > answer.length()) {
-                    if (width > 0) {
-                        answer = answer + sep;
-                    } else {
-                        answer = sep + answer;
-                    }
+                if (max <= answer.length()) {
+                    return answer;
+                }
+                // a separator of several characters is cut so the answer is 
exactly the width
+                StringBuilder padding = new StringBuilder(max);
+                while (padding.length() < max - answer.length()) {
+                    padding.append(sep);
                 }
-                return answer;
+                padding.setLength(max - answer.length());
+                return width > 0 ? answer + padding : padding + answer;
             }
 
             @Override
@@ -451,18 +446,9 @@ public final class StringExpressionBuilder {
                     body = exchange.getMessage().getBody();
                 }
                 if (body != null) {
-                    if (body instanceof byte[] arr) {
-                        return arr.length;
-                    } else if (body instanceof char[] arr) {
-                        return arr.length;
-                    } else if (body instanceof int[] arr) {
-                        return arr.length;
-                    } else if (body instanceof long[] arr) {
-                        return arr.length;
-                    } else if (body instanceof double[] arr) {
-                        return arr.length;
-                    } else if (body instanceof String[] arr) {
-                        return arr.length;
+                    if (body.getClass().isArray()) {
+                        // any kind of array such as byte[], Object[] or 
Integer[]
+                        return Array.getLength(body);
                     } else if (body instanceof Collection<?> c) {
                         return c.size();
                     } else if (body instanceof Map<?, ?> m) {
@@ -509,18 +495,9 @@ public final class StringExpressionBuilder {
                     body = exchange.getMessage().getBody();
                 }
                 try {
-                    if (body instanceof byte[] arr) {
-                        return arr.length;
-                    } else if (body instanceof char[] arr) {
-                        return arr.length;
-                    } else if (body instanceof int[] arr) {
-                        return arr.length;
-                    } else if (body instanceof long[] arr) {
-                        return arr.length;
-                    } else if (body instanceof double[] arr) {
-                        return arr.length;
-                    } else if (body instanceof String[] arr) {
-                        return arr.length;
+                    if (body != null && body.getClass().isArray()) {
+                        // any kind of array such as byte[], Object[] or 
Integer[]
+                        return Array.getLength(body);
                     } else if (body instanceof StreamCache sc) {
                         return (int) sc.length();
                     } else {
@@ -693,8 +670,12 @@ public final class StringExpressionBuilder {
 
             @Override
             public Object evaluate(Exchange exchange) {
-                int num1 = exp1.evaluate(exchange, Integer.class);
-                int num2 = exp2.evaluate(exchange, Integer.class);
+                Integer num1 = exp1.evaluate(exchange, Integer.class);
+                Integer num2 = exp2.evaluate(exchange, Integer.class);
+                if (num1 == null || num2 == null) {
+                    throw new IllegalArgumentException(
+                            "substring number expression evaluated to null: " 
+ head + "," + tail);
+                }
                 if (num1 < 0 && num2 == 0) {
                     // if there is only one value and its negative then we 
want to clip from tail
                     num2 = num1;
@@ -735,6 +716,10 @@ public final class StringExpressionBuilder {
                     return null;
                 }
                 String bef = expBefore.evaluate(exchange, String.class);
+                if (bef == null) {
+                    // no delimiter (such as a missing header) then nothing 
comes before
+                    return null;
+                }
                 return StringHelper.before(body, bef);
             }
 
@@ -768,6 +753,10 @@ public final class StringExpressionBuilder {
                     return null;
                 }
                 String aft = expAfter.evaluate(exchange, String.class);
+                if (aft == null) {
+                    // no delimiter (such as a missing header) then nothing 
comes after
+                    return null;
+                }
                 return StringHelper.after(body, aft);
             }
 
@@ -807,6 +796,10 @@ public final class StringExpressionBuilder {
                 }
                 String aft = expAfter.evaluate(exchange, String.class);
                 String bef = expBefore.evaluate(exchange, String.class);
+                if (aft == null || bef == null) {
+                    // no delimiter (such as a missing header) then nothing 
comes between
+                    return null;
+                }
                 return StringHelper.between(body, aft, bef);
             }
 
diff --git 
a/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/functions/DateFunctionFactory.java
 
b/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/functions/DateFunctionFactory.java
index 83ac233574ca..18ff73ab6b00 100644
--- 
a/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/functions/DateFunctionFactory.java
+++ 
b/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/functions/DateFunctionFactory.java
@@ -16,6 +16,9 @@
  */
 package org.apache.camel.language.simple.functions;
 
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
 import org.apache.camel.CamelContext;
 import org.apache.camel.Expression;
 import org.apache.camel.language.simple.DateExpressionBuilder;
@@ -30,6 +33,8 @@ import static 
org.apache.camel.language.simple.SimpleFunctionHelper.ifStartsWith
  */
 public final class DateFunctionFactory implements 
SimpleLanguageFunctionFactory {
 
+    private static final Pattern TIMEZONE_WITH_COLON = 
Pattern.compile("((?:GMT|UTC)?[+-]\\d{1,2}:\\d{2}):(.+)");
+
     @Override
     public Expression createFunction(CamelContext camelContext, String 
function, int index) {
         String remainder = ifStartsWithReturnRemainder("date-with-timezone:", 
function);
@@ -39,6 +44,11 @@ public final class DateFunctionFactory implements 
SimpleLanguageFunctionFactory
                 throw new SimpleParserException(
                         "Valid syntax: 
${date-with-timezone:command:timezone:pattern} was: " + function, index);
             }
+            // a timezone with an offset holds a colon itself, such as 
GMT+02:00
+            Matcher offset = 
TIMEZONE_WITH_COLON.matcher(remainder.substring(parts[0].length() + 1));
+            if (offset.matches()) {
+                return DateExpressionBuilder.dateExpression(parts[0], 
offset.group(1), offset.group(2));
+            }
             return DateExpressionBuilder.dateExpression(parts[0], parts[1], 
parts[2]);
         }
 
diff --git 
a/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/functions/MathFunctionFactory.java
 
b/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/functions/MathFunctionFactory.java
index 3dc6e11cefe3..175b624c2b97 100644
--- 
a/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/functions/MathFunctionFactory.java
+++ 
b/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/functions/MathFunctionFactory.java
@@ -79,7 +79,8 @@ public final class MathFunctionFactory implements 
SimpleLanguageFunctionFactory
         if (ObjectHelper.isNotEmpty(values)) {
             return StringQuoteHelper.splitSafeQuote(values, ',', true, false);
         }
-        return null;
+        // no values, such as ${sum()}, then use the message body
+        return new String[] { "${body}" };
     }
 
 }
diff --git 
a/core/camel-core/src/test/java/org/apache/camel/language/simple/SimpleStringMathDateFunctionsTest.java
 
b/core/camel-core/src/test/java/org/apache/camel/language/simple/SimpleStringMathDateFunctionsTest.java
new file mode 100644
index 000000000000..898044a229e1
--- /dev/null
+++ 
b/core/camel-core/src/test/java/org/apache/camel/language/simple/SimpleStringMathDateFunctionsTest.java
@@ -0,0 +1,144 @@
+/*
+ * 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.language.simple;
+
+import java.math.BigDecimal;
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.camel.Exchange;
+import org.apache.camel.LanguageTestSupport;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * CAMEL-24968: edge cases of the string, math, date and file functions.
+ */
+public class SimpleStringMathDateFunctionsTest extends LanguageTestSupport {
+
+    @Override
+    protected String getLanguageName() {
+        return "simple";
+    }
+
+    @Test
+    public void testCapitalizeEmpty() {
+        exchange.getMessage().setBody("");
+        assertExpression("${capitalize()}", "");
+    }
+
+    @Test
+    public void testMathWithoutArgumentsUsesTheBody() {
+        exchange.getMessage().setBody(List.of(10, 20, 30));
+        assertExpression("${sum()}", 60L);
+        assertExpression("${max()}", 30L);
+        assertExpression("${min()}", 10L);
+        assertExpression("${average()}", 20L);
+    }
+
+    @Test
+    public void testDateHeaderWithDashOrDot() {
+        Date date = new Date(0);
+        exchange.getMessage().setHeader("Last-Modified", date);
+        exchange.getMessage().setHeader("X-Date", date);
+        exchange.getMessage().setHeader("my.date", date);
+        assertExpression("${date:header.Last-Modified}", date);
+        assertExpression("${date:header.X-Date}", date);
+        assertExpression("${date:header.my.date}", date);
+        // an offset still works after a name with a dash
+        assertExpression("${date:header.Last-Modified+1s}", new Date(1000));
+        assertExpression("${date:header.Last-Modified - 1s}", new Date(-1000));
+    }
+
+    @Test
+    public void testDateWithTimezoneOffset() {
+        exchange.getMessage().setHeader("d", new Date(0));
+        assertExpression("${date-with-timezone:header.d:GMT+02:00:HH:mm}", 
"02:00");
+        assertExpression("${date-with-timezone:header.d:UTC:HH:mm}", "00:00");
+    }
+
+    @Test
+    public void testSafeQuoteIsValidJson() {
+        exchange.getMessage().setBody("He said \"hi\"");
+        assertExpression("${safeQuote()}", "\"He said \\\"hi\\\"\"");
+        exchange.getMessage().setBody("C:\\temp");
+        assertExpression("${safeQuote()}", "\"C:\\\\temp\"");
+        // already quoted once is not quoted twice
+        exchange.getMessage().setBody("\"Hello\"");
+        assertExpression("${safeQuote()}", "\"Hello\"");
+        // numbers are not quoted, also BigDecimal as from a JSON document
+        exchange.getMessage().setBody(new BigDecimal("12.50"));
+        assertExpression("${safeQuote()}", new BigDecimal("12.50"));
+    }
+
+    @Test
+    public void testKindOfType() {
+        exchange.getMessage().setBody(new BigDecimal("1.5"));
+        assertExpression("${kindOfType()}", "number");
+        exchange.getMessage().setBody(Map.of("a", 1));
+        assertExpression("${kindOfType()}", "object");
+        exchange.getMessage().setBody(new String[] { "a" });
+        assertExpression("${kindOfType()}", "array");
+    }
+
+    @Test
+    public void testSubstringWithNullArguments() {
+        exchange.getMessage().setBody("abc");
+        Exception e = assertThrows(Exception.class, () -> 
evaluate("${substring(${header.none})}"));
+        assertTrue(e.getMessage().contains("substring number expression 
evaluated to null"), e.getMessage());
+        // no delimiter then nothing comes before/after/between
+        assertExpression("${substringBefore(${header.none})}", null);
+        assertExpression("${substringAfter(${header.none})}", null);
+        assertExpression("${substringBetween(${header.none},'c')}", null);
+    }
+
+    @Test
+    public void testPadWithSeveralCharacters() {
+        assertExpression("${pad('foo',6,'ab')}", "fooaba");
+        assertExpression("${pad('foo',-6,'ab')}", "abafoo");
+        assertExpression("${pad('42',-5,'0')}", "00042");
+        assertExpression("${pad('Hi',4)}", "Hi  ");
+    }
+
+    @Test
+    public void testSizeAndLengthOfAnyArray() {
+        exchange.getMessage().setBody(new Integer[] { 1, 2, 3 });
+        assertExpression("${size()}", 3);
+        assertExpression("${length()}", 3);
+        exchange.getMessage().setBody(new Object[] { "a", "b" });
+        assertExpression("${size()}", 2);
+        exchange.getMessage().setBody(new boolean[] { true });
+        assertExpression("${size()}", 1);
+    }
+
+    @Test
+    public void testHiddenFileName() {
+        exchange.getMessage().setHeader(Exchange.FILE_NAME, ".bashrc");
+        assertExpression("${file:name.noext}", ".bashrc");
+        assertExpression("${file:ext}", null);
+        exchange.getMessage().setHeader(Exchange.FILE_NAME, ".route.yaml");
+        assertExpression("${file:name.noext}", ".route");
+        assertExpression("${file:ext}", "yaml");
+    }
+
+    private Object evaluate(String text) {
+        return 
context.resolveLanguage("simple").createExpression(text).evaluate(exchange, 
Object.class);
+    }
+}
diff --git 
a/core/camel-core/src/test/java/org/apache/camel/util/FileUtilTest.java 
b/core/camel-core/src/test/java/org/apache/camel/util/FileUtilTest.java
index 2077cfde05c6..2d9bfedebc43 100644
--- a/core/camel-core/src/test/java/org/apache/camel/util/FileUtilTest.java
+++ b/core/camel-core/src/test/java/org/apache/camel/util/FileUtilTest.java
@@ -125,6 +125,11 @@ public class FileUtilTest extends TestSupport {
         assertEquals("foo", FileUtil.stripExt("foo"));
         assertEquals("foo", FileUtil.stripExt("foo.xml"));
         assertEquals("/foo/bar", FileUtil.stripExt("/foo/bar.xml"));
+        // hidden files
+        assertEquals(".bashrc", FileUtil.stripExt(".bashrc"));
+        assertEquals("/home/.bashrc", FileUtil.stripExt("/home/.bashrc"));
+        assertEquals(".route", FileUtil.stripExt(".route.yaml"));
+        assertEquals("/home/.route", FileUtil.stripExt("/home/.route.yaml"));
     }
 
     @Test
@@ -135,6 +140,12 @@ public class FileUtilTest extends TestSupport {
         assertEquals("xml", FileUtil.onlyExt("/foo/bar.xml"));
         assertEquals("tar.gz", FileUtil.onlyExt("/foo/bigfile.tar.gz"));
         assertEquals("tar.gz", FileUtil.onlyExt("/foo.bar/bigfile.tar.gz"));
+        // hidden files
+        assertNull(FileUtil.onlyExt(".bashrc"));
+        assertNull(FileUtil.onlyExt("/home/.bashrc"));
+        assertNull(FileUtil.onlyExt(".bashrc", true));
+        assertEquals("yaml", FileUtil.onlyExt(".route.yaml"));
+        assertEquals("yaml", FileUtil.onlyExt(".route.yaml", true));
     }
 
     @Test
diff --git 
a/core/camel-support/src/main/java/org/apache/camel/support/LanguageHelper.java 
b/core/camel-support/src/main/java/org/apache/camel/support/LanguageHelper.java
index 9cd4e6706511..ea627dafa813 100644
--- 
a/core/camel-support/src/main/java/org/apache/camel/support/LanguageHelper.java
+++ 
b/core/camel-support/src/main/java/org/apache/camel/support/LanguageHelper.java
@@ -244,20 +244,23 @@ public final class LanguageHelper {
 
     public static Date dateFromExchangeProperty(
             Exchange exchange, String command, BiFunction<Exchange, Object, 
Date> orElseFunction) {
-        final String key = command.substring(command.lastIndexOf('.') + 1);
+        // the key may contain dots, such as header.my.date
+        final String key = command.substring(command.indexOf('.') + 1);
         final Object obj = exchange.getProperty(key);
 
         return toDate(exchange, orElseFunction, obj);
     }
 
     public static Date dateFromHeader(Exchange exchange, String command, 
BiFunction<Exchange, Object, Date> orElseFunction) {
-        final String key = command.substring(command.lastIndexOf('.') + 1);
+        // the key may contain dots, such as header.my.date
+        final String key = command.substring(command.indexOf('.') + 1);
         final Object obj = exchange.getMessage().getHeader(key);
         return toDate(exchange, orElseFunction, obj);
     }
 
     public static Date dateFromVariable(Exchange exchange, String command, 
BiFunction<Exchange, Object, Date> orElseFunction) {
-        final String key = command.substring(command.lastIndexOf('.') + 1);
+        // the key may contain dots, such as header.my.date
+        final String key = command.substring(command.indexOf('.') + 1);
         final Object obj = exchange.getVariable(key);
         return toDate(exchange, orElseFunction, obj);
     }
diff --git a/core/camel-util/src/main/java/org/apache/camel/util/FileUtil.java 
b/core/camel-util/src/main/java/org/apache/camel/util/FileUtil.java
index 32162e6fc9f8..2f52477215c2 100644
--- a/core/camel-util/src/main/java/org/apache/camel/util/FileUtil.java
+++ b/core/camel-util/src/main/java/org/apache/camel/util/FileUtil.java
@@ -201,13 +201,14 @@ public final class FileUtil {
 
         if (pos > 0) {
             String onlyName = name.substring(pos + 1);
-            int pos2 = singleMode ? onlyName.lastIndexOf('.') : 
onlyName.indexOf('.');
+            // a leading dot is a hidden file (such as .bashrc), not an 
extension
+            int pos2 = singleMode ? onlyName.lastIndexOf('.') : 
onlyName.indexOf('.', 1);
             if (pos2 > 0) {
                 return name.substring(0, pos + pos2 + 1);
             }
         } else {
             // if single ext mode, then only return last extension
-            int pos2 = singleMode ? name.lastIndexOf('.') : name.indexOf('.');
+            int pos2 = singleMode ? name.lastIndexOf('.') : name.indexOf('.', 
1);
             if (pos2 > 0) {
                 return name.substring(0, pos2);
             }
@@ -228,8 +229,9 @@ public final class FileUtil {
 
         // extension is the first dot, as a file may have double extension 
such as .tar.gz
         // if single ext mode, then only return last extension
-        int pos = singleMode ? name.lastIndexOf('.') : name.indexOf('.');
-        if (pos != -1) {
+        // a leading dot is a hidden file (such as .bashrc), not an extension
+        int pos = singleMode ? name.lastIndexOf('.') : name.indexOf('.', 1);
+        if (pos > 0) {
             return name.substring(pos + 1);
         }
         return null;
diff --git 
a/core/camel-util/src/main/java/org/apache/camel/util/StringHelper.java 
b/core/camel-util/src/main/java/org/apache/camel/util/StringHelper.java
index 7e37a000d964..06ec5a11697e 100644
--- a/core/camel-util/src/main/java/org/apache/camel/util/StringHelper.java
+++ b/core/camel-util/src/main/java/org/apache/camel/util/StringHelper.java
@@ -556,8 +556,8 @@ public final class StringHelper {
     }
 
     private static String doCapitalize(String ret, boolean all) {
-        if (ret == null) {
-            return null;
+        if (ret == null || ret.isEmpty()) {
+            return ret;
         }
 
         final char[] chars = ret.toCharArray();
@@ -566,10 +566,10 @@ public final class StringHelper {
         // for which it does not return the capitalized value should not be 
used here (this is
         // mostly used to capitalize setters/getters)
         chars[0] = Character.toUpperCase(chars[0]);
-        if (all && chars.length > 2) {
-            for (int i = 2; i < chars.length; i++) {
+        if (all) {
+            for (int i = 1; i < chars.length; i++) {
                 char prev = chars[i - 1];
-                if (prev == ' ') {
+                if (Character.isWhitespace(prev)) {
                     chars[i] = Character.toUpperCase(chars[i]);
                 }
             }
diff --git 
a/core/camel-util/src/test/java/org/apache/camel/util/StringHelperTest.java 
b/core/camel-util/src/test/java/org/apache/camel/util/StringHelperTest.java
index c6bf59aaa6a5..18809ec57ac5 100644
--- a/core/camel-util/src/test/java/org/apache/camel/util/StringHelperTest.java
+++ b/core/camel-util/src/test/java/org/apache/camel/util/StringHelperTest.java
@@ -621,6 +621,11 @@ public class StringHelperTest {
     public void testCapitalizeAll() {
         assertNull(StringHelper.capitalizeAll(null));
         assertEquals("Hello World How Are You", 
StringHelper.capitalizeAll("hello world how are you"));
+        assertEquals("", StringHelper.capitalizeAll(""));
+        assertEquals("", StringHelper.capitalize(""));
+        assertEquals(" Hello World", StringHelper.capitalizeAll(" hello 
world"));
+        assertEquals("A B", StringHelper.capitalizeAll("a b"));
+        assertEquals("Hello\tWorld\nAgain", 
StringHelper.capitalizeAll("hello\tworld\nagain"));
     }
 
     @Test

Reply via email to