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.HashMap; 023import java.util.Iterator; 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 column name to index mapping. */ 043 private final Map<String, Integer> mapping; 044 045 /** The record number. */ 046 private final long recordNumber; 047 048 /** The values of the record */ 049 private final String[] values; 050 051 CSVRecord(final String[] values, final Map<String, Integer> mapping, 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.mapping = mapping; 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 if (mapping == null) { 097 throw new IllegalStateException( 098 "No header mapping was specified, the record values can't be accessed by name"); 099 } 100 final Integer index = mapping.get(name); 101 if (index == null) { 102 throw new IllegalArgumentException(String.format("Mapping for %s not found, expected one of %s", name, 103 mapping.keySet())); 104 } 105 try { 106 return values[index.intValue()]; 107 } catch (final ArrayIndexOutOfBoundsException e) { 108 throw new IllegalArgumentException(String.format( 109 "Index for header '%s' is %d but CSVRecord only has %d values!", name, index, 110 Integer.valueOf(values.length))); 111 } 112 } 113 114 /** 115 * Returns the start position of this record as a character position in the source stream. This may or may not 116 * correspond to the byte position depending on the character set. 117 * 118 * @return the position of this record in the source stream. 119 */ 120 public long getCharacterPosition() { 121 return characterPosition; 122 } 123 124 /** 125 * Returns the comment for this record, if any. 126 * Note that comments are attached to the following record. 127 * If there is no following record (i.e. the comment is at EOF) 128 * the comment will be ignored. 129 * 130 * @return the comment for this record, or null if no comment for this record is available. 131 */ 132 public String getComment() { 133 return comment; 134 } 135 136 /** 137 * Returns the number of this record in the parsed CSV file. 138 * 139 * <p> 140 * <strong>ATTENTION:</strong> If your CSV input has multi-line values, the returned number does not correspond to 141 * the current line number of the parser that created this record. 142 * </p> 143 * 144 * @return the number of this record. 145 * @see CSVParser#getCurrentLineNumber() 146 */ 147 public long getRecordNumber() { 148 return recordNumber; 149 } 150 151 /** 152 * Tells whether the record size matches the header size. 153 * 154 * <p> 155 * Returns true if the sizes for this record match and false if not. Some programs can export files that fail this 156 * test but still produce parsable files. 157 * </p> 158 * 159 * @return true of this record is valid, false if not 160 */ 161 public boolean isConsistent() { 162 return mapping == null || mapping.size() == values.length; 163 } 164 165 /** 166 * Checks whether this record has a comment, false otherwise. 167 * Note that comments are attached to the following record. 168 * If there is no following record (i.e. the comment is at EOF) 169 * the comment will be ignored. 170 * 171 * @return true if this record has a comment, false otherwise 172 * @since 1.3 173 */ 174 public boolean hasComment() { 175 return comment != null; 176 } 177 178 /** 179 * Checks whether a given column is mapped, i.e. its name has been defined to the parser. 180 * 181 * @param name 182 * the name of the column to be retrieved. 183 * @return whether a given column is mapped. 184 */ 185 public boolean isMapped(final String name) { 186 return mapping != null && mapping.containsKey(name); 187 } 188 189 /** 190 * Checks whether a given columns is mapped and has a value. 191 * 192 * @param name 193 * the name of the column to be retrieved. 194 * @return whether a given columns is mapped and has a value 195 */ 196 public boolean isSet(final String name) { 197 return isMapped(name) && mapping.get(name).intValue() < values.length; 198 } 199 200 /** 201 * Returns an iterator over the values of this record. 202 * 203 * @return an iterator over the values of this record. 204 */ 205 @Override 206 public Iterator<String> iterator() { 207 return toList().iterator(); 208 } 209 210 /** 211 * Puts all values of this record into the given Map. 212 * 213 * @param map 214 * The Map to populate. 215 * @return the given map. 216 */ 217 <M extends Map<String, String>> M putIn(final M map) { 218 if (mapping == null) { 219 return map; 220 } 221 for (final Entry<String, Integer> entry : mapping.entrySet()) { 222 final int col = entry.getValue().intValue(); 223 if (col < values.length) { 224 map.put(entry.getKey(), values[col]); 225 } 226 } 227 return map; 228 } 229 230 /** 231 * Returns the number of values in this record. 232 * 233 * @return the number of values. 234 */ 235 public int size() { 236 return values.length; 237 } 238 239 /** 240 * Converts the values to a List. 241 * 242 * TODO: Maybe make this public? 243 * 244 * @return a new List 245 */ 246 private List<String> toList() { 247 return Arrays.asList(values); 248 } 249 250 /** 251 * Copies this record into a new Map. The new map is not connect 252 * 253 * @return A new Map. The map is empty if the record has no headers. 254 */ 255 public Map<String, String> toMap() { 256 return putIn(new HashMap<String, String>(values.length)); 257 } 258 259 /** 260 * Returns a string representation of the contents of this record. The result is constructed by comment, mapping, 261 * recordNumber and by passing the internal values array to {@link Arrays#toString(Object[])}. 262 * 263 * @return a String representation of this record. 264 */ 265 @Override 266 public String toString() { 267 return "CSVRecord [comment=" + comment + ", mapping=" + mapping + 268 ", recordNumber=" + recordNumber + ", values=" + 269 Arrays.toString(values) + "]"; 270 } 271 272 String[] values() { 273 return values; 274 } 275 276}