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