diff --git a/src/main/java/org/apache/commons/csv/CSVParser.java b/src/main/java/org/apache/commons/csv/CSVParser.java index 9b96f7c7f..ae2a027e0 100644 --- a/src/main/java/org/apache/commons/csv/CSVParser.java +++ b/src/main/java/org/apache/commons/csv/CSVParser.java @@ -282,11 +282,29 @@ private static final class Headers { /** * Header names in column order */ - final List headerNames; + final List headerNames = new ArrayList<>(); - Headers(final Map headerMap, final List headerNames) { - this.headerMap = headerMap; - this.headerNames = headerNames; + Headers(final CSVFormat format) { + this.headerMap = createEmptyHeaderMap(format); + } + + private Map createEmptyHeaderMap(final CSVFormat format) { + return format.getIgnoreHeaderCase() ? + new TreeMap<>(String.CASE_INSENSITIVE_ORDER) : + new LinkedHashMap<>(); + } + + private boolean contains(final String name) { + return this.headerMap.containsKey(name); + } + + private void put(final String name, final Integer index) { + this.headerMap.put(name, index); + this.headerNames.add(name); + } + + public boolean isEmpty() { + return this.headerMap.isEmpty(); } } @@ -598,28 +616,10 @@ private Map createEmptyHeaderMap() { * @throws CSVException on invalid input. */ private Headers createHeaders() throws IOException { - Map headerMap = null; - List headerNames = null; + final Headers headers = new Headers(format); final String[] formatHeader = format.getHeader(); if (formatHeader != null) { - headerMap = createEmptyHeaderMap(); - String[] headerRecord = null; - if (formatHeader.length == 0) { - // read the header from the first line of the file - final CSVRecord nextRecord = nextRecord(); - if (nextRecord != null) { - headerRecord = nextRecord.values(); - headerComment = nextRecord.getComment(); - } - } else { - if (format.getSkipHeaderRecord()) { - final CSVRecord nextRecord = nextRecord(); - if (nextRecord != null) { - headerComment = nextRecord.getComment(); - } - } - headerRecord = formatHeader; - } + final String[] headerRecord = getHeaderRecord(formatHeader); // build the name to index mappings if (headerRecord != null) { // Track an occurrence of a null, empty or blank header. @@ -630,7 +630,7 @@ private Headers createHeaders() throws IOException { if (blankHeader && !format.getAllowMissingColumnNames()) { throw new IllegalArgumentException("A header name is missing in " + Arrays.toString(headerRecord)); } - final boolean containsHeader = blankHeader ? observedMissing : headerMap.containsKey(header); + final boolean containsHeader = blankHeader ? observedMissing : headers.contains(header); final DuplicateHeaderMode headerMode = format.getDuplicateHeaderMode(); final boolean duplicatesAllowed = headerMode == DuplicateHeaderMode.ALLOW_ALL; final boolean emptyDuplicatesAllowed = headerMode == DuplicateHeaderMode.ALLOW_EMPTY; @@ -641,17 +641,41 @@ private Headers createHeaders() throws IOException { } observedMissing |= blankHeader; if (header != null) { - headerMap.put(header, Integer.valueOf(i)); // Explicit (un)boxing is intentional - if (headerNames == null) { - headerNames = new ArrayList<>(headerRecord.length); - } - headerNames.add(header); + headers.put(header, Integer.valueOf(i)); // Explicit (un)boxing is intentional } } } } - // Make header names Collection immutable - return new Headers(headerMap, headerNames == null ? Collections.emptyList() : Collections.unmodifiableList(headerNames)); + return headers; + } + + /** + * Creates header record from the format. If the format does not contain header record, + * the header record from the first line of the file. + * + * @return the header record from the format. + * If the format does not contain header record, the header record from the first line of the file + * @throws IOException if there is a problem reading the header or skipping the first record + */ + private String[] getHeaderRecord(final String[] formatHeader) throws IOException { + String[] headerRecord = null; + if (formatHeader.length == 0) { + // read the header from the first line of the file + final CSVRecord nextRecord = nextRecord(); + if (nextRecord != null) { + headerRecord = nextRecord.values(); + headerComment = nextRecord.getComment(); + } + } else { + if (format.getSkipHeaderRecord()) { + final CSVRecord nextRecord = nextRecord(); + if (nextRecord != null) { + headerComment = nextRecord.getComment(); + } + } + headerRecord = formatHeader; + } + return headerRecord; } /** @@ -702,7 +726,7 @@ public String getHeaderComment() { * @return a copy of the header map. */ public Map getHeaderMap() { - if (headers.headerMap == null) { + if (headers.isEmpty()) { return null; } final Map map = createEmptyHeaderMap(); diff --git a/src/main/java/org/apache/commons/csv/CSVRecord.java b/src/main/java/org/apache/commons/csv/CSVRecord.java index 148c76ca8..d1f780039 100644 --- a/src/main/java/org/apache/commons/csv/CSVRecord.java +++ b/src/main/java/org/apache/commons/csv/CSVRecord.java @@ -124,7 +124,7 @@ public String get(final int i) { */ public String get(final String name) { final Map headerMap = getHeaderMapRaw(); - if (headerMap == null) { + if (headerMap == null || headerMap.isEmpty()) { throw new IllegalStateException( "No header mapping was specified, the record values can't be accessed by name"); } @@ -233,7 +233,7 @@ public boolean hasComment() { */ public boolean isConsistent() { final Map headerMap = getHeaderMapRaw(); - return headerMap == null || headerMap.size() == values.length; + return headerMap == null || headerMap.isEmpty() || headerMap.size() == values.length; } /** diff --git a/src/test/java/org/apache/commons/csv/CSVParserTest.java b/src/test/java/org/apache/commons/csv/CSVParserTest.java index 1d12357ee..94bc8ef93 100644 --- a/src/test/java/org/apache/commons/csv/CSVParserTest.java +++ b/src/test/java/org/apache/commons/csv/CSVParserTest.java @@ -729,6 +729,30 @@ void testGetHeaderMap() throws Exception { } } + @Test + void testGetHeaderMapWithIgnoreHeaderCase() throws Exception { + try (CSVParser parser = CSVParser.parse("a,b,c,A,B,C\n1,2,3,1,2,3\nx,y,z,x,y,z", CSVFormat.DEFAULT.withHeader("a", "b", "c").withIgnoreHeaderCase())) { + final Map headerMap = parser.getHeaderMap(); + final Iterator columnNames = headerMap.keySet().iterator(); + // Headers are iterated in column order. + assertEquals("a", columnNames.next()); + assertEquals("b", columnNames.next()); + assertEquals("c", columnNames.next()); + final Iterator records = parser.iterator(); + + // Parse to make sure getHeaderMap did not have a side-effect. + for (int i = 0; i < 3; i++) { + assertTrue(records.hasNext()); + final CSVRecord record = records.next(); + assertEquals(record.get(0), record.get("a")); + assertEquals(record.get(1), record.get("b")); + assertEquals(record.get(2), record.get("c")); + } + + assertFalse(records.hasNext()); + } + } + @Test void testGetHeaderNames() throws IOException { try (CSVParser parser = CSVParser.parse("a,b,c\n1,2,3\nx,y,z", CSVFormat.DEFAULT.withHeader("A", "B", "C"))) {