This is an automated email from the ASF dual-hosted git repository.
apupier 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 cb8769029093 CAMEL-25150: camel-bindy - with quotingEscaped a CSV
field ending with a quote is closed again
cb8769029093 is described below
commit cb8769029093a4d9495bcef6ff5b1c6e67f86211
Author: smjain <[email protected]>
AuthorDate: Wed Sep 30 07:51:13 2026 +0530
CAMEL-25150: camel-bindy - with quotingEscaped a CSV field ending with a
quote is closed again
With @CsvRecord(quotingEscaped = true) a quote inside a field is written as
a backslash and the quote, so the value He said "hi" is written
"He said \"hi\"". The doubled-quote rule that CAMEL-22068 added to
BindyCsvDataFormat.unquoteTokens (an odd number of quotes before the final
quote means the final quote is escaped) also ran in this mode, so such a
field was not closed: it swallowed the separator and the next columns, and
the following fields were shifted or empty. Before CAMEL-22068 (4.8.7,
4.10.4) these values were read back correctly.
Apply the RFC 4180 doubled-quote rule only when quotingEscaped is false.
RFC 4180 input (the default) is read exactly as before.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
---
.../camel/dataformat/bindy/BindyCsvFactory.java | 4 +
.../dataformat/bindy/csv/BindyCsvDataFormat.java | 11 +-
.../BindyCsvQuotingEscapedTrailingQuoteTest.java | 125 +++++++++++++++++++++
3 files changed, 137 insertions(+), 3 deletions(-)
diff --git
a/components/camel-bindy/src/main/java/org/apache/camel/dataformat/bindy/BindyCsvFactory.java
b/components/camel-bindy/src/main/java/org/apache/camel/dataformat/bindy/BindyCsvFactory.java
index b9fcf9cbbf9a..adb816897315 100644
---
a/components/camel-bindy/src/main/java/org/apache/camel/dataformat/bindy/BindyCsvFactory.java
+++
b/components/camel-bindy/src/main/java/org/apache/camel/dataformat/bindy/BindyCsvFactory.java
@@ -790,6 +790,10 @@ public class BindyCsvFactory extends BindyAbstractFactory
implements BindyFactor
return quote;
}
+ public boolean isQuotingEscaped() {
+ return quotingEscaped;
+ }
+
public Boolean getRemoveQuotes() {
return removeQuotes;
}
diff --git
a/components/camel-bindy/src/main/java/org/apache/camel/dataformat/bindy/csv/BindyCsvDataFormat.java
b/components/camel-bindy/src/main/java/org/apache/camel/dataformat/bindy/csv/BindyCsvDataFormat.java
index 1bcc6d95a091..1942a866e573 100644
---
a/components/camel-bindy/src/main/java/org/apache/camel/dataformat/bindy/csv/BindyCsvDataFormat.java
+++
b/components/camel-bindy/src/main/java/org/apache/camel/dataformat/bindy/csv/BindyCsvDataFormat.java
@@ -261,7 +261,7 @@ public class BindyCsvDataFormat extends
BindyAbstractDataFormat {
// must unquote tokens before use
if (Boolean.TRUE.equals(removeQuotes)) {
- result = unquoteTokens(result, separators, quote);
+ result = unquoteTokens(result, separators, quote,
factory.isQuotingEscaped());
}
if (result.isEmpty()) {
@@ -293,7 +293,8 @@ public class BindyCsvDataFormat extends
BindyAbstractDataFormat {
* Unquote the tokens, by removing leading and trailing quote chars, as
will handling fixing broken tokens which may
* have been split by a separator inside a quote.
*/
- private List<String> unquoteTokens(List<String> result, List<String>
separators, String quote) {
+ private List<String> unquoteTokens(
+ List<String> result, List<String> separators, String quote,
boolean quotingEscaped) {
// a current quoted token which we assemble from the broken pieces
// we need to do this as we use the split method on the String class
// to split the line using regular expression, and it does not handle
@@ -332,7 +333,11 @@ public class BindyCsvDataFormat extends
BindyAbstractDataFormat {
// "not empty"+quote
if (s.endsWith(quote)) {
boolean escaped = false;
- if (quote.equals("\"")) {
+ // RFC 4180: a double quote inside a quoted field is written
as two double quotes, so the final
+ // quote is escaped when an odd number of quotes precedes it.
With quotingEscaped a quote inside the
+ // field is written as a backslash and the quote (see
BindyCsvFactory#unbind), so this rule does not
+ // apply and the final quote closes the field.
+ if (quote.equals("\"") && !quotingEscaped) {
int i;
for (i = s.length() - 2; i > 0; i--) {
char ch = s.charAt(i);
diff --git
a/components/camel-bindy/src/test/java/org/apache/camel/dataformat/bindy/csv/BindyCsvQuotingEscapedTrailingQuoteTest.java
b/components/camel-bindy/src/test/java/org/apache/camel/dataformat/bindy/csv/BindyCsvQuotingEscapedTrailingQuoteTest.java
new file mode 100644
index 000000000000..ca910c9f175c
--- /dev/null
+++
b/components/camel-bindy/src/test/java/org/apache/camel/dataformat/bindy/csv/BindyCsvQuotingEscapedTrailingQuoteTest.java
@@ -0,0 +1,125 @@
+/*
+ * 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.dataformat.bindy.csv;
+
+import java.math.BigDecimal;
+
+import org.apache.camel.RoutesBuilder;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.dataformat.bindy.annotation.CsvRecord;
+import org.apache.camel.dataformat.bindy.annotation.DataField;
+import org.apache.camel.test.junit6.CamelTestSupport;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+/**
+ * With quotingEscaped=true a quote inside a field is written as a backslash
and the quote. A field whose value ends
+ * with a quote must still be read back as one field.
+ */
+public class BindyCsvQuotingEscapedTrailingQuoteTest extends CamelTestSupport {
+
+ @Test
+ public void testUnmarshalFieldEndingWithEscapedQuote() {
+ String body = "\"123\",\"He said \\\"hi\\\"\",\"10\"\r\n";
+ Row row = template.requestBody("direct:unmarshal", body, Row.class);
+ assertEquals("123", row.getFirstField());
+ assertEquals("He said \"hi\"", row.getSecondField());
+ assertEquals(new BigDecimal("10"), row.getNumber());
+ }
+
+ @Test
+ public void testMarshalUnmarshalFieldEndingWithQuote() {
+ for (String value : new String[] { "He said \"hi\"", "12\"", "\"", "a
\"b\" \"c\"" }) {
+ Row row = new Row();
+ row.setFirstField("123");
+ row.setSecondField(value);
+ row.setNumber(new BigDecimal("10"));
+
+ String csv = template.requestBody("direct:marshal", row,
String.class);
+ Row back = template.requestBody("direct:unmarshal", csv,
Row.class);
+ assertEquals("123", back.getFirstField(), csv);
+ assertEquals(value, back.getSecondField(), csv);
+ assertEquals(new BigDecimal("10"), back.getNumber(), csv);
+ }
+ }
+
+ @Test
+ public void testMarshalUnmarshalFieldWithoutTrailingQuote() {
+ // these values are read back correctly today and must stay so
+ for (String value : new String[] { "\"\"foo\"\"", "C:\\temp\\", "a\\",
"say \"hi\" now" }) {
+ Row row = new Row();
+ row.setFirstField("123");
+ row.setSecondField(value);
+ row.setNumber(new BigDecimal("10"));
+
+ String csv = template.requestBody("direct:marshal", row,
String.class);
+ Row back = template.requestBody("direct:unmarshal", csv,
Row.class);
+ assertEquals(value, back.getSecondField(), csv);
+ assertEquals(new BigDecimal("10"), back.getNumber(), csv);
+ }
+ }
+
+ @Override
+ protected RoutesBuilder createRouteBuilder() {
+ return new RouteBuilder() {
+ @Override
+ public void configure() {
+ BindyCsvDataFormat format = new BindyCsvDataFormat(Row.class);
+ from("direct:marshal").marshal(format);
+ from("direct:unmarshal").unmarshal(format);
+ }
+ };
+ }
+
+ @CsvRecord(separator = ",", quote = "\"", quoting = true, quotingEscaped =
true)
+ public static class Row {
+
+ @DataField(pos = 1)
+ private String firstField;
+
+ @DataField(pos = 2)
+ private String secondField;
+
+ @DataField(pos = 3, pattern = "########.##")
+ private BigDecimal number;
+
+ public String getFirstField() {
+ return firstField;
+ }
+
+ public void setFirstField(String firstField) {
+ this.firstField = firstField;
+ }
+
+ public String getSecondField() {
+ return secondField;
+ }
+
+ public void setSecondField(String secondField) {
+ this.secondField = secondField;
+ }
+
+ public BigDecimal getNumber() {
+ return number;
+ }
+
+ public void setNumber(BigDecimal number) {
+ this.number = number;
+ }
+ }
+}