Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/changes/changes.xml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
<body>
<release version="1.15.0" date="YYYY-MM-DD" description="This is a feature and maintenance release. Java 8 or later is required.">
<!-- FIX -->
<action type="fix" issue="CSV-330" due-to="Aryan Ganjoo">CSVRecord.isConsistent() compares against the number of header columns when duplicate or empty header names are allowed.</action>
<action type="fix" issue="CSV-320" dev="aherbert" due-to="Jan Burkhardt">Remove Spotbugs dependency and use exclude-filter instead #564.</action>
<action type="fix" dev="ggregory" due-to="Cassio Santos">Remove broken website link #577.</action>
<action type="fix" dev="ggregory" due-to="Gary Gregory">Fix Apache RAT plugin console warnings.</action>
Expand Down
21 changes: 19 additions & 2 deletions src/main/java/org/apache/commons/csv/CSVParser.java
Original file line number Diff line number Diff line change
Expand Up @@ -308,9 +308,15 @@ private static final class Headers {
*/
final List<String> headerNames;

Headers(final Map<String, Integer> headerMap, final List<String> headerNames) {
/**
* Number of header columns, including duplicate and null names that do not add a distinct entry to {@link #headerMap}.
*/
final int headerCount;

Headers(final Map<String, Integer> headerMap, final List<String> headerNames, final int headerCount) {
this.headerMap = headerMap;
this.headerNames = headerNames;
this.headerCount = headerCount;
}
}

Expand Down Expand Up @@ -628,6 +634,7 @@ private Map<String, Integer> createEmptyHeaderMap() {
private Headers createHeaders() throws IOException {
Map<String, Integer> headerMap = null;
List<String> headerNames = null;
int headerCount = 0;
final String[] formatHeader = format.getHeader();
if (formatHeader != null) {
headerMap = createEmptyHeaderMap();
Expand All @@ -650,6 +657,7 @@ private Headers createHeaders() throws IOException {
}
// build the name to index mappings
if (headerRecord != null) {
headerCount = headerRecord.length;
// Track an occurrence of a null, empty or blank header.
boolean observedMissing = false;
for (int i = 0; i < headerRecord.length; i++) {
Expand Down Expand Up @@ -679,7 +687,7 @@ private Headers createHeaders() throws IOException {
}
}
// Make header names Collection immutable
return new Headers(headerMap, headerNames == null ? Collections.emptyList() : Collections.unmodifiableList(headerNames));
return new Headers(headerMap, headerNames == null ? Collections.emptyList() : Collections.unmodifiableList(headerNames), headerCount);
}

/**
Expand Down Expand Up @@ -747,6 +755,15 @@ Map<String, Integer> getHeaderMapRaw() {
return headers.headerMap;
}

/**
* Gets the number of header columns, which can exceed the header map size when duplicate or null names are allowed.
*
* @return The number of header columns.
*/
int getHeaderCount() {
return headers.headerCount;
}

/**
* Gets a read-only list of header names that iterates in column order as defined in the CSVFormat's header.
* <p>
Expand Down
5 changes: 3 additions & 2 deletions src/main/java/org/apache/commons/csv/CSVRecord.java
Original file line number Diff line number Diff line change
Expand Up @@ -235,8 +235,9 @@ public boolean hasComment() {
* @return true of this record is valid, false if not.
*/
public boolean isConsistent() {
final Map<String, Integer> headerMap = getHeaderMapRaw();
return headerMap == null || headerMap.size() == values.length;
// Compare with the header column count, not the header map size: duplicate or null header names allowed by
// DuplicateHeaderMode or allowMissingColumnNames share or lack a map entry.
return getHeaderMapRaw() == null || parser.getHeaderCount() == values.length;
}

/**
Expand Down
31 changes: 28 additions & 3 deletions src/test/java/org/apache/commons/csv/CSVRecordTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -194,14 +194,39 @@ void testIsConsistent() {
assertTrue(recordWithHeader.isConsistent());
}

/**
* A record whose size matches the header column count is consistent even when duplicate header names share a
* single header map entry.
*/
@Test
void testIsConsistentWithDuplicateHeaderNames() throws IOException {
final CSVFormat format = CSVFormat.DEFAULT.builder().setHeader().setDuplicateHeaderMode(DuplicateHeaderMode.ALLOW_ALL).get();
try (CSVParser parser = format.parse(new StringReader("a,b,a\n1,2,3\n1,2\n"))) {
final List<CSVRecord> records = parser.getRecords();
assertTrue(records.get(0).isConsistent());
assertFalse(records.get(1).isConsistent());
}
}

/**
* A record whose size matches the header column count is consistent even when empty header names share a single
* header map entry.
*/
@Test
void testIsConsistentWithDuplicateEmptyHeaderNames() throws IOException {
final CSVFormat format = CSVFormat.DEFAULT.builder().setHeader().setAllowMissingColumnNames(true)
.setDuplicateHeaderMode(DuplicateHeaderMode.ALLOW_EMPTY).get();
try (CSVParser parser = format.parse(new StringReader("a,,b,\n1,2,3,4\n"))) {
assertTrue(parser.nextRecord().isConsistent());
}
}

@Test
void testIsInconsistent() throws IOException {
final String[] headers = { "first", "second", "third" };
final String[] headers = { "first", "second", "third", "fourth" };
final String rowData = StringUtils.join(values, ',');
try (CSVParser parser = CSVFormat.DEFAULT.withHeader(headers).parse(new StringReader(rowData))) {
final Map<String, Integer> map = parser.getHeaderMapRaw();
final CSVRecord record1 = parser.iterator().next();
map.put("fourth", Integer.valueOf(4));
assertFalse(record1.isConsistent());
}
}
Expand Down