001    package biweekly.io.json;
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.Arrays;
014    import java.util.HashMap;
015    import java.util.List;
016    import java.util.Map;
017    
018    import biweekly.ICalDataType;
019    import biweekly.ICalendar;
020    import biweekly.Messages;
021    import biweekly.Warning;
022    import biweekly.component.ICalComponent;
023    import biweekly.component.marshaller.ICalComponentMarshaller;
024    import biweekly.component.marshaller.ICalendarMarshaller;
025    import biweekly.io.CannotParseException;
026    import biweekly.io.ICalMarshallerRegistrar;
027    import biweekly.io.SkipMeException;
028    import biweekly.io.json.JCalRawReader.JCalDataStreamListener;
029    import biweekly.parameter.ICalParameters;
030    import biweekly.property.ICalProperty;
031    import biweekly.property.RawProperty;
032    import biweekly.property.marshaller.ICalPropertyMarshaller;
033    import biweekly.property.marshaller.ICalPropertyMarshaller.Result;
034    import biweekly.property.marshaller.RawPropertyMarshaller;
035    
036    import com.fasterxml.jackson.core.JsonParseException;
037    
038    /*
039     Copyright (c) 2013, Michael Angstadt
040     All rights reserved.
041    
042     Redistribution and use in source and binary forms, with or without
043     modification, are permitted provided that the following conditions are met: 
044    
045     1. Redistributions of source code must retain the above copyright notice, this
046     list of conditions and the following disclaimer. 
047     2. Redistributions in binary form must reproduce the above copyright notice,
048     this list of conditions and the following disclaimer in the documentation
049     and/or other materials provided with the distribution. 
050    
051     THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
052     ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
053     WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
054     DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
055     ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
056     (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
057     LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
058     ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
059     (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
060     SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
061     */
062    
063    /**
064     * <p>
065     * Parses {@link ICalendar} objects from a jCal data stream (JSON).
066     * </p>
067     * <p>
068     * <b>Example:</b>
069     * 
070     * <pre class="brush:java">
071     * InputStream in = ...
072     * JCalReader jcalReader = new JCalReader(in);
073     * ICalendar ical;
074     * while ((ical = jcalReader.readNext()) != null){
075     *   ...
076     * }
077     * jcalReader.close();
078     * </pre>
079     * 
080     * </p>
081     * @author Michael Angstadt
082     * @see <a href="http://tools.ietf.org/html/draft-ietf-jcardcal-jcal-05">jCal
083     * draft</a>
084     */
085    public class JCalReader implements Closeable {
086            private static final ICalendarMarshaller icalMarshaller = ICalMarshallerRegistrar.getICalendarMarshaller();
087            private ICalMarshallerRegistrar registrar = new ICalMarshallerRegistrar();
088            private final JCalRawReader reader;
089            private final List<String> warnings = new ArrayList<String>();
090    
091            /**
092             * Creates a jCard reader.
093             * @param json the JSON string
094             */
095            public JCalReader(String json) {
096                    this(new StringReader(json));
097            }
098    
099            /**
100             * Creates a jCard reader.
101             * @param in the input stream to read the vCards from
102             */
103            public JCalReader(InputStream in) {
104                    this(utf8Reader(in));
105            }
106    
107            /**
108             * Creates a jCard reader.
109             * @param file the file to read the vCards from
110             * @throws FileNotFoundException if the file doesn't exist
111             */
112            public JCalReader(File file) throws FileNotFoundException {
113                    this(utf8Reader(file));
114            }
115    
116            /**
117             * Creates a jCard reader.
118             * @param reader the reader to read the vCards from
119             */
120            public JCalReader(Reader reader) {
121                    this.reader = new JCalRawReader(reader);
122            }
123    
124            /**
125             * Gets the warnings from the last iCalendar object that was unmarshalled.
126             * This list is reset every time a new iCalendar object is read.
127             * @return the warnings or empty list if there were no warnings
128             */
129            public List<String> getWarnings() {
130                    return new ArrayList<String>(warnings);
131            }
132    
133            /**
134             * <p>
135             * Registers an experimental property marshaller. Can also be used to
136             * override the marshaller of a standard property (such as DTSTART). Calling
137             * this method is the same as calling:
138             * </p>
139             * <p>
140             * {@code getRegistrar().register(marshaller)}.
141             * </p>
142             * @param marshaller the marshaller to register
143             */
144            public void registerMarshaller(ICalPropertyMarshaller<? extends ICalProperty> marshaller) {
145                    registrar.register(marshaller);
146            }
147    
148            /**
149             * <p>
150             * Registers an experimental component marshaller. Can also be used to
151             * override the marshaller of a standard component (such as VEVENT). Calling
152             * this method is the same as calling:
153             * </p>
154             * <p>
155             * {@code getRegistrar().register(marshaller)}.
156             * </p>
157             * @param marshaller the marshaller to register
158             */
159            public void registerMarshaller(ICalComponentMarshaller<? extends ICalComponent> marshaller) {
160                    registrar.register(marshaller);
161            }
162    
163            /**
164             * Gets the object that manages the component/property marshaller objects.
165             * @return the marshaller registrar
166             */
167            public ICalMarshallerRegistrar getRegistrar() {
168                    return registrar;
169            }
170    
171            /**
172             * Sets the object that manages the component/property marshaller objects.
173             * @param registrar the marshaller registrar
174             */
175            public void setRegistrar(ICalMarshallerRegistrar registrar) {
176                    this.registrar = registrar;
177            }
178    
179            /**
180             * Reads the next iCalendar object from the JSON data stream.
181             * @return the iCalendar object or null if there are no more
182             * @throws JCalParseException if the jCal syntax is incorrect (the JSON
183             * syntax may be valid, but it is not in the correct jCal format).
184             * @throws JsonParseException if the JSON syntax is incorrect
185             * @throws IOException if there is a problem reading from the data stream
186             */
187            public ICalendar readNext() throws IOException {
188                    if (reader.eof()) {
189                            return null;
190                    }
191    
192                    warnings.clear();
193    
194                    JCalDataStreamListenerImpl listener = new JCalDataStreamListenerImpl();
195                    reader.readNext(listener);
196                    return listener.getICalendar();
197            }
198    
199            private void addWarning(String propertyName, Warning warning) {
200                    String key = (propertyName == null) ? "parse.line" : "parse.lineWithProp";
201                    int line = reader.getLineNum();
202    
203                    String message = Messages.INSTANCE.getMessage(key, line, warning, propertyName);
204                    warnings.add(message);
205            }
206    
207            //@Override
208            public void close() throws IOException {
209                    reader.close();
210            }
211    
212            private class JCalDataStreamListenerImpl implements JCalDataStreamListener {
213                    private final Map<List<String>, ICalComponent> components = new HashMap<List<String>, ICalComponent>();
214    
215                    public void readProperty(List<String> componentHierarchy, String propertyName, ICalParameters parameters, ICalDataType dataType, JCalValue value) {
216                            //get the component that the property belongs to
217                            ICalComponent parent = components.get(componentHierarchy);
218    
219                            //unmarshal the property
220                            ICalPropertyMarshaller<? extends ICalProperty> m = registrar.getPropertyMarshaller(propertyName);
221                            ICalProperty property = null;
222                            try {
223                                    Result<? extends ICalProperty> result = m.parseJson(value, dataType, parameters);
224    
225                                    for (Warning warning : result.getWarnings()) {
226                                            addWarning(propertyName, warning);
227                                    }
228    
229                                    property = result.getProperty();
230                            } catch (SkipMeException e) {
231                                    addWarning(propertyName, Warning.parse(0, e.getMessage()));
232                            } catch (CannotParseException e) {
233                                    Result<? extends ICalProperty> result = new RawPropertyMarshaller(propertyName).parseJson(value, dataType, parameters);
234                                    for (Warning warning : result.getWarnings()) {
235                                            addWarning(propertyName, warning);
236                                    }
237                                    property = result.getProperty();
238    
239                                    String valueStr = ((RawProperty) property).getValue();
240                                    addWarning(propertyName, Warning.parse(1, valueStr, e.getMessage()));
241                            }
242    
243                            if (property != null) {
244                                    parent.addProperty(property);
245                            }
246                    }
247    
248                    public void readComponent(List<String> parentHierarchy, String componentName) {
249                            ICalComponentMarshaller<? extends ICalComponent> m = registrar.getComponentMarshaller(componentName);
250                            ICalComponent component = m.emptyInstance();
251    
252                            ICalComponent parent = components.get(parentHierarchy);
253                            if (parent != null) {
254                                    parent.addComponent(component);
255                            }
256    
257                            List<String> hierarchy = new ArrayList<String>(parentHierarchy);
258                            hierarchy.add(componentName);
259                            components.put(hierarchy, component);
260                    }
261    
262                    public ICalendar getICalendar() {
263                            if (components.isEmpty()) {
264                                    //EOF
265                                    return null;
266                            }
267    
268                            ICalComponent component = components.get(Arrays.asList(icalMarshaller.getComponentName().toLowerCase()));
269                            if (component == null) {
270                                    //should never happen because the parser always looks for a "vcalendar" component
271                                    return null;
272                            }
273    
274                            if (component instanceof ICalendar) {
275                                    //should happen every time
276                                    return (ICalendar) component;
277                            }
278    
279                            //this will only happen if the user decides to override the ICalendarMarshaller for some reason
280                            ICalendar ical = icalMarshaller.emptyInstance();
281                            ical.addComponent(component);
282                            return ical;
283                    }
284            }
285    }