LANG-374 - Add escaping for CSV columns to StringEscapeUtils

git-svn-id: https://svn.apache.org/repos/asf/commons/proper/lang/trunk@595541 13f79535-47bb-0310-9956-ffa450edef68
diff --git a/src/java/org/apache/commons/lang/StringEscapeUtils.java b/src/java/org/apache/commons/lang/StringEscapeUtils.java
index db2d7bd..d9ac1c9 100644
--- a/src/java/org/apache/commons/lang/StringEscapeUtils.java
+++ b/src/java/org/apache/commons/lang/StringEscapeUtils.java
@@ -688,4 +688,93 @@
         return StringUtils.replace(str, "'", "''");
     }
 
+    //-----------------------------------------------------------------------
+
+    /**
+     * <p>Returns a <code>String</code> value for a CSV column escaping with double quotes,
+     * if required.</p>
+     *
+     * <p>If the value contains a comma, newline or double quote, then the
+     *    String value is returned enclosed in double quotes.</p>
+     * </p>
+     *
+     * <p>Any double quote characters in the value are escaped with another double quote.</p>
+     *
+     * see <a href="http://en.wikipedia.org/wiki/Comma-separated_values">Wikipedia</a> and
+     * <a href="http://tools.ietf.org/html/rfc4180">RFC 4180</a>.
+     *
+     * @param str  the string to escape, may be null
+     * @return a new String, escaped for CSV, <code>null</code> if null string input
+     * @since 2.4
+     */
+    public static String escapeCsv(String str) {
+        if (!containsCsvChars(str)) {
+            return str;
+        }
+        StringBuffer buffer = new StringBuffer(str.length() + 10);
+        buffer.append('"');
+        for (int i = 0; i < str.length(); i++) {
+            char c = str.charAt(i);
+            if (c == '"') {
+                buffer.append('"'); // escape double quote
+            }
+            buffer.append(c);
+        }
+        buffer.append('"');
+        return buffer.toString();
+    }
+
+    /**
+     * <p>Writes a <code>String</code> value for a CSV column escaping with double quotes,
+     * if required.</p>
+     *
+     * <p>If the value contains a comma, newline or double quote, then the
+     *    String value is written enclosed in double quotes.</p>
+     * </p>
+     *
+     * <p>Any double quote characters in the value are escaped with another double quote.</p>
+     *
+     * see <a href="http://en.wikipedia.org/wiki/Comma-separated_values">Wikipedia</a> and
+     * <a href="http://tools.ietf.org/html/rfc4180">RFC 4180</a>.
+     *
+     * @param str  the string to escape, may be null
+     * @param out  Writer to write escaped string into
+     * in double quotes or only when the value contains double quotes, commas or newline
+     * characters.
+     * @throws IOException if error occurs on underlying Writer
+     * @since 2.4
+     */
+    public static void escapeCsv(Writer out, String str) throws IOException {
+        if (!containsCsvChars(str)) {
+            if (str != null) {
+                out.write(str);
+            }
+            return;
+        }
+        out.write('"');
+        for (int i = 0; i < str.length(); i++) {
+            char c = str.charAt(i);
+            if (c == '"') {
+                out.write('"'); // escape double quote
+            }
+            out.write(c);
+        }
+        out.write('"');
+    }
+
+    /**
+     * Determine if the String contains any characters that need escaping for CSV files.
+     *
+     * @param str  the string to escape, may be null
+     * @return <code>true</code> if the String contains characters that need escaping
+     * for CSV files, otherwise <code>false</code>
+     * @since 2.4
+     */
+    private static boolean containsCsvChars(String str) {
+        return (StringUtils.contains(str, '"') ||
+                StringUtils.contains(str, ',') ||
+                StringUtils.contains(str, CharUtils.CR) ||
+                StringUtils.contains(str, CharUtils.LF));
+    }
+
 }
diff --git a/src/test/org/apache/commons/lang/StringEscapeUtilsTest.java b/src/test/org/apache/commons/lang/StringEscapeUtilsTest.java
index 946af2d..a116163 100644
--- a/src/test/org/apache/commons/lang/StringEscapeUtilsTest.java
+++ b/src/test/org/apache/commons/lang/StringEscapeUtilsTest.java
@@ -332,4 +332,36 @@
         assertEquals("& &", StringEscapeUtils.unescapeHtml("& &amp;"));
     }
 
+
+    public void testEscapeCsvString() throws Exception
+    {
+        assertEquals("foo.bar",          StringEscapeUtils.escapeCsv("foo.bar"));
+        assertEquals("\"foo,bar\"",      StringEscapeUtils.escapeCsv("foo,bar"));
+        assertEquals("\"foo\nbar\"",     StringEscapeUtils.escapeCsv("foo\nbar"));
+        assertEquals("\"foo\rbar\"",     StringEscapeUtils.escapeCsv("foo\rbar"));
+        assertEquals("\"foo\"\"bar\"",   StringEscapeUtils.escapeCsv("foo\"bar"));
+        assertEquals("",   StringEscapeUtils.escapeCsv(""));
+        assertEquals(null, StringEscapeUtils.escapeCsv(null));
+    }
+
+    public void testEscapeCsvWriter() throws Exception
+    {
+        checkCsvEscapeWriter("foo.bar",        "foo.bar");
+        checkCsvEscapeWriter("\"foo,bar\"",    "foo,bar");
+        checkCsvEscapeWriter("\"foo\nbar\"",   "foo\nbar");
+        checkCsvEscapeWriter("\"foo\rbar\"",   "foo\rbar");
+        checkCsvEscapeWriter("\"foo\"\"bar\"", "foo\"bar");
+        checkCsvEscapeWriter("", null);
+        checkCsvEscapeWriter("", "");
+    }
+
+    private void checkCsvEscapeWriter(String expected, String value) {
+        try {
+            StringWriter writer = new StringWriter();
+            StringEscapeUtils.escapeCsv(writer, value);
+            assertEquals(expected, writer.toString());
+        } catch (IOException e) {
+            fail("Threw: " + e);
+        }
+    }
 }