001 package biweekly.io.json;
002
003 import java.io.Closeable;
004 import java.io.IOException;
005 import java.io.Reader;
006 import java.util.ArrayList;
007 import java.util.HashMap;
008 import java.util.List;
009 import java.util.Map;
010
011 import biweekly.ICalDataType;
012 import biweekly.io.ICalMarshallerRegistrar;
013 import biweekly.parameter.ICalParameters;
014
015 import com.fasterxml.jackson.core.JsonFactory;
016 import com.fasterxml.jackson.core.JsonParseException;
017 import com.fasterxml.jackson.core.JsonParser;
018 import com.fasterxml.jackson.core.JsonToken;
019
020 /*
021 Copyright (c) 2013, Michael Angstadt
022 All rights reserved.
023
024 Redistribution and use in source and binary forms, with or without
025 modification, are permitted provided that the following conditions are met:
026
027 1. Redistributions of source code must retain the above copyright notice, this
028 list of conditions and the following disclaimer.
029 2. Redistributions in binary form must reproduce the above copyright notice,
030 this list of conditions and the following disclaimer in the documentation
031 and/or other materials provided with the distribution.
032
033 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
034 ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
035 WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
036 DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
037 ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
038 (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
039 LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
040 ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
041 (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
042 SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
043 */
044
045 /**
046 * Parses an iCalendar JSON data stream (jCal).
047 * @author Michael Angstadt
048 * @see <a href="http://tools.ietf.org/html/draft-ietf-jcardcal-jcal-05">jCal
049 * draft</a>
050 */
051 public class JCalRawReader implements Closeable {
052 private static final String vcalendarComponentName = ICalMarshallerRegistrar.getICalendarMarshaller().getComponentName().toLowerCase(); //"vcalendar"
053 private final Reader reader;
054 private JsonParser jp;
055 private boolean eof = false;
056 private JCalDataStreamListener listener;
057
058 /**
059 * Creates a new reader.
060 * @param reader the reader to the data stream
061 */
062 public JCalRawReader(Reader reader) {
063 this.reader = reader;
064 }
065
066 /**
067 * Gets the current line number.
068 * @return the line number
069 */
070 public int getLineNum() {
071 return (jp == null) ? 0 : jp.getCurrentLocation().getLineNr();
072 }
073
074 /**
075 * Reads the next iCalendar object from the jCal data stream.
076 * @param listener handles the iCalendar data as it is read off the wire
077 * @throws JCalParseException if the jCal syntax is incorrect (the JSON
078 * syntax may be valid, but it is not in the correct jCal format).
079 * @throws JsonParseException if the JSON syntax is incorrect
080 * @throws IOException if there is a problem reading from the data stream
081 */
082 public void readNext(JCalDataStreamListener listener) throws IOException {
083 if (jp == null) {
084 JsonFactory factory = new JsonFactory();
085 jp = factory.createJsonParser(reader);
086 } else if (jp.isClosed()) {
087 return;
088 }
089
090 this.listener = listener;
091
092 //find the next iCalendar object
093 JsonToken prev = null;
094 JsonToken cur;
095 while ((cur = jp.nextToken()) != null) {
096 if (prev == JsonToken.START_ARRAY && cur == JsonToken.VALUE_STRING && vcalendarComponentName.equals(jp.getValueAsString())) {
097 break;
098 }
099 prev = cur;
100 }
101 if (cur == null) {
102 //EOF
103 eof = true;
104 return;
105 }
106
107 parseComponent(new ArrayList<String>());
108 }
109
110 private void parseComponent(List<String> components) throws IOException {
111 if (jp.getCurrentToken() != JsonToken.VALUE_STRING) {
112 throw new JCalParseException(JsonToken.VALUE_STRING, jp.getCurrentToken());
113 }
114 String componentName = jp.getValueAsString();
115 listener.readComponent(components, componentName);
116 components.add(componentName);
117
118 //TODO add messages to the jCal exceptions
119
120 //start properties array
121 if (jp.nextToken() != JsonToken.START_ARRAY) {
122 throw new JCalParseException(JsonToken.START_ARRAY, jp.getCurrentToken());
123 }
124
125 //read properties
126 while (jp.nextToken() != JsonToken.END_ARRAY) { //until we reach the end properties array
127 if (jp.getCurrentToken() != JsonToken.START_ARRAY) {
128 throw new JCalParseException(JsonToken.START_ARRAY, jp.getCurrentToken());
129 }
130 jp.nextToken();
131 parseProperty(components);
132 }
133
134 //start sub-components array
135 if (jp.nextToken() != JsonToken.START_ARRAY) {
136 throw new JCalParseException(JsonToken.START_ARRAY, jp.getCurrentToken());
137 }
138
139 //read sub-components
140 while (jp.nextToken() != JsonToken.END_ARRAY) { //until we reach the end sub-components array
141 if (jp.getCurrentToken() != JsonToken.START_ARRAY) {
142 throw new JCalParseException(JsonToken.START_ARRAY, jp.getCurrentToken());
143 }
144 jp.nextToken();
145 parseComponent(new ArrayList<String>(components));
146 }
147
148 //read the end of the component array (e.g. the last bracket in this example: ["comp", [ /* props */ ], [ /* comps */] ])
149 if (jp.nextToken() != JsonToken.END_ARRAY) {
150 throw new JCalParseException(JsonToken.END_ARRAY, jp.getCurrentToken());
151 }
152 }
153
154 private void parseProperty(List<String> components) throws IOException {
155 //get property name
156 if (jp.getCurrentToken() != JsonToken.VALUE_STRING) {
157 throw new JCalParseException(JsonToken.VALUE_STRING, jp.getCurrentToken());
158 }
159 String propertyName = jp.getValueAsString().toLowerCase();
160
161 ICalParameters parameters = parseParameters();
162
163 //get data type
164 if (jp.nextToken() != JsonToken.VALUE_STRING) {
165 throw new JCalParseException(JsonToken.VALUE_STRING, jp.getCurrentToken());
166 }
167 String dataTypeStr = jp.getText();
168 ICalDataType dataType = "unknown".equals(dataTypeStr) ? null : ICalDataType.get(dataTypeStr);
169
170 //get property value(s)
171 List<JsonValue> values = parseValues();
172
173 JCalValue value = new JCalValue(values);
174 listener.readProperty(components, propertyName, parameters, dataType, value);
175 }
176
177 private ICalParameters parseParameters() throws IOException {
178 if (jp.nextToken() != JsonToken.START_OBJECT) {
179 throw new JCalParseException(JsonToken.START_OBJECT, jp.getCurrentToken());
180 }
181
182 ICalParameters parameters = new ICalParameters();
183 while (jp.nextToken() != JsonToken.END_OBJECT) {
184 String parameterName = jp.getText();
185
186 if (jp.nextToken() == JsonToken.START_ARRAY) {
187 //multi-valued parameter
188 while (jp.nextToken() != JsonToken.END_ARRAY) {
189 parameters.put(parameterName, jp.getText());
190 }
191 } else {
192 parameters.put(parameterName, jp.getValueAsString());
193 }
194 }
195
196 return parameters;
197 }
198
199 private List<JsonValue> parseValues() throws IOException {
200 List<JsonValue> values = new ArrayList<JsonValue>();
201 while (jp.nextToken() != JsonToken.END_ARRAY) { //until we reach the end of the property array
202 JsonValue value = parseValue();
203 values.add(value);
204 }
205 return values;
206 }
207
208 private Object parseValueElement() throws IOException {
209 switch (jp.getCurrentToken()) {
210 case VALUE_FALSE:
211 case VALUE_TRUE:
212 return jp.getBooleanValue();
213 case VALUE_NUMBER_FLOAT:
214 return jp.getDoubleValue();
215 case VALUE_NUMBER_INT:
216 return jp.getLongValue();
217 case VALUE_NULL:
218 return null;
219 default:
220 return jp.getText();
221 }
222 }
223
224 private List<JsonValue> parseValueArray() throws IOException {
225 List<JsonValue> array = new ArrayList<JsonValue>();
226
227 while (jp.nextToken() != JsonToken.END_ARRAY) {
228 JsonValue value = parseValue();
229 array.add(value);
230 }
231
232 return array;
233 }
234
235 private Map<String, JsonValue> parseValueObject() throws IOException {
236 Map<String, JsonValue> object = new HashMap<String, JsonValue>();
237
238 jp.nextToken();
239 while (jp.getCurrentToken() != JsonToken.END_OBJECT) {
240 if (jp.getCurrentToken() != JsonToken.FIELD_NAME) {
241 throw new JCalParseException(JsonToken.FIELD_NAME, jp.getCurrentToken());
242 }
243
244 String key = jp.getText();
245 jp.nextToken();
246 JsonValue value = parseValue();
247 object.put(key, value);
248
249 jp.nextToken();
250 }
251
252 return object;
253 }
254
255 private JsonValue parseValue() throws IOException {
256 switch (jp.getCurrentToken()) {
257 case START_ARRAY:
258 return new JsonValue(parseValueArray());
259 case START_OBJECT:
260 return new JsonValue(parseValueObject());
261 default:
262 return new JsonValue(parseValueElement());
263 }
264 }
265
266 /**
267 * Determines whether the end of the data stream has been reached.
268 * @return true if the end has been reached, false if not
269 */
270 public boolean eof() {
271 return eof;
272 }
273
274 /**
275 * Handles the iCalendar data as it is read off the data stream.
276 * @author Michael Angstadt
277 */
278 public static interface JCalDataStreamListener {
279 /**
280 * Called when the parser begins to read a component.
281 * @param parentHierarchy the component's parent components
282 * @param componentName the component name (e.g. "vevent")
283 */
284 void readComponent(List<String> parentHierarchy, String componentName);
285
286 /**
287 * Called when a property is read.
288 * @param componentHierarchy the hierarchy of components that the
289 * property belongs to
290 * @param propertyName the property name (e.g. "summary")
291 * @param parameters the parameters
292 * @param dataType the data type (e.g. "text")
293 * @param value the property value
294 */
295 void readProperty(List<String> componentHierarchy, String propertyName, ICalParameters parameters, ICalDataType dataType, JCalValue value);
296 }
297
298 /**
299 * Closes the underlying {@link Reader} object.
300 */
301 public void close() throws IOException {
302 reader.close();
303 }
304 }