View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   https://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.commons.csv;
20  
21  import static org.junit.jupiter.api.Assertions.assertAll;
22  import static org.junit.jupiter.api.Assertions.assertArrayEquals;
23  import static org.junit.jupiter.api.Assertions.assertEquals;
24  import static org.junit.jupiter.api.Assertions.assertFalse;
25  import static org.junit.jupiter.api.Assertions.assertInstanceOf;
26  import static org.junit.jupiter.api.Assertions.assertNotNull;
27  import static org.junit.jupiter.api.Assertions.assertNull;
28  import static org.junit.jupiter.api.Assertions.assertThrows;
29  import static org.junit.jupiter.api.Assertions.assertTrue;
30  
31  import java.io.ByteArrayInputStream;
32  import java.io.ByteArrayOutputStream;
33  import java.io.IOException;
34  import java.io.ObjectInputStream;
35  import java.io.ObjectOutputStream;
36  import java.io.StringReader;
37  import java.util.ArrayList;
38  import java.util.List;
39  import java.util.Map;
40  import java.util.TreeMap;
41  import java.util.concurrent.ConcurrentHashMap;
42  import java.util.concurrent.atomic.AtomicInteger;
43  
44  import org.apache.commons.lang3.StringUtils;
45  import org.junit.jupiter.api.BeforeEach;
46  import org.junit.jupiter.api.Test;
47  
48  public class CSVRecordTest {
49  
50      private enum EnumFixture {
51          UNKNOWN_COLUMN
52      }
53  
54      /** This enum overrides toString() but it's the names that matter. */
55      public enum EnumHeader {
56          FIRST("first"), SECOND("second"), THIRD("third");
57  
58          private final String number;
59  
60          EnumHeader(final String number) {
61              this.number = number;
62          }
63  
64          @Override
65          public String toString() {
66              return number;
67          }
68      }
69  
70      private Map<String, Integer> headerMap;
71      private CSVRecord record;
72      private CSVRecord recordWithHeader;
73      private String[] values;
74  
75      @BeforeEach
76      public void setUp() throws Exception {
77          values = new String[] { "A", "B", "C" };
78          final String rowData = StringUtils.join(values, ',');
79          try (CSVParser parser = CSVFormat.DEFAULT.parse(new StringReader(rowData))) {
80              record = parser.iterator().next();
81          }
82          try (CSVParser parser = CSVFormat.DEFAULT.builder().setHeader(EnumHeader.class).get().parse(new StringReader(rowData))) {
83              recordWithHeader = parser.iterator().next();
84              headerMap = parser.getHeaderMap();
85          }
86      }
87  
88      @Test
89      public void testCSVRecordNULLValues() throws IOException {
90          try (CSVParser parser = CSVParser.parse("A,B\r\nONE,TWO", CSVFormat.DEFAULT.withHeader())) {
91              final CSVRecord csvRecord = new CSVRecord(parser, null, null, 0L, 0L, 0L);
92              assertEquals(0, csvRecord.size());
93              assertThrows(IllegalArgumentException.class, () -> csvRecord.get("B"));
94          }
95      }
96  
97      @Test
98      public void testDuplicateHeaderGet() throws IOException {
99          final String csv = "A,A,B,B\n1,2,5,6\n";
100         final CSVFormat format = CSVFormat.DEFAULT.builder().setHeader().get();
101 
102         try (CSVParser parser = CSVParser.parse(csv, format)) {
103             final CSVRecord record = parser.nextRecord();
104 
105             assertAll("Test that it gets the last instance of a column when there are duplicate headings",
106                 () -> assertEquals("2", record.get("A")),
107                 () -> assertEquals("6", record.get("B"))
108             );
109         }
110     }
111 
112     @Test
113     public void testDuplicateHeaderToMap() throws IOException {
114         final String csv = "A,A,B,B\n1,2,5,6\n";
115         final CSVFormat format = CSVFormat.DEFAULT.builder().setHeader().get();
116 
117         try (CSVParser parser = CSVParser.parse(csv, format)) {
118             final CSVRecord record = parser.nextRecord();
119             final Map<String, String> map = record.toMap();
120 
121             assertAll("Test that it gets the last instance of a column when there are duplicate headings",
122                 () -> assertEquals("2", map.get("A")),
123                 () -> assertEquals("6", map.get("B"))
124             );
125         }
126     }
127 
128     @Test
129     public void testGetInt() {
130         assertEquals(values[0], record.get(0));
131         assertEquals(values[1], record.get(1));
132         assertEquals(values[2], record.get(2));
133     }
134 
135     @Test
136     public void testGetNullEnum() {
137         assertThrows(IllegalArgumentException.class, () -> recordWithHeader.get((Enum<?>) null));
138     }
139 
140     @Test
141     public void testGetString() {
142         assertEquals(values[0], recordWithHeader.get(EnumHeader.FIRST.name()));
143         assertEquals(values[1], recordWithHeader.get(EnumHeader.SECOND.name()));
144         assertEquals(values[2], recordWithHeader.get(EnumHeader.THIRD.name()));
145     }
146 
147     @Test
148     public void testGetStringInconsistentRecord() {
149         headerMap.put("fourth", Integer.valueOf(4));
150         assertThrows(IllegalArgumentException.class, () -> recordWithHeader.get("fourth"));
151     }
152 
153     @Test
154     public void testGetStringNoHeader() {
155         assertThrows(IllegalStateException.class, () -> record.get("first"));
156     }
157 
158     @Test
159     public void testGetUnmappedEnum() {
160         assertThrows(IllegalArgumentException.class, () -> recordWithHeader.get(EnumFixture.UNKNOWN_COLUMN));
161     }
162 
163     @Test
164     public void testGetUnmappedName() {
165         assertThrows(IllegalArgumentException.class, () -> assertNull(recordWithHeader.get("fourth")));
166     }
167 
168     @Test
169     public void testGetUnmappedNegativeInt() {
170         assertThrows(ArrayIndexOutOfBoundsException.class, () -> recordWithHeader.get(Integer.MIN_VALUE));
171     }
172 
173     @Test
174     public void testGetUnmappedPositiveInt() {
175         assertThrows(ArrayIndexOutOfBoundsException.class, () -> recordWithHeader.get(Integer.MAX_VALUE));
176     }
177 
178     @Test
179     public void testGetWithEnum() {
180         assertEquals(recordWithHeader.get("FIRST"), recordWithHeader.get(EnumHeader.FIRST));
181         assertEquals(recordWithHeader.get("SECOND"), recordWithHeader.get(EnumHeader.SECOND));
182         assertThrows(IllegalArgumentException.class, () -> recordWithHeader.get(EnumFixture.UNKNOWN_COLUMN));
183     }
184 
185     @Test
186     public void testIsConsistent() {
187         assertTrue(record.isConsistent());
188         assertTrue(recordWithHeader.isConsistent());
189         final Map<String, Integer> map = recordWithHeader.getParser().getHeaderMap();
190         map.put("fourth", Integer.valueOf(4));
191         // We are working on a copy of the map, so the record should still be OK.
192         assertTrue(recordWithHeader.isConsistent());
193     }
194 
195     @Test
196     public void testIsInconsistent() throws IOException {
197         final String[] headers = { "first", "second", "third" };
198         final String rowData = StringUtils.join(values, ',');
199         try (CSVParser parser = CSVFormat.DEFAULT.withHeader(headers).parse(new StringReader(rowData))) {
200             final Map<String, Integer> map = parser.getHeaderMapRaw();
201             final CSVRecord record1 = parser.iterator().next();
202             map.put("fourth", Integer.valueOf(4));
203             assertFalse(record1.isConsistent());
204         }
205     }
206 
207     @Test
208     public void testIsMapped() {
209         assertFalse(record.isMapped("first"));
210         assertTrue(recordWithHeader.isMapped(EnumHeader.FIRST.name()));
211         assertFalse(recordWithHeader.isMapped("fourth"));
212     }
213 
214     @Test
215     public void testIsSetInt() {
216         assertFalse(record.isSet(-1));
217         assertTrue(record.isSet(0));
218         assertTrue(record.isSet(2));
219         assertFalse(record.isSet(3));
220         assertTrue(recordWithHeader.isSet(1));
221         assertFalse(recordWithHeader.isSet(1000));
222     }
223 
224     @Test
225     public void testIsSetString() {
226         assertFalse(record.isSet("first"));
227         assertTrue(recordWithHeader.isSet(EnumHeader.FIRST.name()));
228         assertFalse(recordWithHeader.isSet("DOES NOT EXIST"));
229     }
230 
231     @Test
232     public void testIterator() {
233         int i = 0;
234         for (final String value : record) {
235             assertEquals(values[i], value);
236             i++;
237         }
238     }
239 
240     @Test
241     public void testPutInMap() {
242         final Map<String, String> map = new ConcurrentHashMap<>();
243         this.recordWithHeader.putIn(map);
244         validateMap(map, false);
245         // Test that we can compile with assignment to the same map as the param.
246         final TreeMap<String, String> map2 = recordWithHeader.putIn(new TreeMap<>());
247         validateMap(map2, false);
248     }
249 
250     @Test
251     public void testRemoveAndAddColumns() throws IOException {
252         // do:
253         try (CSVPrinter printer = new CSVPrinter(new StringBuilder(), CSVFormat.DEFAULT)) {
254             final Map<String, String> map = recordWithHeader.toMap();
255             map.remove("OldColumn");
256             map.put("ZColumn", "NewValue");
257             // check:
258             final ArrayList<String> list = new ArrayList<>(map.values());
259             list.sort(null);
260             printer.printRecord(list);
261             assertEquals("A,B,C,NewValue" + CSVFormat.DEFAULT.getRecordSeparator(), printer.getOut().toString());
262         }
263     }
264 
265     @Test
266     public void testSerialization() throws IOException, ClassNotFoundException {
267         final CSVRecord shortRec;
268         try (CSVParser parser = CSVParser.parse("A,B\n#my comment\nOne,Two", CSVFormat.DEFAULT.withHeader().withCommentMarker('#'))) {
269             shortRec = parser.iterator().next();
270         }
271         final ByteArrayOutputStream out = new ByteArrayOutputStream();
272         try (ObjectOutputStream oos = new ObjectOutputStream(out)) {
273             oos.writeObject(shortRec);
274         }
275         final ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray());
276         try (ObjectInputStream ois = new ObjectInputStream(in)) {
277             final Object object = ois.readObject();
278             assertInstanceOf(CSVRecord.class, object);
279             final CSVRecord rec = (CSVRecord) object;
280             assertEquals(1L, rec.getRecordNumber());
281             assertEquals("One", rec.get(0));
282             assertEquals("Two", rec.get(1));
283             assertEquals(2, rec.size());
284             assertEquals(shortRec.getCharacterPosition(), rec.getCharacterPosition());
285             assertEquals("my comment", rec.getComment());
286             // The parser is not serialized
287             assertNull(rec.getParser());
288             // Check all header map functionality is absent
289             assertTrue(rec.isConsistent());
290             assertFalse(rec.isMapped("A"));
291             assertFalse(rec.isSet("A"));
292             assertEquals(0, rec.toMap().size());
293             // This will throw
294             assertThrows(IllegalStateException.class, () -> rec.get("A"));
295         }
296     }
297 
298     @Test
299     public void testStream() {
300         final AtomicInteger i = new AtomicInteger();
301         record.stream().forEach(value -> {
302             assertEquals(values[i.get()], value);
303             i.incrementAndGet();
304         });
305     }
306 
307     @Test
308     public void testToListAdd() {
309         final String[] expected = values.clone();
310         final List<String> list = record.toList();
311         list.add("Last");
312         assertEquals("Last", list.get(list.size() - 1));
313         assertEquals(list.size(), values.length + 1);
314         assertArrayEquals(expected, values);
315     }
316 
317     @Test
318     public void testToListFor() {
319         int i = 0;
320         for (final String value : record.toList()) {
321             assertEquals(values[i], value);
322             i++;
323         }
324     }
325 
326     @Test
327     public void testToListForEach() {
328         final AtomicInteger i = new AtomicInteger();
329         record.toList().forEach(e -> {
330             assertEquals(values[i.getAndIncrement()], e);
331         });
332     }
333 
334     @Test
335     public void testToListSet() {
336         final String[] expected = values.clone();
337         final List<String> list = record.toList();
338         list.set(list.size() - 1, "Last");
339         assertEquals("Last", list.get(list.size() - 1));
340         assertEquals(list.size(), values.length);
341         assertArrayEquals(expected, values);
342     }
343 
344     @Test
345     public void testToMap() {
346         final Map<String, String> map = this.recordWithHeader.toMap();
347         validateMap(map, true);
348     }
349 
350     @Test
351     public void testToMapWithNoHeader() throws Exception {
352         try (CSVParser parser = CSVParser.parse("a,b", CSVFormat.newFormat(','))) {
353             final CSVRecord shortRec = parser.iterator().next();
354             final Map<String, String> map = shortRec.toMap();
355             assertNotNull(map, "Map is not null.");
356             assertTrue(map.isEmpty(), "Map is empty.");
357         }
358     }
359 
360     @Test
361     public void testToMapWithShortRecord() throws Exception {
362         try (CSVParser parser = CSVParser.parse("a,b", CSVFormat.DEFAULT.withHeader("A", "B", "C"))) {
363             final CSVRecord shortRec = parser.iterator().next();
364             shortRec.toMap();
365         }
366     }
367 
368     @Test
369     public void testToString() {
370         assertNotNull(recordWithHeader.toString());
371         assertTrue(recordWithHeader.toString().contains("comment="));
372         assertTrue(recordWithHeader.toString().contains("recordNumber="));
373         assertTrue(recordWithHeader.toString().contains("values="));
374     }
375 
376     private void validateMap(final Map<String, String> map, final boolean allowsNulls) {
377         assertTrue(map.containsKey(EnumHeader.FIRST.name()));
378         assertTrue(map.containsKey(EnumHeader.SECOND.name()));
379         assertTrue(map.containsKey(EnumHeader.THIRD.name()));
380         assertFalse(map.containsKey("fourth"));
381         if (allowsNulls) {
382             assertFalse(map.containsKey(null));
383         }
384         assertEquals("A", map.get(EnumHeader.FIRST.name()));
385         assertEquals("B", map.get(EnumHeader.SECOND.name()));
386         assertEquals("C", map.get(EnumHeader.THIRD.name()));
387         assertNull(map.get("fourth"));
388     }
389 }