View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *      http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  
18  package org.apache.commons.csv;
19  
20  import java.io.Serializable;
21  import java.util.Arrays;
22  import java.util.HashMap;
23  import java.util.Iterator;
24  import java.util.List;
25  import java.util.Map;
26  import java.util.Map.Entry;
27  
28  /**
29   * A CSV record parsed from a CSV file.
30   */
31  public final class CSVRecord implements Serializable, Iterable<String> {
32  
33      private static final String[] EMPTY_STRING_ARRAY = new String[0];
34  
35      private static final long serialVersionUID = 1L;
36  
37      private final long characterPosition;
38  
39      /** The accumulated comments (if any) */
40      private final String comment;
41  
42      /** The column name to index mapping. */
43      private final Map<String, Integer> mapping;
44  
45      /** The record number. */
46      private final long recordNumber;
47  
48      /** The values of the record */
49      private final String[] values;
50  
51      CSVRecord(final String[] values, final Map<String, Integer> mapping, final String comment, final long recordNumber,
52              final long characterPosition) {
53          this.recordNumber = recordNumber;
54          this.values = values != null ? values : EMPTY_STRING_ARRAY;
55          this.mapping = mapping;
56          this.comment = comment;
57          this.characterPosition = characterPosition;
58      }
59  
60      /**
61       * Returns a value by {@link Enum}.
62       *
63       * @param e
64       *            an enum
65       * @return the String at the given enum String
66       */
67      public String get(final Enum<?> e) {
68          return get(e.toString());
69      }
70  
71      /**
72       * Returns a value by index.
73       *
74       * @param i
75       *            a column index (0-based)
76       * @return the String at the given index
77       */
78      public String get(final int i) {
79          return values[i];
80      }
81  
82      /**
83       * Returns a value by name.
84       *
85       * @param name
86       *            the name of the column to be retrieved.
87       * @return the column value, maybe null depending on {@link CSVFormat#getNullString()}.
88       * @throws IllegalStateException
89       *             if no header mapping was provided
90       * @throws IllegalArgumentException
91       *             if {@code name} is not mapped or if the record is inconsistent
92       * @see #isConsistent()
93       * @see CSVFormat#withNullString(String)
94       */
95      public String get(final String name) {
96          if (mapping == null) {
97              throw new IllegalStateException(
98                  "No header mapping was specified, the record values can't be accessed by name");
99          }
100         final Integer index = mapping.get(name);
101         if (index == null) {
102             throw new IllegalArgumentException(String.format("Mapping for %s not found, expected one of %s", name,
103                 mapping.keySet()));
104         }
105         try {
106             return values[index.intValue()];
107         } catch (final ArrayIndexOutOfBoundsException e) {
108             throw new IllegalArgumentException(String.format(
109                 "Index for header '%s' is %d but CSVRecord only has %d values!", name, index,
110                 Integer.valueOf(values.length)));
111         }
112     }
113 
114     /**
115      * Returns the start position of this record as a character position in the source stream. This may or may not
116      * correspond to the byte position depending on the character set.
117      *
118      * @return the position of this record in the source stream.
119      */
120     public long getCharacterPosition() {
121         return characterPosition;
122     }
123 
124     /**
125      * Returns the comment for this record, if any.
126      * Note that comments are attached to the following record.
127      * If there is no following record (i.e. the comment is at EOF)
128      * the comment will be ignored.
129      *
130      * @return the comment for this record, or null if no comment for this record is available.
131      */
132     public String getComment() {
133         return comment;
134     }
135 
136     /**
137      * Returns the number of this record in the parsed CSV file.
138      *
139      * <p>
140      * <strong>ATTENTION:</strong> If your CSV input has multi-line values, the returned number does not correspond to
141      * the current line number of the parser that created this record.
142      * </p>
143      *
144      * @return the number of this record.
145      * @see CSVParser#getCurrentLineNumber()
146      */
147     public long getRecordNumber() {
148         return recordNumber;
149     }
150 
151     /**
152      * Tells whether the record size matches the header size.
153      *
154      * <p>
155      * Returns true if the sizes for this record match and false if not. Some programs can export files that fail this
156      * test but still produce parsable files.
157      * </p>
158      *
159      * @return true of this record is valid, false if not
160      */
161     public boolean isConsistent() {
162         return mapping == null || mapping.size() == values.length;
163     }
164 
165     /**
166      * Checks whether this record has a comment, false otherwise.
167      * Note that comments are attached to the following record.
168      * If there is no following record (i.e. the comment is at EOF)
169      * the comment will be ignored.
170      *
171      * @return true if this record has a comment, false otherwise
172      * @since 1.3
173      */
174     public boolean hasComment() {
175         return comment != null;
176     }
177 
178     /**
179      * Checks whether a given column is mapped, i.e. its name has been defined to the parser.
180      *
181      * @param name
182      *            the name of the column to be retrieved.
183      * @return whether a given column is mapped.
184      */
185     public boolean isMapped(final String name) {
186         return mapping != null && mapping.containsKey(name);
187     }
188 
189     /**
190      * Checks whether a given columns is mapped and has a value.
191      *
192      * @param name
193      *            the name of the column to be retrieved.
194      * @return whether a given columns is mapped and has a value
195      */
196     public boolean isSet(final String name) {
197         return isMapped(name) && mapping.get(name).intValue() < values.length;
198     }
199 
200     /**
201      * Returns an iterator over the values of this record.
202      *
203      * @return an iterator over the values of this record.
204      */
205     @Override
206     public Iterator<String> iterator() {
207         return toList().iterator();
208     }
209 
210     /**
211      * Puts all values of this record into the given Map.
212      *
213      * @param map
214      *            The Map to populate.
215      * @return the given map.
216      */
217     <M extends Map<String, String>> M putIn(final M map) {
218         if (mapping == null) {
219             return map;
220         }
221         for (final Entry<String, Integer> entry : mapping.entrySet()) {
222             final int col = entry.getValue().intValue();
223             if (col < values.length) {
224                 map.put(entry.getKey(), values[col]);
225             }
226         }
227         return map;
228     }
229 
230     /**
231      * Returns the number of values in this record.
232      *
233      * @return the number of values.
234      */
235     public int size() {
236         return values.length;
237     }
238 
239     /**
240      * Converts the values to a List.
241      *
242      * TODO: Maybe make this public?
243      *
244      * @return a new List
245      */
246     private List<String> toList() {
247         return Arrays.asList(values);
248     }
249 
250     /**
251      * Copies this record into a new Map. The new map is not connect
252      *
253      * @return A new Map. The map is empty if the record has no headers.
254      */
255     public Map<String, String> toMap() {
256         return putIn(new HashMap<String, String>(values.length));
257     }
258 
259     /**
260      * Returns a string representation of the contents of this record. The result is constructed by comment, mapping,
261      * recordNumber and by passing the internal values array to {@link Arrays#toString(Object[])}.
262      *
263      * @return a String representation of this record.
264      */
265     @Override
266     public String toString() {
267         return "CSVRecord [comment=" + comment + ", mapping=" + mapping +
268                 ", recordNumber=" + recordNumber + ", values=" +
269                 Arrays.toString(values) + "]";
270     }
271 
272     String[] values() {
273         return values;
274     }
275 
276 }