diff --git a/src/main/java/org/apache/commons/csv/CSVFormat.java b/src/main/java/org/apache/commons/csv/CSVFormat.java index bae5a970..14586667 100644 --- a/src/main/java/org/apache/commons/csv/CSVFormat.java +++ b/src/main/java/org/apache/commons/csv/CSVFormat.java @@ -242,7 +242,7 @@ public final class CSVFormat implements Serializable { * @see Predefined#Default */ public static final CSVFormat DEFAULT = new CSVFormat(COMMA, DOUBLE_QUOTE_CHAR, null, null, null, false, true, CRLF, - null, null, null, false, false, false, false, false); + null, null, null, false, false, false, false, false, false); /** * Excel file format (using a comma as the value delimiter). Note that the actual value delimiter used by Excel is @@ -537,7 +537,7 @@ public final class CSVFormat implements Serializable { */ public static CSVFormat newFormat(final char delimiter) { return new CSVFormat(delimiter, null, null, null, null, false, false, null, null, null, null, false, false, - false, false, false); + false, false, false, false); } /** @@ -570,6 +570,8 @@ public final class CSVFormat implements Serializable { private final boolean ignoreSurroundingSpaces; // Should leading/trailing spaces be ignored around values? + private final boolean mutableRecords; + private final String nullString; // the string to be used for null values private final Character quoteCharacter; // null if quoting is disabled @@ -619,6 +621,7 @@ public final class CSVFormat implements Serializable { * TODO * @param trailingDelimiter * TODO + * @param mutableRecords TODO * @throws IllegalArgumentException * if the delimiter is a line break character */ @@ -627,7 +630,7 @@ public final class CSVFormat implements Serializable { final boolean ignoreEmptyLines, final String recordSeparator, final String nullString, final Object[] headerComments, final String[] header, final boolean skipHeaderRecord, final boolean allowMissingColumnNames, final boolean ignoreHeaderCase, final boolean trim, - final boolean trailingDelimiter) { + final boolean trailingDelimiter, boolean mutableRecords) { this.delimiter = delimiter; this.quoteCharacter = quoteChar; this.quoteMode = quoteMode; @@ -644,6 +647,7 @@ public final class CSVFormat implements Serializable { this.ignoreHeaderCase = ignoreHeaderCase; this.trailingDelimiter = trailingDelimiter; this.trim = trim; + this.mutableRecords = mutableRecords; validate(); } @@ -927,6 +931,10 @@ public final class CSVFormat implements Serializable { return escapeCharacter != null; } + public boolean isMutableRecords() { + return mutableRecords; + } + /** * Returns whether a nullString has been defined. * @@ -1431,7 +1439,7 @@ public final class CSVFormat implements Serializable { public CSVFormat withAllowMissingColumnNames(final boolean allowMissingColumnNames) { return new CSVFormat(delimiter, quoteCharacter, quoteMode, commentMarker, escapeCharacter, ignoreSurroundingSpaces, ignoreEmptyLines, recordSeparator, nullString, headerComments, header, - skipHeaderRecord, allowMissingColumnNames, ignoreHeaderCase, trim, trailingDelimiter); + skipHeaderRecord, allowMissingColumnNames, ignoreHeaderCase, trim, trailingDelimiter, mutableRecords); } /** @@ -1466,7 +1474,7 @@ public final class CSVFormat implements Serializable { } return new CSVFormat(delimiter, quoteCharacter, quoteMode, commentMarker, escapeCharacter, ignoreSurroundingSpaces, ignoreEmptyLines, recordSeparator, nullString, headerComments, header, - skipHeaderRecord, allowMissingColumnNames, ignoreHeaderCase, trim, trailingDelimiter); + skipHeaderRecord, allowMissingColumnNames, ignoreHeaderCase, trim, trailingDelimiter, mutableRecords); } /** @@ -1484,7 +1492,7 @@ public final class CSVFormat implements Serializable { } return new CSVFormat(delimiter, quoteCharacter, quoteMode, commentMarker, escapeCharacter, ignoreSurroundingSpaces, ignoreEmptyLines, recordSeparator, nullString, headerComments, header, - skipHeaderRecord, allowMissingColumnNames, ignoreHeaderCase, trim, trailingDelimiter); + skipHeaderRecord, allowMissingColumnNames, ignoreHeaderCase, trim, trailingDelimiter, mutableRecords); } /** @@ -1515,7 +1523,7 @@ public final class CSVFormat implements Serializable { } return new CSVFormat(delimiter, quoteCharacter, quoteMode, commentMarker, escape, ignoreSurroundingSpaces, ignoreEmptyLines, recordSeparator, nullString, headerComments, header, skipHeaderRecord, - allowMissingColumnNames, ignoreHeaderCase, trim, trailingDelimiter); + allowMissingColumnNames, ignoreHeaderCase, trim, trailingDelimiter, mutableRecords); } /** @@ -1670,7 +1678,7 @@ public final class CSVFormat implements Serializable { public CSVFormat withHeader(final String... header) { return new CSVFormat(delimiter, quoteCharacter, quoteMode, commentMarker, escapeCharacter, ignoreSurroundingSpaces, ignoreEmptyLines, recordSeparator, nullString, headerComments, header, - skipHeaderRecord, allowMissingColumnNames, ignoreHeaderCase, trim, trailingDelimiter); + skipHeaderRecord, allowMissingColumnNames, ignoreHeaderCase, trim, trailingDelimiter, mutableRecords); } /** @@ -1691,7 +1699,7 @@ public final class CSVFormat implements Serializable { public CSVFormat withHeaderComments(final Object... headerComments) { return new CSVFormat(delimiter, quoteCharacter, quoteMode, commentMarker, escapeCharacter, ignoreSurroundingSpaces, ignoreEmptyLines, recordSeparator, nullString, headerComments, header, - skipHeaderRecord, allowMissingColumnNames, ignoreHeaderCase, trim, trailingDelimiter); + skipHeaderRecord, allowMissingColumnNames, ignoreHeaderCase, trim, trailingDelimiter, mutableRecords); } /** @@ -1716,7 +1724,7 @@ public final class CSVFormat implements Serializable { public CSVFormat withIgnoreEmptyLines(final boolean ignoreEmptyLines) { return new CSVFormat(delimiter, quoteCharacter, quoteMode, commentMarker, escapeCharacter, ignoreSurroundingSpaces, ignoreEmptyLines, recordSeparator, nullString, headerComments, header, - skipHeaderRecord, allowMissingColumnNames, ignoreHeaderCase, trim, trailingDelimiter); + skipHeaderRecord, allowMissingColumnNames, ignoreHeaderCase, trim, trailingDelimiter, mutableRecords); } /** @@ -1742,7 +1750,7 @@ public final class CSVFormat implements Serializable { public CSVFormat withIgnoreHeaderCase(final boolean ignoreHeaderCase) { return new CSVFormat(delimiter, quoteCharacter, quoteMode, commentMarker, escapeCharacter, ignoreSurroundingSpaces, ignoreEmptyLines, recordSeparator, nullString, headerComments, header, - skipHeaderRecord, allowMissingColumnNames, ignoreHeaderCase, trim, trailingDelimiter); + skipHeaderRecord, allowMissingColumnNames, ignoreHeaderCase, trim, trailingDelimiter, mutableRecords); } /** @@ -1767,7 +1775,25 @@ public final class CSVFormat implements Serializable { public CSVFormat withIgnoreSurroundingSpaces(final boolean ignoreSurroundingSpaces) { return new CSVFormat(delimiter, quoteCharacter, quoteMode, commentMarker, escapeCharacter, ignoreSurroundingSpaces, ignoreEmptyLines, recordSeparator, nullString, headerComments, header, - skipHeaderRecord, allowMissingColumnNames, ignoreHeaderCase, trim, trailingDelimiter); + skipHeaderRecord, allowMissingColumnNames, ignoreHeaderCase, trim, trailingDelimiter, mutableRecords); + } + + /** + * Returns a new {@code CSVFormat} with whether to generate CSVRecord or CSVMutableRecord. + * + * + * @param mutableRecords + * whether to generate CSVRecord or CSVMutableRecord + * + * @return A new CSVFormat that is equal to this but with the specified null conversion string. + */ + public CSVFormat withMutableRecords(final boolean mutableRecords) { + return new CSVFormat(delimiter, quoteCharacter, quoteMode, commentMarker, escapeCharacter, + ignoreSurroundingSpaces, ignoreEmptyLines, recordSeparator, nullString, headerComments, header, + skipHeaderRecord, allowMissingColumnNames, ignoreHeaderCase, trim, trailingDelimiter, mutableRecords); } /** @@ -1786,7 +1812,7 @@ public final class CSVFormat implements Serializable { public CSVFormat withNullString(final String nullString) { return new CSVFormat(delimiter, quoteCharacter, quoteMode, commentMarker, escapeCharacter, ignoreSurroundingSpaces, ignoreEmptyLines, recordSeparator, nullString, headerComments, header, - skipHeaderRecord, allowMissingColumnNames, ignoreHeaderCase, trim, trailingDelimiter); + skipHeaderRecord, allowMissingColumnNames, ignoreHeaderCase, trim, trailingDelimiter, mutableRecords); } /** @@ -1817,7 +1843,7 @@ public final class CSVFormat implements Serializable { } return new CSVFormat(delimiter, quoteChar, quoteMode, commentMarker, escapeCharacter, ignoreSurroundingSpaces, ignoreEmptyLines, recordSeparator, nullString, headerComments, header, skipHeaderRecord, - allowMissingColumnNames, ignoreHeaderCase, trim, trailingDelimiter); + allowMissingColumnNames, ignoreHeaderCase, trim, trailingDelimiter, mutableRecords); } /** @@ -1831,7 +1857,7 @@ public final class CSVFormat implements Serializable { public CSVFormat withQuoteMode(final QuoteMode quoteModePolicy) { return new CSVFormat(delimiter, quoteCharacter, quoteModePolicy, commentMarker, escapeCharacter, ignoreSurroundingSpaces, ignoreEmptyLines, recordSeparator, nullString, headerComments, header, - skipHeaderRecord, allowMissingColumnNames, ignoreHeaderCase, trim, trailingDelimiter); + skipHeaderRecord, allowMissingColumnNames, ignoreHeaderCase, trim, trailingDelimiter, mutableRecords); } /** @@ -1869,7 +1895,7 @@ public final class CSVFormat implements Serializable { public CSVFormat withRecordSeparator(final String recordSeparator) { return new CSVFormat(delimiter, quoteCharacter, quoteMode, commentMarker, escapeCharacter, ignoreSurroundingSpaces, ignoreEmptyLines, recordSeparator, nullString, headerComments, header, - skipHeaderRecord, allowMissingColumnNames, ignoreHeaderCase, trim, trailingDelimiter); + skipHeaderRecord, allowMissingColumnNames, ignoreHeaderCase, trim, trailingDelimiter, mutableRecords); } /** @@ -1896,7 +1922,7 @@ public final class CSVFormat implements Serializable { public CSVFormat withSkipHeaderRecord(final boolean skipHeaderRecord) { return new CSVFormat(delimiter, quoteCharacter, quoteMode, commentMarker, escapeCharacter, ignoreSurroundingSpaces, ignoreEmptyLines, recordSeparator, nullString, headerComments, header, - skipHeaderRecord, allowMissingColumnNames, ignoreHeaderCase, trim, trailingDelimiter); + skipHeaderRecord, allowMissingColumnNames, ignoreHeaderCase, trim, trailingDelimiter, mutableRecords); } /** @@ -1921,7 +1947,7 @@ public final class CSVFormat implements Serializable { public CSVFormat withTrailingDelimiter(final boolean trailingDelimiter) { return new CSVFormat(delimiter, quoteCharacter, quoteMode, commentMarker, escapeCharacter, ignoreSurroundingSpaces, ignoreEmptyLines, recordSeparator, nullString, headerComments, header, - skipHeaderRecord, allowMissingColumnNames, ignoreHeaderCase, trim, trailingDelimiter); + skipHeaderRecord, allowMissingColumnNames, ignoreHeaderCase, trim, trailingDelimiter, mutableRecords); } /** @@ -1946,6 +1972,7 @@ public final class CSVFormat implements Serializable { public CSVFormat withTrim(final boolean trim) { return new CSVFormat(delimiter, quoteCharacter, quoteMode, commentMarker, escapeCharacter, ignoreSurroundingSpaces, ignoreEmptyLines, recordSeparator, nullString, headerComments, header, - skipHeaderRecord, allowMissingColumnNames, ignoreHeaderCase, trim, trailingDelimiter); + skipHeaderRecord, allowMissingColumnNames, ignoreHeaderCase, trim, trailingDelimiter, mutableRecords); } + } diff --git a/src/main/java/org/apache/commons/csv/CSVMutableRecord.java b/src/main/java/org/apache/commons/csv/CSVMutableRecord.java new file mode 100644 index 00000000..b529cfb8 --- /dev/null +++ b/src/main/java/org/apache/commons/csv/CSVMutableRecord.java @@ -0,0 +1,41 @@ +/* + * 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.commons.csv; + +import java.util.Map; + +public final class CSVMutableRecord extends CSVRecord { + + private static final long serialVersionUID = 1L; + + CSVMutableRecord(String[] values, Map mapping, String comment, long recordNumber, + long characterPosition) { + super(values, mapping, comment, recordNumber, characterPosition); + } + + @Override + public void put(int index, String value) { + super.put(index, value); + } + + @Override + public void put(String name, String value) { + super.put(name, value); + } + +} diff --git a/src/main/java/org/apache/commons/csv/CSVParser.java b/src/main/java/org/apache/commons/csv/CSVParser.java index 88512111..23319257 100644 --- a/src/main/java/org/apache/commons/csv/CSVParser.java +++ b/src/main/java/org/apache/commons/csv/CSVParser.java @@ -300,7 +300,7 @@ public final class CSVParser implements Iterable, Closeable { private final long characterOffset; private final Token reusableToken = new Token(); - + /** * Customized CSV parser using the given {@link CSVFormat} * @@ -614,8 +614,10 @@ public final class CSVParser implements Iterable, Closeable { if (!this.recordList.isEmpty()) { this.recordNumber++; final String comment = sb == null ? null : sb.toString(); - result = new CSVRecord(this.recordList.toArray(new String[this.recordList.size()]), this.headerMap, comment, - this.recordNumber, startCharPosition); + String[] array = this.recordList.toArray(new String[this.recordList.size()]); + result = format.isMutableRecords() + ? new CSVMutableRecord(array, this.headerMap, comment, this.recordNumber, startCharPosition) + : new CSVRecord(array, this.headerMap, comment, this.recordNumber, startCharPosition); } return result; } diff --git a/src/main/java/org/apache/commons/csv/CSVRecord.java b/src/main/java/org/apache/commons/csv/CSVRecord.java index 34a3ba21..35812ae0 100644 --- a/src/main/java/org/apache/commons/csv/CSVRecord.java +++ b/src/main/java/org/apache/commons/csv/CSVRecord.java @@ -28,7 +28,7 @@ import java.util.Map.Entry; /** * A CSV record parsed from a CSV file. */ -public final class CSVRecord implements Serializable, Iterable { +public class CSVRecord implements Serializable, Iterable { private static final String[] EMPTY_STRING_ARRAY = new String[0]; @@ -95,22 +95,28 @@ public final class CSVRecord implements Serializable, Iterable { public String get(final String name) { if (mapping == null) { throw new IllegalStateException( - "No header mapping was specified, the record values can't be accessed by name"); - } - final Integer index = mapping.get(name); - if (index == null) { - throw new IllegalArgumentException(String.format("Mapping for %s not found, expected one of %s", name, - mapping.keySet())); + "No header mapping was specified, the record values can't be accessed by name"); } + final int intIndex = getIndex(name); try { - return values[index.intValue()]; + return values[intIndex]; } catch (final ArrayIndexOutOfBoundsException e) { - throw new IllegalArgumentException(String.format( - "Index for header '%s' is %d but CSVRecord only has %d values!", name, index, - Integer.valueOf(values.length))); + throw new IllegalArgumentException( + String.format("Index for header '%s' is %d but CSVRecord only has %d values!", name, intIndex, + Integer.valueOf(values.length))); } } + int getIndex(final String name) { + final Integer integerIndex = mapping.get(name); + if (integerIndex == null) { + throw new IllegalArgumentException( + String.format("Mapping for %s not found, expected one of %s", name, mapping.keySet())); + } + int intIndex = integerIndex.intValue(); + return intIndex; + } + /** * Returns the start position of this record as a character position in the source stream. This may or may not * correspond to the byte position depending on the character set. @@ -207,6 +213,14 @@ public final class CSVRecord implements Serializable, Iterable { return toList().iterator(); } + void put(final int index, String value) { + values[index] = value; + } + + void put(final String name, String value) { + values[getIndex(name)] = value; + } + /** * Puts all values of this record into the given Map. * diff --git a/src/test/java/org/apache/commons/csv/CSVMutableRecordTest.java b/src/test/java/org/apache/commons/csv/CSVMutableRecordTest.java new file mode 100644 index 00000000..8f97dcd9 --- /dev/null +++ b/src/test/java/org/apache/commons/csv/CSVMutableRecordTest.java @@ -0,0 +1,32 @@ +package org.apache.commons.csv; + +import org.junit.Assert; + +public class CSVMutableRecordTest extends CSVRecordTest { + + @Override + protected CSVFormat createCommaFormat() { + return super.createCommaFormat().withMutableRecords(true); + } + + @Override + protected CSVFormat createDefaultFormat() { + return super.createDefaultFormat().withMutableRecords(true); + } + + @Override + protected CSVRecord newRecord() { + return new CSVMutableRecord(values, null, null, 0, -1); + } + + @Override + protected CSVRecord newRecordWithHeader() { + return new CSVMutableRecord(values, header, null, 0, -1); + } + + @Override + protected void validate(final CSVRecord anyRecord) { + Assert.assertEquals(CSVMutableRecord.class, anyRecord.getClass()); + } + +} diff --git a/src/test/java/org/apache/commons/csv/CSVRecordTest.java b/src/test/java/org/apache/commons/csv/CSVRecordTest.java index 6347cc51..cc3f3c63 100644 --- a/src/test/java/org/apache/commons/csv/CSVRecordTest.java +++ b/src/test/java/org/apache/commons/csv/CSVRecordTest.java @@ -38,19 +38,32 @@ public class CSVRecordTest { private enum EnumFixture { UNKNOWN_COLUMN } - private String[] values; - private CSVRecord record, recordWithHeader; - private Map header; + protected String[] values; + protected CSVRecord record, recordWithHeader; + protected Map header; @Before public void setUp() throws Exception { values = new String[] { "A", "B", "C" }; - record = new CSVRecord(values, null, null, 0, -1); + record = newRecord(); header = new HashMap<>(); header.put("first", Integer.valueOf(0)); header.put("second", Integer.valueOf(1)); header.put("third", Integer.valueOf(2)); - recordWithHeader = new CSVRecord(values, header, null, 0, -1); + recordWithHeader = newRecordWithHeader(); + validate(recordWithHeader); + } + + protected CSVRecord newRecord() { + return new CSVRecord(values, null, null, 0, -1); + } + + protected void validate(final CSVRecord anyRecord) { + Assert.assertEquals(CSVRecord.class, anyRecord.getClass()); + } + + protected CSVRecord newRecordWithHeader() { + return new CSVRecord(values, header, null, 0, -1); } @Test @@ -143,7 +156,7 @@ public class CSVRecordTest { @Test public void testRemoveAndAddColumns() throws IOException { // do: - try (final CSVPrinter printer = new CSVPrinter(new StringBuilder(), CSVFormat.DEFAULT)) { + try (final CSVPrinter printer = new CSVPrinter(new StringBuilder(), createDefaultFormat())) { final Map map = recordWithHeader.toMap(); map.remove("OldColumn"); map.put("ZColumn", "NewValue"); @@ -151,10 +164,14 @@ public class CSVRecordTest { final ArrayList list = new ArrayList<>(map.values()); Collections.sort(list); printer.printRecord(list); - Assert.assertEquals("A,B,C,NewValue" + CSVFormat.DEFAULT.getRecordSeparator(), printer.getOut().toString()); + Assert.assertEquals("A,B,C,NewValue" + createDefaultFormat().getRecordSeparator(), printer.getOut().toString()); } } + protected CSVFormat createDefaultFormat() { + return CSVFormat.DEFAULT; + } + @Test public void testToMap() { final Map map = this.recordWithHeader.toMap(); @@ -163,22 +180,28 @@ public class CSVRecordTest { @Test public void testToMapWithShortRecord() throws Exception { - try (final CSVParser parser = CSVParser.parse("a,b", CSVFormat.DEFAULT.withHeader("A", "B", "C"))) { + try (final CSVParser parser = CSVParser.parse("a,b", createDefaultFormat().withHeader("A", "B", "C"))) { final CSVRecord shortRec = parser.iterator().next(); + validate(shortRec); shortRec.toMap(); } } @Test public void testToMapWithNoHeader() throws Exception { - try (final CSVParser parser = CSVParser.parse("a,b", CSVFormat.newFormat(','))) { + try (final CSVParser parser = CSVParser.parse("a,b", createCommaFormat())) { final CSVRecord shortRec = parser.iterator().next(); + validate(shortRec); final Map map = shortRec.toMap(); assertNotNull("Map is not null.", map); assertTrue("Map is empty.", map.isEmpty()); } } + protected CSVFormat createCommaFormat() { + return CSVFormat.newFormat(','); + } + private void validateMap(final Map map, final boolean allowsNulls) { assertTrue(map.containsKey("first")); assertTrue(map.containsKey("second"));