001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017 018package org.apache.commons.csv; 019 020import java.io.Serializable; 021import java.util.Arrays; 022import java.util.Iterator; 023import java.util.LinkedHashMap; 024import java.util.List; 025import java.util.Map; 026import java.util.stream.Collectors; 027import java.util.stream.Stream; 028 029/** 030 * A CSV record parsed from a CSV file. 031 * 032 * <p> 033 * Note: Support for {@link Serializable} is scheduled to be removed in version 2.0. 034 * In version 1.8 the mapping between the column header and the column index was 035 * removed from the serialized state. The class maintains serialization compatibility 036 * with versions pre-1.8 for the record values; these must be accessed by index 037 * following deserialization. There will be a loss of any functionally linked to the header 038 * mapping when transferring serialized forms pre-1.8 to 1.8 and vice versa. 039 * </p> 040 */ 041public final class CSVRecord implements Serializable, Iterable<String> { 042 043 private static final long serialVersionUID = 1L; 044 045 /** 046 * 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 047 * character set. 048 */ 049 private final long characterPosition; 050 051 /** The accumulated comments (if any) */ 052 private final String comment; 053 054 /** The record number. */ 055 private final long recordNumber; 056 057 /** The values of the record */ 058 private final String[] values; 059 060 /** The parser that originates this record. This is not serialized. */ 061 private final transient CSVParser parser; 062 063 CSVRecord(final CSVParser parser, final String[] values, final String comment, final long recordNumber, 064 final long characterPosition) { 065 this.recordNumber = recordNumber; 066 this.values = values != null ? values : Constants.EMPTY_STRING_ARRAY; 067 this.parser = parser; 068 this.comment = comment; 069 this.characterPosition = characterPosition; 070 } 071 072 /** 073 * Returns a value by {@link Enum}. 074 * 075 * @param e 076 * an enum 077 * @return the String at the given enum String 078 */ 079 public String get(final Enum<?> e) { 080 return get(e == null ? null : e.name()); 081 } 082 083 /** 084 * Returns a value by index. 085 * 086 * @param i 087 * a column index (0-based) 088 * @return the String at the given index 089 */ 090 public String get(final int i) { 091 return values[i]; 092 } 093 094 /** 095 * Returns a value by name. If multiple instances of the header name exists, only the last occurrence is returned. 096 * 097 * <p> 098 * Note: This requires a field mapping obtained from the original parser. 099 * 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}