Open Source Repository

Home /supercsv/super-csv-2.4.0 | Repository Home



org/supercsv/io/Tokenizer.java
/*
 * Copyright 2007 Kasper B. Graversen
 
 * 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 org.supercsv.io;

import java.io.IOException;
import java.io.Reader;
import java.util.List;

import org.supercsv.comment.CommentMatcher;
import org.supercsv.exception.SuperCsvException;
import org.supercsv.prefs.CsvPreference;

/**
 * Reads the CSV file, line by line. If you want the line-reading functionality of this class, but want to define your
 * own implementation of {@link #readColumns(List)}, then consider writing your own Tokenizer by extending
 * AbstractTokenizer.
 
 @author Kasper B. Graversen
 @author James Bassett
 */
public class Tokenizer extends AbstractTokenizer {
  
  private static final char NEWLINE = '\n';
  
  private static final char SPACE = ' ';
  
  private final StringBuilder currentColumn = new StringBuilder();
  
  /* the raw, untokenized CSV row (may span multiple lines) */
  private final StringBuilder currentRow = new StringBuilder();
  
  private final int quoteChar;
  
  private final int delimeterChar;
  
  private final boolean surroundingSpacesNeedQuotes;
  
  private final boolean ignoreEmptyLines;
  
  private final CommentMatcher commentMatcher;

  private final int maxLinesPerRow;
  
  /**
   * Enumeration of tokenizer states. QUOTE_MODE is activated between quotes.
   */
  private enum TokenizerState {
    NORMAL, QUOTE_MODE;
  }
  
  /**
   * Constructs a new <tt>Tokenizer</tt>, which reads the CSV file, line by line.
   
   @param reader
   *            the reader
   @param preferences
   *            the CSV preferences
   @throws NullPointerException
   *             if reader or preferences is null
   */
  public Tokenizer(final Reader reader, final CsvPreference preferences) {
    super(reader, preferences);
    this.quoteChar = preferences.getQuoteChar();
    this.delimeterChar = preferences.getDelimiterChar();
    this.surroundingSpacesNeedQuotes = preferences.isSurroundingSpacesNeedQuotes();
    this.ignoreEmptyLines = preferences.isIgnoreEmptyLines();
    this.commentMatcher = preferences.getCommentMatcher();
    this.maxLinesPerRow = preferences.getMaxLinesPerRow();
  }
  
