content: // ===============================================================================
  // Authors: AFRL/RQQD
  // Organization: Air Force Research Laboratory, Aerospace Systems Directorate, Power and Control Division
  // 
  // Copyright (c) 2017 Government of the United State of America, as represented by
  // the Secretary of the Air Force.  No copyright is claimed in the United States under
  // Title 17, U.S. Code.  All Other Rights Reserved.
  // ===============================================================================
  
  /*
   * GRAL: GRAphing Library for Java(R)
   *
   * (C) Copyright 2009-2013 Erich Seifert <dev[at]erichseifert.de>,
   * Michael Seifert <mseifert[at]error-reports.org>
   *
   * This file is part of GRAL.
   *
   * GRAL is free software: you can redistribute it and/or modify
   * it under the terms of the GNU Lesser General Public License as published by
   * the Free Software Foundation, either version 3 of the License, or
   * (at your option) any later version.
   *
   * GRAL is distributed in the hope that it will be useful,
   * but WITHOUT ANY WARRANTY; without even the implied warranty of
   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   * GNU Lesser General Public License for more details.
   *
   * You should have received a copy of the GNU Lesser General Public License
   * along with GRAL.  If not, see <http://www.gnu.org/licenses/>.
   */
  package de.erichseifert.gral.io.data;
  
  import java.io.IOException;
  import java.io.InputStream;
  import java.lang.reflect.InvocationTargetException;
  import java.lang.reflect.Method;
  import java.text.MessageFormat;
  import java.util.HashMap;
  import java.util.LinkedList;
  import java.util.List;
  import java.util.Map;
  import java.util.Scanner;
  import java.util.regex.Pattern;
  
  import de.erichseifert.gral.data.DataSource;
  import de.erichseifert.gral.data.DataTable;
  import de.erichseifert.gral.io.IOCapabilities;
  import de.erichseifert.gral.util.Messages;
  import de.erichseifert.gral.util.StatefulTokenizer;
  import de.erichseifert.gral.util.StatefulTokenizer.Token;
  
  
  /**
   * <p>Class that creates a {@code DataSource} from file contents which are
   * separated by a certain delimiter character. The delimiter is chosen based on
   * the file type but can also be set manually. By default the comma character
   * will be used as a delimiter for separating columns.</p>
   * <p>{@code CSVReader} instances should be obtained by the
   * {@link DataReaderFactory} rather than being created manually:</p>
   * <pre>
   * DataReaderFactory factory = DataReaderFactory.getInstance();
   * DataReader reader = factory.get("text/csv");
   * reader.read(new FileInputStream(filename), Integer.class, Double.class);
   * </pre>
   * @see <a href="http://tools.ietf.org/html/rfc4180">RFC 4180</a>
   */
  public class CSVReader extends AbstractDataReader {
  	/** Key for specifying a {@link Character} value that defines the
  	delimiting character used to separate columns. */
  	public static final String SEPARATOR_CHAR = "separator"; //$NON-NLS-1$
  
  	static {
  		addCapabilities(new IOCapabilities(
  			"CSV", //$NON-NLS-1$
  			Messages.getString("DataIO.csvDescription"), //$NON-NLS-1$
  			"text/csv", //$NON-NLS-1$
  			new String[] {"csv", "txt"} //$NON-NLS-1$ //$NON-NLS-2$
  		));
  
  		addCapabilities(new IOCapabilities(
  			"TSV", //$NON-NLS-1$
  			Messages.getString("DataIO.tsvDescription"), //$NON-NLS-1$
  			"text/tab-separated-values", //$NON-NLS-1$
  			new String[] {
  				"tsv", "tab", "txt"} //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
  		));
  	}
  
  	/**
  	 * Token types for analyzing CSV or TSV input.
  	 */
  	private static enum CSVTokenType {
  		/** Type for text tokens containing value content. */
  		TEXT,
  		/** Type for quotes that may wrap value content. */
  		QUOTE,
  		/** Type for row separators. */
  		ROW,
  		/** Type for column separators. */
  		COLUMN,
  	}
  
  	/**
  	 *
  	 */
  	private static final class CSVTokenizer extends StatefulTokenizer {
  		/**
  		 * Initializes a new tokenizer instance with a grammar to analyze CSV
  		 * or TSV content. The character that separates columns must be
  		 * provided.
  		 * @param separator Column separator character.
  		 */
  		public CSVTokenizer(char separator) {
  			addJoinedType(CSVTokenType.TEXT);
  			addIgnoredType(CSVTokenType.QUOTE);
  
  			// Basic Set of rules for analyzing CSV content
  			putRules(
  				new Rule("\n|\r\n|\r", CSVTokenType.ROW),
  				new Rule("\\s*("+Pattern.quote(String.valueOf(separator))+")\\s*",
  					CSVTokenType.COLUMN),
  				new Rule("\"", CSVTokenType.QUOTE, "quoted"),
  				new Rule(".", CSVTokenType.TEXT)
  			);
  			// Set of rules that is valid inside quoted content
  			putRules("quoted",
  				new Rule("(\")\"", CSVTokenType.TEXT),
  				new Rule("\"", CSVTokenType.QUOTE, "#pop"),
  				new Rule(".", CSVTokenType.TEXT)
  			);
  		}
  	}
  
  	/**
  	 * Creates a new instance with the specified MIME type. The delimiter is
  	 * set depending on the MIME type parameter. By default a comma is used as
  	 * a delimiter.
  	 * @param mimeType MIME type of the file format to be read.
  	 */
  	public CSVReader(String mimeType) {
  		super(mimeType);
  		if ("text/tab-separated-values".equals(mimeType)) { //$NON-NLS-1$
  			setDefault(SEPARATOR_CHAR, '\t');
  		} else {
  			setDefault(SEPARATOR_CHAR, ',');
  		}
  	}
  
  	/**
  	 * Returns a DataSource that was imported.
  	 * @param input Input to be read.
  	 * @param types Number types for the columns of the DataSource.
  	 * @return DataSource Imported data.
  	 * @throws IOException when the file format is not valid or when
  	 *         experiencing an error during file operations.
  	 */
  	public DataSource read(InputStream input, Class<? extends Comparable<?>>... types)
  			throws IOException {
  		// Read all contents from the input stream
  		Scanner scanner = new Scanner(input).useDelimiter("\\Z");
  		String content = scanner.next();
  
  		// Tokenize the string
  		Character separator = getSetting(SEPARATOR_CHAR);
  		CSVTokenizer tokenizer = new CSVTokenizer(separator);
  		List<Token> tokens = tokenizer.tokenize(content);
  
  		// Add row token if there was no trailing line break
  		Token lastToken = tokens.get(tokens.size() - 1);
  		if (lastToken.getType() != CSVTokenType.ROW) {
  			Token eof = new Token(lastToken.getEnd(), lastToken.getEnd(), CSVTokenType.ROW, "");
  			tokens.add(eof);
  		}
  
  		// Find methods for all column data types that can be used to convert
  		// the text to the column data type
  		Map<Class<? extends Comparable<?>>, Method> parseMethods =
  			new HashMap<Class<? extends Comparable<?>>, Method>();
  		for (Class<? extends Comparable<?>> type : types) {
  			if (parseMethods.containsKey(type)) {
  				continue;
  			}
  			Method parseMethod = getParseMethod(type);
  			if (parseMethod != null) {
  				parseMethods.put(type, parseMethod);
  			}
  		}
  
  		// Process the data and store the data.
  		DataTable data = new DataTable(types);
  		List<Comparable<?>> row = new LinkedList<Comparable<?>>();
  		int rowIndex = 0;
  		int colIndex = 0;
  		String cellContent = "";
  		for (Token token : tokens) {
  			if (token.getType() == CSVTokenType.TEXT) {
  				// Store the token text
  				cellContent = token.getContent();
  			} else if (token.getType() == CSVTokenType.COLUMN ||
  					token.getType() == CSVTokenType.ROW) {
  				// Check for a valid number of columns
  				if (colIndex >= types.length) {
  					throw new IllegalArgumentException(MessageFormat.format(
  						"Too many columns in line {0,number,integer}: got {1,number,integer}, but expected {2,number,integer}.", //$NON-NLS-1$
  						rowIndex + 1, colIndex + 1, types.length));
  				}
  
  				// We need to add the cell to the row in both cases because
  				// rows don't have a trailing column token
  				Class<? extends Comparable<?>> colType = types[colIndex];
  				Method parseMethod = parseMethods.get(colType);
  				try {
  					Comparable<?> cell = (Comparable<?>) parseMethod.invoke(
  						null, cellContent);
  					row.add(cell);
  				} catch (IllegalArgumentException e) {
  					throw new RuntimeException(MessageFormat.format(
  						"Could not invoke method for parsing data type {0} in column {1,number,integer}.", //$NON-NLS-1$
  						types[colIndex].getSimpleName(), colIndex));
  				} catch (IllegalAccessException e) {
  					throw new RuntimeException(MessageFormat.format(
  						"Could not access method for parsing data type {0} in column {1,number,integer}.", //$NON-NLS-1$
  						types[colIndex].getSimpleName(), colIndex));
  				} catch (InvocationTargetException e) {
  					throw new IOException(MessageFormat.format(
  						"Type mismatch in line {0,number,integer}, column {1,number,integer}: got \"{2}\", but expected {3} value.", //$NON-NLS-1$
  						rowIndex + 1, colIndex + 1, cellContent, colType.getSimpleName()));
  				}
  				colIndex++;
  
  				if (token.getType() == CSVTokenType.ROW) {
  					// Check for a valid number of columns
  					if (row.size() < types.length) {
  						throw new IllegalArgumentException(MessageFormat.format(
  							"Not enough columns in line {0,number,integer}: got {1,number,integer}, but expected {2,number,integer}.", //$NON-NLS-1$
  							rowIndex + 1, row.size(), types.length));
  					}
  
  					// Add the row to the table
  					data.add(row);
  					rowIndex++;
  
  					// Start a new row
  					row.clear();
  					colIndex = 0;
  					cellContent = "";
  				}
  			}
  		}
  
  		return data;
  	}
  
  	/**
  	 * Returns a method that can return a parsed value of the specified type.
  	 * @param c Desired type.
  	 * @return Method that parses a data type.
  	 */
  	private static Method getParseMethod(Class<?> c) {
  		Method parse = null;
  
  		if (String.class.isAssignableFrom(c)) {
  			try {
  				parse = String.class.getMethod("valueOf", Object.class);
  			} catch (NoSuchMethodException e) {
  			}
  		} else {
  			for (Method m : c.getMethods()) {
  				boolean isStatic = m.toString().indexOf("static") >= 0; //$NON-NLS-1$
  				if (!isStatic) {
  					continue;
  				}
  				Class<?>[] types = m.getParameterTypes();
  				boolean hasStringParameter =
  					(types.length == 1) && String.class.equals(types[0]);
  				if (!hasStringParameter) {
  					continue;
  				}
  				boolean parseName = m.getName().startsWith("parse"); //$NON-NLS-1$
  				if (!parseName) {
  					continue;
  				}
  				parse = m;
  			}
  		}
  
  		return parse;
  	}
  }
  