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 `\"` as double quote, `\'` as single quote, and `\∅` 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 `\"` as double quote, `\'` as single quote, and `\∅` 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
