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