001package biweekly.io.json;
002
003import static biweekly.util.IOUtils.utf8Reader;
004
005import java.io.File;
006import java.io.FileNotFoundException;
007import java.io.IOException;
008import java.io.InputStream;
009import java.io.Reader;
010import java.io.StringReader;
011import java.util.ArrayList;
012import java.util.Arrays;
013import java.util.HashMap;
014import java.util.List;
015import java.util.Map;
016
017import biweekly.ICalDataType;
018import biweekly.ICalVersion;
019import biweekly.ICalendar;
020import biweekly.Warning;
021import biweekly.component.ICalComponent;
022import biweekly.io.CannotParseException;
023import biweekly.io.SkipMeException;
024import biweekly.io.StreamReader;
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.RawPropertyScribe;
031import biweekly.parameter.ICalParameters;
032import biweekly.property.ICalProperty;
033import biweekly.property.RawProperty;
034import biweekly.property.Version;
035
036import com.fasterxml.jackson.core.JsonParseException;
037
038/*
039 Copyright (c) 2013-2015, 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 * 
082 * <p>
083 * <b>Getting timezone information:</b>
084 * 
085 * <pre class="brush:java">
086 * JCalReader reader = ...
087 * ICalendar ical = reader.readNext();
088 * TimezoneInfo tzinfo = reader.getTimezoneInfo();
089 * 
090 * //get the VTIMEZONE components that were parsed
091 * //the VTIMEZONE components will NOT be in the ICalendar object
092 * Collection&ltVTimezone&gt; vtimezones = tzinfo.getComponents();
093 * 
094 * //get the timezone that a property was originally formatted in
095 * DateStart dtstart = ical.getEvents().get(0).getDateStart();
096 * TimeZone tz = tzinfo.getTimeZone(dtstart);
097 * </pre>
098 * 
099 * </p>
100 * @author Michael Angstadt
101 * @see <a href="http://tools.ietf.org/html/rfc7265">RFC 7265</a>
102 */
103public class JCalReader extends StreamReader {
104        private static final ICalendarScribe icalScribe = ScribeIndex.getICalendarScribe();
105        private final JCalRawReader reader;
106
107        /**
108         * Creates a jCard reader.
109         * @param json the JSON string
110         */
111        public JCalReader(String json) {
112                this(new StringReader(json));
113        }
114
115        /**
116         * Creates a jCard reader.
117         * @param in the input stream to read the vCards from
118         */
119        public JCalReader(InputStream in) {
120                this(utf8Reader(in));
121        }
122
123        /**
124         * Creates a jCard reader.
125         * @param file the file to read the vCards from
126         * @throws FileNotFoundException if the file doesn't exist
127         */
128        public JCalReader(File file) throws FileNotFoundException {
129                this(utf8Reader(file));
130        }
131
132        /**
133         * Creates a jCard reader.
134         * @param reader the reader to read the vCards from
135         */
136        public JCalReader(Reader reader) {
137                this.reader = new JCalRawReader(reader);
138        }
139
140        /**
141         * Reads the next iCalendar object from the JSON data stream.
142         * @return the iCalendar object or null if there are no more
143         * @throws JCalParseException if the jCal syntax is incorrect (the JSON
144         * syntax may be valid, but it is not in the correct jCal format).
145         * @throws JsonParseException if the JSON syntax is incorrect
146         * @throws IOException if there is a problem reading from the data stream
147         */
148        @Override
149        public ICalendar _readNext() throws IOException {
150                if (reader.eof()) {
151                        return null;
152                }
153
154                context.setVersion(ICalVersion.V2_0);
155
156                JCalDataStreamListenerImpl listener = new JCalDataStreamListenerImpl();
157                reader.readNext(listener);
158
159                return listener.getICalendar();
160        }
161
162        //@Override
163        public void close() throws IOException {
164                reader.close();
165        }
166
167        private class JCalDataStreamListenerImpl implements JCalDataStreamListener {
168                private final Map<List<String>, ICalComponent> components = new HashMap<List<String>, ICalComponent>();
169
170                public void readProperty(List<String> componentHierarchy, String propertyName, ICalParameters parameters, ICalDataType dataType, JCalValue value) {
171                        context.getWarnings().clear();
172
173                        //get the component that the property belongs to
174                        ICalComponent parent = components.get(componentHierarchy);
175
176                        //unmarshal the property
177                        ICalPropertyScribe<? extends ICalProperty> scribe = index.getPropertyScribe(propertyName, ICalVersion.V2_0);
178                        try {
179                                ICalProperty property = scribe.parseJson(value, dataType, parameters, context);
180                                for (Warning warning : context.getWarnings()) {
181                                        warnings.add(reader.getLineNum(), propertyName, warning);
182                                }
183
184                                //set "ICalendar.version" if the value of the VERSION property is recognized
185                                //otherwise, unmarshal VERSION like a normal property
186                                if (parent instanceof ICalendar && property instanceof Version) {
187                                        Version version = (Version) property;
188                                        ICalVersion icalVersion = version.toICalVersion();
189                                        if (icalVersion != null) {
190                                                context.setVersion(icalVersion);
191                                                return;
192                                        }
193                                }
194
195                                parent.addProperty(property);
196                        } catch (SkipMeException e) {
197                                warnings.add(reader.getLineNum(), propertyName, 0, e.getMessage());
198                        } catch (CannotParseException e) {
199                                RawProperty property = new RawPropertyScribe(propertyName).parseJson(value, dataType, parameters, context);
200                                parent.addProperty(property);
201
202                                String valueStr = property.getValue();
203                                warnings.add(reader.getLineNum(), propertyName, 1, valueStr, e.getMessage());
204                        }
205                }
206
207                public void readComponent(List<String> parentHierarchy, String componentName) {
208                        ICalComponentScribe<? extends ICalComponent> scribe = index.getComponentScribe(componentName, ICalVersion.V2_0);
209                        ICalComponent component = scribe.emptyInstance();
210
211                        ICalComponent parent = components.get(parentHierarchy);
212                        if (parent != null) {
213                                parent.addComponent(component);
214                        }
215
216                        List<String> hierarchy = new ArrayList<String>(parentHierarchy);
217                        hierarchy.add(componentName);
218                        components.put(hierarchy, component);
219                }
220
221                public ICalendar getICalendar() {
222                        if (components.isEmpty()) {
223                                //EOF
224                                return null;
225                        }
226
227                        ICalComponent component = components.get(Arrays.asList(icalScribe.getComponentName().toLowerCase()));
228                        if (component == null) {
229                                //should never happen because the parser always looks for a "vcalendar" component
230                                return null;
231                        }
232
233                        if (component instanceof ICalendar) {
234                                //should happen every time
235                                return (ICalendar) component;
236                        }
237
238                        //this will only happen if the user decides to override the ICalendarScribe for some reason
239                        ICalendar ical = icalScribe.emptyInstance();
240                        ical.addComponent(component);
241                        return ical;
242                }
243        }
244}