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