Tue, 10 Feb 2015 19:58:00 +0100
Upgrade the upgraded ical4j component to use org.apache.commons.lang3.
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, in);
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 = nextToken(tokeniser, in);
233 while (nextToken != StreamTokenizer.TT_EOL) {
235 if (tokeniser.ttype == StreamTokenizer.TT_WORD) {
236 value.append(tokeniser.sval);
237 }
238 else {
239 value.append((char) tokeniser.ttype);
240 }
242 nextToken = nextToken(tokeniser, in);
243 }
245 // reset DQUOTE to be quote char
246 tokeniser.quoteChar('"');
248 try {
249 handler.propertyValue(value.toString());
250 }
251 catch (ParseException e) {
252 final ParseException eNew = new ParseException("[" + name + "] "
253 + e.getMessage(), e.getErrorOffset());
254 eNew.initCause(e);
255 throw eNew;
256 }
258 handler.endProperty(name);
260 }
261 }
263 /**
264 * Parses a list of iCalendar parameters by parsing the specified stream tokeniser.
265 * @param tokeniser
266 * @throws IOException
267 * @throws ParserException
268 * @throws URISyntaxException
269 */
270 private class ParameterListParser {
272 public void parse(final StreamTokenizer tokeniser, Reader in,
273 final ContentHandler handler) throws IOException, ParserException,
274 URISyntaxException {
276 while (nextToken(tokeniser, in) == ';') {
277 paramParser.parse(tokeniser, in, handler);
278 }
279 }
280 }
282 /**
283 * @param tokeniser
284 * @param handler
285 * @throws IOException
286 * @throws ParserException
287 * @throws URISyntaxException
288 */
289 private class ParameterParser {
291 private void parse(final StreamTokenizer tokeniser, Reader in,
292 final ContentHandler handler) throws IOException, ParserException,
293 URISyntaxException {
295 assertToken(tokeniser, in, StreamTokenizer.TT_WORD);
297 final String paramName = tokeniser.sval;
299 // debugging..
300 if (log.isDebugEnabled()) {
301 log.debug("Parameter [" + paramName + "]");
302 }
304 assertToken(tokeniser, in, '=');
306 final StringBuffer paramValue = new StringBuffer();
308 // preserve quote chars..
309 if (nextToken(tokeniser, in) == '"') {
310 paramValue.append('"');
311 paramValue.append(tokeniser.sval);
312 paramValue.append('"');
313 }
314 else if (tokeniser.sval != null) {
315 paramValue.append(tokeniser.sval);
316 // check for additional words to account for equals (=) in param-value
317 int nextToken = nextToken(tokeniser, in);
319 while (nextToken != ';' && nextToken != ':' && nextToken != ',') {
321 if (tokeniser.ttype == StreamTokenizer.TT_WORD) {
322 paramValue.append(tokeniser.sval);
323 }
324 else {
325 paramValue.append((char) tokeniser.ttype);
326 }
328 nextToken = nextToken(tokeniser, in);
329 }
330 tokeniser.pushBack();
331 } else if(tokeniser.sval == null) {
332 tokeniser.pushBack();
333 }
335 try {
336 handler.parameter(paramName, paramValue.toString());
337 }
338 catch (ClassCastException cce) {
339 throw new ParserException("Error parsing parameter", getLineNumber(tokeniser, in), cce);
340 }
341 }
342 }
344 /**
345 * Parses an iCalendar component list from the specified stream tokeniser.
346 * @param tokeniser
347 * @throws IOException
348 * @throws ParseException
349 * @throws URISyntaxException
350 * @throws ParserException
351 */
352 private class ComponentListParser {
354 private void parse(final StreamTokenizer tokeniser, Reader in,
355 final ContentHandler handler) throws IOException, ParseException,
356 URISyntaxException, ParserException {
358 while (Component.BEGIN.equals(tokeniser.sval)) {
359 componentParser.parse(tokeniser, in, handler);
360 absorbWhitespace(tokeniser, in);
361 // assertToken(tokeniser, StreamTokenizer.TT_WORD);
362 }
363 }
364 }
366 /**
367 * Parses an iCalendar component from the specified stream tokeniser.
368 * @param tokeniser
369 * @throws IOException
370 * @throws ParseException
371 * @throws URISyntaxException
372 * @throws ParserException
373 */
374 private class ComponentParser {
376 private void parse(final StreamTokenizer tokeniser, Reader in,
377 final ContentHandler handler) throws IOException, ParseException,
378 URISyntaxException, ParserException {
380 assertToken(tokeniser, in, ':');
382 assertToken(tokeniser, in, StreamTokenizer.TT_WORD);
384 final String name = tokeniser.sval;
386 handler.startComponent(name);
388 assertToken(tokeniser, in, StreamTokenizer.TT_EOL);
390 propertyListParser.parse(tokeniser, in, handler);
392 /*
393 * // a special case for VTIMEZONE component which contains
394 * // sub-components..
395 * if (Component.VTIMEZONE.equals(name)) {
396 * parseComponentList(tokeniser, handler);
397 * }
398 * // VEVENT/VTODO components may optionally have embedded VALARM
399 * // components..
400 * else if ((Component.VEVENT.equals(name) || Component.VTODO.equals(name))
401 * && Component.BEGIN.equals(tokeniser.sval)) {
402 * parseComponentList(tokeniser, handler);
403 * }
404 */
406 assertToken(tokeniser, in, ':');
408 assertToken(tokeniser, in, name);
410 assertToken(tokeniser, in, StreamTokenizer.TT_EOL);
412 handler.endComponent(name);
413 }
414 }
416 /**
417 * Asserts that the next token in the stream matches the specified token.
418 * @param tokeniser stream tokeniser to perform assertion on
419 * @param token expected token
420 * @throws IOException when unable to read from stream
421 * @throws ParserException when next token in the stream does not match the expected token
422 */
423 private void assertToken(final StreamTokenizer tokeniser, Reader in, final int token)
424 throws IOException, ParserException {
426 if (nextToken(tokeniser, in) != token) {
427 throw new ParserException(MessageFormat.format(UNEXPECTED_TOKEN_MESSAGE, new Object[] {
428 new Integer(token), new Integer(tokeniser.ttype),
429 }), getLineNumber(tokeniser, in));
430 }
432 if (log.isDebugEnabled()) {
433 log.debug("[" + token + "]");
434 }
435 }
437 /**
438 * Asserts that the next token in the stream matches the specified token. This method is case-sensitive.
439 * @param tokeniser
440 * @param token
441 * @throws IOException
442 * @throws ParserException
443 */
444 private void assertToken(final StreamTokenizer tokeniser, Reader in, final String token)
445 throws IOException, ParserException {
446 assertToken(tokeniser, in, token, false);
447 }
449 /**
450 * Asserts that the next token in the stream matches the specified token.
451 * @param tokeniser stream tokeniser to perform assertion on
452 * @param token expected token
453 * @throws IOException when unable to read from stream
454 * @throws ParserException when next token in the stream does not match the expected token
455 */
456 private void assertToken(final StreamTokenizer tokeniser, Reader in,
457 final String token, final boolean ignoreCase) throws IOException,
458 ParserException {
460 // ensure next token is a word token..
461 assertToken(tokeniser, in, StreamTokenizer.TT_WORD);
463 if (ignoreCase) {
464 if (!token.equalsIgnoreCase(tokeniser.sval)) {
465 throw new ParserException(MessageFormat.format(UNEXPECTED_TOKEN_MESSAGE, new Object[] {
466 token, tokeniser.sval,
467 }), getLineNumber(tokeniser, in));
468 }
469 }
470 else if (!token.equals(tokeniser.sval)) {
471 throw new ParserException(MessageFormat.format(UNEXPECTED_TOKEN_MESSAGE, new Object[] {
472 token, tokeniser.sval,
473 }), getLineNumber(tokeniser, in));
474 }
476 if (log.isDebugEnabled()) {
477 log.debug("[" + token + "]");
478 }
479 }
481 /**
482 * Absorbs extraneous newlines.
483 * @param tokeniser
484 * @throws IOException
485 */
486 private void absorbWhitespace(final StreamTokenizer tokeniser, Reader in) throws IOException, ParserException {
487 // HACK: absorb extraneous whitespace between components (KOrganizer)..
488 while (nextToken(tokeniser, in) == StreamTokenizer.TT_EOL) {
489 if (log.isTraceEnabled()) {
490 log.trace("Absorbing extra whitespace..");
491 }
492 }
493 if (log.isTraceEnabled()) {
494 log.trace("Aborting: absorbing extra whitespace complete");
495 }
496 }
498 /**
499 * @param tokeniser
500 * @param in
501 * @return
502 */
503 private int getLineNumber(StreamTokenizer tokeniser, Reader in) {
504 int line = tokeniser.lineno();
505 if (tokeniser.ttype == StreamTokenizer.TT_EOL) {
506 line -= 1;
507 }
508 if (in instanceof UnfoldingReader) {
509 // need to take unfolded lines into account
510 final int unfolded = ((UnfoldingReader) in).getLinesUnfolded();
511 line += unfolded;
512 }
513 return line;
514 }
516 /**
517 * Reads the next token from the tokeniser.
518 * This method throws a ParseException when reading EOF.
519 * @param tokeniser
520 * @param in
521 * @return
522 * @throws ParseException When reading EOF.
523 */
524 private int nextToken(StreamTokenizer tokeniser, Reader in) throws IOException, ParserException {
525 int token = tokeniser.nextToken();
526 if (token == StreamTokenizer.TT_EOF) {
527 throw new ParserException("Unexpected end of file", getLineNumber(tokeniser, in));
528 }
529 return token;
530 }
531 }