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.Iterator; 23 import java.util.LinkedHashMap; 24 import java.util.List; 25 import java.util.Map; 26 import java.util.stream.Collectors; 27 import java.util.stream.Stream; 28 29 /** 30 * A CSV record parsed from a CSV file. 31 * 32 * <p> 33 * Note: Support for {@link Serializable} is scheduled to be removed in version 2.0. 34 * In version 1.8 the mapping between the column header and the column index was 35 * removed from the serialized state. The class maintains serialization compatibility 36 * with versions pre-1.8 for the record values; these must be accessed by index 37 * following deserialization. There will be a loss of any functionally linked to the header 38 * mapping when transferring serialized forms pre-1.8 to 1.8 and vice versa. 39 * </p> 40 */ 41 public final class CSVRecord implements Serializable, Iterable<String> { 42 43 private static final long serialVersionUID = 1L; 44 45 /** 46 * The start position of this record as a character position in the source stream. This may or may not correspond to the byte position depending on the 47 * character set. 48 */ 49 private final long characterPosition; 50 51 /** The accumulated comments (if any) */ 52 private final String comment; 53 54 /** The record number. */ 55 private final long recordNumber; 56 57 /** The values of the record */ 58 private final String[] values; 59 60 /** The parser that originates this record. This is not serialized. */ 61 private final transient CSVParser parser; 62 63 CSVRecord(final CSVParser parser, final String[] values, final String comment, final long recordNumber, 64 final long characterPosition) { 65 this.recordNumber = recordNumber; 66 this.values = values != null ? values : Constants.EMPTY_STRING_ARRAY; 67 this.parser = parser; 68 this.comment = comment; 69 this.characterPosition = characterPosition; 70 } 71 72 /** 73 * Returns a value by {@link Enum}. 74 * 75 * @param e 76 * an enum 77 * @return the String at the given enum String 78 */ 79 public String get(final Enum<?> e) { 80 return get(e == null ? null : e.name()); 81 } 82 83 /** 84 * Returns a value by index. 85 * 86 * @param i 87 * a column index (0-based) 88 * @return the String at the given index 89 */ 90 public String get(final int i) { 91 return values[i]; 92 } 93 94 /** 95 * Returns a value by name. If multiple instances of the header name exists, only the last occurrence is returned. 96 * 97 * <p> 98 * Note: This requires a field mapping obtained from the original parser. 99 * A check using {@link #isMapped(String)} should be used to determine if a 100 * mapping exists from the provided {@code name} to a field index. In this case an 101 * exception will only be thrown if the record does not contain a field corresponding 102 * to the mapping, that is the record length is not consistent with the mapping size. 103 * </p> 104 * 105 * @param name 106 * the name of the column to be retrieved. 107 * @return the column value, maybe null depending on {@link CSVFormat#getNullString()}. 108 * @throws IllegalStateException 109 * if no header mapping was provided 110 * @throws IllegalArgumentException 111 * if {@code name} is not mapped or if the record is inconsistent 112 * @see #isMapped(String) 113 * @see #isConsistent() 114 * @see #getParser() 115 * @see CSVFormat.Builder#setNullString(String) 116 */ 117 public String get(final String name) { 118 final Map<String, Integer> headerMap = getHeaderMapRaw(); 119 if (headerMap == null) { 120 throw new IllegalStateException( 121 "No header mapping was specified, the record values can't be accessed by name"); 122 } 123 final Integer index = headerMap.get(name); 124 if (index == null) { 125 throw new IllegalArgumentException(String.format("Mapping for %s not found, expected one of %s", name, 126 headerMap.keySet())); 127 } 128 try { 129 return values[index.intValue()]; // N.B. Explicit (un)boxing is intentional 130 } catch (final ArrayIndexOutOfBoundsException e) { 131 throw new IllegalArgumentException(String.format( 132 "Index for header '%s' is %d but CSVRecord only has %d values!", name, index, 133 Integer.valueOf(values.length))); // N.B. Explicit (un)boxing is intentional 134 } 135 } 136 137 /** 138 * Returns the start position of this record as a character position in the source stream. This may or may not 139 * correspond to the byte position depending on the character set. 140 * 141 * @return the position of this record in the source stream. 142 */ 143 public long getCharacterPosition() { 144 return characterPosition; 145 } 146 147 /** 148 * Returns the comment for this record, if any. 149 * Note that comments are attached to the following record. 150 * If there is no following record (i.e. the comment is at EOF), 151 * then the comment will be ignored. 152 * 153 * @return the comment for this record, or null if no comment for this record is available. 154 */ 155 public String getComment() { 156 return comment; 157 } 158 159 private Map<String, Integer> getHeaderMapRaw() { 160 return parser == null ? null : parser.getHeaderMapRaw(); 161 } 162 163 /** 164 * Returns the parser. 165 * 166 * <p> 167 * Note: The parser is not part of the serialized state of the record. A null check 168 * should be used when the record may have originated from a serialized form. 169 * </p> 170 * 171 * @return the parser. 172 * @since 1.7 173 */ 174 public CSVParser getParser() { 175 return parser; 176 } 177 178 /** 179 * Returns the number of this record in the parsed CSV file. 180 * 181 * <p> 182 * <strong>ATTENTION:</strong> If your CSV input has multi-line values, the returned number does not correspond to 183 * the current line number of the parser that created this record. 184 * </p> 185 * 186 * @return the number of this record. 187 * @see CSVParser#getCurrentLineNumber() 188 */ 189 public long getRecordNumber() { 190 return recordNumber; 191 } 192 193 /** 194 * Checks whether this record has a comment, false otherwise. 195 * Note that comments are attached to the following record. 196 * If there is no following record (i.e. the comment is at EOF), 197 * then the comment will be ignored. 198 * 199 * @return true if this record has a comment, false otherwise 200 * @since 1.3 201 */ 202 public boolean hasComment() { 203 return comment != null; 204 } 205 206 /** 207 * Tells whether the record size matches the header size. 208 * 209 * <p> 210 * Returns true if the sizes for this record match and false if not. Some programs can export files that fail this 211 * test but still produce parsable files. 212 * </p> 213 * 214 * @return true of this record is valid, false if not 215 */ 216 public boolean isConsistent() { 217 final Map<String, Integer> headerMap = getHeaderMapRaw(); 218 return headerMap == null || headerMap.size() == values.length; 219 } 220 221 /** 222 * Checks whether a given column is mapped, i.e. its name has been defined to the parser. 223 * 224 * @param name 225 * the name of the column to be retrieved. 226 * @return whether a given column is mapped. 227 */ 228 public boolean isMapped(final String name) { 229 final Map<String, Integer> headerMap = getHeaderMapRaw(); 230 return headerMap != null && headerMap.containsKey(name); 231 } 232 233 /** 234 * Checks whether a column with a given index has a value. 235 * 236 * @param index 237 * a column index (0-based) 238 * @return whether a column with a given index has a value 239 */ 240 public boolean isSet(final int index) { 241 return 0 <= index && index < values.length; 242 } 243 244 /** 245 * Checks whether a given column is mapped and has a value. 246 * 247 * @param name 248 * the name of the column to be retrieved. 249 * @return whether a given column is mapped and has a value 250 */ 251 public boolean isSet(final String name) { 252 return isMapped(name) && getHeaderMapRaw().get(name).intValue() < values.length; // N.B. Explicit (un)boxing is intentional 253 } 254 255 /** 256 * Returns an iterator over the values of this record. 257 * 258 * @return an iterator over the values of this record. 259 */ 260 @Override 261 public Iterator<String> iterator() { 262 return toList().iterator(); 263 } 264 265 /** 266 * Puts all values of this record into the given Map. 267 * 268 * @param <M> the map type 269 * @param map The Map to populate. 270 * @return the given map. 271 * @since 1.9.0 272 */ 273 public <M extends Map<String, String>> M putIn(final M map) { 274 if (getHeaderMapRaw() == null) { 275 return map; 276 } 277 getHeaderMapRaw().forEach((key, value) -> { 278 if (value < values.length) { 279 map.put(key, values[value]); 280 } 281 }); 282 return map; 283 } 284 285 /** 286 * Returns the number of values in this record. 287 * 288 * @return the number of values. 289 */ 290 public int size() { 291 return values.length; 292 } 293 294 /** 295 * Returns a sequential ordered stream whose elements are the values. 296 * 297 * @return the new stream. 298 * @since 1.9.0 299 */ 300 public Stream<String> stream() { 301 return Stream.of(values); 302 } 303 304 /** 305 * Converts the values to a new List. 306 * <p> 307 * Editing the list does not update this instance. 308 * </p> 309 * 310 * @return a new List 311 * @since 1.9.0 312 */ 313 public List<String> toList() { 314 return stream().collect(Collectors.toList()); 315 } 316 317 /** 318 * Copies this record into a new Map of header name to record value. If multiple instances of a header name exist, 319 * then only the last occurrence is mapped. 320 * 321 * <p> 322 * Editing the map does not update this instance. 323 * </p> 324 * 325 * @return A new Map. The map is empty if the record has no headers. 326 */ 327 public Map<String, String> toMap() { 328 return putIn(new LinkedHashMap<>(values.length)); 329 } 330 331 /** 332 * Returns a string representation of the contents of this record. The result is constructed by comment, mapping, 333 * recordNumber and by passing the internal values array to {@link Arrays#toString(Object[])}. 334 * 335 * @return a String representation of this record. 336 */ 337 @Override 338 public String toString() { 339 return "CSVRecord [comment='" + comment + "', recordNumber=" + recordNumber + ", values=" + 340 Arrays.toString(values) + "]"; 341 } 342 343 /** 344 * Gets the values for this record. This is not a copy. 345 * 346 * @return the values for this record. 347 * @since 1.10.0 348 */ 349 public String[] values() { 350 return values; 351 } 352 353 }