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}