/* * $Id: CalendarParserImpl.java,v 1.19 2006/05/27 13:20:06 fortuna Exp $ [Nov * 5, 2004] * * Copyright (c) 2004, Ben Fortuna All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * o Redistributions of source code must retain the above copyright notice, this * list of conditions and the following disclaimer. * * o Redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the documentation * and/or other materials provided with the distribution. * * o Neither the name of Ben Fortuna nor the names of any other contributors may * be used to endorse or promote products derived from this software without * specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ package net.fortuna.ical4j.data; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.Reader; import java.io.StreamTokenizer; import java.net.URISyntaxException; import java.text.ParseException; import net.fortuna.ical4j.model.Calendar; import net.fortuna.ical4j.model.Component; import net.fortuna.ical4j.util.Strings; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; /** * The default implementation of a calendar parser. * * @author Ben Fortuna */ public class CalendarParserImpl implements CalendarParser { private static final int WORD_CHAR_START = 32; private static final int WORD_CHAR_END = 255; private static final int WHITESPACE_CHAR_START = 0; private static final int WHITESPACE_CHAR_END = 20; private Log log = LogFactory.getLog(CalendarParserImpl.class); /* (non-Javadoc) * @see net.fortuna.ical4j.data.CalendarParser#parse(java.io.InputStream, net.fortuna.ical4j.data.ContentHandler) */ public final void parse(final InputStream in, final ContentHandler handler) throws IOException, ParserException { parse(new InputStreamReader(in), handler); } /* (non-Javadoc) * @see net.fortuna.ical4j.data.CalendarParser#parse(java.io.Reader, net.fortuna.ical4j.data.ContentHandler) */ public final void parse(final Reader in, final ContentHandler handler) throws IOException, ParserException { StreamTokenizer tokeniser = null; try { tokeniser = new StreamTokenizer(in); tokeniser.resetSyntax(); tokeniser.wordChars(WORD_CHAR_START, WORD_CHAR_END); tokeniser.whitespaceChars(WHITESPACE_CHAR_START, WHITESPACE_CHAR_END); tokeniser.ordinaryChar(':'); tokeniser.ordinaryChar(';'); tokeniser.ordinaryChar('='); tokeniser.eolIsSignificant(true); tokeniser.whitespaceChars(0, 0); tokeniser.quoteChar('"'); // BEGIN:VCALENDAR assertToken(tokeniser, Calendar.BEGIN); assertToken(tokeniser, ':'); assertToken(tokeniser, Calendar.VCALENDAR, true); assertToken(tokeniser, StreamTokenizer.TT_EOL); handler.startCalendar(); // parse calendar properties.. parsePropertyList(tokeniser, handler); // parse components.. parseComponentList(tokeniser, handler); // END:VCALENDAR //assertToken(tokeniser,Calendar.END); assertToken(tokeniser, ':'); assertToken(tokeniser, Calendar.VCALENDAR, true); handler.endCalendar(); } catch (Exception e) { if (e instanceof IOException) { throw (IOException) e; } if (e instanceof ParserException) { throw (ParserException) e; } else { String error = "An error ocurred during parsing"; if (tokeniser != null) { int line = tokeniser.lineno(); if (in instanceof UnfoldingReader) { // need to take unfolded lines into account line += ((UnfoldingReader) in).getLinesUnfolded(); } error += " - line: " + line; } throw new ParserException(error, e); } } } /** * Parses an iCalendar property list from the specified stream tokeniser. * * @param tokeniser * @throws IOException * @throws ParseException * @throws URISyntaxException * @throws URISyntaxException * @throws ParserException */ private void parsePropertyList(final StreamTokenizer tokeniser, final ContentHandler handler) throws IOException, ParseException, URISyntaxException, ParserException { assertToken(tokeniser, StreamTokenizer.TT_WORD); while (/*!Component.BEGIN.equals(tokeniser.sval) && */!Component.END.equals(tokeniser.sval)) { // check for timezones observances or vevent/vtodo alarms.. if (Component.BEGIN.equals(tokeniser.sval)) { parseComponent(tokeniser, handler); } else { parseProperty(tokeniser, handler); } absorbWhitespace(tokeniser); // assertToken(tokeniser, StreamTokenizer.TT_WORD); } } /** * Parses an iCalendar property from the specified stream tokeniser. * * @param tokeniser * @throws IOException * @throws ParserException * @throws URISyntaxException * @throws ParseException */ private void parseProperty(final StreamTokenizer tokeniser, final ContentHandler handler) throws IOException, ParserException, URISyntaxException, ParseException { String name = tokeniser.sval; // debugging.. if (log.isDebugEnabled()) { log.debug("Property [" + name + "]"); } handler.startProperty(name); parseParameterList(tokeniser, handler); // it appears that control tokens (ie. ':') are allowed // after the first instance on a line is used.. as such // we must continue appending to value until EOL is // reached.. //assertToken(tokeniser, StreamTokenizer.TT_WORD); //String value = tokeniser.sval; StringBuffer value = new StringBuffer(); //assertToken(tokeniser,StreamTokenizer.TT_EOL); int nextToken = tokeniser.nextToken(); while (nextToken != StreamTokenizer.TT_EOL && nextToken != StreamTokenizer.TT_EOF) { if (tokeniser.ttype == StreamTokenizer.TT_WORD) { value.append(tokeniser.sval); } else if (tokeniser.ttype == '"') { value.append((char) tokeniser.ttype); value.append(tokeniser.sval); value.append((char) tokeniser.ttype); } else { value.append((char) tokeniser.ttype); } nextToken = tokeniser.nextToken(); } if (nextToken == StreamTokenizer.TT_EOF) { throw new ParserException("Unexpected end of file at line " + tokeniser.lineno()); } handler.propertyValue(Strings.unescape(value.toString())); handler.endProperty(name); } /** * Parses a list of iCalendar parameters by parsing the specified stream * tokeniser. * * @param tokeniser * @throws IOException * @throws ParserException * @throws URISyntaxException */ private void parseParameterList(final StreamTokenizer tokeniser, final ContentHandler handler) throws IOException, ParserException, URISyntaxException { while (tokeniser.nextToken() == ';') { parseParameter(tokeniser, handler); } } /** * @param tokeniser * @param handler * @throws IOException * @throws ParserException * @throws URISyntaxException */ private void parseParameter(final StreamTokenizer tokeniser, final ContentHandler handler) throws IOException, ParserException, URISyntaxException { assertToken(tokeniser, StreamTokenizer.TT_WORD); String paramName = tokeniser.sval; // debugging.. if (log.isDebugEnabled()) { log.debug("Parameter [" + paramName + "]"); } assertToken(tokeniser, '='); StringBuffer paramValue = new StringBuffer(); // preserve quote chars.. if (tokeniser.nextToken() == '"') { paramValue.append('"'); paramValue.append(tokeniser.sval); paramValue.append('"'); } else { paramValue.append(tokeniser.sval); } handler.parameter(paramName, Strings .unescape(paramValue.toString())); } /** * Parses an iCalendar component list from the specified stream tokeniser. * * @param tokeniser * @throws IOException * @throws ParseException * @throws URISyntaxException * @throws ParserException */ private void parseComponentList(final StreamTokenizer tokeniser, final ContentHandler handler) throws IOException, ParseException, URISyntaxException, ParserException { while (Component.BEGIN.equals(tokeniser.sval)) { parseComponent(tokeniser, handler); absorbWhitespace(tokeniser); // assertToken(tokeniser, StreamTokenizer.TT_WORD); } } /** * Parses an iCalendar component from the specified stream tokeniser. * * @param tokeniser * @throws IOException * @throws ParseException * @throws URISyntaxException * @throws ParserException */ private void parseComponent(final StreamTokenizer tokeniser, final ContentHandler handler) throws IOException, ParseException, URISyntaxException, ParserException { assertToken(tokeniser, ':'); assertToken(tokeniser, StreamTokenizer.TT_WORD); String name = tokeniser.sval; handler.startComponent(name); assertToken(tokeniser, StreamTokenizer.TT_EOL); parsePropertyList(tokeniser, handler); /* // a special case for VTIMEZONE component which contains // sub-components.. if (Component.VTIMEZONE.equals(name)) { parseComponentList(tokeniser, handler); } // VEVENT/VTODO components may optionally have embedded VALARM // components.. else if ((Component.VEVENT.equals(name) || Component.VTODO.equals(name)) && Component.BEGIN.equals(tokeniser.sval)) { parseComponentList(tokeniser, handler); } */ assertToken(tokeniser, ':'); assertToken(tokeniser, name); assertToken(tokeniser, StreamTokenizer.TT_EOL); handler.endComponent(name); } /** * Asserts that the next token in the stream matches the specified token. * * @param tokeniser * stream tokeniser to perform assertion on * @param token * expected token * @throws IOException * when unable to read from stream * @throws ParserException * when next token in the stream does not match the expected * token */ private void assertToken(final StreamTokenizer tokeniser, final int token) throws IOException, ParserException { if (tokeniser.nextToken() != token) { throw new ParserException("Expected [" + token + "], read [" + tokeniser.ttype + "] at line " + tokeniser.lineno()); } if (log.isDebugEnabled()) { log.debug("[" + token + "]"); } } /** * Asserts that the next token in the stream matches the specified token. This method * is case-sensitive. * @param tokeniser * @param token * @throws IOException * @throws ParserException */ private void assertToken(final StreamTokenizer tokeniser, final String token) throws IOException, ParserException { assertToken(tokeniser, token, false); } /** * Asserts that the next token in the stream matches the specified token. * * @param tokeniser * stream tokeniser to perform assertion on * @param token * expected token * @throws IOException * when unable to read from stream * @throws ParserException * when next token in the stream does not match the expected * token */ private void assertToken(final StreamTokenizer tokeniser, final String token, final boolean ignoreCase) throws IOException, ParserException { // ensure next token is a word token.. assertToken(tokeniser, StreamTokenizer.TT_WORD); if (ignoreCase) { if (!token.equalsIgnoreCase(tokeniser.sval)) { throw new ParserException("Expected [" + token + "], read [" + tokeniser.sval + "] at line " + tokeniser.lineno()); } } else if (!token.equals(tokeniser.sval)) { throw new ParserException("Expected [" + token + "], read [" + tokeniser.sval + "] at line " + tokeniser.lineno()); } if (log.isDebugEnabled()) { log.debug("[" + token + "]"); } } /** * Absorbs extraneous newlines. * @param tokeniser * @throws IOException */ private void absorbWhitespace(final StreamTokenizer tokeniser) throws IOException { // HACK: absorb extraneous whitespace between components (KOrganizer).. try { while (true) { assertToken(tokeniser, StreamTokenizer.TT_EOL); } } catch (ParserException pe) { if (log.isDebugEnabled()) { log.debug("Aborting absorbing extra whitespace [" + pe.getMessage() + "]"); } } } }