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