  /**
   * {@inheritDoc}
   */
  public boolean readColumns(final List<String> columnsthrows IOException {
    
    ifcolumns == null ) {
      throw new NullPointerException("columns should not be null");
    }
    
    // clear the reusable List and StringBuilders
    columns.clear();
    currentColumn.setLength(0);
    currentRow.setLength(0);
    
    // read a line (ignoring empty lines/comments if necessary)
    String line;
    do {
      line = readLine();
      ifline == null ) {
        return false// EOF
      }
    }
    whileignoreEmptyLines && line.length() == || (commentMatcher != null && commentMatcher.isComment(line)) );
    
    // update the untokenized CSV row
    currentRow.append(line);
    
    // process each character in the line, catering for surrounding quotes (QUOTE_MODE)
    TokenizerState state = TokenizerState.NORMAL;
    int quoteScopeStartingLine = -1// the line number where a potential multi-line cell starts
    int potentialSpaces = 0// keep track of spaces (so leading/trailing space can be removed if required)
    int charIndex = 0;
    whiletrue ) {
      boolean endOfLineReached = charIndex == line.length();
      
      ifendOfLineReached )
      {
        ifTokenizerState.NORMAL.equals(state) ) {
          /*
           * Newline. Add any required spaces (if surrounding spaces don't need quotes) and return (we've read
           * a line!).
           */
          if!surroundingSpacesNeedQuotes ) {
            appendSpaces(currentColumn, potentialSpaces);
          }
          columns.add(currentColumn.length() ? currentColumn.toString() null)// "" -> null
          return true;
        }
        else
        {
          /*
           * Newline. Doesn't count as newline while in QUOTESCOPE. Add the newline char, reset the charIndex
           * (will update to 0 for next iteration), read in the next line, then then continue to next
           * character.
           */
          currentColumn.append(NEWLINE);
          currentRow.append(NEWLINE)// specific line terminator lost, \n will have to suffice
          
          charIndex = 0;

          if (maxLinesPerRow > && getLineNumber() - quoteScopeStartingLine + >= maxLinesPerRow) {
            /*
             * The quoted section that is being parsed spans too many lines, so to avoid excessive memory
             * usage parsing something that is probably human error anyways, throw an exception. If each
             * row is suppose to be a single line and this has been exceeded, throw a more descriptive
             * exception
             */
            String msg = maxLinesPerRow == ?
                String.format("unexpected end of line while reading quoted column on line %d",
                        getLineNumber()) :
                String.format("max number of lines to read exceeded while reading quoted column" +
                        " beginning on line %d and ending on line %d",
                        quoteScopeStartingLine, getLineNumber());
            throw new SuperCsvException(msg);
          }
          else if( (line = readLine()) == null ) {
            throw new SuperCsvException(
              String
                .format(
                  "unexpected end of file while reading quoted column beginning on line %d and ending on line %d",
                  quoteScopeStartingLine, getLineNumber()));
          }
          
          currentRow.append(line)// update untokenized CSV row
          
            if (line.length() == 0){
              // consecutive newlines
                        continue;
            }
        }
      }
      
      final char c = line.charAt(charIndex);
      
      ifTokenizerState.NORMAL.equals(state) ) {
        
        /*
         * NORMAL mode (not within quotes).
         */
        
        ifc == delimeterChar ) {
          /*
           * Delimiter. Save the column (trim trailing space if required) then continue to next character.
           */
          if!surroundingSpacesNeedQuotes ) {
            appendSpaces(currentColumn, potentialSpaces);
          }
          columns.add(currentColumn.length() ? currentColumn.toString() null)// "" -> null
          potentialSpaces = 0;
          currentColumn.setLength(0);
          
        else ifc == SPACE ) {
          /*
           * Space. Remember it, then continue to next character.
           */
          potentialSpaces++;
          
        }
        else ifc == quoteChar ) {
          /*
           * A single quote ("). Update to QUOTESCOPE (but don't save quote), then continue to next character.
           */
          state = TokenizerState.QUOTE_MODE;
          quoteScopeStartingLine = getLineNumber();
          
          // cater for spaces before a quoted section (be lenient!)
          if!surroundingSpacesNeedQuotes || currentColumn.length() ) {
            appendSpaces(currentColumn, potentialSpaces);
          }
          potentialSpaces = 0;
          
        else {
          /*
           * Just a normal character. Add any required spaces (but trim any leading spaces if surrounding
           * spaces need quotes), add the character, then continue to next character.
           */
          if!surroundingSpacesNeedQuotes || currentColumn.length() ) {
            appendSpaces(currentColumn, potentialSpaces);
          }
          
          potentialSpaces = 0;
          currentColumn.append(c);
        }
        
      else {
        
        /*
         * QUOTE_MODE (within quotes).
         */
        
        ifc == quoteChar ) {
          int nextCharIndex = charIndex + 1;
          boolean availableCharacters = nextCharIndex < line.length();
          boolean nextCharIsQuote = availableCharacters && line.charAt(nextCharIndex== quoteChar;
          ifnextCharIsQuote ) {
            /*
             * An escaped quote (""). Add a single quote, then move the cursor so the next iteration of the
             * loop will read the character following the escaped quote.
             */
            currentColumn.append(c);
            charIndex++;
            
          else {
            /*
             * A single quote ("). Update to NORMAL (but don't save quote), then continue to next character.
             */
            state = TokenizerState.NORMAL;
            quoteScopeStartingLine = -1// reset ready for next multi-line cell
          }
        else {
          /*
           * Just a normal character, delimiter (they don't count in QUOTESCOPE) or space. Add the character,
           * then continue to next character.
           */
          currentColumn.append(c);
        }
      }
      
      charIndex++; // read next char of the line
    }
  }
  
  /**
   * Appends the required number of spaces to the StringBuilder.
   
   @param sb
   *            the StringBuilder
   @param spaces
   *            the required number of spaces to append
   */
  private static void appendSpaces(final StringBuilder sb, final int spaces) {
    forint i = 0; i < spaces; i++ ) {
      sb.append(SPACE);
    }
  }
  
  /**
   * {@inheritDoc}
   */
  public String getUntokenizedRow() {
    return currentRow.toString();
  }
}