// Copyright 2010 Google Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.api.ads.common.lib.utils;
import com.google.common.base.Preconditions;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import au.com.bytecode.opencsv.CSVReader;
import au.com.bytecode.opencsv.CSVWriter;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.Reader;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
/**
* A utility class for processing and handling CSV files.
*/
public final class CsvFiles {
/**
* {@code CsvFiles} is meant to be used statically.
*/
private CsvFiles() {}
/**
* Returns a {@code Map<String, String>} mapping of the column designated by
* {@code key} to the column designated by {@code value}. This method also
* ignores all columns other than the columns specified by {@code key} and
* {@code value}.
*
* @param fileName the CSV file to load
* @param key the 0-indexed column number to map to the key of the returned
* data map
* @param value the column number to map to the value of the returned data
* map
* @param headerPresent {@code true} if the fist line is the header
* @return a {@code Map<String, String>} mapping of the columns specified by
* {@code key} and {@code value}
* @throws IOException if there was an error while reading the file
* @throws IllegalArgumentException if CSV file does not have the
* columns specified by {@code key} or {@code value}
*/
public static Map<String, String> getCsvDataMap(String fileName,
final int key, final int value, boolean headerPresent) throws IOException {
final Map<String, String> result = Maps.newHashMap();
new CsvReader(fileName, headerPresent).processReader(
new CsvReader.CsvWorker() {
@Override
public void processLine(String[] header, String[] line, int lineNumber) {
result.put(line[key], line[value]);
}
});
return result;
}
/**
* Returns a {@code Map<String, String>} mapping of the first column to the
* second column. This method also ignores all columns other than the first
* two.
*
* @param fileName the CSV file to load
* @param headerPresent {@code true} if the fist line is the header
* @return a {@code Map<String, String>} mapping of the first to the second
* column
* @throws IOException if there was an exception reading the file
* @throws IllegalArgumentException if CSV file has fewer than two
* columns
*/
public static Map<String, String> getCsvDataMap(String fileName, boolean headerPresent)
throws IOException {
return getCsvDataMap(fileName, 0, 1, headerPresent);
}
/**
* Returns a {@code Map<String, String[]>} mapping of the first column to an
* array of the rest of the columns.
*
* @param fileName the CSV file to load
* @param headerPresent {@code true} if the fist line is the header
* @return a {@code Map<String, String[]>} mapping of the first column to an
* array of the rest of the columns
* @throws IllegalArgumentException if there is fewer than 2 columns in
* the CSV
* @throws IOException if there was an exception reading the file
*/
public static Map<String, String[]> getCsvDataMapArray(String fileName, boolean headerPresent)
throws IOException {
final Map<String, String[]> result = Maps.newHashMap();
new CsvReader(fileName, headerPresent).processReader(
new CsvReader.CsvWorker() {
@Override
public void processLine(String[] header, String[] line, int lineNumber) {
result.put(line[0],
Arrays.asList(line)
.subList(1, line.length).toArray(new String[line.length - 1]));
}
});
return result;
}
/**
* Returns a {@code List<Map<String, String>>} that contains all rows with
* a field mapping defined by the header. If no header is present,
* then each field is the 0-indexed column number.
*
* @param fileName the CSV file to load
* @param headerPresent {@code true} if the first line is the header
* @return a {@code List<Map<String, String>>} that contains all rows with
* with a field mapping defined by the header if present.
* @throws IOException if there was an exception reading the file
*/
public static List<Map<String, String>> getCsvDataListMap(String fileName,
boolean headerPresent) throws IOException {
final List<Map<String, String>> result = Lists.newArrayList();
new CsvReader(fileName, headerPresent).processReader(
new CsvReader.CsvWorker() {
@Override
public void processLine(String[] headers, String[] line, int lineNumber) {
Map<String, String> data = Maps.newHashMap();
for (int i = 0; i < line.length; i++) {
if (headers != null) {
data.put(headers[i], line[i]);
} else {
data.put(i + "", line[i]);
}
}
result.add(data);
}
});
return result;
}
/**
* Returns a {@code List<String>} representing a single 0-indexed column.
*
* @param fileName the CSV file to load
* @param column the 0-indexed column to return
* @param headerPresent {@code true} if the first line is the header
* @return a {@code List<String>} representing a single column
* @throws IOException if there was an exception reading the file
* @throws IllegalArgumentException if the column index does not exist in
* the CSV
*/
public static List<String> getCsvDataByColumn(String fileName, final int column,
boolean headerPresent) throws IOException {
final List<String> result = Lists.newArrayList();
new CsvReader(fileName, headerPresent).processReader(
new CsvReader.CsvWorker() {
@Override
public void processLine(String[] headers, String[] line, int lineNumber) {
result.add(line[column]);
}
});
return result;
}
/**
* Returns a {@code List<String[]>} that contains all rows of the CSV file.
* The header will be removed, if present.
*
* @param fileName the CSV file to load
* @param headerPresent {@code true} if the first line is the header
* @return a {@code List<String[]>} that contains all rows of the CSV file
* @throws IOException if there was an exception reading the file
*/
public static List<String[]> getCsvDataArray(String fileName, boolean headerPresent)
throws IOException {
final List<String[]> result = Lists.newArrayList();
new CsvReader(fileName, headerPresent).processReader(
new CsvReader.CsvWorker() {
@Override
public void processLine(String[] headers, String[] line, int lineNumber) {
result.add(line);
}
});
return result;
}
/**
* Returns a {@code List<String[]>} that contains all rows of the CSV file.
* The header will be removed, if present.
*
* @param csvReader reader used to read the csv
* @param headerPresent {@code true} if the first line is the header
* @return a {@code List<String[]>} that contains all rows of the CSV file
* @throws IOException if there was an exception reading the file
*/
public static List<String[]> getCsvDataArray(Reader csvReader, boolean headerPresent)
throws IOException {
final List<String[]> result = Lists.newArrayList();
new CsvReader(new CSVReader(csvReader), headerPresent).processReader(
new CsvReader.CsvWorker() {
@Override
public void processLine(String[] headers, String[] line, int lineNumber) {
result.add(line);
}
});
return result;
}
/**
* Writes the CSV data located in {@code csvData} to the file located at
* {@code fileName}.
*
* @param csvData the CSV data including the header
* @param fileName the file to write the CSV data to
* @throws IOException if there was an error writing to the file
* @throws NullPointerException if {@code csvData == null} or {@code fileName == null}
*/
public static void writeCsv(List<String[]> csvData, String fileName) throws IOException {
Preconditions.checkNotNull(csvData, "Null CSV data");
Preconditions.checkNotNull(fileName, "Null file name");
CSVWriter writer = null;
try {
writer = new CSVWriter(new FileWriter(fileName));
for (String[] line : csvData) {
writer.writeNext(line);
}
} finally {
if (writer != null) {
writer.close();
}
}
}
/**
* Iterates through and processes each line of a CSV file. This is done by
* passing a {@link CsvWorker} object into the
* {@link CsvReader#processReader(CsvWorker)} method, which in turn, calls the
* {@link CsvWorker#processLine(String[], String[], int)} method for each line
* of the CSV.
*/
private static class CsvReader {
private final String fileName;
private final boolean headerPresent;
private CSVReader reader;
private String[] header;
private int lineNumber;
/**
* Constructs a {@link CsvReader} object which will load the file located
* at {@code fileName}.
*
* @param fileName the file name of the CSV file to load
* @param headerPresent {@code true} if the file's first line is the
* header for each column
*/
public CsvReader(String fileName, boolean headerPresent) {
this.fileName = fileName;
this.headerPresent = headerPresent;
}
/**
* Constructs a {@link CsvReader} object which uses the supplied reader.
*
* @param reader the {@link CSVReader} to use
* @param headerPresent {@code true} if the file's first line is the
* header for each column
*/
public CsvReader(CSVReader reader, boolean headerPresent) {
this.fileName = null;
this.reader = reader;
this.headerPresent = headerPresent;
}
/**
* Creates a {@link CSVReader} for the file and sets the {@code header}
* if present. After this method is called,
* {@link #processReader(CsvWorker)} may be called.
*
* @throws IOException if the CSV file cannot be read
*/
private void createCsvReader() throws IOException {
lineNumber = 1;
if (reader == null) {
reader = new CSVReader(new FileReader(fileName));
}
if (headerPresent) {
header = reader.readNext();
lineNumber++;
}
}
/**
* Performs the {@link CsvWorker#processLine(String[], String[], int)}
* method of the {@code worker} parameter for each link in the CSV,
* and closes the underlying {@link CSVReader}.
*
* @param worker the {@code CsvWorker} that performs work on each line
* @throws IOException if the CSV file cannot be read
*/
public void processReader(CsvWorker worker) throws IOException {
createCsvReader();
try {
String[] line;
while ((line = reader.readNext()) != null) {
worker.processLine(header, line, lineNumber);
lineNumber++;
}
} finally {
reader.close();
}
}
/**
* A worker that is called within {@link CsvReader} on each line of the CSV
* via the {@link #processLine(String[], String[], int)} method.
*/
private interface CsvWorker {
/**
* Processes the current line of the CSV.
*
* @param headers the headers usually represented by the first line of the
* CSV. If the original file did not contain headers, then
* this field may be {@code null}.
* @param currentLine the current line of the CSV that work will be done on
* @param currentLineNumber the current one-indexed line number of the CSV
* @throws IllegalArgumentException if the current line or headers is not
* valid to have work performed on
*/
void processLine(String[] headers, String[] currentLine, int currentLineNumber);
}
}
}