001package biweekly.io.json;
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.Arrays;
014import java.util.HashMap;
015import java.util.List;
016import java.util.Map;
017
018import biweekly.ICalDataType;
019import biweekly.ICalendar;
020import biweekly.Warning;
021import biweekly.component.ICalComponent;
022import biweekly.io.CannotParseException;
023import biweekly.io.ParseWarnings;
024import biweekly.io.SkipMeException;
025import biweekly.io.json.JCalRawReader.JCalDataStreamListener;
026import biweekly.io.scribe.ScribeIndex;
027import biweekly.io.scribe.component.ICalComponentScribe;
028import biweekly.io.scribe.component.ICalendarScribe;
029import biweekly.io.scribe.property.ICalPropertyScribe;
030import biweekly.io.scribe.property.ICalPropertyScribe.Result;
031import biweekly.io.scribe.property.RawPropertyScribe;
032import biweekly.parameter.ICalParameters;
033import biweekly.property.ICalProperty;
034import biweekly.property.RawProperty;
035
036import 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/rfc7265">RFC 7265</a>
083 */
084public class JCalReader implements Closeable {
085        private static final ICalendarScribe icalScribe = ScribeIndex.getICalendarScribe();
086        private ScribeIndex index = new ScribeIndex();
087        private final JCalRawReader reader;
088        private final ParseWarnings warnings = new ParseWarnings();
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 warnings.copy();
130        }
131
132        /**
133         * <p>
134         * Registers an experimental property scribe. Can also be used to override
135         * the scribe of a standard property (such as DTSTART). Calling this method
136         * is the same as calling:
137         * </p>
138         * <p>
139         * {@code getScribeIndex().register(scribe)}.
140         * </p>
141         * @param scribe the scribe to register
142         */
143        public void registerScribe(ICalPropertyScribe<? extends ICalProperty> scribe) {
144                index.register(scribe);
145        }
146
147        /**
148         * <p>
149         * Registers an experimental component scribe. Can also be used to override
150         * the scribe of a standard component (such as VEVENT). Calling this method
151         * is the same as calling:
152         * </p>
153         * <p>
154         * {@code getScribeIndex().register(scribe)}.
155         * </p>
156         * @param scribe the scribe to register
157         */
158        public void registerScribe(ICalComponentScribe<? extends ICalComponent> scribe) {
159                index.register(scribe);
160        }
161
162        /**
163         * Gets the object that manages the component/property scribes.
164         * @return the scribe index
165         */
166        public ScribeIndex getScribeIndex() {
167                return index;
168        }
169
170        /**
171         * Sets the object that manages the component/property scribes.
172         * @param index the scribe index
173         */
174        public void setScribeIndex(ScribeIndex index) {
175                this.index = index;
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        //@Override
199        public void close() throws IOException {
200                reader.close();
201        }
202
203        private class JCalDataStreamListenerImpl implements JCalDataStreamListener {
204                private final Map<List<String>, ICalComponent> components = new HashMap<List<String>, ICalComponent>();
205
206                public void readProperty(List<String> componentHierarchy, String propertyName, ICalParameters parameters, ICalDataType dataType, JCalValue value) {
207                        //get the component that the property belongs to
208                        ICalComponent parent = components.get(componentHierarchy);
209
210                        //unmarshal the property
211                        ICalPropertyScribe<? extends ICalProperty> scribe = index.getPropertyScribe(propertyName);
212                        try {
213                                Result<? extends ICalProperty> result = scribe.parseJson(value, dataType, parameters);
214                                for (Warning warning : result.getWarnings()) {
215                                        warnings.add(reader.getLineNum(), propertyName, warning);
216                                }
217                                ICalProperty property = result.getProperty();
218                                parent.addProperty(property);
219                        } catch (SkipMeException e) {
220                                warnings.add(reader.getLineNum(), propertyName, 0, e.getMessage());
221                        } catch (CannotParseException e) {
222                                Result<? extends ICalProperty> result = new RawPropertyScribe(propertyName).parseJson(value, dataType, parameters);
223                                ICalProperty property = result.getProperty();
224                                parent.addProperty(property);
225
226                                String valueStr = ((RawProperty) property).getValue();
227                                warnings.add(reader.getLineNum(), propertyName, 1, valueStr, e.getMessage());
228                        }
229                }
230
231                public void readComponent(List<String> parentHierarchy, String componentName) {
232                        ICalComponentScribe<? extends ICalComponent> scribe = index.getComponentScribe(componentName);
233                        ICalComponent component = scribe.emptyInstance();
234
235                        ICalComponent parent = components.get(parentHierarchy);
236                        if (parent != null) {
237                                parent.addComponent(component);
238                        }
239
240                        List<String> hierarchy = new ArrayList<String>(parentHierarchy);
241                        hierarchy.add(componentName);
242                        components.put(hierarchy, component);
243                }
244
245                public ICalendar getICalendar() {
246                        if (components.isEmpty()) {
247                                //EOF
248                                return null;
249                        }
250
251                        ICalComponent component = components.get(Arrays.asList(icalScribe.getComponentName().toLowerCase()));
252                        if (component == null) {
253                                //should never happen because the parser always looks for a "vcalendar" component
254                                return null;
255                        }
256
257                        if (component instanceof ICalendar) {
258                                //should happen every time
259                                return (ICalendar) component;
260                        }
261
262                        //this will only happen if the user decides to override the ICalendarScribe for some reason
263                        ICalendar ical = icalScribe.emptyInstance();
264                        ical.addComponent(component);
265                        return ical;
266                }
267        }
268}