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