CSVTable.java

/*
 * $Source$
 * $Revision$
 *
 * Part of Melati (http://melati.org), a framework for the rapid
 * development of clean, maintainable web applications.
 *
 *  Copyright (C) 2001 Myles Chippendale
 *
 * Melati is free software; Permission is granted to copy, distribute
 * and/or modify this software under the terms either:
 *
 * a) the GNU General Public License as published by the Free Software
 *    Foundation; either version 2 of the License, or (at your option)
 *    any later version,
 *
 *    or
 *
 * b) any version of the Melati Software License, as published
 *    at http://melati.org
 *
 * You should have received a copy of the GNU General Public License and
 * the Melati Software License along with this program;
 * if not, write to the Free Software Foundation, Inc.,
 * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA to obtain the
 * GNU General Public License and visit http://melati.org to obtain the
 * Melati Software License.
 *
 * Feel free to contact the Developers of Melati (http://melati.org),
 * if you would like to work out a different arrangement than the options
 * outlined here.  It is our intention to allow Melati to be used by as
 * wide an audience as possible.
 *
 * This program 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 General Public License for more details.
 *
 * Contact details for copyright holder:
 *
 *     Myles Chippendale <mylesc At paneris.org>
 *
 *
 * ------
 *  Note
 * ------
 *
 * I will assign copyright to PanEris (http://paneris.org) as soon as
 * we have sorted out what sort of legal existence we need to have for
 * that to make sense. 
 * In the meantime, if you want to use Melati on non-GPL terms,
 * contact me!
 */

package org.melati.poem.csv;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.io.Writer;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.NoSuchElementException;
import java.util.Vector;

import org.melati.poem.Persistent;
import org.melati.poem.Table;

/**
 * A representation of a CSV file as a POEM Table.
 */
public class CSVTable {

  protected Table<?> table = null;
  protected File data = null;
  protected Hashtable<String,CSVColumn> columns = new Hashtable<String,CSVColumn>();
  protected Vector<CSVColumn> columnsInUploadOrder = new Vector<CSVColumn>();
  protected CSVColumn primaryKey = null;
  protected Vector<CSVRecord> records = new Vector<CSVRecord>();
  protected BufferedReader reader = null;
  protected CSVFileParser parser = null;
  
  /** The line number of the CSV file. */
  private int lineNo;
  
  /** The record number of the CSV file. */
  private int recordNo;


 /**
  * Constructor.
  * 
  * @param table POEM table to load data into
  * @param data CSV file to read from
  */
  public CSVTable(Table<?> table, File data) {
    this.table = table;
    this.data = data;
    try {
      reader = new BufferedReader(new FileReader(this.data));
      parser = new CSVFileParser(this.reader);
    } catch (Exception e) {
      throw new RuntimeException(e);
    }
  }

  /**
   * Process the first line to define columns. 
   * The first line contains the field names - this needs to be
   * validated against expected values, and the order of
   * the fields established.
   * 
   * @throws IOException
   */
  public void define() throws IOException  {
      parser.nextRecord();
      lineNo = 1;
      recordNo = 0;

      while (parser.recordHasMoreFields()) {
        String key = parser.nextField();
        CSVColumn col = columns.get(key);
        if (col == null)
          throw new CSVParseException(
            "I don't know what to do with the column in " + data.getPath() +
            " called " + key); 
        columnsInUploadOrder.addElement(col);
      }
  }  

 /**
  * Add column definitions to this table.
  */
  public void addColumn(String csvName, String poemName) {
    columns.put(csvName, new CSVColumn(poemName));
  }

 /**
  * Add column definitions, perhaps Primary Keys, to this table.
  */
  public void addColumn(String csvName, String poemName, boolean isPrimaryKey)
      throws CSVPrimaryKeyColumnAlreadySetException {
    if (isPrimaryKey && primaryKey != null)
      throw new CSVPrimaryKeyColumnAlreadySetException(table.getName());
    CSVColumn col = new CSVColumn(poemName, isPrimaryKey);
    columns.put(csvName, col);
    if (isPrimaryKey)
      primaryKey = col;
  }

   /**
    * Add column definitions for foreign keys to this table.
    */
    public void addColumn(String csvName,  
                          String foreignPoemName, CSVTable foreignTable) {
      columns.put(csvName, new CSVColumn(foreignPoemName, foreignTable));
    }


