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 }