001    package biweekly.io.text;
002    
003    import java.io.Closeable;
004    import java.io.IOException;
005    import java.io.Reader;
006    
007    import biweekly.ICalException;
008    import biweekly.parameter.ICalParameters;
009    
010    /*
011     Copyright (c) 2013, Michael Angstadt
012     All rights reserved.
013    
014     Redistribution and use in source and binary forms, with or without
015     modification, are permitted provided that the following conditions are met: 
016    
017     1. Redistributions of source code must retain the above copyright notice, this
018     list of conditions and the following disclaimer. 
019     2. Redistributions in binary form must reproduce the above copyright notice,
020     this list of conditions and the following disclaimer in the documentation
021     and/or other materials provided with the distribution. 
022    
023     THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
024     ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
025     WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
026     DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
027     ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
028     (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
029     LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
030     ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
031     (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
032     SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
033     */
034    
035    /**
036     * Parses an iCalendar data stream.
037     * @author Michael Angstadt
038     */
039    public class ICalRawReader implements Closeable {
040            private static final String NEWLINE = System.getProperty("line.separator");
041            private final FoldedLineReader reader;
042            private boolean caretDecodingEnabled = true;
043            private boolean eof = false;
044    
045            /**
046             * Creates a new reader.
047             * @param reader the reader to the data stream
048             */
049            public ICalRawReader(Reader reader) {
050                    this.reader = new FoldedLineReader(reader);
051            }
052    
053            /**
054             * Gets the line number of the last line that was read.
055             * @return the line number
056             */
057            public int getLineNum() {
058                    return reader.getLineNum();
059            }
060    
061            /**
062             * Starts or continues reading from the iCalendar data stream.
063             * @param listener handles the iCalendar data as it is read off the wire
064             * @throws IOException if there is an I/O problem
065             */
066            public void start(ICalDataStreamListener listener) throws IOException {
067                    String line;
068                    while ((line = reader.readLine()) != null) {
069                            try {
070                                    parseLine(line, listener);
071                            } catch (StopReadingException e) {
072                                    return;
073                            }
074                    }
075                    eof = true;
076            }
077    
078            private void parseLine(String line, ICalDataStreamListener listener) {
079                    String propertyName = null;
080                    ICalParameters parameters = new ICalParameters();
081                    String value = null;
082    
083                    char escapeChar = 0; //is the next char escaped?
084                    boolean inQuotes = false; //are we inside of double quotes?
085                    StringBuilder buffer = new StringBuilder();
086                    String curParamName = null;
087                    for (int i = 0; i < line.length(); i++) {
088                            char ch = line.charAt(i);
089                            if (escapeChar != 0) {
090                                    if (escapeChar == '\\') {
091                                            //backslash escaping in parameter values is not part of the standard
092                                            if (ch == '\\') {
093                                                    buffer.append(ch);
094                                            } else if (ch == 'n' || ch == 'N') {
095                                                    //newlines
096                                                    buffer.append(NEWLINE);
097                                            } else if (ch == '"') {
098                                                    //incase a double quote is escaped with a backslash
099                                                    buffer.append(ch);
100                                            } else {
101                                                    //treat the escape character as a normal character because it's not a valid escape sequence
102                                                    buffer.append(escapeChar).append(ch);
103                                            }
104                                    } else if (escapeChar == '^') {
105                                            if (ch == '^') {
106                                                    buffer.append(ch);
107                                            } else if (ch == 'n') {
108                                                    buffer.append(NEWLINE);
109                                            } else if (ch == '\'') {
110                                                    buffer.append('"');
111                                            } else {
112                                                    //treat the escape character as a normal character because it's not a valid escape sequence
113                                                    buffer.append(escapeChar).append(ch);
114                                            }
115                                    }
116                                    escapeChar = 0;
117                            } else if (ch == '\\' || (ch == '^' && caretDecodingEnabled)) {
118                                    escapeChar = ch;
119                            } else if ((ch == ';' || ch == ':') && !inQuotes) {
120                                    if (propertyName == null) {
121                                            propertyName = buffer.toString();
122                                    } else {
123                                            //parameter value
124                                            String paramValue = buffer.toString();
125                                            parameters.put(curParamName, paramValue);
126                                            curParamName = null;
127                                    }
128                                    buffer.setLength(0);
129    
130                                    if (ch == ':') {
131                                            if (i < line.length() - 1) {
132                                                    value = line.substring(i + 1);
133                                            } else {
134                                                    value = "";
135                                            }
136                                            break;
137                                    }
138                            } else if (ch == ',' && !inQuotes) {
139                                    //multi-valued parameter
140                                    parameters.put(curParamName, buffer.toString());
141                                    buffer.setLength(0);
142                            } else if (ch == '=' && curParamName == null) {
143                                    //parameter name
144                                    curParamName = buffer.toString();
145                                    buffer.setLength(0);
146                            } else if (ch == '"') {
147                                    inQuotes = !inQuotes;
148                            } else {
149                                    buffer.append(ch);
150                            }
151                    }
152    
153                    if (propertyName == null || value == null) {
154                            listener.invalidLine(line);
155                            return;
156                    }
157                    if ("BEGIN".equalsIgnoreCase(propertyName)) {
158                            listener.beginComponent(value);
159                            return;
160                    }
161                    if ("END".equalsIgnoreCase(propertyName)) {
162                            listener.endComponent(value);
163                            return;
164                    }
165                    listener.readProperty(propertyName, parameters, value);
166            }
167    
168            /**
169             * <p>
170             * Gets whether the reader will decode parameter values that use circumflex
171             * accent encoding (enabled by default). This escaping mechanism allows
172             * newlines and double quotes to be included in parameter values.
173             * </p>
174             * 
175             * <table border="1">
176             * <tr>
177             * <th>Raw Character</th>
178             * <th>Encoded Character</th>
179             * </tr>
180             * <tr>
181             * <td><code>"</code></td>
182             * <td><code>^'</code></td>
183             * </tr>
184             * <tr>
185             * <td><i>newline</i></td>
186             * <td><code>^n</code></td>
187             * </tr>
188             * <tr>
189             * <td><code>^</code></td>
190             * <td><code>^^</code></td>
191             * </tr>
192             * </table>
193             * 
194             * <p>
195             * Example:
196             * </p>
197             * 
198             * <pre>
199             * GEO;X-ADDRESS="Pittsburgh Pirates^n115 Federal St^nPitt
200             *  sburgh, PA 15212":40.446816;80.00566
201             * </pre>
202             * 
203             * @return true if circumflex accent decoding is enabled, false if not
204             * @see <a href="http://tools.ietf.org/html/rfc6868">RFC 6868</a>
205             */
206            public boolean isCaretDecodingEnabled() {
207                    return caretDecodingEnabled;
208            }
209    
210            /**
211             * <p>
212             * Sets whether the reader will decode parameter values that use circumflex
213             * accent encoding (enabled by default). This escaping mechanism allows
214             * newlines and double quotes to be included in parameter values.
215             * </p>
216             * 
217             * <table border="1">
218             * <tr>
219             * <th>Raw Character</th>
220             * <th>Encoded Character</th>
221             * </tr>
222             * <tr>
223             * <td><code>"</code></td>
224             * <td><code>^'</code></td>
225             * </tr>
226             * <tr>
227             * <td><i>newline</i></td>
228             * <td><code>^n</code></td>
229             * </tr>
230             * <tr>
231             * <td><code>^</code></td>
232             * <td><code>^^</code></td>
233             * </tr>
234             * </table>
235             * 
236             * <p>
237             * Example:
238             * </p>
239             * 
240             * <pre>
241             * GEO;X-ADDRESS="Pittsburgh Pirates^n115 Federal St^nPitt
242             *  sburgh, PA 15212":geo:40.446816,-80.00566
243             * </pre>
244             * 
245             * @param enable true to use circumflex accent decoding, false not to
246             * @see <a href="http://tools.ietf.org/html/rfc6868">RFC 6868</a>
247             */
248            public void setCaretDecodingEnabled(boolean enable) {
249                    caretDecodingEnabled = enable;
250            }
251    
252            /**
253             * Determines whether the end of the data stream has been reached.
254             * @return true if the end has been reached, false if not
255             */
256            public boolean eof() {
257                    return eof;
258            }
259    
260            /**
261             * Handles the iCalendar data as it is read off the data stream. Each one of
262             * this interface's methods may throw a {@link StopReadingException} at any
263             * time to force the parser to stop reading from the data stream. This will
264             * cause the reader to return from the {@link ICalRawReader#start} method.
265             * To continue reading from the data stream, simply call the
266             * {@link ICalRawReader#start} method again.
267             * @author Michael Angstadt
268             */
269            public static interface ICalDataStreamListener {
270                    /**
271                     * Called when a component begins (when a "BEGIN:NAME" property is
272                     * reached).
273                     * @param name the component name (e.g. "VEVENT")
274                     * @throws StopReadingException to force the reader to stop reading from
275                     * the data stream
276                     */
277                    void beginComponent(String name);
278    
279                    /**
280                     * Called when a property is read.
281                     * @param name the property name (e.g. "VERSION")
282                     * @param parameters the parameters
283                     * @param value the property value
284                     * @throws StopReadingException to force the reader to stop reading from
285                     * the data stream
286                     */
287                    void readProperty(String name, ICalParameters parameters, String value);
288    
289                    /**
290                     * Called when a component ends (when a "END:NAME" property is reached).
291                     * @param name the component name (e.g. "VEVENT")
292                     * @throws StopReadingException to force the reader to stop reading from
293                     * the data stream
294                     */
295                    void endComponent(String name);
296    
297                    /**
298                     * Called when a line cannot be parsed.
299                     * @param line the unparseable line
300                     * @throws StopReadingException to force the reader to stop reading from
301                     * the data stream
302                     */
303                    void invalidLine(String line);
304            }
305    
306            /**
307             * Instructs an {@link ICalRawReader} to stop reading from the data stream
308             * when thrown from an {@link ICalDataStreamListener} implementation.
309             * @author Michael Angstadt
310             */
311            @SuppressWarnings("serial")
312            public static class StopReadingException extends ICalException {
313                    //empty
314            }
315    
316            /**
317             * Closes the underlying {@link Reader} object.
318             */
319            public void close() throws IOException {
320                    reader.close();
321            }
322    }