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 442799cd9f4c CAMEL-25015: camel-core - Tokenize language: the token is 
literal text unless regex is true (#28801)
442799cd9f4c is described below

commit 442799cd9f4c0c7461e86c7d551f06d6ca5c3b4a
Author: Claus Ibsen <[email protected]>
AuthorDate: Mon Nov 2 07:33:38 2026 +0100

    CAMEL-25015: camel-core - Tokenize language: the token is literal text 
unless regex is true (#28801)
    
    …t unless regex is true
    
    The token was always compiled as a regular expression, so tokenize("|") 
split every character,
    tokenize(".") gave empty parts and tokenize("*") failed with a 
PatternSyntaxException. The token is
    now quoted unless regex is true, and so is the end token of the pair mode, 
which replaces the escaping
    of [ and ] by hand. The group delimiter of a token with a Simple function 
is the token, not the
    quoted pattern. In the XML and YAML DSL the escapes \n, \r and \t in a 
token that is not a regular
    expression are turned into the characters, so token="\r\n" keeps working.
    
    Co-Authored-By: Claude Opus 5.5 <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
---
 .../camel/catalog/docs/tokenize-language.adoc      |   5 +
 .../modules/languages/pages/tokenize-language.adoc |   5 +
 .../language/TokenizerExpressionReifier.java       |  20 +--
 .../org/apache/camel/language/TokenizerTest.java   |   8 +-
 .../tokenizer/TokenizeLiteralTokenTest.java        | 139 +++++++++++++++++++++
 .../apache/camel/support/GroupTokenIterator.java   |  14 ++-
 .../camel/support/builder/ExpressionBuilder.java   |   9 +-
 .../builder/TokenPairExpressionIterator.java       |  22 +---
 .../ROOT/pages/camel-4x-upgrade-guide-4_24.adoc    |  17 +++
 9 files changed, 208 insertions(+), 31 deletions(-)

diff --git 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/tokenize-language.adoc
 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/tokenize-language.adoc
index 729cb7db7f83..2891d7f124a0 100644
--- 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/tokenize-language.adoc
+++ 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/tokenize-language.adoc
@@ -37,6 +37,11 @@ then forward each piece to direct:b:
 IMPORTANT: When using new-line (`\n`) as tokenizer, then XML and YAML DSL must 
use the `token` option\
 as done in the following example.
 
+The token is matched as literal text, so a delimiter such as `|`, `.` or `*` 
splits at that character.
+To use a regular expression as token, such as `\s+`, set the `regex` option to 
`true`.
+In the XML and YAML DSL, the escapes `\n`, `\r` and `\t` in a token are the 
new line, carriage return
+and tab characters, so `\r\n` splits Windows lines.
+
 [tabs]
 ====
 
diff --git 
a/core/camel-core-languages/src/main/docs/modules/languages/pages/tokenize-language.adoc
 
b/core/camel-core-languages/src/main/docs/modules/languages/pages/tokenize-language.adoc
index 729cb7db7f83..2891d7f124a0 100644
--- 
a/core/camel-core-languages/src/main/docs/modules/languages/pages/tokenize-language.adoc
+++ 
b/core/camel-core-languages/src/main/docs/modules/languages/pages/tokenize-language.adoc
@@ -37,6 +37,11 @@ then forward each piece to direct:b:
 IMPORTANT: When using new-line (`\n`) as tokenizer, then XML and YAML DSL must 
use the `token` option\
 as done in the following example.
 
+The token is matched as literal text, so a delimiter such as `|`, `.` or `*` 
splits at that character.
+To use a regular expression as token, such as `\s+`, set the `regex` option to 
`true`.
+In the XML and YAML DSL, the escapes `\n`, `\r` and `\t` in a token are the 
new line, carriage return
+and tab characters, so `\r\n` splits Windows lines.
+
 [tabs]
 ====
 
diff --git 
a/core/camel-core-reifier/src/main/java/org/apache/camel/reifier/language/TokenizerExpressionReifier.java
 
b/core/camel-core-reifier/src/main/java/org/apache/camel/reifier/language/TokenizerExpressionReifier.java
index e1c7bd553418..47b40bb5b537 100644
--- 
a/core/camel-core-reifier/src/main/java/org/apache/camel/reifier/language/TokenizerExpressionReifier.java
+++ 
b/core/camel-core-reifier/src/main/java/org/apache/camel/reifier/language/TokenizerExpressionReifier.java
@@ -30,17 +30,23 @@ public class TokenizerExpressionReifier extends 
SingleInputTypedExpressionReifie
         Object[] properties = new Object[11];
         properties[0] = asResultType();
         properties[1] = parseString(definition.getSource());
-        // special for new line tokens, if defined from XML then its 2
-        // characters, so we replace that back to a single char
-        String token = definition.getToken();
-        if (token != null && token.startsWith("\\n")) {
-            token = '\n' + token.substring(2);
+        Boolean regex = parseBoolean(definition.getRegex());
+        // special for new line, carriage return and tab in tokens, if defined 
from XML then they are 2
+        // characters, so we replace them back to a single char. A regular 
expression understands them
+        // as they are, so only a leading new line is replaced there
+        String token = parseString(definition.getToken());
+        if (token != null && Boolean.TRUE.equals(regex)) {
+            if (token.startsWith("\\n")) {
+                token = '\n' + token.substring(2);
+            }
+        } else if (token != null) {
+            token = token.replace("\\n", "\n").replace("\\r", 
"\r").replace("\\t", "\t");
         }
-        properties[2] = parseString(token);
+        properties[2] = token;
         properties[3] = parseString(definition.getEndToken());
         properties[4] = parseString(definition.getInheritNamespaceTagName());
         properties[5] = parseString(definition.getGroupDelimiter());
-        properties[6] = parseBoolean(definition.getRegex());
+        properties[6] = regex;
         properties[7] = parseBoolean(definition.getXml());
         properties[8] = parseBoolean(definition.getIncludeTokens());
         properties[9] = parseString(definition.getGroup());
diff --git 
a/core/camel-core/src/test/java/org/apache/camel/language/TokenizerTest.java 
b/core/camel-core/src/test/java/org/apache/camel/language/TokenizerTest.java
index 559b6d0b46a7..365b1bfd1138 100644
--- a/core/camel-core/src/test/java/org/apache/camel/language/TokenizerTest.java
+++ b/core/camel-core/src/test/java/org/apache/camel/language/TokenizerTest.java
@@ -40,9 +40,13 @@ public class TokenizerTest extends ExchangeTestSupport {
     }
 
     Expression tokenize(String headerName, String token) {
+        return tokenize(headerName, token, false);
+    }
+
+    Expression tokenize(String headerName, String token, boolean regex) {
         TokenizeLanguage language = new TokenizeLanguage();
         language.setCamelContext(context);
-        return language.createExpression(null, new Object[] { null, "header:" 
+ headerName, token });
+        return language.createExpression(null, new Object[] { null, "header:" 
+ headerName, token, null, null, null, regex });
     }
 
     Expression tokenizePair(String startToken, String endToken, boolean 
includeTokens) {
@@ -119,7 +123,7 @@ public class TokenizerTest extends ExchangeTestSupport {
 
     @Test
     public void testTokenizeHeaderRegEx() {
-        Expression exp = tokenize("quote", "(\\W+)\\s*");
+        Expression exp = tokenize("quote", "(\\W+)\\s*", true);
         exp.init(context);
 
         exchange.getIn().setHeader("quote", "Camel rocks");
diff --git 
a/core/camel-core/src/test/java/org/apache/camel/language/tokenizer/TokenizeLiteralTokenTest.java
 
b/core/camel-core/src/test/java/org/apache/camel/language/tokenizer/TokenizeLiteralTokenTest.java
new file mode 100644
index 000000000000..4cdec4a2870b
--- /dev/null
+++ 
b/core/camel-core/src/test/java/org/apache/camel/language/tokenizer/TokenizeLiteralTokenTest.java
@@ -0,0 +1,139 @@
+/*
+ * 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.tokenizer;
+
+import java.util.List;
+
+import org.apache.camel.ContextTestSupport;
+import org.apache.camel.Exchange;
+import org.apache.camel.Expression;
+import org.apache.camel.builder.Builder;
+import org.apache.camel.builder.LanguageBuilderFactory;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.component.mock.MockEndpoint;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * A token is literal text unless regex is true (CAMEL-25015)
+ */
+public class TokenizeLiteralTokenTest extends ContextTestSupport {
+
+    @Override
+    public boolean isUseRouteBuilder() {
+        return false;
+    }
+
+    private static LanguageBuilderFactory lang() {
+        return new LanguageBuilderFactory();
+    }
+
+    private List<String> split(Expression expression, String body) throws 
Exception {
+        context.addRoutes(new RouteBuilder() {
+            @Override
+            public void configure() {
+                from("direct:start").split(expression).to("mock:split");
+            }
+        });
+        context.start();
+        Exchange out = template.send("direct:start", e -> {
+            e.getMessage().setBody(body);
+            e.getMessage().setHeader("sep", "|");
+        });
+        assertThat(out.getException()).isNull();
+        MockEndpoint mock = getMockEndpoint("mock:split");
+        return mock.getReceivedExchanges().stream().map(e -> 
e.getMessage().getBody(String.class)).toList();
+    }
+
+    @Test
+    public void testPipe() throws Exception {
+        assertThat(split(lang().tokenize().token("|").end(), 
"a|b|c")).containsExactly("a", "b", "c");
+    }
+
+    @Test
+    public void testDot() throws Exception {
+        assertThat(split(lang().tokenize().token(".").end(), 
"a.b.c")).containsExactly("a", "b", "c");
+    }
+
+    @Test
+    public void testDollar() throws Exception {
+        assertThat(split(lang().tokenize().token("$").end(), 
"a$b$c")).containsExactly("a", "b", "c");
+    }
+
+    @Test
+    public void testStar() throws Exception {
+        assertThat(split(lang().tokenize().token("*").end(), 
"a*b*c")).containsExactly("a", "b", "c");
+    }
+
+    @Test
+    public void testPipeFromValueBuilder() throws Exception {
+        assertThat(split(Builder.body().tokenize("|"), 
"a|b|c")).containsExactly("a", "b", "c");
+    }
+
+    @Test
+    public void testPipeGroupKeepsTokenAsDelimiter() throws Exception {
+        assertThat(split(lang().tokenize().token("|").group(2).end(), 
"a|b|c|d")).containsExactly("a|b", "c|d");
+    }
+
+    @Test
+    public void testPipeFromSimpleGroupKeepsTokenAsDelimiter() throws 
Exception {
+        
assertThat(split(lang().tokenize().token("${header.sep}").group(2).end(), 
"a|b|c|d")).containsExactly("a|b", "c|d");
+    }
+
+    @Test
+    public void testRegexCharactersWithoutRegexAreLiteral() throws Exception {
+        assertThat(split(lang().tokenize().token("\\s+").end(), "a 
b\\s+c")).containsExactly("a b", "c");
+    }
+
+    @Test
+    public void testRegex() throws Exception {
+        assertThat(split(lang().tokenize().token("\\s+").regex(true).end(), "a 
 b\tc")).containsExactly("a", "b", "c");
+    }
+
+    @Test
+    public void testEscapedPipeWithRegex() throws Exception {
+        assertThat(split(lang().tokenize().token("\\|").regex(true).end(), 
"a|b|c")).containsExactly("a", "b", "c");
+    }
+
+    @Test
+    public void testEscapedCarriageReturnNewLine() throws Exception {
+        // as the token is written in XML or YAML: backslash r backslash n (4 
characters)
+        assertThat(split(lang().tokenize().token("\\r\\n").end(), 
"a\r\nb\r\nc")).containsExactly("a", "b", "c");
+    }
+
+    @Test
+    public void testEscapedTab() throws Exception {
+        assertThat(split(lang().tokenize().token("\\t").end(), 
"a\tb\tc")).containsExactly("a", "b", "c");
+    }
+
+    @Test
+    public void testPairWithParentheses() throws Exception {
+        assertThat(split(lang().tokenize().token("(").endToken(")").end(), 
"(a)(b)")).containsExactly("a", "b");
+    }
+
+    @Test
+    public void testPairWithDotInEndToken() throws Exception {
+        
assertThat(split(lang().tokenize().token("[rec]").endToken("END.").end(), 
"[rec]1 ENDS HERE END.[rec]2END."))
+                .containsExactly("1 ENDS HERE ", "2");
+    }
+
+    @Test
+    public void testPairWithBrackets() throws Exception {
+        assertThat(split(lang().tokenize().token("[").endToken("]").end(), 
"[a][b]")).containsExactly("a", "b");
+    }
+}
diff --git 
a/core/camel-support/src/main/java/org/apache/camel/support/GroupTokenIterator.java
 
b/core/camel-support/src/main/java/org/apache/camel/support/GroupTokenIterator.java
index d3e8a28ac0cf..8e7f105df9c6 100644
--- 
a/core/camel-support/src/main/java/org/apache/camel/support/GroupTokenIterator.java
+++ 
b/core/camel-support/src/main/java/org/apache/camel/support/GroupTokenIterator.java
@@ -68,7 +68,7 @@ public final class GroupTokenIterator implements 
Iterator<Object>, Closeable {
         // if the iterator is a scanner then it may have a dynamic delimiter
         // so we need to use the actual evaluated delimiter as token
         if (LanguageSupport.hasSimpleFunction(token) && it instanceof Scanner 
scanner) {
-            this.token = scanner.getDelim();
+            this.token = unquote(scanner.getDelim());
         } else {
             this.token = token;
         }
@@ -165,4 +165,16 @@ public final class GroupTokenIterator implements 
Iterator<Object>, Closeable {
     public void remove() {
         it.remove();
     }
+
+    /**
+     * Turns a delimiter quoted with {@link 
java.util.regex.Pattern#quote(String)} back into its literal text, so the
+     * parts are joined with the token and not with the quoted pattern. Any 
other delimiter is returned as is.
+     */
+    private static String unquote(String delim) {
+        if (delim != null && delim.length() >= 4 && delim.startsWith("\\Q") && 
delim.endsWith("\\E")) {
+            // Pattern.quote writes a \E in the text as \E\\E\Q
+            return delim.substring(2, delim.length() - 
2).replace("\\E\\\\E\\Q", "\\E");
+        }
+        return delim;
+    }
 }
diff --git 
a/core/camel-support/src/main/java/org/apache/camel/support/builder/ExpressionBuilder.java
 
b/core/camel-support/src/main/java/org/apache/camel/support/builder/ExpressionBuilder.java
index bddd202ada29..2a9ec43d0f37 100644
--- 
a/core/camel-support/src/main/java/org/apache/camel/support/builder/ExpressionBuilder.java
+++ 
b/core/camel-support/src/main/java/org/apache/camel/support/builder/ExpressionBuilder.java
@@ -1850,7 +1850,8 @@ public class ExpressionBuilder {
     }
 
     /**
-     * Returns a tokenize expression which will tokenize the string with the 
given token
+     * Returns a tokenize expression which will tokenize the string with the 
given token. The token is matched as
+     * literal text, use {@link #regexTokenizeExpression(Expression, String)} 
for a regular expression.
      */
     public static Expression tokenizeExpression(
             final Expression expression,
@@ -1859,7 +1860,8 @@ public class ExpressionBuilder {
     }
 
     /**
-     * Returns a tokenize expression which will tokenize the string with the 
given token
+     * Returns a tokenize expression which will tokenize the string with the 
given token. The token is matched as
+     * literal text, use {@link #regexTokenizeExpression(Expression, String)} 
for a regular expression.
      */
     public static Expression tokenizeExpression(
             final Expression expression,
@@ -1869,7 +1871,8 @@ public class ExpressionBuilder {
             public Object evaluate(Exchange exchange) {
                 String text = token.evaluate(exchange, String.class);
                 Object value = expression.evaluate(exchange, Object.class);
-                return ExchangeHelper.getScanner(exchange, value, text);
+                // the scanner is regular expression based, so quote the token 
to match it as literal text
+                return ExchangeHelper.getScanner(exchange, value, text != null 
? Pattern.quote(text) : null);
             }
 
             @Override
diff --git 
a/core/camel-support/src/main/java/org/apache/camel/support/builder/TokenPairExpressionIterator.java
 
b/core/camel-support/src/main/java/org/apache/camel/support/builder/TokenPairExpressionIterator.java
index dcf4dfa98e8e..f6b73132c617 100644
--- 
a/core/camel-support/src/main/java/org/apache/camel/support/builder/TokenPairExpressionIterator.java
+++ 
b/core/camel-support/src/main/java/org/apache/camel/support/builder/TokenPairExpressionIterator.java
@@ -20,6 +20,7 @@ import java.io.Closeable;
 import java.io.IOException;
 import java.io.InputStream;
 import java.util.Iterator;
+import java.util.regex.Pattern;
 
 import org.apache.camel.CamelContext;
 import org.apache.camel.Exchange;
@@ -166,9 +167,8 @@ public class TokenPairExpressionIterator extends 
ExpressionAdapter {
     static class TokenPairIterator implements Iterator<Object>, Closeable {
 
         final String startToken;
-        String scanStartToken;
         final String endToken;
-        String scanEndToken;
+        final String scanEndToken;
         final boolean includeTokens;
         final InputStream in;
         final String charset;
@@ -182,22 +182,8 @@ public class TokenPairExpressionIterator extends 
ExpressionAdapter {
             this.in = in;
             this.charset = charset;
 
-            // make sure [ and ] is escaped as we use scanner which is reg exp 
based
-            // where [ and ] have special meaning
-            scanStartToken = startToken;
-            if (scanStartToken.startsWith("[")) {
-                scanStartToken = "\\" + scanStartToken;
-            }
-            if (scanStartToken.endsWith("]")) {
-                scanStartToken = scanStartToken.substring(0, 
startToken.length() - 1) + "\\]";
-            }
-            scanEndToken = endToken;
-            if (scanEndToken.startsWith("[")) {
-                scanEndToken = "\\" + scanEndToken;
-            }
-            if (scanEndToken.endsWith("]")) {
-                scanEndToken = scanEndToken.substring(0, scanEndToken.length() 
- 1) + "\\]";
-            }
+            // the scanner is regular expression based, so quote the end token 
to match it as literal text
+            scanEndToken = Pattern.quote(endToken);
         }
 
         void init() {
diff --git 
a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
index cc485d8925c6..8198a8b7ff92 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
@@ -373,6 +373,23 @@ now receive the input that was split, instead of that one 
split item. A route th
 read the `CamelSplitResult` exchange property, set as before: 
`SplitResult.getFailures()` has the index and the
 exception of the failed items.
 
+=== camel-core - Tokenize: the token is literal text unless regex is true
+
+The token of the xref:components:languages:tokenize-language.adoc[Tokenize] 
language is now matched as literal text, as the
+`regex` option (default `false`) describes. Before, the token was always 
compiled as a regular expression, so a
+delimiter such as `|`, `.`, `$` or `*` split every character, did not split, 
or failed with a `PatternSyntaxException`.
+The end token of the pair mode (`endToken`) is also literal text now.
+
+A route that relies on a regular expression as token, such as `\s+` or 
`(\W+)\s*`, must now set `regex=true`:
+
+[source,java]
+----
+.split().tokenize("\\s+", true)
+----
+
+In the XML and YAML DSL, a token with the escapes `\n`, `\r` or `\t` (for 
example `\r\n`) is turned into the new line,
+carriage return and tab characters, so it works as before. Before, only a 
leading `\n` was turned into a new line.
+
 === camel-main - a failure to start is logged
 
 A Camel Main application that fails to start now logs why, at ERROR: `Error 
starting Camel: ...` with the stack trace,

Reply via email to