    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}