 /**
  * Parse the CSV data file and store the data for saving later.
  * 
  * @param writeOnFly whether to commit each line to db as we go
  * @throws IOException if there is a file system problem
  * @throws CSVParseException if the is a malformed field in the CSV
  * @throws CSVWriteDownException
  * @throws NoPrimaryKeyInCSVTableException
  */
  public void load(boolean writeOnFly) 
      throws IOException, CSVParseException, NoPrimaryKeyInCSVTableException, 
             CSVWriteDownException {

    try {
      define();
      // Suck in all the data
      CSVRecord record;
      while(null != (record = parseRecord())) {
        record.setLineNo(lineNo++);
        record.setRecordNo(recordNo++);
        if (writeOnFly)
          record.makePersistent();
        else 
          records.addElement(record);
      }

    }
    catch (IllegalArgumentException e) {
       throw new CSVParseException("Failed to load field in " +
                                    data.getPath() + 
                                    " line " + lineNo +
                                    ": " + e.toString());
    }
    catch (NoSuchElementException f) {
       throw new CSVParseException("Failed to read column header in " +
                                    data.getPath() + 
                                    " line " + lineNo +
                                    ": " + f.toString());
    }
    finally {
      reader.close();
    }
  }


 /**
  * Reads the file until is has seen an object's-worth of
  * field values (ie until it sees an EOF or a line starting
  * with '$') which it returns in a hashtable (null if there are
  * no field values).
   * @return a new CSVRecord
   * @throws IOException if there is a problem with the file system
   * @throws CSVParseException if there is a problem parsing the input
   */
  public CSVRecord parseRecord() throws IOException, CSVParseException {
    if (!parser.nextRecord())
      return null;

    int i = 0;
    String value = null;
    try {
      CSVRecord record = new CSVRecord(table);
      for (; i < columnsInUploadOrder.size(); i++) {
        value = parser.nextField();
        CSVColumn col = (CSVColumn)columnsInUploadOrder.elementAt(i);
        record.addField(new CSVField(col, value));
      }
      record.setLineNo(parser.getLineNo());
      return record;
    }
    catch (IllegalArgumentException e) {
       throw new CSVParseException("Failed to read data field no. " + 
                                    (i+1) + 
                                    " in " +
                                    data + 
                                    " line " + lineNo +
                                    ": " + e.toString());
    }
    catch (NoSuchElementException f) {
      String message = "Problem with data field no. " + (i+1) +
      " of " + columnsInUploadOrder.size() +
      " in " + data +
      " line " + lineNo;
      
      if (value == null) {
        message += " (Check last line of file) : " + 
                    f.toString();
      } else {
        message += ", Value:" + value + ": " + f.toString();
      }
      throw new CSVParseException(message);
    }
  }


 /**
  * Delete all Persistents from the Poem table.
  */
  public void emptyTable() {
    Enumeration<?> rows = table.selection();
    while(rows.hasMoreElements()) {
      Persistent p = (Persistent)rows.nextElement();
      p.delete();
    }
  }

 /**
  * Write the records to the database, 
  * called if we are not writing each record to db as we go.
  */
  public void writeRecords() 
      throws NoPrimaryKeyInCSVTableException,  CSVWriteDownException {
    for (int i = 0; i < records.size(); i++) {
      ((CSVRecord)records.elementAt(i)).makePersistent();
    }
  }
  

  /**
   * Lookup the Persistent corresponding to the CSV record
   * with the given value for the CSV table's primary key.
   * @throws NoPrimaryKeyInCSVTableException
   * @throws CSVWriteDownException
   */
  protected Persistent getRecordWithID(String csvValue)
      throws NoPrimaryKeyInCSVTableException, CSVWriteDownException {
    if (primaryKey == null)
      throw new NoPrimaryKeyInCSVTableException(table.getName(), csvValue);

    for (int i = 0; i < records.size(); i++) {
      CSVRecord record = (CSVRecord)records.elementAt(i);
      if (record.primaryKeyValue != null &&
          record.primaryKeyValue.equals(csvValue))
        return record.getPersistent();
    }
    return null;
  }

 /**
  * Return a string reporting on the data added to this table.
  */
  public void report(boolean recordDetails, boolean fieldDetails,
                       Writer output) throws IOException {

    output.write("*** TABLE: " + table.getName().toUpperCase() + " **\n\n");
    output.write("** I have read " + records.size() + " records of " +
                   columnsInUploadOrder.size() + " fields\n");

    if (recordDetails) {
      for (int i = 0; i < records.size(); i++) {
        CSVRecord record = (CSVRecord)records.elementAt(i);
        output.write("   Record: CSV primary key = " +
                        record.primaryKeyValue);

        if (record.poemRecord == null)
          output.write(", No Poem Persistent written\n");
        else
          output.write(", Poem Troid = " + record.poemRecord.getTroid() + "\n");

        if (fieldDetails) {
          for (int j = 0; j < record.getFields().size(); j++) {
            CSVField field = (CSVField)record.getFields().elementAt(j);
            output.write(field.column + "=\"" + field.value);
            if (j < record.getFields().size()-1)
              output.write("\",");
            else
              output.write("\"\n");
          }
        }
      }
    }
    output.write("** Currently " + table.count(null) +
                  " Persistents in this table\n\n");
  }

  /**
   * Used in debugging to display name of table being emptied.
   * 
   * @return the POEM Table's name
   */
  public String getName() {
    return table.getName();
  }
}