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.Map.Entry; 027import java.util.Objects; 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 String[] EMPTY_STRING_ARRAY = new String[0]; 044 045 private static final long serialVersionUID = 1L; 046 047 private final long characterPosition; 048 049 /** The accumulated comments (if any) */ 050 private final String comment; 051 052 /** The record number. */ 053 private final long recordNumber; 054 055 /** The values of the record */ 056 private final String[] values; 057 058 /** The parser that originates this record. This is not serialized. */ 059 private final transient CSVParser parser; 060 061 CSVRecord(final CSVParser parser, final String[] values, final String comment, final long recordNumber, 062 final long characterPosition) { 063 this.recordNumber = recordNumber; 064 this.values = values != null ? values : EMPTY_STRING_ARRAY; 065 this.parser = parser; 066 this.comment = comment; 067 this.characterPosition = characterPosition; 068 } 069 070 /** 071 * Returns a value by {@link Enum}. 072 * 073 * @param e 074 * an enum 075 * @return the String at the given enum String 076 */ 077 public String get(final Enum<?> e) { 078 return get(Objects.toString(e, null)); 079 } 080 081 /** 082 * Returns a value by index. 083 * 084 * @param i 085 * a column index (0-based) 086 * @return the String at the given index 087 */ 088 public String get(final int i) { 089 return values[i]; 090 } 091 092 /** 093 * Returns a value by name. 094 * 095 * <p> 096 * Note: This requires a field mapping obtained from the original parser. 097 * A check using {@link #isMapped(String)} should be used to determine if a 098 * mapping exists from the provided {@code name} to a field index. In this case an 099 * exception will only be thrown if the record does not contain a field corresponding 100 * to the mapping, that is the record length is not consistent with the mapping size. 101 * </p> 102 * 103 * @param name 104 * the name of the column to be retrieved. 105 * @return the column value, maybe null depending on {@link CSVFormat#getNullString()}. 106 * @throws IllegalStateException 107 * if no header mapping was provided 108 * @throws IllegalArgumentException 109 * if {@code name} is not mapped or if the record is inconsistent 110 * @see #isMapped(String) 111 * @see #isConsistent() 112 * @see #getParser() 113 * @see CSVFormat#withNullString(String) 114 */ 115 public String get(final String name) { 116 final Map<String, Integer> headerMap = getHeaderMapRaw(); 117 if (headerMap == null) { 118 throw new IllegalStateException( 119 "No header mapping was specified, the record values can't be accessed by name"); 120 } 121 final Integer index = headerMap.get(name); 122 if (index == null) { 123 throw new IllegalArgumentException(String.format("Mapping for %s not found, expected one of %s", name, 124 headerMap.keySet())); 125 } 126 try { 127 return values[index.intValue()]; 128 } catch (final ArrayIndexOutOfBoundsException e) { 129 throw new IllegalArgumentException(String.format( 130 "Index for header '%s' is %d but CSVRecord only has %d values!", name, index, 131 Integer.valueOf(values.length))); 132 } 133 } 134 135 /** 136 * Returns the start position of this record as a character position in the source stream. This may or may not 137 * correspond to the byte position depending on the character set. 138 * 139 * @return the position of this record in the source stream. 140 */ 141 public long getCharacterPosition() { 142 return characterPosition; 143 } 144 145 /** 146 * Returns the comment for this record, if any. 147 * Note that comments are attached to the following record. 148 * If there is no following record (i.e. the comment is at EOF) 149 * the comment will be ignored. 150 * 151 * @return the comment for this record, or null if no comment for this record is available. 152 */ 153 public String getComment() { 154 return comment; 155 } 156 157 private Map<String, Integer> getHeaderMapRaw() { 158 return parser == null ? null : parser.getHeaderMapRaw(); 159 } 160 161 /** 162 * Returns the parser. 163 * 164 * <p> 165 * Note: The parser is not part of the serialized state of the record. A null check 166 * should be used when the record may have originated from a serialized form. 167 * </p> 168 * 169 * @return the parser. 170 * @since 1.7 171 */ 172 public CSVParser getParser() { 173 return parser; 174 } 175 176 /** 177 * Returns the number of this record in the parsed CSV file. 178 * 179 * <p> 180 * <strong>ATTENTION:</strong> If your CSV input has multi-line values, the returned number does not correspond to 181 * the current line number of the parser that created this record. 182 * </p> 183 * 184 * @return the number of this record. 185 * @see CSVParser#getCurrentLineNumber() 186 */ 187 public long getRecordNumber() { 188 return recordNumber; 189 } 190 191 /** 192 * Checks whether this record has a comment, false otherwise. 193 * Note that comments are attached to the following record. 194 * If there is no following record (i.e. the comment is at EOF) 195 * the comment will be ignored. 196 * 197 * @return true if this record has a comment, false otherwise 198 * @since 1.3 199 */ 200 public boolean hasComment() { 201 return comment != null; 202 } 203 204 /** 205 * Tells whether the record size matches the header size. 206 * 207 * <p> 208 * Returns true if the sizes for this record match and false if not. Some programs can export files that fail this 209 * test but still produce parsable files. 210 * </p> 211 * 212 * @return true of this record is valid, false if not 213 */ 214 public boolean isConsistent() { 215 final Map<String, Integer> headerMap = getHeaderMapRaw(); 216 return headerMap == null || headerMap.size() == values.length; 217 } 218 219 /** 220 * Checks whether a given column is mapped, i.e. its name has been defined to the parser. 221 * 222 * @param name 223 * the name of the column to be retrieved. 224 * @return whether a given column is mapped. 225 */ 226 public boolean isMapped(final String name) { 227 final Map<String, Integer> headerMap = getHeaderMapRaw(); 228 return headerMap != null && headerMap.containsKey(name); 229 } 230 231 /** 232 * Checks whether a given columns is mapped and has a value. 233 * 234 * @param name 235 * the name of the column to be retrieved. 236 * @return whether a given columns is mapped and has a value 237 */ 238 public boolean isSet(final String name) { 239 return isMapped(name) && getHeaderMapRaw().get(name).intValue() < values.length; 240 } 241 242 /** 243 * Checks whether a column with given index has a value. 244 * 245 * @param index 246 * a column index (0-based) 247 * @return whether a column with given index has a value 248 */ 249 public boolean isSet(final int index) { 250 return 0 <= index && index < values.length; 251 } 252 253 /** 254 * Returns an iterator over the values of this record. 255 * 256 * @return an iterator over the values of this record. 257 */ 258 @Override 259 public Iterator<String> iterator() { 260 return toList().iterator(); 261 } 262 263 /** 264 * Puts all values of this record into the given Map. 265 * 266 * @param map 267 * The Map to populate. 268 * @return the given map. 269 */ 270 <M extends Map<String, String>> M putIn(final M map) { 271 if (getHeaderMapRaw() == null) { 272 return map; 273 } 274 for (final Entry<String, Integer> entry : getHeaderMapRaw().entrySet()) { 275 final int col = entry.getValue().intValue(); 276 if (col < values.length) { 277 map.put(entry.getKey(), values[col]); 278 } 279 } 280 return map; 281 } 282 283 /** 284 * Returns the number of values in this record. 285 * 286 * @return the number of values. 287 */ 288 public int size() { 289 return values.length; 290 } 291 292 /** 293 * Converts the values to a List. 294 * 295 * TODO: Maybe make this public? 296 * 297 * @return a new List 298 */ 299 private List<String> toList() { 300 return Arrays.asList(values); 301 } 302 303 /** 304 * Copies this record into a new Map of header name to record value. 305 * 306 * @return A new Map. The map is empty if the record has no headers. 307 */ 308 public Map<String, String> toMap() { 309 return putIn(new LinkedHashMap<String, String>(values.length)); 310 } 311 312 /** 313 * Returns a string representation of the contents of this record. The result is constructed by comment, mapping, 314 * recordNumber and by passing the internal values array to {@link Arrays#toString(Object[])}. 315 * 316 * @return a String representation of this record. 317 */ 318 @Override 319 public String toString() { 320 return "CSVRecord [comment='" + comment + "', recordNumber=" + recordNumber + ", values=" + 321 Arrays.toString(values) + "]"; 322 } 323 324 String[] values() { 325 return values; 326 } 327 328}