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;
+        }
+    }
+}

Reply via email to