001/*
002 * The contents of this file are subject to the terms of the Common Development and
003 * Distribution License (the License). You may not use this file except in compliance with the
004 * License.
005 *
006 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
007 * specific language governing permission and limitations under the License.
008 *
009 * When distributing Covered Software, include this CDDL Header Notice in each file and include
010 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
011 * Header, with the fields enclosed by brackets [] replaced by your own identifying
012 * information: "Portions Copyright [year] [name of copyright owner]".
013 *
014 * Copyright 2010–2011 ApexIdentity Inc.
015 * Portions Copyright 2011-2014 ForgeRock AS.
016 */
017
018package org.forgerock.openig.text;
019
020import java.io.File;
021import java.io.FileInputStream;
022import java.io.IOException;
023import java.io.InputStreamReader;
024import java.nio.charset.Charset;
025import java.util.ArrayList;
026import java.util.HashMap;
027import java.util.Iterator;
028import java.util.List;
029import java.util.Map;
030
031/**
032 * Allows records to be retrieved from a delimiter-separated file using key and value. Once
033 * constructed, an instance of this class is thread-safe, meaning the object can be long-lived,
034 * and multiple concurrent calls to {@link #getRecord(String, String) getRecord} is fully
035 * supported.
036 */
037public class SeparatedValuesFile {
038
039    /** The file containing the separated values to be read. */
040    private final File file;
041
042    /** The character set the file is encoded in. */
043    private final Charset charset;
044
045    /** The separator specification to split lines into fields. */
046    private final Separator separator;
047
048    /** Does the first line of the file contain the set of defined field keys. */
049    private boolean header;
050
051    /**
052     * Explicit field keys in the order they appear in a record, overriding any existing field header,
053     * or empty to use field header.
054     */
055    private final List<String> fields = new ArrayList<String>();
056
057    /**
058     * Builds a new SeparatedValuesFile reading the given {@code file} using a the {@link Separators#COMMA}
059     * separator specification and {@code UTF-8} charset. This constructor consider the file has a header line.
060     * <p>
061     * It is equivalent to call:
062     * <code> new SeparatedValuesFile(file, "UTF-8"); </code>
063     *
064     * @param file
065     *         file to read from
066     * @see #SeparatedValuesFile(File, Charset)
067     */
068    public SeparatedValuesFile(final File file) {
069        this(file, Charset.forName("UTF-8"));
070    }
071
072    /**
073     * Builds a new SeparatedValuesFile reading the given {@code file} using a the {@link Separators#COMMA}
074     * separator specification. This constructor consider the file has a header line.
075     * <p>
076     * It is equivalent to call:
077     * <code> new SeparatedValuesFile(file, charset, Separators.COMMA.getSeparator()); </code>
078     *
079     * @param file
080     *         file to read from
081     * @param charset
082     *         {@link Charset} of the file (non-null)
083     * @see #SeparatedValuesFile(File, Charset, Separator)
084     */
085    public SeparatedValuesFile(final File file, final Charset charset) {
086        this(file, charset, Separators.COMMA.getSeparator());
087    }
088
089    /**
090     * Builds a new SeparatedValuesFile reading the given {@code file}. This constructor consider the file has a header
091     * line.
092     * <p>
093     * It is equivalent to call:
094     * <code> new SeparatedValuesFile(file, charset, separator, true); </code>
095     *
096     * @param file
097     *         file to read from
098     * @param charset
099     *         {@link Charset} of the file (non-null)
100     * @param separator
101     *         separator specification
102     * @see #SeparatedValuesFile(File, Charset, Separator, boolean)
103     */
104    public SeparatedValuesFile(final File file, final Charset charset, final Separator separator) {
105        this(file, charset, separator, true);
106    }
107
108    /**
109     * Builds a new SeparatedValuesFile reading the given {@code file}.
110     *
111     * @param file
112     *         file to read from
113     * @param charset
114     *         {@link Charset} of the file (non-null)
115     * @param separator
116     *         separator specification
117     * @param header
118     *         does the file has a header first line ?
119     */
120    public SeparatedValuesFile(final File file,
121                               final Charset charset,
122                               final Separator separator,
123                               final boolean header) {
124        this.file = file;
125        this.charset = charset;
126        this.separator = separator;
127        this.header = header;
128    }
129
130    /**
131     * Returns the explicit field keys in the order they appear in a record, overriding any existing field header,
132     * or empty to use field header.
133     * @return the explicit field keys in the order they appear in a record
134     */
135    public List<String> getFields() {
136        return fields;
137    }
138
139    /**
140     * Returns a record from the file where the specified key is equal to the specified value.
141     *
142     * @param key the key to use to lookup the record
143     * @param value the value that the key should have to find a matching record.
144     * @return the record with the matching value, or {@code null} if no such record could be found.
145     * @throws IOException if an I/O exception occurs.
146     */
147    public Map<String, String> getRecord(String key, String value) throws IOException {
148        Map<String, String> map = null;
149        SeparatedValuesReader reader = new SeparatedValuesReader(
150                new InputStreamReader(new FileInputStream(file), charset),
151                separator
152        );
153        try {
154            List<String> fields = this.fields;
155            if (header) {
156                // first line in the file is the field header
157                List<String> record = reader.next();
158                if (record != null && fields.size() == 0) {
159                    // use header fields
160                    fields = record;
161                }
162            }
163            if (fields.size() > 0) {
164                int index = fields.indexOf(key);
165                if (index >= 0) {
166                    // requested key exists
167                    List<String> record;
168                    while ((record = reader.next()) != null) {
169                        if (record.get(index).equals(value)) {
170                            map = new HashMap<String, String>(fields.size());
171                            Iterator<String> fi = fields.iterator();
172                            Iterator<String> ri = record.iterator();
173                            while (fi.hasNext() && ri.hasNext()) {
174                                // assign field-value pairs in map
175                                map.put(fi.next(), ri.next());
176                            }
177                            break;
178                        }
179                    }
180                }
181            }
182        } finally {
183            reader.close();
184        }
185        return map;
186    }
187}