View Javadoc

1   /*
2    * $Id: CalendarBuilder.java [Apr 5, 2004]
3    *
4    * Copyright (c) 2004, Ben Fortuna
5    * All rights reserved.
6    *
7    * Redistribution and use in source and binary forms, with or without
8    * modification, are permitted provided that the following conditions
9    * are met:
10   *
11   * 	o Redistributions of source code must retain the above copyright
12   * notice, this list of conditions and the following disclaimer.
13   *
14   * 	o Redistributions in binary form must reproduce the above copyright
15   * notice, this list of conditions and the following disclaimer in the
16   * documentation and/or other materials provided with the distribution.
17   *
18   * 	o Neither the name of Ben Fortuna nor the names of any other contributors
19   * may be used to endorse or promote products derived from this software
20   * without specific prior written permission.
21   *
22   * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
23   * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
24   * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
25   * A PARTICULAR PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE COPYRIGHT OWNER OR
26   * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
27   * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
28   * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
29   * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
30   * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
31   * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
32   * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
33   */
34  package net.fortuna.ical4j.data;
35  
36  import java.io.IOException;
37  import java.io.InputStream;
38  import java.io.InputStreamReader;
39  import java.io.Reader;
40  import java.io.StreamTokenizer;
41  import java.net.URISyntaxException;
42  import java.text.ParseException;
43  
44  import net.fortuna.ical4j.model.Calendar;
45  import net.fortuna.ical4j.model.Component;
46  import net.fortuna.ical4j.model.ComponentFactory;
47  import net.fortuna.ical4j.model.ComponentList;
48  import net.fortuna.ical4j.model.Parameter;
49  import net.fortuna.ical4j.model.ParameterFactory;
50  import net.fortuna.ical4j.model.ParameterList;
51  import net.fortuna.ical4j.model.Property;
52  import net.fortuna.ical4j.model.PropertyFactory;
53  import net.fortuna.ical4j.model.PropertyList;
54  
55  import org.apache.commons.logging.Log;
56  import org.apache.commons.logging.LogFactory;
57  
58  /***
59   * Parses and builds an iCalendar model from an input stream.
60   *
61   * @author benf
62   */
63  public class CalendarBuilder {
64  
65      private static final int WORD_CHAR_START = 32;
66  
67      private static final int WORD_CHAR_END = 126;
68  
69      private static final int WHITESPACE_CHAR_START = 0;
70  
71      private static final int WHITESPACE_CHAR_END = 20;
72  
73      private static Log log = LogFactory.getLog(CalendarBuilder.class);
74  
75      /***
76       * Builds an iCalendar model from the specified input stream.
77       *
78       * @param in
79       * @return a calendar
80       * @throws IOException
81       * @throws ParseException
82       * @throws URISyntaxException
83       * @throws BuilderException
84       */
85      public final Calendar build(final InputStream in) throws IOException,
86              BuilderException {
87  
88          return build(new InputStreamReader(in));
89      }
90  
91      /***
92       * Builds an iCalendar model from the specified reader.
93       * An <code>UnfoldingReader</code> is applied to the specified
94       * reader to ensure the data stream is correctly unfolded where
95       * appropriate.
96       *
97       * @param in
98       * @return a calendar
99       * @throws IOException
100      * @throws ParseException
101      * @throws URISyntaxException
102      * @throws BuilderException
103      */
104     public final Calendar build(final Reader in) throws IOException,
105             BuilderException {
106 
107         try {
108             UnfoldingReader uin = new UnfoldingReader(in);
109 
110             StreamTokenizer tokeniser = new StreamTokenizer(uin);
111             tokeniser.resetSyntax();
112             tokeniser.wordChars(WORD_CHAR_START, WORD_CHAR_END);
113             tokeniser.whitespaceChars(WHITESPACE_CHAR_START,
114                     WHITESPACE_CHAR_END);
115             tokeniser.ordinaryChar(':');
116             tokeniser.ordinaryChar(';');
117             tokeniser.ordinaryChar('=');
118             tokeniser.eolIsSignificant(true);
119             tokeniser.whitespaceChars(0, 0);
120             tokeniser.quoteChar('"');
121 
122             PropertyList properties = null;
123             ComponentList components = null;
124 
125             // BEGIN:VCALENDAR
126             assertToken(tokeniser, Calendar.BEGIN);
127 
128             assertToken(tokeniser, ':');
129 
130             assertToken(tokeniser, Calendar.VCALENDAR);
131 
132             assertToken(tokeniser, StreamTokenizer.TT_EOL);
133 
134             // build calendar properties..
135             properties = buildPropertyList(tokeniser);
136 
137             // build components..
138             components = buildComponentList(tokeniser);
139 
140             // END:VCALENDAR
141             //assertToken(tokeniser,Calendar.END);
142 
143             assertToken(tokeniser, ':');
144 
145             assertToken(tokeniser, Calendar.VCALENDAR);
146 
147             // construct calendar instance..
148             return new Calendar(properties, components);
149         }
150         catch (Exception e) {
151 
152             if (e instanceof IOException) { throw (IOException) e; }
153             if (e instanceof BuilderException) {
154                 throw (BuilderException) e;
155             }
156             else {
157                 throw new BuilderException("An error ocurred during parsing", e);
158             }
159         }
160     }
161 
162     /***
163      * Builds am iCalendar property list from the specified stream tokeniser.
164      *
165      * @param tokeniser
166      * @return a property list
167      * @throws IOException
168      * @throws ParseException
169      * @throws URISyntaxException
170      * @throws URISyntaxException
171      * @throws BuilderException
172      */
173     private PropertyList buildPropertyList(final StreamTokenizer tokeniser)
174             throws IOException, ParseException, URISyntaxException,
175             BuilderException {
176 
177         PropertyList list = new PropertyList();
178 
179         assertToken(tokeniser, StreamTokenizer.TT_WORD);
180 
181         while (!Component.BEGIN.equals(tokeniser.sval)
182                 && !Component.END.equals(tokeniser.sval)) {
183 
184             list.add(buildProperty(tokeniser));
185 
186             assertToken(tokeniser, StreamTokenizer.TT_WORD);
187         }
188 
189         return list;
190     }
191 
192     /***
193      * Builds an iCalendar property from the specified stream tokeniser.
194      *
195      * @param tokeniser
196      * @return a property
197      * @throws IOException
198      * @throws BuilderException
199      * @throws URISyntaxException
200      * @throws ParseException
201      */
202     private Property buildProperty(final StreamTokenizer tokeniser)
203             throws IOException, BuilderException, URISyntaxException,
204             ParseException {
205 
206         String name = tokeniser.sval;
207 
208         // debugging..
209         log.debug("Property [" + name + "]");
210 
211         ParameterList parameters = buildParameterList(tokeniser);
212 
213         // it appears that control tokens (ie. ':') are allowed
214         // after the first instance on a line is used.. as such
215         // we must continue appending to value until EOL is
216         // reached..
217         //assertToken(tokeniser, StreamTokenizer.TT_WORD);
218 
219         //String value = tokeniser.sval;
220         StringBuffer value = new StringBuffer();
221 
222         //assertToken(tokeniser,StreamTokenizer.TT_EOL);
223 
224         while (tokeniser.nextToken() != StreamTokenizer.TT_EOL) {
225 
226             if (tokeniser.ttype == StreamTokenizer.TT_WORD) {
227                 value.append(tokeniser.sval);
228             }
229             else {
230                 value.append((char) tokeniser.ttype);
231             }
232         }
233 
234         return PropertyFactory.getInstance().createProperty(name, parameters,
235                 value.toString());
236     }
237 
238     /***
239      * Build a list of iCalendar parameters by parsing the specified stream
240      * tokeniser.
241      *
242      * @param tokeniser
243      * @return @throws
244      *         IOException
245      * @throws BuilderException
246      * @throws URISyntaxException
247      * @throws ParseException
248      */
249     private ParameterList buildParameterList(final StreamTokenizer tokeniser)
250             throws IOException, BuilderException, URISyntaxException,
251             ParseException {
252 
253         ParameterList parameters = new ParameterList();
254 
255         while (tokeniser.nextToken() == ';') {
256             parameters.add(buildParameter(tokeniser));
257         }
258 
259         return parameters;
260     }
261 
262     private Parameter buildParameter(final StreamTokenizer tokeniser)
263             throws IOException, BuilderException, URISyntaxException,
264             ParseException {
265 
266         assertToken(tokeniser, StreamTokenizer.TT_WORD);
267 
268         String paramName = tokeniser.sval;
269 
270         // debugging..
271         log.debug("Parameter [" + paramName + "]");
272 
273         assertToken(tokeniser, '=');
274 
275         StringBuffer paramValue = new StringBuffer();
276 
277         // preserve quote chars..
278         if (tokeniser.nextToken() == '"') {
279             paramValue.append('"');
280             paramValue.append(tokeniser.sval);
281             paramValue.append('"');
282         }
283         else {
284             paramValue.append(tokeniser.sval);
285         }
286 
287         return ParameterFactory.getInstance().createParameter(paramName,
288                 paramValue.toString());
289     }
290 
291     /***
292      * Builds an iCalendar component list from the specified stream tokeniser.
293      *
294      * @param tokeniser
295      * @return a component list
296      * @throws IOException
297      * @throws ParseException
298      * @throws URISyntaxException
299      * @throws BuilderException
300      */
301     private ComponentList buildComponentList(final StreamTokenizer tokeniser)
302             throws IOException, ParseException, URISyntaxException,
303             BuilderException {
304 
305         ComponentList list = new ComponentList();
306 
307         while (Component.BEGIN.equals(tokeniser.sval)) {
308 
309             list.add(buildComponent(tokeniser));
310 
311             assertToken(tokeniser, StreamTokenizer.TT_WORD);
312         }
313 
314         return list;
315     }
316 
317     /***
318      * Builds an iCalendar component from the specified stream tokeniser.
319      *
320      * @param tokeniser
321      * @return a component
322      * @throws IOException
323      * @throws ParseException
324      * @throws URISyntaxException
325      * @throws BuilderException
326      */
327     private Component buildComponent(final StreamTokenizer tokeniser)
328             throws IOException, ParseException, URISyntaxException,
329             BuilderException {
330 
331         assertToken(tokeniser, ':');
332 
333         assertToken(tokeniser, StreamTokenizer.TT_WORD);
334 
335         String name = tokeniser.sval;
336 
337         assertToken(tokeniser, StreamTokenizer.TT_EOL);
338 
339         PropertyList properties = buildPropertyList(tokeniser);
340 
341         ComponentList subComponents = null;
342 
343         // a special case for VTIMEZONE component which contains
344         // sub-components..
345         if (Component.VTIMEZONE.equals(name)) {
346 
347             subComponents = buildComponentList(tokeniser);
348         }
349         // VEVENT components may optionally have embedded VALARM
350         // components..
351         else if (Component.VEVENT.equals(name)
352                 && Component.BEGIN.equals(tokeniser.sval)) {
353 
354             subComponents = buildComponentList(tokeniser);
355         }
356 
357         assertToken(tokeniser, ':');
358 
359         assertToken(tokeniser, name);
360 
361         assertToken(tokeniser, StreamTokenizer.TT_EOL);
362 
363         return ComponentFactory.getInstance().createComponent(name, properties,
364                 subComponents);
365     }
366 
367     /***
368      * Asserts that the next token in the stream matches the specified token.
369      *
370      * @param tokeniser
371      *            stream tokeniser to perform assertion on
372      * @param token
373      *            expected token
374      * @throws IOException
375      *             when unable to read from stream
376      * @throws ParseException
377      *             when next token in the stream does not match the expected
378      *             token
379      */
380     private void assertToken(final StreamTokenizer tokeniser, final int token)
381             throws IOException, BuilderException, ParseException {
382 
383         if (tokeniser.nextToken() != token) {
384 
385         throw new BuilderException("Expected [" + token + "], read ["
386                 + tokeniser.ttype + "] at line " + tokeniser.lineno()); }
387 
388         log.debug("[" + token + "]");
389     }
390 
391     /***
392      * Asserts that the next token in the stream matches the specified token.
393      *
394      * @param tokeniser
395      *            stream tokeniser to perform assertion on
396      * @param token
397      *            expected token
398      * @throws IOException
399      *             when unable to read from stream
400      * @throws ParseException
401      *             when next token in the stream does not match the expected
402      *             token
403      */
404     private void assertToken(final StreamTokenizer tokeniser, final String token)
405             throws IOException, ParseException, BuilderException {
406 
407         // ensure next token is a word token..
408         assertToken(tokeniser, StreamTokenizer.TT_WORD);
409 
410         if (!token.equals(tokeniser.sval)) {
411 
412         throw new BuilderException("Expected [" + token + "], read ["
413                 + tokeniser.sval + "] at line " + tokeniser.lineno()); }
414 
415         log.debug("[" + token + "]");
416     }
417 }