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