Tue, 10 Feb 2015 18:12:00 +0100
Import initial revisions of existing project AndroidCaldavSyncAdapater,
forked from upstream repository at 27e8a0f8495c92e0780d450bdf0c7cec77a03a55.
1 /**
2 * Copyright (c) 2012, Ben Fortuna
3 * All rights reserved.
4 *
5 * Redistribution and use in source and binary forms, with or without
6 * modification, are permitted provided that the following conditions
7 * are met:
8 *
9 * o Redistributions of source code must retain the above copyright
10 * notice, this list of conditions and the following disclaimer.
11 *
12 * o Redistributions in binary form must reproduce the above copyright
13 * notice, this list of conditions and the following disclaimer in the
14 * documentation and/or other materials provided with the distribution.
15 *
16 * o Neither the name of Ben Fortuna nor the names of any other contributors
17 * may be used to endorse or promote products derived from this software
18 * without specific prior written permission.
19 *
20 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
21 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
22 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
23 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
24 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
25 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
26 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
27 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
28 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
29 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
30 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
31 */
32 package net.fortuna.ical4j.data;
34 import java.io.IOException;
35 import java.io.InputStream;
36 import java.io.InputStreamReader;
37 import java.io.Reader;
38 import java.io.StreamTokenizer;
39 import java.net.URISyntaxException;
40 import java.text.MessageFormat;
41 import java.text.ParseException;
43 import net.fortuna.ical4j.model.Calendar;
44 import net.fortuna.ical4j.model.Component;
46 import org.apache.commons.logging.Log;
47 import org.apache.commons.logging.LogFactory;
49 /**
50 * <pre>
51 * $Id$
52 *
53 * Created [Nov 5, 2004]
54 * </pre>
55 *
56 * The default implementation of a calendar parser.
57 * @author Ben Fortuna
58 */
59 public class CalendarParserImpl implements CalendarParser {
61 private static final int WORD_CHAR_START = 32;
63 private static final int WORD_CHAR_END = 255;
65 private static final int WHITESPACE_CHAR_START = 0;
67 private static final int WHITESPACE_CHAR_END = 20;
69 private static final String UNEXPECTED_TOKEN_MESSAGE = "Expected [{0}], read [{1}]";
71 private Log log = LogFactory.getLog(CalendarParserImpl.class);
73 private final ComponentListParser componentListParser = new ComponentListParser();
75 private final ComponentParser componentParser = new ComponentParser();
77 private final PropertyListParser propertyListParser = new PropertyListParser();
79 private final PropertyParser propertyParser = new PropertyParser();
81 private final ParameterListParser paramListParser = new ParameterListParser();
83 private final ParameterParser paramParser = new ParameterParser();
85 /**
86 * {@inheritDoc}
87 */
88 public final void parse(final InputStream in, final ContentHandler handler)
89 throws IOException, ParserException {
90 parse(new InputStreamReader(in), handler);
91 }
93 /**
94 * {@inheritDoc}
95 */
96 public final void parse(final Reader in, final ContentHandler handler)
97 throws IOException, ParserException {
99 final StreamTokenizer tokeniser = new StreamTokenizer(in);
100 try {
101 tokeniser.resetSyntax();
102 tokeniser.wordChars(WORD_CHAR_START, WORD_CHAR_END);
103 tokeniser.whitespaceChars(WHITESPACE_CHAR_START,
104 WHITESPACE_CHAR_END);
105 tokeniser.ordinaryChar(':');
106 tokeniser.ordinaryChar(';');
107 tokeniser.ordinaryChar('=');
108 tokeniser.ordinaryChar('\t');
109 tokeniser.eolIsSignificant(true);
110 tokeniser.whitespaceChars(0, 0);
111 tokeniser.quoteChar('"');
113 // BEGIN:VCALENDAR
114 assertToken(tokeniser, in, Calendar.BEGIN);
116 assertToken(tokeniser, in, ':');
118 assertToken(tokeniser, in, Calendar.VCALENDAR, true);
120 assertToken(tokeniser, in, StreamTokenizer.TT_EOL);
122 handler.startCalendar();
124 // parse calendar properties..
125 propertyListParser.parse(tokeniser, in, handler);
127 // parse components..
128 componentListParser.parse(tokeniser, in, handler);
130 // END:VCALENDAR
131 // assertToken(tokeniser,Calendar.END);
133 assertToken(tokeniser, in, ':');
135 assertToken(tokeniser, in, Calendar.VCALENDAR, true);
137 handler.endCalendar();
138 }
139 catch (Exception e) {
141 if (e instanceof IOException) {
142 throw (IOException) e;
143 }
144 if (e instanceof ParserException) {
145 throw (ParserException) e;
146 }
147 else {
148 throw new ParserException(e.getMessage(), getLineNumber(tokeniser, in), e);
149 }
150 }
151 }
153 /**
154 * Parses an iCalendar property list from the specified stream tokeniser.
155 * @param tokeniser
156 * @throws IOException
157 * @throws ParseException
158 * @throws URISyntaxException
159 * @throws URISyntaxException
160 * @throws ParserException
161 */
162 private class PropertyListParser {
164 public void parse(final StreamTokenizer tokeniser, Reader in,
165 final ContentHandler handler) throws IOException, ParseException,
166 URISyntaxException, ParserException {
168 assertToken(tokeniser, in, StreamTokenizer.TT_WORD);
170 while (/*
171 * !Component.BEGIN.equals(tokeniser.sval) &&
172 */!Component.END.equals(tokeniser.sval)) {
173 // check for timezones observances or vevent/vtodo alarms..
174 if (Component.BEGIN.equals(tokeniser.sval)) {
175 componentParser.parse(tokeniser, in, handler);
176 }
177 else {
178 propertyParser.parse(tokeniser, in, handler);
179 }
180 absorbWhitespace(tokeniser);
181 // assertToken(tokeniser, StreamTokenizer.TT_WORD);
182 }
183 }
184 }
186 /**
187 * Parses an iCalendar property from the specified stream tokeniser.
188 * @param tokeniser
189 * @throws IOException
190 * @throws ParserException
191 * @throws URISyntaxException
192 * @throws ParseException
193 */
194 private class PropertyParser {
196 private static final String PARSE_DEBUG_MESSAGE = "Property [{0}]";
198 private static final String PARSE_EXCEPTION_MESSAGE = "Property [{0}]";
200 private void parse(final StreamTokenizer tokeniser, Reader in,
201 final ContentHandler handler) throws IOException, ParserException,
202 URISyntaxException, ParseException {
204 final String name = tokeniser.sval;
206 // debugging..
207 if (log.isDebugEnabled()) {
208 log.debug(MessageFormat.format(PARSE_DEBUG_MESSAGE, new Object[] {name}));
209 }
211 handler.startProperty(name);
213 paramListParser.parse(tokeniser, in, handler);
215 // it appears that control tokens (ie. ':') are allowed
216 // after the first instance on a line is used.. as such
217 // we must continue appending to value until EOL is
218 // reached..
219 // assertToken(tokeniser, StreamTokenizer.TT_WORD);
221 // String value = tokeniser.sval;
222 final StringBuffer value = new StringBuffer();
224 // assertToken(tokeniser,StreamTokenizer.TT_EOL);
226 // DQUOTE is ordinary char for property value
227 // From sec 4.3.11 of rfc-2445:
228 // text = *(TSAFE-CHAR / ":" / DQUOTE / ESCAPED-CHAR)
229 //
230 tokeniser.ordinaryChar('"');
231 int nextToken = tokeniser.nextToken();
233 while (nextToken != StreamTokenizer.TT_EOL
234 && nextToken != StreamTokenizer.TT_EOF) {
236 if (tokeniser.ttype == StreamTokenizer.TT_WORD) {
237 value.append(tokeniser.sval);
238 }
239 else {
240 value.append((char) tokeniser.ttype);
241 }
243 nextToken = tokeniser.nextToken();
244 }
246 // reset DQUOTE to be quote char
247 tokeniser.quoteChar('"');
249 if (nextToken == StreamTokenizer.TT_EOF) {
250 throw new ParserException("Unexpected end of file",
251 getLineNumber(tokeniser, in));
252 }
254 try {
255 handler.propertyValue(value.toString());
256 }
257 catch (ParseException e) {
258 final ParseException eNew = new ParseException("[" + name + "] "
259 + e.getMessage(), e.getErrorOffset());
260 eNew.initCause(e);
261 throw eNew;
262 }
264 handler.endProperty(name);
266 }
267 }
269 /**
270 * Parses a list of iCalendar parameters by parsing the specified stream tokeniser.
271 * @param tokeniser
272 * @throws IOException
273 * @throws ParserException
274 * @throws URISyntaxException
275 */
276 private class ParameterListParser {
278 public void parse(final StreamTokenizer tokeniser, Reader in,
279 final ContentHandler handler) throws IOException, ParserException,
280 URISyntaxException {
282 while (tokeniser.nextToken() == ';') {
283 paramParser.parse(tokeniser, in, handler);
284 }
285 }
286 }
288 /**
289 * @param tokeniser
290 * @param handler
291 * @throws IOException
292 * @throws ParserException
293 * @throws URISyntaxException
294 */
295 private class ParameterParser {
297 private void parse(final StreamTokenizer tokeniser, Reader in,
298 final ContentHandler handler) throws IOException, ParserException,
299 URISyntaxException {
301 assertToken(tokeniser, in, StreamTokenizer.TT_WORD);
303 final String paramName = tokeniser.sval;
305 // debugging..
306 if (log.isDebugEnabled()) {
307 log.debug("Parameter [" + paramName + "]");
308 }
310 assertToken(tokeniser, in, '=');
312 final StringBuffer paramValue = new StringBuffer();
314 // preserve quote chars..
315 if (tokeniser.nextToken() == '"') {
316 paramValue.append('"');
317 paramValue.append(tokeniser.sval);
318 paramValue.append('"');
319 }
320 else if (tokeniser.sval != null) {
321 paramValue.append(tokeniser.sval);
322 // check for additional words to account for equals (=) in param-value
323 int nextToken = tokeniser.nextToken();
325 while (nextToken != ';' && nextToken != ':' && nextToken != ',') {
327 if (tokeniser.ttype == StreamTokenizer.TT_WORD) {
328 paramValue.append(tokeniser.sval);
329 }
330 else {
331 paramValue.append((char) tokeniser.ttype);
332 }
334 nextToken = tokeniser.nextToken();
335 }
336 tokeniser.pushBack();
337 } else if(tokeniser.sval == null) {
338 tokeniser.pushBack();
339 }
341 try {
342 handler.parameter(paramName, paramValue.toString());
343 }
344 catch (ClassCastException cce) {
345 throw new ParserException("Error parsing parameter", getLineNumber(tokeniser, in), cce);
346 }
347 }
348 }
350 /**
351 * Parses an iCalendar component list from the specified stream tokeniser.
352 * @param tokeniser
353 * @throws IOException
354 * @throws ParseException
355 * @throws URISyntaxException
356 * @throws ParserException
357 */
358 private class ComponentListParser {
360 private void parse(final StreamTokenizer tokeniser, Reader in,
361 final ContentHandler handler) throws IOException, ParseException,
362 URISyntaxException, ParserException {
364 while (Component.BEGIN.equals(tokeniser.sval)) {
365 componentParser.parse(tokeniser, in, handler);
366 absorbWhitespace(tokeniser);
367 // assertToken(tokeniser, StreamTokenizer.TT_WORD);
368 }
369 }
370 }
372 /**
373 * Parses an iCalendar component from the specified stream tokeniser.
374 * @param tokeniser
375 * @throws IOException
376 * @throws ParseException
377 * @throws URISyntaxException
378 * @throws ParserException
379 */
380 private class ComponentParser {
382 private void parse(final StreamTokenizer tokeniser, Reader in,
383 final ContentHandler handler) throws IOException, ParseException,
384 URISyntaxException, ParserException {
386 assertToken(tokeniser, in, ':');
388 assertToken(tokeniser, in, StreamTokenizer.TT_WORD);
390 final String name = tokeniser.sval;
392 handler.startComponent(name);
394 assertToken(tokeniser, in, StreamTokenizer.TT_EOL);
396 propertyListParser.parse(tokeniser, in, handler);
398 /*
399 * // a special case for VTIMEZONE component which contains
400 * // sub-components..
401 * if (Component.VTIMEZONE.equals(name)) {
402 * parseComponentList(tokeniser, handler);
403 * }
404 * // VEVENT/VTODO components may optionally have embedded VALARM
405 * // components..
406 * else if ((Component.VEVENT.equals(name) || Component.VTODO.equals(name))
407 * && Component.BEGIN.equals(tokeniser.sval)) {
408 * parseComponentList(tokeniser, handler);
409 * }
410 */
412 assertToken(tokeniser, in, ':');
414 assertToken(tokeniser, in, name);
416 assertToken(tokeniser, in, StreamTokenizer.TT_EOL);
418 handler.endComponent(name);
419 }
420 }
422 /**
423 * Asserts that the next token in the stream matches the specified token.
424 * @param tokeniser stream tokeniser to perform assertion on
425 * @param token expected token
426 * @throws IOException when unable to read from stream
427 * @throws ParserException when next token in the stream does not match the expected token
428 */
429 private void assertToken(final StreamTokenizer tokeniser, Reader in, final int token)
430 throws IOException, ParserException {
432 if (tokeniser.nextToken() != token) {
433 throw new ParserException(MessageFormat.format(UNEXPECTED_TOKEN_MESSAGE, new Object[] {
434 new Integer(token), new Integer(tokeniser.ttype),
435 }), getLineNumber(tokeniser, in));
436 }
438 if (log.isDebugEnabled()) {
439 log.debug("[" + token + "]");
440 }
441 }
443 /**
444 * Asserts that the next token in the stream matches the specified token. This method is case-sensitive.
445 * @param tokeniser
446 * @param token
447 * @throws IOException
448 * @throws ParserException
449 */
450 private void assertToken(final StreamTokenizer tokeniser, Reader in, final String token)
451 throws IOException, ParserException {
452 assertToken(tokeniser, in, token, false);
453 }
455 /**
456 * Asserts that the next token in the stream matches the specified token.
457 * @param tokeniser stream tokeniser to perform assertion on
458 * @param token expected token
459 * @throws IOException when unable to read from stream
460 * @throws ParserException when next token in the stream does not match the expected token
461 */
462 private void assertToken(final StreamTokenizer tokeniser, Reader in,
463 final String token, final boolean ignoreCase) throws IOException,
464 ParserException {
466 // ensure next token is a word token..
467 assertToken(tokeniser, in, StreamTokenizer.TT_WORD);
469 if (ignoreCase) {
470 if (!token.equalsIgnoreCase(tokeniser.sval)) {
471 throw new ParserException(MessageFormat.format(UNEXPECTED_TOKEN_MESSAGE, new Object[] {
472 token, tokeniser.sval,
473 }), getLineNumber(tokeniser, in));
474 }
475 }
476 else if (!token.equals(tokeniser.sval)) {
477 throw new ParserException(MessageFormat.format(UNEXPECTED_TOKEN_MESSAGE, new Object[] {
478 token, tokeniser.sval,
479 }), getLineNumber(tokeniser, in));
480 }
482 if (log.isDebugEnabled()) {
483 log.debug("[" + token + "]");
484 }
485 }
487 /**
488 * Absorbs extraneous newlines.
489 * @param tokeniser
490 * @throws IOException
491 */
492 private void absorbWhitespace(final StreamTokenizer tokeniser) throws IOException {
493 // HACK: absorb extraneous whitespace between components (KOrganizer)..
494 while (tokeniser.nextToken() == StreamTokenizer.TT_EOL) {
495 if (log.isTraceEnabled()) {
496 log.trace("Absorbing extra whitespace..");
497 }
498 }
499 if (log.isTraceEnabled()) {
500 log.trace("Aborting: absorbing extra whitespace complete");
501 }
502 }
504 /**
505 * @param tokeniser
506 * @param in
507 * @return
508 */
509 private int getLineNumber(StreamTokenizer tokeniser, Reader in) {
510 int line = tokeniser.lineno();
511 if (tokeniser.ttype == StreamTokenizer.TT_EOL) {
512 line -= 1;
513 }
514 if (in instanceof UnfoldingReader) {
515 // need to take unfolded lines into account
516 final int unfolded = ((UnfoldingReader) in).getLinesUnfolded();
517 line += unfolded;
518 }
519 return line;
520 }
521 }