Skip to content

Commit b23f963

Browse files
committed
[CSV-216] Allow for mutable CSV records.
1 parent 259812e commit b23f963

6 files changed

Lines changed: 181 additions & 42 deletions

File tree

src/main/java/org/apache/commons/csv/CSVFormat.java

Lines changed: 46 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -242,7 +242,7 @@ public CSVFormat getFormat() {
242242
* @see Predefined#Default
243243
*/
244244
public static final CSVFormat DEFAULT = new CSVFormat(COMMA, DOUBLE_QUOTE_CHAR, null, null, null, false, true, CRLF,
245-
null, null, null, false, false, false, false, false);
245+
null, null, null, false, false, false, false, false, false);
246246

247247
/**
248248
* Excel file format (using a comma as the value delimiter). Note that the actual value delimiter used by Excel is
@@ -537,7 +537,7 @@ private static boolean isLineBreak(final Character c) {
537537
*/
538538
public static CSVFormat newFormat(final char delimiter) {
539539
return new CSVFormat(delimiter, null, null, null, null, false, false, null, null, null, null, false, false,
540-
false, false, false);
540+
false, false, false, false);
541541
}
542542

543543
/**
@@ -570,6 +570,8 @@ public static CSVFormat valueOf(final String format) {
570570

571571
private final boolean ignoreSurroundingSpaces; // Should leading/trailing spaces be ignored around values?
572572

573+
private final boolean mutableRecords;
574+
573575
private final String nullString; // the string to be used for null values
574576

575577
private final Character quoteCharacter; // null if quoting is disabled
@@ -619,6 +621,7 @@ public static CSVFormat valueOf(final String format) {
619621
* TODO
620622
* @param trailingDelimiter
621623
* TODO
624+
* @param mutableRecords TODO
622625
* @throws IllegalArgumentException
623626
* if the delimiter is a line break character
624627
*/
@@ -627,7 +630,7 @@ private CSVFormat(final char delimiter, final Character quoteChar, final QuoteMo
627630
final boolean ignoreEmptyLines, final String recordSeparator, final String nullString,
628631
final Object[] headerComments, final String[] header, final boolean skipHeaderRecord,
629632
final boolean allowMissingColumnNames, final boolean ignoreHeaderCase, final boolean trim,
630-
final boolean trailingDelimiter) {
633+
final boolean trailingDelimiter, boolean mutableRecords) {
631634
this.delimiter = delimiter;
632635
this.quoteCharacter = quoteChar;
633636
this.quoteMode = quoteMode;
@@ -644,6 +647,7 @@ private CSVFormat(final char delimiter, final Character quoteChar, final QuoteMo
644647
this.ignoreHeaderCase = ignoreHeaderCase;
645648
this.trailingDelimiter = trailingDelimiter;
646649
this.trim = trim;
650+
this.mutableRecords = mutableRecords;
647651
validate();
648652
}
649653

@@ -927,6 +931,10 @@ public boolean isEscapeCharacterSet() {
927931
return escapeCharacter != null;
928932
}
929933

934+
public boolean isMutableRecords() {
935+
return mutableRecords;
936+
}
937+
930938
/**
931939
* Returns whether a nullString has been defined.
932940
*
@@ -1431,7 +1439,7 @@ public CSVFormat withAllowMissingColumnNames() {
14311439
public CSVFormat withAllowMissingColumnNames(final boolean allowMissingColumnNames) {
14321440
return new CSVFormat(delimiter, quoteCharacter, quoteMode, commentMarker, escapeCharacter,
14331441
ignoreSurroundingSpaces, ignoreEmptyLines, recordSeparator, nullString, headerComments, header,
1434-
skipHeaderRecord, allowMissingColumnNames, ignoreHeaderCase, trim, trailingDelimiter);
1442+
skipHeaderRecord, allowMissingColumnNames, ignoreHeaderCase, trim, trailingDelimiter, mutableRecords);
14351443
}
14361444

14371445
/**
@@ -1466,7 +1474,7 @@ public CSVFormat withCommentMarker(final Character commentMarker) {
14661474
}
14671475
return new CSVFormat(delimiter, quoteCharacter, quoteMode, commentMarker, escapeCharacter,
14681476
ignoreSurroundingSpaces, ignoreEmptyLines, recordSeparator, nullString, headerComments, header,
1469-
skipHeaderRecord, allowMissingColumnNames, ignoreHeaderCase, trim, trailingDelimiter);
1477+
skipHeaderRecord, allowMissingColumnNames, ignoreHeaderCase, trim, trailingDelimiter, mutableRecords);
14701478
}
14711479

14721480
/**
@@ -1484,7 +1492,7 @@ public CSVFormat withDelimiter(final char delimiter) {
14841492
}
14851493
return new CSVFormat(delimiter, quoteCharacter, quoteMode, commentMarker, escapeCharacter,
14861494
ignoreSurroundingSpaces, ignoreEmptyLines, recordSeparator, nullString, headerComments, header,
1487-
skipHeaderRecord, allowMissingColumnNames, ignoreHeaderCase, trim, trailingDelimiter);
1495+
skipHeaderRecord, allowMissingColumnNames, ignoreHeaderCase, trim, trailingDelimiter, mutableRecords);
14881496
}
14891497

14901498
/**
@@ -1515,7 +1523,7 @@ public CSVFormat withEscape(final Character escape) {
15151523
}
15161524
return new CSVFormat(delimiter, quoteCharacter, quoteMode, commentMarker, escape, ignoreSurroundingSpaces,
15171525
ignoreEmptyLines, recordSeparator, nullString, headerComments, header, skipHeaderRecord,
1518-
allowMissingColumnNames, ignoreHeaderCase, trim, trailingDelimiter);
1526+
allowMissingColumnNames, ignoreHeaderCase, trim, trailingDelimiter, mutableRecords);
15191527
}
15201528

15211529
/**
@@ -1670,7 +1678,7 @@ public CSVFormat withHeader(final ResultSetMetaData metaData) throws SQLExceptio
16701678
public CSVFormat withHeader(final String... header) {
16711679
return new CSVFormat(delimiter, quoteCharacter, quoteMode, commentMarker, escapeCharacter,
16721680
ignoreSurroundingSpaces, ignoreEmptyLines, recordSeparator, nullString, headerComments, header,
1673-
skipHeaderRecord, allowMissingColumnNames, ignoreHeaderCase, trim, trailingDelimiter);
1681+
skipHeaderRecord, allowMissingColumnNames, ignoreHeaderCase, trim, trailingDelimiter, mutableRecords);
16741682
}
16751683

16761684
/**
@@ -1691,7 +1699,7 @@ public CSVFormat withHeader(final String... header) {
16911699
public CSVFormat withHeaderComments(final Object... headerComments) {
16921700
return new CSVFormat(delimiter, quoteCharacter, quoteMode, commentMarker, escapeCharacter,
16931701
ignoreSurroundingSpaces, ignoreEmptyLines, recordSeparator, nullString, headerComments, header,
1694-
skipHeaderRecord, allowMissingColumnNames, ignoreHeaderCase, trim, trailingDelimiter);
1702+
skipHeaderRecord, allowMissingColumnNames, ignoreHeaderCase, trim, trailingDelimiter, mutableRecords);
16951703
}
16961704

16971705
/**
@@ -1716,7 +1724,7 @@ public CSVFormat withIgnoreEmptyLines() {
17161724
public CSVFormat withIgnoreEmptyLines(final boolean ignoreEmptyLines) {
17171725
return new CSVFormat(delimiter, quoteCharacter, quoteMode, commentMarker, escapeCharacter,
17181726
ignoreSurroundingSpaces, ignoreEmptyLines, recordSeparator, nullString, headerComments, header,
1719-
skipHeaderRecord, allowMissingColumnNames, ignoreHeaderCase, trim, trailingDelimiter);
1727+
skipHeaderRecord, allowMissingColumnNames, ignoreHeaderCase, trim, trailingDelimiter, mutableRecords);
17201728
}
17211729

17221730
/**
@@ -1742,7 +1750,7 @@ public CSVFormat withIgnoreHeaderCase() {
17421750
public CSVFormat withIgnoreHeaderCase(final boolean ignoreHeaderCase) {
17431751
return new CSVFormat(delimiter, quoteCharacter, quoteMode, commentMarker, escapeCharacter,
17441752
ignoreSurroundingSpaces, ignoreEmptyLines, recordSeparator, nullString, headerComments, header,
1745-
skipHeaderRecord, allowMissingColumnNames, ignoreHeaderCase, trim, trailingDelimiter);
1753+
skipHeaderRecord, allowMissingColumnNames, ignoreHeaderCase, trim, trailingDelimiter, mutableRecords);
17461754
}
17471755

17481756
/**
@@ -1767,7 +1775,25 @@ public CSVFormat withIgnoreSurroundingSpaces() {
17671775
public CSVFormat withIgnoreSurroundingSpaces(final boolean ignoreSurroundingSpaces) {
17681776
return new CSVFormat(delimiter, quoteCharacter, quoteMode, commentMarker, escapeCharacter,
17691777
ignoreSurroundingSpaces, ignoreEmptyLines, recordSeparator, nullString, headerComments, header,
1770-
skipHeaderRecord, allowMissingColumnNames, ignoreHeaderCase, trim, trailingDelimiter);
1778+
skipHeaderRecord, allowMissingColumnNames, ignoreHeaderCase, trim, trailingDelimiter, mutableRecords);
1779+
}
1780+
1781+
/**
1782+
* Returns a new {@code CSVFormat} with whether to generate CSVRecord or CSVMutableRecord.
1783+
* <ul>
1784+
* <li><strong>Reading:</strong> Whether to generate CSVRecord or CSVMutableRecord.</li>
1785+
* <li><strong>Writing:</strong> No effect.</li>
1786+
* </ul>
1787+
*
1788+
* @param mutableRecords
1789+
* whether to generate CSVRecord or CSVMutableRecord
1790+
*
1791+
* @return A new CSVFormat that is equal to this but with the specified null conversion string.
1792+
*/
1793+
public CSVFormat withMutableRecords(final boolean mutableRecords) {
1794+
return new CSVFormat(delimiter, quoteCharacter, quoteMode, commentMarker, escapeCharacter,
1795+
ignoreSurroundingSpaces, ignoreEmptyLines, recordSeparator, nullString, headerComments, header,
1796+
skipHeaderRecord, allowMissingColumnNames, ignoreHeaderCase, trim, trailingDelimiter, mutableRecords);
17711797
}
17721798

17731799
/**
@@ -1786,7 +1812,7 @@ public CSVFormat withIgnoreSurroundingSpaces(final boolean ignoreSurroundingSpac
17861812
public CSVFormat withNullString(final String nullString) {
17871813
return new CSVFormat(delimiter, quoteCharacter, quoteMode, commentMarker, escapeCharacter,
17881814
ignoreSurroundingSpaces, ignoreEmptyLines, recordSeparator, nullString, headerComments, header,
1789-
skipHeaderRecord, allowMissingColumnNames, ignoreHeaderCase, trim, trailingDelimiter);
1815+
skipHeaderRecord, allowMissingColumnNames, ignoreHeaderCase, trim, trailingDelimiter, mutableRecords);
17901816
}
17911817

17921818
/**
@@ -1817,7 +1843,7 @@ public CSVFormat withQuote(final Character quoteChar) {
18171843
}
18181844
return new CSVFormat(delimiter, quoteChar, quoteMode, commentMarker, escapeCharacter, ignoreSurroundingSpaces,
18191845
ignoreEmptyLines, recordSeparator, nullString, headerComments, header, skipHeaderRecord,
1820-
allowMissingColumnNames, ignoreHeaderCase, trim, trailingDelimiter);
1846+
allowMissingColumnNames, ignoreHeaderCase, trim, trailingDelimiter, mutableRecords);
18211847
}
18221848

18231849
/**
@@ -1831,7 +1857,7 @@ public CSVFormat withQuote(final Character quoteChar) {
18311857
public CSVFormat withQuoteMode(final QuoteMode quoteModePolicy) {
18321858
return new CSVFormat(delimiter, quoteCharacter, quoteModePolicy, commentMarker, escapeCharacter,
18331859
ignoreSurroundingSpaces, ignoreEmptyLines, recordSeparator, nullString, headerComments, header,
1834-
skipHeaderRecord, allowMissingColumnNames, ignoreHeaderCase, trim, trailingDelimiter);
1860+
skipHeaderRecord, allowMissingColumnNames, ignoreHeaderCase, trim, trailingDelimiter, mutableRecords);
18351861
}
18361862

18371863
/**
@@ -1869,7 +1895,7 @@ public CSVFormat withRecordSeparator(final char recordSeparator) {
18691895
public CSVFormat withRecordSeparator(final String recordSeparator) {
18701896
return new CSVFormat(delimiter, quoteCharacter, quoteMode, commentMarker, escapeCharacter,
18711897
ignoreSurroundingSpaces, ignoreEmptyLines, recordSeparator, nullString, headerComments, header,
1872-
skipHeaderRecord, allowMissingColumnNames, ignoreHeaderCase, trim, trailingDelimiter);
1898+
skipHeaderRecord, allowMissingColumnNames, ignoreHeaderCase, trim, trailingDelimiter, mutableRecords);
18731899
}
18741900

18751901
/**
@@ -1896,7 +1922,7 @@ public CSVFormat withSkipHeaderRecord() {
18961922
public CSVFormat withSkipHeaderRecord(final boolean skipHeaderRecord) {
18971923
return new CSVFormat(delimiter, quoteCharacter, quoteMode, commentMarker, escapeCharacter,
18981924
ignoreSurroundingSpaces, ignoreEmptyLines, recordSeparator, nullString, headerComments, header,
1899-
skipHeaderRecord, allowMissingColumnNames, ignoreHeaderCase, trim, trailingDelimiter);
1925+
skipHeaderRecord, allowMissingColumnNames, ignoreHeaderCase, trim, trailingDelimiter, mutableRecords);
19001926
}
19011927

19021928
/**
@@ -1921,7 +1947,7 @@ public CSVFormat withTrailingDelimiter() {
19211947
public CSVFormat withTrailingDelimiter(final boolean trailingDelimiter) {
19221948
return new CSVFormat(delimiter, quoteCharacter, quoteMode, commentMarker, escapeCharacter,
19231949
ignoreSurroundingSpaces, ignoreEmptyLines, recordSeparator, nullString, headerComments, header,
1924-
skipHeaderRecord, allowMissingColumnNames, ignoreHeaderCase, trim, trailingDelimiter);
1950+
skipHeaderRecord, allowMissingColumnNames, ignoreHeaderCase, trim, trailingDelimiter, mutableRecords);
19251951
}
19261952

19271953
/**
@@ -1946,6 +1972,7 @@ public CSVFormat withTrim() {
19461972
public CSVFormat withTrim(final boolean trim) {
19471973
return new CSVFormat(delimiter, quoteCharacter, quoteMode, commentMarker, escapeCharacter,
19481974
ignoreSurroundingSpaces, ignoreEmptyLines, recordSeparator, nullString, headerComments, header,
1949-
skipHeaderRecord, allowMissingColumnNames, ignoreHeaderCase, trim, trailingDelimiter);
1975+
skipHeaderRecord, allowMissingColumnNames, ignoreHeaderCase, trim, trailingDelimiter, mutableRecords);
19501976
}
1977+
19511978
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to You under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
package org.apache.commons.csv;
19+
20+
import java.util.Map;
21+
22+
public final class CSVMutableRecord extends CSVRecord {
23+
24+
private static final long serialVersionUID = 1L;
25+
26+
CSVMutableRecord(String[] values, Map<String, Integer> mapping, String comment, long recordNumber,
27+
long characterPosition) {
28+
super(values, mapping, comment, recordNumber, characterPosition);
29+
}
30+
31+
@Override
32+
public void put(int index, String value) {
33+
super.put(index, value);
34+
}
35+
36+
@Override
37+
public void put(String name, String value) {
38+
super.put(name, value);
39+
}
40+
41+
}

src/main/java/org/apache/commons/csv/CSVParser.java

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -300,7 +300,7 @@ public static CSVParser parse(final URL url, final Charset charset, final CSVFor
300300
private final long characterOffset;
301301

302302
private final Token reusableToken = new Token();
303-
303+
304304
/**
305305
* Customized CSV parser using the given {@link CSVFormat}
306306
*
@@ -614,8 +614,10 @@ CSVRecord nextRecord() throws IOException {
614614
if (!this.recordList.isEmpty()) {
615615
this.recordNumber++;
616616
final String comment = sb == null ? null : sb.toString();
617-
result = new CSVRecord(this.recordList.toArray(new String[this.recordList.size()]), this.headerMap, comment,
618-
this.recordNumber, startCharPosition);
617+
String[] array = this.recordList.toArray(new String[this.recordList.size()]);
618+
result = format.isMutableRecords()
619+
? new CSVMutableRecord(array, this.headerMap, comment, this.recordNumber, startCharPosition)
620+
: new CSVRecord(array, this.headerMap, comment, this.recordNumber, startCharPosition);
619621
}
620622
return result;
621623
}

src/main/java/org/apache/commons/csv/CSVRecord.java

Lines changed: 25 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
/**
2929
* A CSV record parsed from a CSV file.
3030
*/
31-
public final class CSVRecord implements Serializable, Iterable<String> {
31+
public class CSVRecord implements Serializable, Iterable<String> {
3232

3333
private static final String[] EMPTY_STRING_ARRAY = new String[0];
3434

@@ -95,20 +95,26 @@ public String get(final int i) {
9595
public String get(final String name) {
9696
if (mapping == null) {
9797
throw new IllegalStateException(
98-
"No header mapping was specified, the record values can't be accessed by name");
99-
}
100-
final Integer index = mapping.get(name);
101-
if (index == null) {
102-
throw new IllegalArgumentException(String.format("Mapping for %s not found, expected one of %s", name,
103-
mapping.keySet()));
98+
"No header mapping was specified, the record values can't be accessed by name");
10499
}
100+
final int intIndex = getIndex(name);
105101
try {
106-
return values[index.intValue()];
102+
return values[intIndex];
107103
} catch (final ArrayIndexOutOfBoundsException e) {
108-
throw new IllegalArgumentException(String.format(
109-
"Index for header '%s' is %d but CSVRecord only has %d values!", name, index,
110-
Integer.valueOf(values.length)));
104+
throw new IllegalArgumentException(
105+
String.format("Index for header '%s' is %d but CSVRecord only has %d values!", name, intIndex,
106+
Integer.valueOf(values.length)));
107+
}
108+
}
109+
110+
int getIndex(final String name) {
111+
final Integer integerIndex = mapping.get(name);
112+
if (integerIndex == null) {
113+
throw new IllegalArgumentException(
114+
String.format("Mapping for %s not found, expected one of %s", name, mapping.keySet()));
111115
}
116+
int intIndex = integerIndex.intValue();
117+
return intIndex;
112118
}
113119

114120
/**
@@ -207,6 +213,14 @@ public Iterator<String> iterator() {
207213
return toList().iterator();
208214
}
209215

216+
void put(final int index, String value) {
217+
values[index] = value;
218+
}
219+
220+
void put(final String name, String value) {
221+
values[getIndex(name)] = value;
222+
}
223+
210224
/**
211225
* Puts all values of this record into the given Map.
212226
*
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package org.apache.commons.csv;
2+
3+
import org.junit.Assert;
4+
5+
public class CSVMutableRecordTest extends CSVRecordTest {
6+
7+
@Override
8+
protected CSVFormat createCommaFormat() {
9+
return super.createCommaFormat().withMutableRecords(true);
10+
}
11+
12+
@Override
13+
protected CSVFormat createDefaultFormat() {
14+
return super.createDefaultFormat().withMutableRecords(true);
15+
}
16+
17+
@Override
18+
protected CSVRecord newRecord() {
19+
return new CSVMutableRecord(values, null, null, 0, -1);
20+
}
21+
22+
@Override
23+
protected CSVRecord newRecordWithHeader() {
24+
return new CSVMutableRecord(values, header, null, 0, -1);
25+
}
26+
27+
@Override
28+
protected void validate(final CSVRecord anyRecord) {
29+
Assert.assertEquals(CSVMutableRecord.class, anyRecord.getClass());
30+
}
31+
32+
}

0 commit comments

Comments
 (0)