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