1 package com.opencsv;
2
3 import com.opencsv.exceptions.CsvValidationException;
4 import com.opencsv.processor.RowProcessor;
5 import com.opencsv.validators.LineValidatorAggregator;
6 import com.opencsv.validators.RowValidatorAggregator;
7
8 import java.io.IOException;
9 import java.io.Reader;
10 import java.util.HashMap;
11 import java.util.Locale;
12 import java.util.Map;
13 import java.util.ResourceBundle;
14
15 /**
16 * Handy reader when there's insufficient motivation to use the bean binding but
17 * the header mapping is still desired.
18 *
19 * @author Andre Rosot
20 * @since 4.2
21 */
22 public class CSVReaderHeaderAware extends CSVReader {
23
24 private final Map<String, Integer> headerIndex = new HashMap<>();
25
26 /**
27 * Constructor with supplied reader.
28 *
29 * @param reader The reader to an underlying CSV source
30 * @throws IOException If there is an error when reading the header
31 */
32 public CSVReaderHeaderAware(Reader reader) throws IOException {
33 super(reader);
34 initializeHeader();
35 }
36
37 /**
38 * Supports non-deprecated constructor from the parent class.
39 * Like the CSVReader this constructor is package scope so only the builder can use it.
40 *
41 * @param reader The reader to an underlying CSV source
42 * @param skipLines The number of lines to skip before reading
43 * @param parser The parser to use to parse input
44 * @param keepCR True to keep carriage returns in data read, false otherwise
45 * @param verifyReader True to verify reader before each read, false otherwise
46 * @param multilineLimit Allow the user to define the limit to the number of lines in a multiline record. Less than one means no limit.
47 * @param errorLocale Set the locale for error messages. If null, the default locale is used.
48 * @param lineValidatorAggregator contains all the custom defined line validators.
49 * @param rowValidatorAggregator contains all the custom defined row validators.
50 * @param rowProcessor Custom row processor to run on all columns on a csv record.
51 * @throws IOException If bad things happen while initializing the header
52 */
53 CSVReaderHeaderAware(Reader reader, int skipLines, ICSVParser parser, boolean keepCR, boolean verifyReader,
54 int multilineLimit, Locale errorLocale, LineValidatorAggregator lineValidatorAggregator,
55 RowValidatorAggregator rowValidatorAggregator, RowProcessor rowProcessor) throws IOException {
56 super(reader, skipLines, parser, keepCR, verifyReader, multilineLimit, errorLocale, lineValidatorAggregator, rowValidatorAggregator, rowProcessor);
57 initializeHeader();
58 }
59
60 /**
61 * Retrieves a specific data element from a line based on the value of the header.
62 *
63 * @param headerNames Name of the header element whose data we are trying to find
64 * @return The data element whose position matches that of the header whose value is passed in. Will return null when there are no more data elements.
65 * @throws IOException An error occured during the read or there is a mismatch in the number of data items in a row
66 * and the number of header items
67 * @throws IllegalArgumentException If headerName does not exist
68 * @throws CsvValidationException If a custom defined validator fails.
69 */
70 public String[] readNext(String... headerNames) throws IOException, CsvValidationException {
71 if (headerNames == null) {
72 return super.readNextSilently();
73 }
74
75 String[] strings = readNext();
76 if (strings == null) {
77 return null;
78 }
79
80 if (strings.length != headerIndex.size()) {
81 throw new IOException(String.format(
82 ResourceBundle.getBundle(ICSVParser.DEFAULT_BUNDLE_NAME, errorLocale)
83 .getString("header.data.mismatch.with.line.number"),
84 getRecordsRead(), headerIndex.size(), strings.length));
85 }
86
87 String[] response = new String[headerNames.length];
88
89 for (int i = 0; i < headerNames.length; i++) {
90 String headerName = headerNames[i];
91
92 Integer index = headerIndex.get(headerName);
93 if (index == null) {
94 throw new IllegalArgumentException(String.format(
95 ResourceBundle.getBundle(ICSVParser.DEFAULT_BUNDLE_NAME, errorLocale)
96 .getString("header.nonexistant"),
97 headerName));
98 }
99
100 response[i] = strings[index];
101 }
102 return response;
103 }
104
105 /**
106 * Reads the next line and returns a map of header values and data values.
107 *
108 * @return A map whose key is the header row of the data file and the values is the data values. Or null if the line is blank.
109 * @throws IOException An error occurred during the read or there is a mismatch in the number of data items in a row
110 * and the number of header items.
111 * @throws CsvValidationException If a custom defined validator fails.
112 */
113 public Map<String, String> readMap() throws IOException, CsvValidationException {
114 String[] strings = readNext();
115 if (strings == null) {
116 return null;
117 }
118 if (strings.length != headerIndex.size()) {
119 throw new IOException(String.format(
120 ResourceBundle.getBundle(ICSVParser.DEFAULT_BUNDLE_NAME, errorLocale)
121 .getString("header.data.mismatch.with.line.number"),
122 getRecordsRead(), headerIndex.size(), strings.length));
123 }
124
125 // This code cannot be done with a stream and Collectors.toMap()
126 // because Map.merge() does not play well with null values. Some
127 // implementations throw a NullPointerException, others simply remove
128 // the key from the map.
129 Map<String, String> resultMap = new HashMap<>(headerIndex.size()*2);
130 for(Map.Entry<String, Integer> entry : headerIndex.entrySet()) {
131 if(entry.getValue() < strings.length) {
132 resultMap.put(entry.getKey(), strings[entry.getValue()]);
133 }
134 }
135 return resultMap;
136 }
137
138 private void initializeHeader() throws IOException {
139 String[] headers = super.readNextSilently();
140 for (int i = 0; i < headers.length; i++) {
141 headerIndex.put(headers[i], i);
142 }
143 }
144
145 }