001    package biweekly.io.text;
002    
003    import java.io.Closeable;
004    import java.io.File;
005    import java.io.FileNotFoundException;
006    import java.io.FileReader;
007    import java.io.IOException;
008    import java.io.InputStream;
009    import java.io.InputStreamReader;
010    import java.io.Reader;
011    import java.io.StringReader;
012    import java.util.ArrayList;
013    import java.util.HashMap;
014    import java.util.List;
015    import java.util.Map;
016    
017    import biweekly.ICalendar;
018    import biweekly.component.ICalComponent;
019    import biweekly.component.marshaller.ComponentLibrary;
020    import biweekly.component.marshaller.ICalComponentMarshaller;
021    import biweekly.component.marshaller.RawComponentMarshaller;
022    import biweekly.io.CannotParseException;
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    import biweekly.property.marshaller.PropertyLibrary;
030    import biweekly.property.marshaller.RawPropertyMarshaller;
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     * 
062     * <pre>
063     * Reader reader = ...
064     * ICalReader icalReader = new ICalReader(reader);
065     * ICalendar ical;
066     * while ((ical = icalReader.readNext()) != null){
067     *   ...
068     * }
069     * icalReader.close();
070     * </pre>
071     * @author Michael Angstadt
072     */
073    public class ICalReader implements Closeable {
074            private final List<String> warnings = new ArrayList<String>();
075            private final Map<String, ICalPropertyMarshaller<? extends ICalProperty>> propertyMarshallers = new HashMap<String, ICalPropertyMarshaller<? extends ICalProperty>>(0);
076            private final Map<String, ICalComponentMarshaller<? extends ICalComponent>> componentMarshallers = new HashMap<String, ICalComponentMarshaller<? extends ICalComponent>>(0);
077            private final ICalRawReader reader;
078    
079            /**
080             * Creates a reader that parses iCalendar objects from a string.
081             * @param string the string
082             */
083            public ICalReader(String string) {
084                    this(new StringReader(string));
085            }
086    
087            /**
088             * Creates a reader that parses iCalendar objects from an input stream.
089             * @param in the input stream
090             */
091            public ICalReader(InputStream in) {
092                    this(new InputStreamReader(in));
093            }
094    
095            /**
096             * Creates a reader that parses iCalendar objects from a file.
097             * @param file the file
098             * @throws FileNotFoundException if the file doesn't exist
099             */
100            public ICalReader(File file) throws FileNotFoundException {
101                    this(new FileReader(file));
102            }
103    
104            /**
105             * Creates a reader that parses iCalendar objects from a reader.
106             * @param reader the reader
107             */
108            public ICalReader(Reader reader) {
109                    this.reader = new ICalRawReader(reader);
110            }
111    
112            /**
113             * Gets whether the reader will decode parameter values that use circumflex
114             * accent encoding (enabled by default). This escaping mechanism allows
115             * newlines and double quotes to be included in parameter values.
116             * @return true if circumflex accent decoding is enabled, false if not
117             * @see ICalRawReader#isCaretDecodingEnabled()
118             */
119            public boolean isCaretDecodingEnabled() {
120                    return reader.isCaretDecodingEnabled();
121            }
122    
123            /**
124             * Sets whether the reader will decode parameter values that use circumflex
125             * accent encoding (enabled by default). This escaping mechanism allows
126             * newlines and double quotes to be included in parameter values.
127             * @param enable true to use circumflex accent decoding, false not to
128             * @see ICalRawReader#setCaretDecodingEnabled(boolean)
129             */
130            public void setCaretDecodingEnabled(boolean enable) {
131                    reader.setCaretDecodingEnabled(enable);
132            }
133    
134            /**
135             * Registers a marshaller for an experimental property.
136             * @param marshaller the marshaller to register
137             */
138            public void registerMarshaller(ICalPropertyMarshaller<? extends ICalProperty> marshaller) {
139                    propertyMarshallers.put(marshaller.getPropertyName().toUpperCase(), marshaller);
140            }
141    
142            /**
143             * Registers a marshaller for an experimental component.
144             * @param marshaller the marshaller to register
145             */
146            public void registerMarshaller(ICalComponentMarshaller<? extends ICalComponent> marshaller) {
147                    componentMarshallers.put(marshaller.getComponentName().toUpperCase(), marshaller);
148            }
149    
150            /**
151             * Gets the warnings from the last iCalendar object that was unmarshalled.
152             * This list is reset every time a new iCalendar object is read.
153             * @return the warnings or empty list if there were no warnings
154             */
155            public List<String> getWarnings() {
156                    return new ArrayList<String>(warnings);
157            }
158    
159            /**
160             * Reads the next iCalendar object.
161             * @return the next iCalendar object or null if there are no more
162             * @throws IOException if there's a problem reading from the stream
163             */
164            public ICalendar readNext() throws IOException {
165                    if (reader.eof()) {
166                            return null;
167                    }
168    
169                    warnings.clear();
170    
171                    ICalDataStreamListenerImpl listener = new ICalDataStreamListenerImpl();
172                    reader.start(listener);
173    
174                    if (!listener.dataWasRead) {
175                            //EOF was reached without reading anything
176                            return null;
177                    }
178    
179                    ICalendar ical;
180                    if (listener.orphanedComponents.isEmpty()) {
181                            //there were no components in the iCalendar object
182                            ical = new ICalendar();
183                            ical.getProperties().clear(); //clear properties that are created in the constructor
184                    } else {
185                            ICalComponent first = listener.orphanedComponents.get(0);
186                            if (first instanceof ICalendar) {
187                                    //this is the code-path that valid iCalendar objects should reach
188                                    ical = (ICalendar) first;
189                            } else {
190                                    ical = new ICalendar();
191                                    ical.getProperties().clear(); //clear properties that are created in the constructor
192                                    for (ICalComponent component : listener.orphanedComponents) {
193                                            ical.addComponent(component);
194                                    }
195                            }
196                    }
197    
198                    //add any properties that were not part of a component (will never happen if the iCalendar object is valid)
199                    for (ICalProperty property : listener.orphanedProperties) {
200                            ical.addProperty(property);
201                    }
202    
203                    return ical;
204            }
205    
206            /**
207             * Finds a component marshaller.
208             * @param componentName the name of the component
209             * @return the component marshallerd
210             */
211            private ICalComponentMarshaller<? extends ICalComponent> findComponentMarshaller(String componentName) {
212                    ICalComponentMarshaller<? extends ICalComponent> m = componentMarshallers.get(componentName.toUpperCase());
213                    if (m == null) {
214                            m = ComponentLibrary.getMarshaller(componentName);
215                            if (m == null) {
216                                    m = new RawComponentMarshaller(componentName);
217                            }
218                    }
219                    return m;
220            }
221    
222            /**
223             * Finds a property marshaller.
224             * @param propertyName the name of the property
225             * @return the property marshaller
226             */
227            private ICalPropertyMarshaller<? extends ICalProperty> findPropertyMarshaller(String propertyName) {
228                    ICalPropertyMarshaller<? extends ICalProperty> m = propertyMarshallers.get(propertyName);
229                    if (m == null) {
230                            m = PropertyLibrary.getMarshaller(propertyName);
231                            if (m == null) {
232                                    m = new RawPropertyMarshaller(propertyName);
233                            }
234                    }
235                    return m;
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 = ComponentLibrary.getMarshaller(ICalendar.class).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 = findComponentMarshaller(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 = findPropertyMarshaller(name);
271                            ICalProperty property = null;
272                            try {
273                                    Result<? extends ICalProperty> result = m.parseText(value, parameters);
274    
275                                    for (String warning : result.getWarnings()) {
276                                            addWarning(warning, name);
277                                    }
278    
279                                    property = result.getValue();
280                            } catch (SkipMeException e) {
281                                    if (e.getMessage() == null) {
282                                            addWarning("Property has requested that it be skipped.", name);
283                                    } else {
284                                            addWarning("Property has requested that it be skipped: " + e.getMessage(), name);
285                                    }
286                            } catch (CannotParseException e) {
287                                    if (e.getMessage() == null) {
288                                            addWarning("Property value could not be unmarshalled: " + value, name);
289                                    } else {
290                                            addWarning("Property value could not be unmarshalled.\n  Value: " + value + "\n  Reason: " + e.getMessage(), name);
291                                    }
292                                    property = new RawProperty(name, value);
293                            }
294    
295                            if (property != null) {
296                                    ICalComponent parentComponent = getCurrentComponent();
297                                    if (parentComponent == null) {
298                                            orphanedProperties.add(property);
299                                    } else {
300                                            parentComponent.addProperty(property);
301                                    }
302                            }
303                    }
304    
305                    public void endComponent(String name) {
306                            //stop reading when "END:VCALENDAR" is reached
307                            if (icalComponentName.equalsIgnoreCase(name)) {
308                                    throw new ICalRawReader.StopReadingException();
309                            }
310    
311                            //find the component that this END property matches up with
312                            int popIndex = -1;
313                            for (int i = componentStack.size() - 1; i >= 0; i--) {
314                                    String n = componentNamesStack.get(i);
315                                    if (n.equalsIgnoreCase(name)) {
316                                            popIndex = i;
317                                            break;
318                                    }
319                            }
320                            if (popIndex == -1) {
321                                    //END property does not match up with any BEGIN properties, so ignore
322                                    addWarning("Ignoring END property that does not match up with any BEGIN properties: " + name, "END");
323                                    return;
324                            }
325    
326                            componentStack = componentStack.subList(0, popIndex);
327                            componentNamesStack = componentNamesStack.subList(0, popIndex);
328                    }
329    
330                    public void invalidLine(String line) {
331                            addWarning("Skipping malformed line: \"" + line + "\"");
332                    }
333    
334                    private ICalComponent getCurrentComponent() {
335                            if (componentStack.isEmpty()) {
336                                    return null;
337                            }
338                            return componentStack.get(componentStack.size() - 1);
339                    }
340            }
341    
342            private void addWarning(String message) {
343                    addWarning(message, null);
344            }
345    
346            private void addWarning(String message, String propertyName) {
347                    addWarning(message, propertyName, reader.getLineNum());
348            }
349    
350            private void addWarning(String message, String propertyName, int lineNum) {
351                    StringBuilder sb = new StringBuilder();
352                    sb.append("Line ").append(lineNum);
353                    if (propertyName != null) {
354                            sb.append(" (").append(propertyName).append(" property)");
355                    }
356                    sb.append(": ").append(message);
357    
358                    warnings.add(sb.toString());
359            }
360    
361            /**
362             * Closes the underlying {@link Reader} object.
363             */
364            //@Override
365            public void close() throws IOException {
366                    reader.close();
367            }
368    }