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 serialised 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 loss of any functionally linked to the header 038 * mapping when transferring serialised 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 private final long characterPosition; 046 047 /** The accumulated comments (if any) */ 048 private final String comment; 049 050 /** The record number. */ 051 private final long recordNumber; 052 053 /** The values of the record */ 054 private final String[] values; 055 056 /** The parser that originates this record. This is not serialized. */ 057 private final transient CSVParser parser; 058 059 CSVRecord(final CSVParser parser, final String[] values, final String comment, final long recordNumber, 060 final long characterPosition) { 061 this.recordNumber = recordNumber; 062 this.values = values != null ? values : Constants.EMPTY_STRING_ARRAY; 063 this.parser = parser; 064 this.comment = comment; 065 this.characterPosition = characterPosition; 066 } 067 068 /** 069 * Returns a value by {@link Enum}. 070 * 071 * @param e 072 * an enum 073 * @return the String at the given enum String 074 */ 075 public String get(final Enum<?> e) { 076 return get(e == null ? null : e.name()); 077 } 078 079 /** 080 * Returns a value by index. 081 * 082 * @param i 083 * a column index (0-based) 084 * @return the String at the given index 085 */ 086 public String get(final int i) { 087 return values[i]; 088 } 089 090 /** 091 * Returns a value by name. 092 * 093 * <p> 094 * Note: This requires a field mapping obtained from the original parser. 095 * A check using {@link #isMapped(String)} should be used to determine if a 096 * mapping exists from the provided {@code name} to a field index. In this case an 097 * exception will only be thrown if the record does not contain a field corresponding 098 * to the mapping, that is the record length is not consistent with the mapping size. 099 * </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}