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    }