001    package biweekly.io.text;
002    
003    import static biweekly.util.IOUtils.utf8Reader;
004    
005    import java.io.Closeable;
006    import java.io.File;
007    import java.io.FileNotFoundException;
008    import java.io.IOException;
009    import java.io.InputStream;
010    import java.io.Reader;
011    import java.io.StringReader;
012    import java.util.ArrayList;
013    import java.util.List;
014    
015    import biweekly.ICalDataType;
016    import biweekly.ICalendar;
017    import biweekly.Messages;
018    import biweekly.Warning;
019    import biweekly.component.ICalComponent;
020    import biweekly.component.marshaller.ICalComponentMarshaller;
021    import biweekly.component.marshaller.ICalendarMarshaller;
022    import biweekly.io.CannotParseException;
023    import biweekly.io.ICalMarshallerRegistrar;
024    import biweekly.io.SkipMeException;
025    import biweekly.parameter.ICalParameters;
026    import biweekly.property.ICalProperty;
027    import biweekly.property.RawProperty;
028    import biweekly.property.marshaller.ICalPropertyMarshaller;
029    import biweekly.property.marshaller.ICalPropertyMarshaller.Result;
030    
031    /*
032     Copyright (c) 2013, Michael Angstadt
033     All rights reserved.
034    
035     Redistribution and use in source and binary forms, with or without
036     modification, are permitted provided that the following conditions are met: 
037    
038     1. Redistributions of source code must retain the above copyright notice, this
039     list of conditions and the following disclaimer. 
040     2. Redistributions in binary form must reproduce the above copyright notice,
041     this list of conditions and the following disclaimer in the documentation
042     and/or other materials provided with the distribution. 
043    
044     THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
045     ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
046     WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
047     DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
048     ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
049     (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
050     LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
051     ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
052     (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
053     SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
054     */
055    
056    /**
057     * <p>
058     * Parses {@link ICalendar} objects from an iCalendar data stream.
059     * </p>
060     * <p>
061     * <b>Example:</b>
062     * 
063     * <pre class="brush:java">
064     * InputStream in = ...
065     * ICalReader icalReader = new ICalReader(in);
066     * ICalendar ical;
067     * while ((ical = icalReader.readNext()) != null){
068     *   ...
069     * }
070     * icalReader.close();
071     * </pre>
072     * 
073     * </p>
074     * @author Michael Angstadt
075     * @rfc 5545
076     */
077    public class ICalReader implements Closeable {
078            private static final ICalendarMarshaller icalMarshaller = ICalMarshallerRegistrar.getICalendarMarshaller();
079            private final List<String> warnings = new ArrayList<String>();
080            private ICalMarshallerRegistrar registrar = new ICalMarshallerRegistrar();
081            private final ICalRawReader reader;
082    
083            /**
084             * Creates a reader that parses iCalendar objects from a string.
085             * @param string the string
086             */
087            public ICalReader(String string) {
088                    this(new StringReader(string));
089            }
090    
091            /**
092             * Creates a reader that parses iCalendar objects from an input stream.
093             * @param in the input stream
094             */
095            public ICalReader(InputStream in) {
096                    this(utf8Reader(in));
097            }
098    
099            /**
100             * Creates a reader that parses iCalendar objects from a file.
101             * @param file the file
102             * @throws FileNotFoundException if the file doesn't exist
103             */
104            public ICalReader(File file) throws FileNotFoundException {
105                    this(utf8Reader(file));
106            }
107    
108            /**
109             * Creates a reader that parses iCalendar objects from a reader.
110             * @param reader the reader
111             */
112            public ICalReader(Reader reader) {
113                    this.reader = new ICalRawReader(reader);
114            }
115    
116            /**
117             * Gets whether the reader will decode parameter values that use circumflex
118             * accent encoding (enabled by default). This escaping mechanism allows
119             * newlines and double quotes to be included in parameter values.
120             * @return true if circumflex accent decoding is enabled, false if not
121             * @see ICalRawReader#isCaretDecodingEnabled()
122             */
123            public boolean isCaretDecodingEnabled() {
124                    return reader.isCaretDecodingEnabled();
125            }
126    
127            /**
128             * Sets whether the reader will decode parameter values that use circumflex
129             * accent encoding (enabled by default). This escaping mechanism allows
130             * newlines and double quotes to be included in parameter values.
131             * @param enable true to use circumflex accent decoding, false not to
132             * @see ICalRawReader#setCaretDecodingEnabled(boolean)
133             */
134            public void setCaretDecodingEnabled(boolean enable) {
135                    reader.setCaretDecodingEnabled(enable);
136            }
137    
138            /**
139             * <p>
140             * Registers an experimental property marshaller. Can also be used to
141             * override the marshaller of a standard property (such as DTSTART). Calling
142             * this method is the same as calling:
143             * </p>
144             * <p>
145             * {@code getRegistrar().register(marshaller)}.
146             * </p>
147             * @param marshaller the marshaller to register
148             */
149            public void registerMarshaller(ICalPropertyMarshaller<? extends ICalProperty> marshaller) {
150                    registrar.register(marshaller);
151            }
152    
153            /**
154             * <p>
155             * Registers an experimental component marshaller. Can also be used to
156             * override the marshaller of a standard component (such as VEVENT). Calling
157             * this method is the same as calling:
158             * </p>
159             * <p>
160             * {@code getRegistrar().register(marshaller)}.
161             * </p>
162             * @param marshaller the marshaller to register
163             */
164            public void registerMarshaller(ICalComponentMarshaller<? extends ICalComponent> marshaller) {
165                    registrar.register(marshaller);
166            }
167    
168            /**
169             * Gets the object that manages the component/property marshaller objects.
170             * @return the marshaller registrar
171             */
172            public ICalMarshallerRegistrar getRegistrar() {
173                    return registrar;
174            }
175    
176            /**
177             * Sets the object that manages the component/property marshaller objects.
178             * @param registrar the marshaller registrar
179             */
180            public void setRegistrar(ICalMarshallerRegistrar registrar) {
181                    this.registrar = registrar;
182            }
183    
184            /**
185             * Gets the warnings from the last iCalendar object that was unmarshalled.
186             * This list is reset every time a new iCalendar object is read.
187             * @return the warnings or empty list if there were no warnings
188             */
189            public List<String> getWarnings() {
190                    return new ArrayList<String>(warnings);
191            }
192    
193            /**
194             * Reads the next iCalendar object.
195             * @return the next iCalendar object or null if there are no more
196             * @throws IOException if there's a problem reading from the stream
197             */
198            public ICalendar readNext() throws IOException {
199                    if (reader.eof()) {
200                            return null;
201                    }
202    
203                    warnings.clear();
204    
205                    ICalDataStreamListenerImpl listener = new ICalDataStreamListenerImpl();
206                    reader.start(listener);
207    
208                    if (!listener.dataWasRead) {
209                            //EOF was reached without reading anything
210                            return null;
211                    }
212    
213                    ICalendar ical;
214                    if (listener.orphanedComponents.isEmpty()) {
215                            //there were no components in the iCalendar object
216                            ical = icalMarshaller.emptyInstance();
217                    } else {
218                            ICalComponent first = listener.orphanedComponents.get(0);
219                            if (first instanceof ICalendar) {
220                                    //this is the code-path that valid iCalendar objects should reach
221                                    ical = (ICalendar) first;
222                            } else {
223                                    ical = icalMarshaller.emptyInstance();
224                                    for (ICalComponent component : listener.orphanedComponents) {
225                                            ical.addComponent(component);
226                                    }
227                            }
228                    }
229    
230                    //add any properties that were not part of a component (will never happen if the iCalendar object is valid)
231                    for (ICalProperty property : listener.orphanedProperties) {
232                            ical.addProperty(property);
233                    }
234    
235                    return ical;
236            }
237    
238            //TODO how to unmarshal the alarm components (a different class should be created, depending on the ACTION property)
239            //TODO buffer properties in a list before the component class is created
240            private class ICalDataStreamListenerImpl implements ICalRawReader.ICalDataStreamListener {
241                    private final String icalComponentName = icalMarshaller.getComponentName();
242    
243                    private List<ICalProperty> orphanedProperties = new ArrayList<ICalProperty>();
244                    private List<ICalComponent> orphanedComponents = new ArrayList<ICalComponent>();
245    
246                    private List<ICalComponent> componentStack = new ArrayList<ICalComponent>();
247                    private List<String> componentNamesStack = new ArrayList<String>();
248                    private boolean dataWasRead = false;
249    
250                    public void beginComponent(String name) {
251                            dataWasRead = true;
252    
253                            ICalComponent parentComponent = getCurrentComponent();
254    
255                            ICalComponentMarshaller<? extends ICalComponent> m = registrar.getComponentMarshaller(name);
256                            ICalComponent component = m.emptyInstance();
257                            componentStack.add(component);
258                            componentNamesStack.add(name);
259    
260                            if (parentComponent == null) {
261                                    orphanedComponents.add(component);
262                            } else {
263                                    parentComponent.addComponent(component);
264                            }
265                    }
266    
267                    public void readProperty(String name, ICalParameters parameters, String value) {
268                            dataWasRead = true;
269    
270                            ICalPropertyMarshaller<? extends ICalProperty> m = registrar.getPropertyMarshaller(name);
271    
272                            //get the data type
273                            ICalDataType dataType = parameters.getValue();
274                            if (dataType == null) {
275                                    //use the default data type if there is no VALUE parameter
276                                    dataType = m.getDefaultDataType();
277                            } else {
278                                    //remove VALUE parameter if it is set
279                                    parameters.setValue(null);
280                            }
281    
282                            ICalProperty property = null;
283                            try {
284                                    Result<? extends ICalProperty> result = m.parseText(value, dataType, parameters);
285    
286                                    for (Warning warning : result.getWarnings()) {
287                                            addWarning(name, warning);
288                                    }
289    
290                                    property = result.getProperty();
291                            } catch (SkipMeException e) {
292                                    addWarning(name, Warning.parse(0, e.getMessage()));
293                            } catch (CannotParseException e) {
294                                    addWarning(name, Warning.parse(1, value, e.getMessage()));
295                                    property = new RawProperty(name, dataType, value);
296                            }
297    
298                            if (property != null) {
299                                    ICalComponent parentComponent = getCurrentComponent();
300                                    if (parentComponent == null) {
301                                            orphanedProperties.add(property);
302                                    } else {
303                                            parentComponent.addProperty(property);
304                                    }
305                            }
306                    }
307    
308                    public void endComponent(String name) {
309                            //stop reading when "END:VCALENDAR" is reached
310                            if (icalComponentName.equalsIgnoreCase(name)) {
311                                    throw new ICalRawReader.StopReadingException();
312                            }
313    
314                            //find the component that this END property matches up with
315                            int popIndex = -1;
316                            for (int i = componentStack.size() - 1; i >= 0; i--) {
317                                    String n = componentNamesStack.get(i);
318                                    if (n.equalsIgnoreCase(name)) {
319                                            popIndex = i;
320                                            break;
321                                    }
322                            }
323                            if (popIndex == -1) {
324                                    //END property does not match up with any BEGIN properties, so ignore
325                                    addWarning("END", Warning.parse(2));
326                                    return;
327                            }
328    
329                            componentStack.subList(popIndex, componentStack.size()).clear();
330                            componentNamesStack.subList(popIndex, componentNamesStack.size()).clear();
331                    }
332    
333                    public void invalidLine(String line) {
334                            addWarning(null, Warning.parse(3, line));
335                    }
336    
337                    public void valuelessParameter(String propertyName, String parameterName) {
338                            addWarning(propertyName, Warning.parse(4, parameterName));
339                    }
340    
341                    private ICalComponent getCurrentComponent() {
342                            if (componentStack.isEmpty()) {
343                                    return null;
344                            }
345                            return componentStack.get(componentStack.size() - 1);
346                    }
347            }
348    
349            private void addWarning(String propertyName, Warning warning) {
350                    String key = (propertyName == null) ? "parse.line" : "parse.lineWithProp";
351                    int line = reader.getLineNum();
352    
353                    String message = Messages.INSTANCE.getMessage(key, line, warning, propertyName);
354                    warnings.add(message);
355            }
356    
357            /**
358             * Closes the underlying {@link Reader} object.
359             */
360            //@Override
361            public void close() throws IOException {
362                    reader.close();
363            }
364    }