1 /*
2 * $Source$
3 * $Revision$
4 *
5 * Copyright (C) 2001 Myles Chippendale
6 *
7 * Part of Melati (http://melati.org), a framework for the rapid
8 * development of clean, maintainable web applications.
9 *
10 * Melati is free software; Permission is granted to copy, distribute
11 * and/or modify this software under the terms either:
12 *
13 * a) the GNU General Public License as published by the Free Software
14 * Foundation; either version 2 of the License, or (at your option)
15 * any later version,
16 *
17 * or
18 *
19 * b) any version of the Melati Software License, as published
20 * at http://melati.org
21 *
22 * You should have received a copy of the GNU General Public License and
23 * the Melati Software License along with this program;
24 * if not, write to the Free Software Foundation, Inc.,
25 * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA to obtain the
26 * GNU General Public License and visit http://melati.org to obtain the
27 * Melati Software License.
28 *
29 * Feel free to contact the Developers of Melati (http://melati.org),
30 * if you would like to work out a different arrangement than the options
31 * outlined here. It is our intention to allow Melati to be used by as
32 * wide an audience as possible.
33 *
34 * This program is distributed in the hope that it will be useful,
35 * but WITHOUT ANY WARRANTY; without even the implied warranty of
36 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
37 * GNU General Public License for more details.
38 *
39 * Contact details for copyright holder:
40 *
41 * Myles Chippendale <mylesc At paneris.org>
42 */
43 package org.melati.poem.csv;
44
45 import java.io.BufferedReader;
46 import java.io.File;
47 import java.io.FileReader;
48 import java.io.IOException;
49 import java.util.NoSuchElementException;
50
51 /**
52 * A utility for tokenising a file made up of comma-separated
53 * variables. We allow for fields having returns in them.
54 *
55 * <PRE>
56 * foo, bar om,,"baz, ,oof",xyz, ->
57 * "foo", " bar om", "", "baz, , oof", "xyz", ""
58 *
59 * foo, "bar
60 * bar
61 * bar", baz ->
62 * "foo", "bar\u0015bar\u0015bar", "baz"
63 * </PRE>
64 *
65 * Each record (which is usually a line, unless some fields have
66 * a line break in them) is accessed one at a time by calling
67 * <code>nextRecord()</code>. Within each record
68 * <code>recordHasMoreFields()</code> and <code>nextField()</code>
69 * can be used like an Enumeration to iterate through the fields.
70 *
71 * @author mylesc, based heavily on
72 * orginal CSVStringEnumeration williamc
73 */
74
75 public class CSVFileParser {
76
77 private BufferedReader reader = null;
78
79 int lineNo = 0; // The first line will be line '1'
80 private String line = "";
81 private boolean emptyLastField = false;
82 int p = 0;
83
84 /**
85 * Constructor.
86 * @param reader file reader
87 */
88 public CSVFileParser(BufferedReader reader) {
89 this.reader = reader;
90 }
91
92 /**
93 * @return whether there is another line
94 */
95 public boolean nextRecord() throws IOException {
96 return nextLine();
97 }
98
99 private boolean nextLine() throws IOException {
100 // Not confident about this
101 // but we need to return false if we have reached end and closed the file
102 if (!reader.ready()) return false;
103 line = reader.readLine();
104 // This should be false anyway if we're called from nextToken()
105 emptyLastField = false;
106 p = 0;
107 if (line == null) {
108 reader.close();
109 return false;
110 }
111 lineNo++;
112 return true;
113 }
114
115 /**
116 * Return the line number.
117 *
118 * @return the current lineNo
119 */
120 public int getLineNo() {
121 return lineNo;
122 }
123
124 /**
125 * Are there any more tokens to come?
126 * @return whether there are more fields
127 */
128 public boolean recordHasMoreFields() {
129 return emptyLastField || p < line.length();
130 }
131
132 /**
133 * @return the next token as a String
134 */
135 public String nextField() throws IOException {
136 return nextToken(false);
137 }
138
139 /**
140 * @return the next token as a String
141 */
142 private String nextToken(boolean inUnclosedQuotes) throws IOException {
143
144 if (emptyLastField) {
145 emptyLastField = false;
146 return "";
147 }
148
149 if (p >= line.length()) throw new NoSuchElementException();
150
151 if (inUnclosedQuotes || (line.charAt(p) == '"' && (++p>0))) {
152
153 // we need to allow for quotes inside quoted fields, so now test for ",
154 int q = line.indexOf("\",", p);
155 // if it is not there, we are (hopefully) at the end of a line
156 if (q == -1 && (line.indexOf('"', p) == line.length()-1))
157 q = line.length()-1;
158
159 // If we don't find the end quote try reading in more lines
160 // since fields can have \n in them
161 if (q == -1) {
162 String sofar = line.substring(p, line.length());
163 if (!nextLine())
164 throw new IllegalArgumentException("Unclosed quotes on line "
165 + lineNo);
166 return sofar + "\n" + nextToken(true);
167 }
168
169 String it = line.substring(p, q);
170
171 ++q;
172 p = q+1;
173 if (q < line.length()) {
174 if (line.charAt(q) != ',') {
175 p = line.length();
176 throw new IllegalArgumentException("No comma after quotes on line "
177 + lineNo);
178 }
179 else if (q == line.length() - 1)
180 emptyLastField = true;
181 }
182 return it;
183 } else {
184 int q = line.indexOf(',', p);
185 if (q == -1) {
186 String it = line.substring(p);
187 p = line.length();
188 return it;
189 } else {
190 String it = line.substring(p, q);
191 if (q == line.length() - 1)
192 emptyLastField = true;
193 p = q + 1;
194 return it;
195 }
196 }
197 }
198
199 /**
200 * Test harness.
201 * @param args arguments
202 * @throws Exception if anything fails
203 */
204 public static void main(String[] args) throws Exception {
205
206 System.out.println("***** Reading file " + args[0]);
207
208 BufferedReader reader = new BufferedReader(
209 new FileReader(new File(args[0])));
210 CSVFileParser toks = new CSVFileParser(reader);
211
212 int recordCount = 0;
213 while(toks.nextRecord()) {
214 System.out.println("*** Record " + ++recordCount);
215 int i = 0;
216 while (toks.recordHasMoreFields()) {
217 System.out.println("Field " + ++i + ":" + toks.nextField());
218 }
219 }
220 }
221
222
223 }