001package biweekly.io.json;
002
003import java.io.Closeable;
004import java.io.IOException;
005import java.io.Reader;
006import java.util.ArrayList;
007import java.util.HashMap;
008import java.util.List;
009import java.util.Map;
010
011import biweekly.ICalDataType;
012import biweekly.io.scribe.ScribeIndex;
013import biweekly.parameter.ICalParameters;
014
015import com.fasterxml.jackson.core.JsonFactory;
016import com.fasterxml.jackson.core.JsonParseException;
017import com.fasterxml.jackson.core.JsonParser;
018import com.fasterxml.jackson.core.JsonToken;
019
020/*
021 Copyright (c) 2013-2015, 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/rfc7265">RFC 7265</a>
049 */
050public class JCalRawReader implements Closeable {
051        private static final String vcalendarComponentName = ScribeIndex.getICalendarScribe().getComponentName().toLowerCase(); //"vcalendar"
052
053        private final Reader reader;
054        private JsonParser parser;
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 (parser == null) ? 0 : parser.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 (parser == null) {
084                        JsonFactory factory = new JsonFactory();
085                        parser = factory.createParser(reader);
086                }
087
088                if (parser.isClosed()) {
089                        return;
090                }
091
092                this.listener = listener;
093
094                //find the next iCalendar object
095                JsonToken prev = null;
096                JsonToken cur;
097                while ((cur = parser.nextToken()) != null) {
098                        if (prev == JsonToken.START_ARRAY && cur == JsonToken.VALUE_STRING && vcalendarComponentName.equals(parser.getValueAsString())) {
099                                break;
100                        }
101                        prev = cur;
102                }
103                if (cur == null) {
104                        //EOF
105                        eof = true;
106                        return;
107                }
108
109                parseComponent(new ArrayList<String>());
110        }
111
112        private void parseComponent(List<String> components) throws IOException {
113                checkCurrent(JsonToken.VALUE_STRING);
114                String componentName = parser.getValueAsString();
115                listener.readComponent(components, componentName);
116                components.add(componentName);
117
118                //start properties array
119                checkNext(JsonToken.START_ARRAY);
120
121                //read properties
122                while (parser.nextToken() != JsonToken.END_ARRAY) { //until we reach the end properties array
123                        checkCurrent(JsonToken.START_ARRAY);
124                        parser.nextToken();
125                        parseProperty(components);
126                }
127
128                //start sub-components array
129                checkNext(JsonToken.START_ARRAY);
130
131                //read sub-components
132                while (parser.nextToken() != JsonToken.END_ARRAY) { //until we reach the end sub-components array
133                        checkCurrent(JsonToken.START_ARRAY);
134                        parser.nextToken();
135                        parseComponent(new ArrayList<String>(components));
136                }
137
138                //read the end of the component array (e.g. the last bracket in this example: ["comp", [ /* props */ ], [ /* comps */] ])
139                checkNext(JsonToken.END_ARRAY);
140        }
141
142        private void parseProperty(List<String> components) throws IOException {
143                //get property name
144                checkCurrent(JsonToken.VALUE_STRING);
145                String propertyName = parser.getValueAsString().toLowerCase();
146
147                ICalParameters parameters = parseParameters();
148
149                //get data type
150                checkNext(JsonToken.VALUE_STRING);
151                String dataTypeStr = parser.getText();
152                ICalDataType dataType = "unknown".equals(dataTypeStr) ? null : ICalDataType.get(dataTypeStr);
153
154                //get property value(s)
155                List<JsonValue> values = parseValues();
156
157                JCalValue value = new JCalValue(values);
158                listener.readProperty(components, propertyName, parameters, dataType, value);
159        }
160
161        private ICalParameters parseParameters() throws IOException {
162                checkNext(JsonToken.START_OBJECT);
163
164                ICalParameters parameters = new ICalParameters();
165                while (parser.nextToken() != JsonToken.END_OBJECT) {
166                        String parameterName = parser.getText();
167
168                        if (parser.nextToken() == JsonToken.START_ARRAY) {
169                                //multi-valued parameter
170                                while (parser.nextToken() != JsonToken.END_ARRAY) {
171                                        parameters.put(parameterName, parser.getText());
172                                }
173                        } else {
174                                parameters.put(parameterName, parser.getValueAsString());
175                        }
176                }
177
178                return parameters;
179        }
180
181        private List<JsonValue> parseValues() throws IOException {
182                List<JsonValue> values = new ArrayList<JsonValue>();
183                while (parser.nextToken() != JsonToken.END_ARRAY) { //until we reach the end of the property array
184                        JsonValue value = parseValue();
185                        values.add(value);
186                }
187                return values;
188        }
189
190        private Object parseValueElement() throws IOException {
191                switch (parser.getCurrentToken()) {
192                case VALUE_FALSE:
193                case VALUE_TRUE:
194                        return parser.getBooleanValue();
195                case VALUE_NUMBER_FLOAT:
196                        return parser.getDoubleValue();
197                case VALUE_NUMBER_INT:
198                        return parser.getLongValue();
199                case VALUE_NULL:
200                        return null;
201                default:
202                        return parser.getText();
203                }
204        }
205
206        private List<JsonValue> parseValueArray() throws IOException {
207                List<JsonValue> array = new ArrayList<JsonValue>();
208
209                while (parser.nextToken() != JsonToken.END_ARRAY) {
210                        JsonValue value = parseValue();
211                        array.add(value);
212                }
213
214                return array;
215        }
216
217        private Map<String, JsonValue> parseValueObject() throws IOException {
218                Map<String, JsonValue> object = new HashMap<String, JsonValue>();
219
220                parser.nextToken();
221                while (parser.getCurrentToken() != JsonToken.END_OBJECT) {
222                        checkCurrent(JsonToken.FIELD_NAME);
223
224                        String key = parser.getText();
225                        parser.nextToken();
226                        JsonValue value = parseValue();
227                        object.put(key, value);
228
229                        parser.nextToken();
230                }
231
232                return object;
233        }
234
235        private JsonValue parseValue() throws IOException {
236                switch (parser.getCurrentToken()) {
237                case START_ARRAY:
238                        return new JsonValue(parseValueArray());
239                case START_OBJECT:
240                        return new JsonValue(parseValueObject());
241                default:
242                        return new JsonValue(parseValueElement());
243                }
244        }
245
246        private void checkNext(JsonToken expected) throws IOException {
247                JsonToken actual = parser.nextToken();
248                check(expected, actual);
249        }
250
251        private void checkCurrent(JsonToken expected) throws JCalParseException {
252                JsonToken actual = parser.getCurrentToken();
253                check(expected, actual);
254        }
255
256        private void check(JsonToken expected, JsonToken actual) throws JCalParseException {
257                if (actual != expected) {
258                        throw new JCalParseException(expected, actual);
259                }
260        }
261
262        /**
263         * Determines whether the end of the data stream has been reached.
264         * @return true if the end has been reached, false if not
265         */
266        public boolean eof() {
267                return eof;
268        }
269
270        /**
271         * Handles the iCalendar data as it is read off the data stream.
272         * @author Michael Angstadt
273         */
274        public static interface JCalDataStreamListener {
275                /**
276                 * Called when the parser begins to read a component.
277                 * @param parentHierarchy the component's parent components
278                 * @param componentName the component name (e.g. "vevent")
279                 */
280                void readComponent(List<String> parentHierarchy, String componentName);
281
282                /**
283                 * Called when a property is read.
284                 * @param componentHierarchy the hierarchy of components that the
285                 * property belongs to
286                 * @param propertyName the property name (e.g. "summary")
287                 * @param parameters the parameters
288                 * @param dataType the data type (e.g. "text")
289                 * @param value the property value
290                 */
291                void readProperty(List<String> componentHierarchy, String propertyName, ICalParameters parameters, ICalDataType dataType, JCalValue value);
292        }
293
294        /**
295         * Closes the underlying {@link Reader} object.
296         */
297        public void close() throws IOException {
298                reader.close();
299        }
300}