View Javadoc
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,   -&gt;
57   *     "foo", " bar om", "", "baz, , oof", "xyz", ""
58   *
59   *   foo, "bar
60   *   bar
61   *   bar", baz -&gt;
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 }