This is an automated email from the ASF dual-hosted git repository. davsclaus pushed a commit to branch fix/CAMEL-24147 in repository https://gitbox.apache.org/repos/asf/camel.git
commit 3549c603e343e3697433ab045d6291856d35c239 Author: Claus Ibsen <[email protected]> AuthorDate: Fri Jul 17 10:39:25 2026 +0200 CAMEL-24147: camel-lra - URL-encode callback query parameters and fix parseQuery split Co-Authored-By: Claude Opus 4.6 <[email protected]> Signed-off-by: Claus Ibsen <[email protected]> --- .../apache/camel/service/lra/LRASagaRoutes.java | 4 +- .../apache/camel/service/lra/LRAUrlBuilder.java | 8 +- .../camel/service/lra/LRASagaRoutesTest.java | 30 ++++++ .../camel/service/lra/LRAUrlBuilderTest.java | 110 +++++++++++++++++++++ 4 files changed, 149 insertions(+), 3 deletions(-) diff --git a/components/camel-lra/src/main/java/org/apache/camel/service/lra/LRASagaRoutes.java b/components/camel-lra/src/main/java/org/apache/camel/service/lra/LRASagaRoutes.java index 4d442cff869c..b2538ac31c67 100644 --- a/components/camel-lra/src/main/java/org/apache/camel/service/lra/LRASagaRoutes.java +++ b/components/camel-lra/src/main/java/org/apache/camel/service/lra/LRASagaRoutes.java @@ -69,8 +69,8 @@ public class LRASagaRoutes extends RouteBuilder { // first, split by parameter separator '&' // then collect the map with the variable name '[0]' and value '[1]', both url decoded result = Arrays.stream(queryStr.split("&")).collect( - Collectors.toMap(element -> decode(saveArrayAccess(element.split("="), 0)), - element -> decode(saveArrayAccess(element.split("="), 1)))); + Collectors.toMap(element -> decode(saveArrayAccess(element.split("=", 2), 0)), + element -> decode(saveArrayAccess(element.split("=", 2), 1)))); } else { LOG.debug("query param is empty, nothing to parse."); diff --git a/components/camel-lra/src/main/java/org/apache/camel/service/lra/LRAUrlBuilder.java b/components/camel-lra/src/main/java/org/apache/camel/service/lra/LRAUrlBuilder.java index e3f4bf80c9c4..b222ec250356 100644 --- a/components/camel-lra/src/main/java/org/apache/camel/service/lra/LRAUrlBuilder.java +++ b/components/camel-lra/src/main/java/org/apache/camel/service/lra/LRAUrlBuilder.java @@ -16,6 +16,8 @@ */ package org.apache.camel.service.lra; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; import java.util.Map; import java.util.Optional; @@ -93,7 +95,7 @@ public class LRAUrlBuilder { } else { copy.query += "&"; } - copy.query += toNonnullString(key) + "=" + toNonnullString(value); + copy.query += encode(toNonnullString(key)) + "=" + encode(toNonnullString(value)); return copy; } @@ -116,6 +118,10 @@ public class LRAUrlBuilder { return first + "/" + second; } + private String encode(String value) { + return URLEncoder.encode(value, StandardCharsets.UTF_8); + } + private String toNonnullString(Object obj) { return obj != null ? obj.toString() : ""; } diff --git a/components/camel-lra/src/test/java/org/apache/camel/service/lra/LRASagaRoutesTest.java b/components/camel-lra/src/test/java/org/apache/camel/service/lra/LRASagaRoutesTest.java index c9762505cf6e..022bec7885d5 100644 --- a/components/camel-lra/src/test/java/org/apache/camel/service/lra/LRASagaRoutesTest.java +++ b/components/camel-lra/src/test/java/org/apache/camel/service/lra/LRASagaRoutesTest.java @@ -79,6 +79,36 @@ public class LRASagaRoutesTest { "query parameter value for name 'key2' has unexpected content"); } + @DisplayName("Tests parseQuery() preserves values containing '=' characters") + @Test + void testParseQueryWithEqualsInValue() throws InvocationTargetException, IllegalAccessException, NoSuchMethodException { + + Map<String, String> testResult = (Map<String, String>) getParseQueryMethod() + .invoke(new LRASagaRoutes(null), + "key1=value%3Dwith%3Dequals&key2=normal"); + + Assertions.assertEquals(2, testResult.size(), "query parameter count must be two"); + Assertions.assertEquals("value=with=equals", testResult.get("key1"), + "value containing '=' characters must be preserved"); + Assertions.assertEquals("normal", testResult.get("key2")); + } + + @DisplayName("Tests parseQuery() decodes URL-encoded special characters correctly") + @Test + void testParseQueryWithEncodedSpecialChars() + throws InvocationTargetException, IllegalAccessException, NoSuchMethodException { + + Map<String, String> testResult = (Map<String, String>) getParseQueryMethod() + .invoke(new LRASagaRoutes(null), + "key1=value+with%26ampersand&key2=a%2Bb"); + + Assertions.assertEquals(2, testResult.size(), "query parameter count must be two"); + Assertions.assertEquals("value with&ersand", testResult.get("key1"), + "encoded '&' and '+' must be decoded correctly"); + Assertions.assertEquals("a+b", testResult.get("key2"), + "encoded '+' must be decoded correctly"); + } + @DisplayName("Tests parseQuery() to handle incorrect query string (no value is given)") @Test void testParseQuerySuccessEncoded() throws InvocationTargetException, IllegalAccessException, NoSuchMethodException { diff --git a/components/camel-lra/src/test/java/org/apache/camel/service/lra/LRAUrlBuilderTest.java b/components/camel-lra/src/test/java/org/apache/camel/service/lra/LRAUrlBuilderTest.java new file mode 100644 index 000000000000..3af26e8a1507 --- /dev/null +++ b/components/camel-lra/src/test/java/org/apache/camel/service/lra/LRAUrlBuilderTest.java @@ -0,0 +1,110 @@ +/* + * 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.service.lra; + +import java.lang.reflect.Method; +import java.net.URI; +import java.util.Map; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class LRAUrlBuilderTest { + + @DisplayName("Tests that query() URL-encodes special characters in values") + @Test + void testQueryEncodesSpecialCharacters() { + String url = new LRAUrlBuilder() + .host("http://localhost:8080") + .path("lra-participant") + .query("key", "value with spaces&and=special+chars#hash") + .build(); + + assertTrue(url.contains("key=value+with+spaces%26and%3Dspecial%2Bchars%23hash"), + "Special characters must be URL-encoded in query values: " + url); + } + + @DisplayName("Tests that compensation URI with query params is properly encoded") + @Test + void testCompensationUriWithQueryParams() { + String url = new LRAUrlBuilder() + .host("http://localhost:8080") + .path("lra-participant") + .compensation("seda:cancelOrder?concurrentConsumers=2&size=1000") + .build(); + + URI uri = URI.create(url); + assertNotNull(uri.getQuery(), "URL must have a valid query string"); + + String query = uri.getRawQuery(); + assertTrue(query.contains("Camel-Saga-Compensate="), + "Query must contain the compensation key"); + assertTrue(query.contains("seda%3AcancelOrder%3FconcurrentConsumers%3D2%26size%3D1000"), + "Compensation URI must be fully encoded: " + query); + } + + @DisplayName("Tests full round-trip: LRAUrlBuilder encodes, parseQuery decodes back to original") + @Test + void testCallbackUrlRoundTrip() throws Exception { + String compensationUri = "seda:cancelOrder?concurrentConsumers=2&size=1000"; + String completionUri = "direct:complete"; + String optionValue = "ACME®ion=EU"; + + String url = new LRAUrlBuilder() + .host("http://localhost:8080") + .path("lra-participant") + .query("myOption", optionValue) + .compensation(compensationUri) + .completion(completionUri) + .path("compensate") + .build(); + + URI uri = URI.create(url); + String rawQuery = uri.getRawQuery(); + + Method parseQuery = LRASagaRoutes.class.getDeclaredMethod("parseQuery", String.class); + parseQuery.setAccessible(true); + @SuppressWarnings("unchecked") + Map<String, String> parsed = (Map<String, String>) parseQuery.invoke(new LRASagaRoutes(null), rawQuery); + + assertEquals(compensationUri, parsed.get("Camel-Saga-Compensate"), + "Compensation URI must survive encode/decode round-trip"); + assertEquals(completionUri, parsed.get("Camel-Saga-Complete"), + "Completion URI must survive encode/decode round-trip"); + assertEquals(optionValue, parsed.get("myOption"), + "Option value with special characters must survive encode/decode round-trip"); + } + + @DisplayName("Tests that simple direct: URIs still work after encoding") + @Test + void testSimpleDirectUri() { + String url = new LRAUrlBuilder() + .host("http://localhost:8080") + .path("lra-participant") + .compensation("direct://saga1_participant1_compensate") + .completion("direct://saga1_participant1_complete") + .path("compensate") + .build(); + + URI uri = URI.create(url); + assertNotNull(uri.getQuery(), "URL must have a valid query string"); + } +}
