/* * Java CSV is a stream based library for reading and writing * CSV and other delimited data. * * Copyright (C) Bruce Dunwiddie bruce@csvreader.com * * This library 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 2.1 of the License, or (at your option) any later version. * * This library 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 this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA */ package jef.tools.csvreader; import java.io.BufferedReader; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.Reader; import java.io.StringReader; import java.nio.charset.Charset; import java.text.NumberFormat; import java.util.HashMap; /** * A stream based parser for parsing delimited text data from a file or a * stream. */ public class CsvReader { private Reader inputStream = null; private String fileName = null; // this holds all the values for switches that the user is allowed to set private UserSettings userSettings = new UserSettings(); private Charset charset = null; private boolean useCustomRecordDelimiter = false; // this will be our working buffer to hold data chunks // read in from the data file private DataBuffer dataBuffer = new DataBuffer(); private ColumnBuffer columnBuffer = new ColumnBuffer(); private RawRecordBuffer rawBuffer = new RawRecordBuffer(); private boolean[] isQualified = null; private String rawRecord = ""; private HeadersHolder headersHolder = new HeadersHolder(); // these are all more or less global loop variables // to keep from needing to pass them all into various // methods during parsing private boolean startedColumn = false; private boolean startedWithQualifier = false; private boolean hasMoreData = true; private char lastLetter = '\0'; private boolean hasReadNextLine = false; private int columnsCount = 0; private long currentRecord = 0; private String[] values = new String[StaticSettings.INITIAL_COLUMN_COUNT]; private boolean initialized = false; private boolean closed = false; /** * Double up the text qualifier to represent an occurance of the text * qualifier. */ public static final int ESCAPE_MODE_DOUBLED = 1; /** * Use a backslash character before the text qualifier to represent an * occurance of the text qualifier. */ public static final int ESCAPE_MODE_BACKSLASH = 2; /** * Creates a {@link com.csvreader.CsvReader CsvReader} object using a file * as the data source. * * @param fileName * The path to the file to use as the data source. * @param delimiter * The character to use as the column delimiter. * @param charset * The {@link java.nio.charset.Charset Charset} to use while * parsing the data. */ public CsvReader(String fileName, char delimiter, Charset charset) throws FileNotFoundException { if (fileName == null) { throw new IllegalArgumentException( "Parameter fileName can not be null."); } if (charset == null) { throw new IllegalArgumentException( "Parameter charset can not be null."); } if (!new File(fileName).exists()) { throw new FileNotFoundException("File " + fileName + " does not exist."); } this.fileName = fileName; this.userSettings.Delimiter = delimiter; this.charset = charset; isQualified = new boolean[values.length]; } /** * Creates a {@link com.csvreader.CsvReader CsvReader} object using a file * as the data source. Uses ISO-8859-1 as the * {@link java.nio.charset.Charset Charset}. * * @param fileName * The path to the file to use as the data source. * @param delimiter * The character to use as the column delimiter. */ public CsvReader(String fileName, char delimiter) throws FileNotFoundException { this(fileName, delimiter, Charset.forName("ISO-8859-1")); } /** * Creates a {@link com.csvreader.CsvReader CsvReader} object using a file * as the data source. Uses a comma as the column delimiter and * ISO-8859-1 as the {@link java.nio.charset.Charset Charset}. * * @param fileName * The path to the file to use as the data source. */ public CsvReader(String fileName) throws FileNotFoundException { this(fileName, Letters.COMMA); } /** * Constructs a {@link com.csvreader.CsvReader CsvReader} object using a * {@link java.io.Reader Reader} object as the data source. * * @param inputStream * The stream to use as the data source. * @param delimiter * The character to use as the column delimiter. */ public CsvReader(Reader inputStream, char delimiter) { if (inputStream == null) { throw new IllegalArgumentException( "Parameter inputStream can not be null."); } this.inputStream = inputStream; this.userSettings.Delimiter = delimiter; initialized = true; isQualified = new boolean[values.length]; } /** * Constructs a {@link com.csvreader.CsvReader CsvReader} object using a * {@link java.io.Reader Reader} object as the data source. Uses a * comma as the column delimiter. * * @param inputStream * The stream to use as the data source. */ public CsvReader(Reader inputStream) { this(inputStream, Letters.COMMA); } /** * Constructs a {@link com.csvreader.CsvReader CsvReader} object using an * {@link java.io.InputStream InputStream} object as the data source. * * @param inputStream * The stream to use as the data source. * @param delimiter * The character to use as the column delimiter. * @param charset * The {@link java.nio.charset.Charset Charset} to use while * parsing the data. */ public CsvReader(InputStream inputStream, char delimiter, Charset charset) { this(new InputStreamReader(inputStream, charset), delimiter); } /** * Constructs a {@link com.csvreader.CsvReader CsvReader} object using an * {@link java.io.InputStream InputStream} object as the data * source. Uses a comma as the column delimiter. * * @param inputStream * The stream to use as the data source. * @param charset * The {@link java.nio.charset.Charset Charset} to use while * parsing the data. */ public CsvReader(InputStream inputStream, Charset charset) { this(new InputStreamReader(inputStream, charset)); } public boolean getCaptureRawRecord() { return userSettings.CaptureRawRecord; } public void setCaptureRawRecord(boolean captureRawRecord) { userSettings.CaptureRawRecord = captureRawRecord; } public String getRawRecord() { return rawRecord; } /** * Gets whether leading and trailing whitespace characters are being trimmed * from non-textqualified column data. Default is true. * * @return Whether leading and trailing whitespace characters are being * trimmed from non-textqualified column data. */ public boolean getTrimWhitespace() { return userSettings.TrimWhitespace; } /** * Sets whether leading and trailing whitespace characters should be trimmed * from non-textqualified column data or not. Default is true. * * @param trimWhitespace * Whether leading and trailing whitespace characters should be * trimmed from non-textqualified column data or not. */ public void setTrimWhitespace(boolean trimWhitespace) { userSettings.TrimWhitespace = trimWhitespace; } /** * Gets the character being used as the column delimiter. Default is comma, * ','. * * @return The character being used as the column delimiter. */ public char getDelimiter() { return userSettings.Delimiter; } /** * Sets the character to use as the column delimiter. Default is comma, ','. * * @param delimiter * The character to use as the column delimiter. */ public void setDelimiter(char delimiter) { userSettings.Delimiter = delimiter; } public char getRecordDelimiter() { return userSettings.RecordDelimiter; } /** * Sets the character to use as the record delimiter. * * @param recordDelimiter * The character to use as the record delimiter. Default is * combination of standard end of line characters for Windows, * Unix, or Mac. */ public void setRecordDelimiter(char recordDelimiter) { useCustomRecordDelimiter = true; userSettings.RecordDelimiter = recordDelimiter; } /** * Gets the character to use as a text qualifier in the data. * * @return The character to use as a text qualifier in the data. */ public char getTextQualifier() { return userSettings.TextQualifier; } /** * Sets the character to use as a text qualifier in the data. * * @param textQualifier * The character to use as a text qualifier in the data. */ public void setTextQualifier(char textQualifier) { userSettings.TextQualifier = textQualifier; } /** * Whether text qualifiers will be used while parsing or not. * * @return Whether text qualifiers will be used while parsing or not. */ public boolean getUseTextQualifier() { return userSettings.UseTextQualifier; } /** * Sets whether text qualifiers will be used while parsing or not. * * @param useTextQualifier * Whether to use a text qualifier while parsing or not. */ public void setUseTextQualifier(boolean useTextQualifier) { userSettings.UseTextQualifier = useTextQualifier; } /** * Gets the character being used as a comment signal. * * @return The character being used as a comment signal. */ public char getComment() { return userSettings.Comment; } /** * Sets the character to use as a comment signal. * * @param comment * The character to use as a comment signal. */ public void setComment(char comment) { userSettings.Comment = comment; } /** * Gets whether comments are being looked for while parsing or not. * * @return Whether comments are being looked for while parsing or not. */ public boolean getUseComments() { return userSettings.UseComments; } /** * Sets whether comments are being looked for while parsing or not. * * @param useComments * Whether comments are being looked for while parsing or not. */ public void setUseComments(boolean useComments) { userSettings.UseComments = useComments; } /** * Gets the current way to escape an occurance of the text qualifier inside * qualified data. * * @return The current way to escape an occurance of the text qualifier * inside qualified data. */ public int getEscapeMode() { return userSettings.EscapeMode; } /** * Sets the current way to escape an occurance of the text qualifier inside * qualified data. * * @param escapeMode * The way to escape an occurance of the text qualifier inside * qualified data. * @exception IllegalArgumentException * When an illegal value is specified for escapeMode. */ public void setEscapeMode(int escapeMode) throws IllegalArgumentException { if (escapeMode != ESCAPE_MODE_DOUBLED && escapeMode != ESCAPE_MODE_BACKSLASH) { throw new IllegalArgumentException( "Parameter escapeMode must be a valid value."); } userSettings.EscapeMode = escapeMode; } public boolean getSkipEmptyRecords() { return userSettings.SkipEmptyRecords; } public void setSkipEmptyRecords(boolean skipEmptyRecords) { userSettings.SkipEmptyRecords = skipEmptyRecords; } /** * Safety caution to prevent the parser from using large amounts of memory * in the case where parsing settings like file encodings don't end up * matching the actual format of a file. This switch can be turned off if * the file format is known and tested. With the switch off, the max column * lengths and max column count per record supported by the parser will * greatly increase. Default is true. * * @return The current setting of the safety switch. */ public boolean getSafetySwitch() { return userSettings.SafetySwitch; } /** * Safety caution to prevent the parser from using large amounts of memory * in the case where parsing settings like file encodings don't end up * matching the actual format of a file. This switch can be turned off if * the file format is known and tested. With the switch off, the max column * lengths and max column count per record supported by the parser will * greatly increase. Default is true. * * @param safetySwitch */ public void setSafetySwitch(boolean safetySwitch) { userSettings.SafetySwitch = safetySwitch; } /** * Gets the count of columns found in this record. * * @return The count of columns found in this record. */ public int getColumnCount() { return columnsCount; } /** * Gets the index of the current record. * * @return The index of the current record. */ public long getCurrentRecord() { return currentRecord - 1; } /** * Gets the count of headers read in by a previous call to * {@link com.csvreader.CsvReader#readHeaders readHeaders()}. * * @return The count of headers read in by a previous call to * {@link com.csvreader.CsvReader#readHeaders readHeaders()}. */ public int getHeaderCount() { return headersHolder.Length; } /** * Returns the header values as a string array. * * @return The header values as a String array. * @exception IOException * Thrown if this object has already been closed. */ public String[] getHeaders() throws IOException { checkClosed(); if (headersHolder.Headers == null) { return null; } else { // use clone here to prevent the outside code from // setting values on the array directly, which would // throw off the index lookup based on header name String[] clone = new String[headersHolder.Length]; System.arraycopy(headersHolder.Headers, 0, clone, 0, headersHolder.Length); return clone; } } public void setHeaders(String[] headers) { headersHolder.Headers = headers; headersHolder.IndexByName.clear(); if (headers != null) { headersHolder.Length = headers.length; } else { headersHolder.Length = 0; } // use headersHolder.Length here in case headers is null for (int i = 0; i < headersHolder.Length; i++) { headersHolder.IndexByName.put(headers[i], new Integer(i)); } } public String[] getValues() throws IOException { checkClosed(); // need to return a clone, and can't use clone because values.Length // might be greater than columnsCount String[] clone = new String[columnsCount]; System.arraycopy(values, 0, clone, 0, columnsCount); return clone; } /** * Returns the current column value for a given column index. * * @param columnIndex * The index of the column. * @return The current column value. * @exception IOException * Thrown if this object has already been closed. */ public String get(int columnIndex) throws IOException { checkClosed(); if (columnIndex > -1 && columnIndex < columnsCount) { return values[columnIndex]; } else { return ""; } } /** * Returns the current column value for a given column header name. * * @param headerName * The header name of the column. * @return The current column value. * @exception IOException * Thrown if this object has already been closed. */ public String get(String headerName) throws IOException { checkClosed(); return get(getIndex(headerName)); } /** * Creates a {@link com.csvreader.CsvReader CsvReader} object using a string * of data as the source. Uses ISO-8859-1 as the * {@link java.nio.charset.Charset Charset}. * * @param data * The String of data to use as the source. * @return A {@link com.csvreader.CsvReader CsvReader} object using the * String of data as the source. */ public static CsvReader parse(String data) { if (data == null) { throw new IllegalArgumentException( "Parameter data can not be null."); } return new CsvReader(new StringReader(data)); } /** * Reads another record. * * @return Whether another record was successfully read or not. * @exception IOException * Thrown if an error occurs while reading data from the * source stream. */ public boolean readRecord() throws IOException { checkClosed(); columnsCount = 0; rawBuffer.Position = 0; dataBuffer.LineStart = dataBuffer.Position; hasReadNextLine = false; // check to see if we've already found the end of data if (hasMoreData) { // loop over the data stream until the end of data is found // or the end of the record is found do { if (dataBuffer.Position == dataBuffer.Count) { checkDataLength(); } else { startedWithQualifier = false; // grab the current letter as a char char currentLetter = dataBuffer.Buffer[dataBuffer.Position]; if (userSettings.UseTextQualifier && currentLetter == userSettings.TextQualifier) { // this will be a text qualified column, so // we need to set startedWithQualifier to make it // enter the seperate branch to handle text // qualified columns lastLetter = currentLetter; // read qualified startedColumn = true; dataBuffer.ColumnStart = dataBuffer.Position + 1; startedWithQualifier = true; boolean lastLetterWasQualifier = false; char escapeChar = userSettings.TextQualifier; if (userSettings.EscapeMode == ESCAPE_MODE_BACKSLASH) { escapeChar = Letters.BACKSLASH; } boolean eatingTrailingJunk = false; boolean lastLetterWasEscape = false; boolean readingComplexEscape = false; int escape = ComplexEscape.UNICODE; int escapeLength = 0; char escapeValue = (char) 0; dataBuffer.Position++; do { if (dataBuffer.Position == dataBuffer.Count) { checkDataLength(); } else { // grab the current letter as a char currentLetter = dataBuffer.Buffer[dataBuffer.Position]; if (eatingTrailingJunk) { dataBuffer.ColumnStart = dataBuffer.Position + 1; if (currentLetter == userSettings.Delimiter) { endColumn(); } else if ((!useCustomRecordDelimiter && (currentLetter == Letters.CR || currentLetter == Letters.LF)) || (useCustomRecordDelimiter && currentLetter == userSettings.RecordDelimiter)) { endColumn(); endRecord(); } } else if (readingComplexEscape) { escapeLength++; switch (escape) { case ComplexEscape.UNICODE: escapeValue *= (char) 16; escapeValue += hexToDec(currentLetter); if (escapeLength == 4) { readingComplexEscape = false; } break; case ComplexEscape.OCTAL: escapeValue *= (char) 8; escapeValue += (char) (currentLetter - '0'); if (escapeLength == 3) { readingComplexEscape = false; } break; case ComplexEscape.DECIMAL: escapeValue *= (char) 10; escapeValue += (char) (currentLetter - '0'); if (escapeLength == 3) { readingComplexEscape = false; } break; case ComplexEscape.HEX: escapeValue *= (char) 16; escapeValue += hexToDec(currentLetter); if (escapeLength == 2) { readingComplexEscape = false; } break; } if (!readingComplexEscape) { appendLetter(escapeValue); } else { dataBuffer.ColumnStart = dataBuffer.Position + 1; } } else if (currentLetter == userSettings.TextQualifier) { if (lastLetterWasEscape) { lastLetterWasEscape = false; lastLetterWasQualifier = false; } else { updateCurrentValue(); if (userSettings.EscapeMode == ESCAPE_MODE_DOUBLED) { lastLetterWasEscape = true; } lastLetterWasQualifier = true; } } else if (userSettings.EscapeMode == ESCAPE_MODE_BACKSLASH && lastLetterWasEscape) { switch (currentLetter) { case 'n': appendLetter(Letters.LF); break; case 'r': appendLetter(Letters.CR); break; case 't': appendLetter(Letters.TAB); break; case 'b': appendLetter(Letters.BACKSPACE); break; case 'f': appendLetter(Letters.FORM_FEED); break; case 'e': appendLetter(Letters.ESCAPE); break; case 'v': appendLetter(Letters.VERTICAL_TAB); break; case 'a': appendLetter(Letters.ALERT); break; case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': escape = ComplexEscape.OCTAL; readingComplexEscape = true; escapeLength = 1; escapeValue = (char) (currentLetter - '0'); dataBuffer.ColumnStart = dataBuffer.Position + 1; break; case 'u': case 'x': case 'o': case 'd': case 'U': case 'X': case 'O': case 'D': switch (currentLetter) { case 'u': case 'U': escape = ComplexEscape.UNICODE; break; case 'x': case 'X': escape = ComplexEscape.HEX; break; case 'o': case 'O': escape = ComplexEscape.OCTAL; break; case 'd': case 'D': escape = ComplexEscape.DECIMAL; break; } readingComplexEscape = true; escapeLength = 0; escapeValue = (char) 0; dataBuffer.ColumnStart = dataBuffer.Position + 1; break; default: break; } lastLetterWasEscape = false; // can only happen for ESCAPE_MODE_BACKSLASH } else if (currentLetter == escapeChar) { updateCurrentValue(); lastLetterWasEscape = true; } else { if (lastLetterWasQualifier) { if (currentLetter == userSettings.Delimiter) { endColumn(); } else if ((!useCustomRecordDelimiter && (currentLetter == Letters.CR || currentLetter == Letters.LF)) || (useCustomRecordDelimiter && currentLetter == userSettings.RecordDelimiter)) { endColumn(); endRecord(); } else { dataBuffer.ColumnStart = dataBuffer.Position + 1; eatingTrailingJunk = true; } // make sure to clear the flag for next // run of the loop lastLetterWasQualifier = false; } } // keep track of the last letter because we need // it for several key decisions lastLetter = currentLetter; if (startedColumn) { dataBuffer.Position++; if (userSettings.SafetySwitch && dataBuffer.Position - dataBuffer.ColumnStart + columnBuffer.Position > 100000) { close(); throw new IOException( "Maximum column length of 100,000 exceeded in column " + NumberFormat .getIntegerInstance() .format( columnsCount) + " in record " + NumberFormat .getIntegerInstance() .format( currentRecord) + ". Set the SafetySwitch property to false" + " if you're expecting column lengths greater than 100,000 characters to" + " avoid this error."); } } } // end else } while (hasMoreData && startedColumn); } else if (currentLetter == userSettings.Delimiter) { // we encountered a column with no data, so // just send the end column lastLetter = currentLetter; endColumn(); } else if (useCustomRecordDelimiter && currentLetter == userSettings.RecordDelimiter) { // this will skip blank lines if (startedColumn || columnsCount > 0 || !userSettings.SkipEmptyRecords) { endColumn(); endRecord(); } else { dataBuffer.LineStart = dataBuffer.Position + 1; } lastLetter = currentLetter; } else if (!useCustomRecordDelimiter && (currentLetter == Letters.CR || currentLetter == Letters.LF)) { // this will skip blank lines if (startedColumn || columnsCount > 0 || (!userSettings.SkipEmptyRecords && (currentLetter == Letters.CR || lastLetter != Letters.CR))) { endColumn(); endRecord(); } else { dataBuffer.LineStart = dataBuffer.Position + 1; } lastLetter = currentLetter; } else if (userSettings.UseComments && columnsCount == 0 && currentLetter == userSettings.Comment) { // encountered a comment character at the beginning of // the line so just ignore the rest of the line lastLetter = currentLetter; skipLine(); } else if (userSettings.TrimWhitespace && (currentLetter == Letters.SPACE || currentLetter == Letters.TAB)) { // do nothing, this will trim leading whitespace // for both text qualified columns and non startedColumn = true; dataBuffer.ColumnStart = dataBuffer.Position + 1; } else { // since the letter wasn't a special letter, this // will be the first letter of our current column startedColumn = true; dataBuffer.ColumnStart = dataBuffer.Position; boolean lastLetterWasBackslash = false; boolean readingComplexEscape = false; int escape = ComplexEscape.UNICODE; int escapeLength = 0; char escapeValue = (char) 0; boolean firstLoop = true; do { if (!firstLoop && dataBuffer.Position == dataBuffer.Count) { checkDataLength(); } else { if (!firstLoop) { // grab the current letter as a char currentLetter = dataBuffer.Buffer[dataBuffer.Position]; } if (!userSettings.UseTextQualifier && userSettings.EscapeMode == ESCAPE_MODE_BACKSLASH && currentLetter == Letters.BACKSLASH) { if (lastLetterWasBackslash) { lastLetterWasBackslash = false; } else { updateCurrentValue(); lastLetterWasBackslash = true; } } else if (readingComplexEscape) { escapeLength++; switch (escape) { case ComplexEscape.UNICODE: escapeValue *= (char) 16; escapeValue += hexToDec(currentLetter); if (escapeLength == 4) { readingComplexEscape = false; } break; case ComplexEscape.OCTAL: escapeValue *= (char) 8; escapeValue += (char) (currentLetter - '0'); if (escapeLength == 3) { readingComplexEscape = false; } break; case ComplexEscape.DECIMAL: escapeValue *= (char) 10; escapeValue += (char) (currentLetter - '0'); if (escapeLength == 3) { readingComplexEscape = false; } break; case ComplexEscape.HEX: escapeValue *= (char) 16; escapeValue += hexToDec(currentLetter); if (escapeLength == 2) { readingComplexEscape = false; } break; } if (!readingComplexEscape) { appendLetter(escapeValue); } else { dataBuffer.ColumnStart = dataBuffer.Position + 1; } } else if (userSettings.EscapeMode == ESCAPE_MODE_BACKSLASH && lastLetterWasBackslash) { switch (currentLetter) { case 'n': appendLetter(Letters.LF); break; case 'r': appendLetter(Letters.CR); break; case 't': appendLetter(Letters.TAB); break; case 'b': appendLetter(Letters.BACKSPACE); break; case 'f': appendLetter(Letters.FORM_FEED); break; case 'e': appendLetter(Letters.ESCAPE); break; case 'v': appendLetter(Letters.VERTICAL_TAB); break; case 'a': appendLetter(Letters.ALERT); break; case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': escape = ComplexEscape.OCTAL; readingComplexEscape = true; escapeLength = 1; escapeValue = (char) (currentLetter - '0'); dataBuffer.ColumnStart = dataBuffer.Position + 1; break; case 'u': case 'x': case 'o': case 'd': case 'U': case 'X': case 'O': case 'D': switch (currentLetter) { case 'u': case 'U': escape = ComplexEscape.UNICODE; break; case 'x': case 'X': escape = ComplexEscape.HEX; break; case 'o': case 'O': escape = ComplexEscape.OCTAL; break; case 'd': case 'D': escape = ComplexEscape.DECIMAL; break; } readingComplexEscape = true; escapeLength = 0; escapeValue = (char) 0; dataBuffer.ColumnStart = dataBuffer.Position + 1; break; default: break; } lastLetterWasBackslash = false; } else { if (currentLetter == userSettings.Delimiter) { endColumn(); } else if ((!useCustomRecordDelimiter && (currentLetter == Letters.CR || currentLetter == Letters.LF)) || (useCustomRecordDelimiter && currentLetter == userSettings.RecordDelimiter)) { endColumn(); endRecord(); } } // keep track of the last letter because we need // it for several key decisions lastLetter = currentLetter; firstLoop = false; if (startedColumn) { dataBuffer.Position++; if (userSettings.SafetySwitch && dataBuffer.Position - dataBuffer.ColumnStart + columnBuffer.Position > 100000) { close(); throw new IOException( "Maximum column length of 100,000 exceeded in column " + NumberFormat .getIntegerInstance() .format( columnsCount) + " in record " + NumberFormat .getIntegerInstance() .format( currentRecord) + ". Set the SafetySwitch property to false" + " if you're expecting column lengths greater than 100,000 characters to" + " avoid this error."); } } } // end else } while (hasMoreData && startedColumn); } if (hasMoreData) { dataBuffer.Position++; } } // end else } while (hasMoreData && !hasReadNextLine); // check to see if we hit the end of the file // without processing the current record if (startedColumn || lastLetter == userSettings.Delimiter) { endColumn(); endRecord(); } } if (userSettings.CaptureRawRecord) { if (hasMoreData) { if (rawBuffer.Position == 0) { rawRecord = new String(dataBuffer.Buffer, dataBuffer.LineStart, dataBuffer.Position - dataBuffer.LineStart - 1); } else { rawRecord = new String(rawBuffer.Buffer, 0, rawBuffer.Position) + new String(dataBuffer.Buffer, dataBuffer.LineStart, dataBuffer.Position - dataBuffer.LineStart - 1); } } else { // for hasMoreData to ever be false, all data would have had to // have been // copied to the raw buffer rawRecord = new String(rawBuffer.Buffer, 0, rawBuffer.Position); } } else { rawRecord = ""; } return hasReadNextLine; } /** * @exception IOException * Thrown if an error occurs while reading data from the * source stream. */ private void checkDataLength() throws IOException { if (!initialized) { if (fileName != null) { inputStream = new BufferedReader(new InputStreamReader( new FileInputStream(fileName), charset), StaticSettings.MAX_FILE_BUFFER_SIZE); } charset = null; initialized = true; } updateCurrentValue(); if (userSettings.CaptureRawRecord && dataBuffer.Count > 0) { if (rawBuffer.Buffer.length - rawBuffer.Position < dataBuffer.Count - dataBuffer.LineStart) { int newLength = rawBuffer.Buffer.length + Math.max(dataBuffer.Count - dataBuffer.LineStart, rawBuffer.Buffer.length); char[] holder = new char[newLength]; System.arraycopy(rawBuffer.Buffer, 0, holder, 0, rawBuffer.Position); rawBuffer.Buffer = holder; } System.arraycopy(dataBuffer.Buffer, dataBuffer.LineStart, rawBuffer.Buffer, rawBuffer.Position, dataBuffer.Count - dataBuffer.LineStart); rawBuffer.Position += dataBuffer.Count - dataBuffer.LineStart; } try { dataBuffer.Count = inputStream.read(dataBuffer.Buffer, 0, dataBuffer.Buffer.length); } catch (IOException ex) { close(); throw ex; } // if no more data could be found, set flag stating that // the end of the data was found if (dataBuffer.Count == -1) { hasMoreData = false; } dataBuffer.Position = 0; dataBuffer.LineStart = 0; dataBuffer.ColumnStart = 0; } /** * Read the first record of data as column headers. * * @return Whether the header record was successfully read or not. * @exception IOException * Thrown if an error occurs while reading data from the * source stream. */ public boolean readHeaders() throws IOException { boolean result = readRecord(); // copy the header data from the column array // to the header string array headersHolder.Length = columnsCount; headersHolder.Headers = new String[columnsCount]; for (int i = 0; i < headersHolder.Length; i++) { String columnValue = get(i); headersHolder.Headers[i] = columnValue; // if there are duplicate header names, we will save the last one headersHolder.IndexByName.put(columnValue, new Integer(i)); } if (result) { currentRecord--; } columnsCount = 0; return result; } /** * Returns the column header value for a given column index. * * @param columnIndex * The index of the header column being requested. * @return The value of the column header at the given column index. * @exception IOException * Thrown if this object has already been closed. */ public String getHeader(int columnIndex) throws IOException { checkClosed(); // check to see if we have read the header record yet // check to see if the column index is within the bounds // of our header array if (columnIndex > -1 && columnIndex < headersHolder.Length) { // return the processed header data for this column return headersHolder.Headers[columnIndex]; } else { return ""; } } public boolean isQualified(int columnIndex) throws IOException { checkClosed(); if (columnIndex < columnsCount && columnIndex > -1) { return isQualified[columnIndex]; } else { return false; } } /** * @exception IOException * Thrown if a very rare extreme exception occurs during * parsing, normally resulting from improper data format. */ private void endColumn() throws IOException { String currentValue = ""; // must be called before setting startedColumn = false if (startedColumn) { if (columnBuffer.Position == 0) { if (dataBuffer.ColumnStart < dataBuffer.Position) { int lastLetter = dataBuffer.Position - 1; if (userSettings.TrimWhitespace && !startedWithQualifier) { while (lastLetter >= dataBuffer.ColumnStart && (dataBuffer.Buffer[lastLetter] == Letters.SPACE || dataBuffer.Buffer[lastLetter] == Letters.TAB)) { lastLetter--; } } currentValue = new String(dataBuffer.Buffer, dataBuffer.ColumnStart, lastLetter - dataBuffer.ColumnStart + 1); } } else { updateCurrentValue(); int lastLetter = columnBuffer.Position - 1; if (userSettings.TrimWhitespace && !startedWithQualifier) { while (lastLetter >= 0 && (columnBuffer.Buffer[lastLetter] == Letters.SPACE || columnBuffer.Buffer[lastLetter] == Letters.SPACE)) { lastLetter--; } } currentValue = new String(columnBuffer.Buffer, 0, lastLetter + 1); } } columnBuffer.Position = 0; startedColumn = false; if (columnsCount >= 100000 && userSettings.SafetySwitch) { close(); throw new IOException( "Maximum column count of 100,000 exceeded in record " + NumberFormat.getIntegerInstance().format( currentRecord) + ". Set the SafetySwitch property to false" + " if you're expecting more than 100,000 columns per record to" + " avoid this error."); } // check to see if our current holder array for // column chunks is still big enough to handle another // column chunk if (columnsCount == values.length) { // holder array needs to grow to be able to hold another column int newLength = values.length * 2; String[] holder = new String[newLength]; System.arraycopy(values, 0, holder, 0, values.length); values = holder; boolean[] qualifiedHolder = new boolean[newLength]; System.arraycopy(isQualified, 0, qualifiedHolder, 0, isQualified.length); isQualified = qualifiedHolder; } values[columnsCount] = currentValue; isQualified[columnsCount] = startedWithQualifier; currentValue = ""; columnsCount++; } private void appendLetter(char letter) { if (columnBuffer.Position == columnBuffer.Buffer.length) { int newLength = columnBuffer.Buffer.length * 2; char[] holder = new char[newLength]; System.arraycopy(columnBuffer.Buffer, 0, holder, 0, columnBuffer.Position); columnBuffer.Buffer = holder; } columnBuffer.Buffer[columnBuffer.Position++] = letter; dataBuffer.ColumnStart = dataBuffer.Position + 1; } private void updateCurrentValue() { if (startedColumn && dataBuffer.ColumnStart < dataBuffer.Position) { if (columnBuffer.Buffer.length - columnBuffer.Position < dataBuffer.Position - dataBuffer.ColumnStart) { int newLength = columnBuffer.Buffer.length + Math.max( dataBuffer.Position - dataBuffer.ColumnStart, columnBuffer.Buffer.length); char[] holder = new char[newLength]; System.arraycopy(columnBuffer.Buffer, 0, holder, 0, columnBuffer.Position); columnBuffer.Buffer = holder; } System.arraycopy(dataBuffer.Buffer, dataBuffer.ColumnStart, columnBuffer.Buffer, columnBuffer.Position, dataBuffer.Position - dataBuffer.ColumnStart); columnBuffer.Position += dataBuffer.Position - dataBuffer.ColumnStart; } dataBuffer.ColumnStart = dataBuffer.Position + 1; } /** * @exception IOException * Thrown if an error occurs while reading data from the * source stream. */ private void endRecord() throws IOException { // this flag is used as a loop exit condition // during parsing hasReadNextLine = true; currentRecord++; } /** * Gets the corresponding column index for a given column header name. * * @param headerName * The header name of the column. * @return The column index for the given column header name. Returns * -1 if not found. * @exception IOException * Thrown if this object has already been closed. */ public int getIndex(String headerName) throws IOException { checkClosed(); Object indexValue = headersHolder.IndexByName.get(headerName); if (indexValue != null) { return ((Integer) indexValue).intValue(); } else { return -1; } } /** * Skips the next record of data by parsing each column. Does not * increment * {@link com.csvreader.CsvReader#getCurrentRecord getCurrentRecord()}. * * @return Whether another record was successfully skipped or not. * @exception IOException * Thrown if an error occurs while reading data from the * source stream. */ public boolean skipRecord() throws IOException { checkClosed(); boolean recordRead = false; if (hasMoreData) { recordRead = readRecord(); if (recordRead) { currentRecord--; } } return recordRead; } /** * Skips the next line of data using the standard end of line characters and * does not do any column delimited parsing. * * @return Whether a line was successfully skipped or not. * @exception IOException * Thrown if an error occurs while reading data from the * source stream. */ public boolean skipLine() throws IOException { checkClosed(); // clear public column values for current line columnsCount = 0; boolean skippedLine = false; if (hasMoreData) { boolean foundEol = false; do { if (dataBuffer.Position == dataBuffer.Count) { checkDataLength(); } else { skippedLine = true; // grab the current letter as a char char currentLetter = dataBuffer.Buffer[dataBuffer.Position]; if (currentLetter == Letters.CR || currentLetter == Letters.LF) { foundEol = true; } // keep track of the last letter because we need // it for several key decisions lastLetter = currentLetter; if (!foundEol) { dataBuffer.Position++; } } // end else } while (hasMoreData && !foundEol); columnBuffer.Position = 0; dataBuffer.LineStart = dataBuffer.Position + 1; } rawBuffer.Position = 0; rawRecord = ""; return skippedLine; } /** * Closes and releases all related resources. */ public void close() { if (!closed) { close(true); closed = true; } } /** * */ private void close(boolean closing) { if (!closed) { if (closing) { charset = null; headersHolder.Headers = null; headersHolder.IndexByName = null; dataBuffer.Buffer = null; columnBuffer.Buffer = null; rawBuffer.Buffer = null; } try { if (initialized) { inputStream.close(); } } catch (Exception e) { // just eat the exception } inputStream = null; closed = true; } } /** * @exception IOException * Thrown if this object has already been closed. */ private void checkClosed() throws IOException { if (closed) { throw new IOException( "This instance of the CsvReader class has already been closed."); } } /** * */ protected void finalize() { close(false); } private class ComplexEscape { private static final int UNICODE = 1; private static final int OCTAL = 2; private static final int DECIMAL = 3; private static final int HEX = 4; } private static char hexToDec(char hex) { char result; if (hex >= 'a') { result = (char) (hex - 'a' + 10); } else if (hex >= 'A') { result = (char) (hex - 'A' + 10); } else { result = (char) (hex - '0'); } return result; } private class DataBuffer { public char[] Buffer; public int Position; // / <summary> // / How much usable data has been read into the stream, // / which will not always be as long as Buffer.Length. // / </summary> public int Count; // / <summary> // / The position of the cursor in the buffer when the // / current column was started or the last time data // / was moved out to the column buffer. // / </summary> public int ColumnStart; public int LineStart; public DataBuffer() { Buffer = new char[StaticSettings.MAX_BUFFER_SIZE]; Position = 0; Count = 0; ColumnStart = 0; LineStart = 0; } } private class ColumnBuffer { public char[] Buffer; public int Position; public ColumnBuffer() { Buffer = new char[StaticSettings.INITIAL_COLUMN_BUFFER_SIZE]; Position = 0; } } private class RawRecordBuffer { public char[] Buffer; public int Position; public RawRecordBuffer() { Buffer = new char[StaticSettings.INITIAL_COLUMN_BUFFER_SIZE * StaticSettings.INITIAL_COLUMN_COUNT]; Position = 0; } } private class Letters { public static final char LF = '\n'; public static final char CR = '\r'; public static final char QUOTE = '"'; public static final char COMMA = ','; public static final char SPACE = ' '; public static final char TAB = '\t'; public static final char POUND = '#'; public static final char BACKSLASH = '\\'; public static final char NULL = '\0'; public static final char BACKSPACE = '\b'; public static final char FORM_FEED = '\f'; public static final char ESCAPE = '\u001B'; // ASCII/ANSI escape public static final char VERTICAL_TAB = '\u000B'; public static final char ALERT = '\u0007'; } private class UserSettings { // having these as publicly accessible members will prevent // the overhead of the method call that exists on properties public boolean CaseSensitive; public char TextQualifier; public boolean TrimWhitespace; public boolean UseTextQualifier; public char Delimiter; public char RecordDelimiter; public char Comment; public boolean UseComments; public int EscapeMode; public boolean SafetySwitch; public boolean SkipEmptyRecords; public boolean CaptureRawRecord; public UserSettings() { CaseSensitive = true; TextQualifier = Letters.QUOTE; TrimWhitespace = true; UseTextQualifier = true; Delimiter = Letters.COMMA; RecordDelimiter = Letters.NULL; Comment = Letters.POUND; UseComments = false; EscapeMode = CsvReader.ESCAPE_MODE_DOUBLED; SafetySwitch = true; SkipEmptyRecords = true; CaptureRawRecord = true; } } private class HeadersHolder { public String[] Headers; public int Length; public HashMap<String,Integer> IndexByName; public HeadersHolder() { Headers = null; Length = 0; IndexByName = new HashMap<String,Integer>(); } } private class StaticSettings { // these are static instead of final so they can be changed in unit test // isn't visible outside this class and is only accessed once during // CsvReader construction public static final int MAX_BUFFER_SIZE = 1024; public static final int MAX_FILE_BUFFER_SIZE = 4 * 1024; public static final int INITIAL_COLUMN_COUNT = 10; public static final int INITIAL_COLUMN_BUFFER_SIZE = 50; } }