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

Tue, 10 Feb 2015 18:12:00 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Tue, 10 Feb 2015 18:12:00 +0100
changeset 0
fb9019fb1bf7
child 3
73bdfa70b04e
permissions
-rw-r--r--

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

mercurial