src/net/fortuna/ical4j/data/CalendarParserImpl.java

Tue, 10 Feb 2015 21:32:00 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Tue, 10 Feb 2015 21:32:00 +0100
changeset 5
e0e108e77052
parent 0
fb9019fb1bf7
permissions
-rw-r--r--

Complete porting of ical4j to post getContextClassLoader() Android era,
correcting critical flaws reported by users on the upstream bugtracker
including https://github.com/gggard/AndroidCaldavSyncAdapater/issues/224/

     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              *         &amp;&amp; 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 }

mercurial