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-2016, 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 VCALENDAR_COMPONENT_NAME = 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        private boolean strict = false;
058
059        /**
060         * @param reader the reader to wrap
061         */
062        public JCalRawReader(Reader reader) {
063                this.reader = reader;
064        }
065
066        /**
067         * @param parser the parser to read from
068         * @param strict true if the parser's current token is expected to be
069         * positioned at the start of a jCard, false if not. If this is true, and
070         * the parser is not positioned at the beginning of a jCard, a
071         * {@link JCalParseException} will be thrown. If this if false, the parser
072         * will consume input until it reaches the beginning of a jCard.
073         */
074        public JCalRawReader(JsonParser parser, boolean strict) {
075                reader = null;
076                this.parser = parser;
077                this.strict = strict;
078        }
079
080        /**
081         * Gets the current line number.
082         * @return the line number
083         */
084        public int getLineNum() {
085                return (parser == null) ? 0 : parser.getCurrentLocation().getLineNr();
086        }
087
088        /**
089         * Reads the next iCalendar object from the jCal data stream.
090         * @param listener handles the iCalendar data as it is read off the wire
091         * @throws JCalParseException if the jCal syntax is incorrect (the JSON
092         * syntax may be valid, but it is not in the correct jCal format).
093         * @throws JsonParseException if the JSON syntax is incorrect
094         * @throws IOException if there is a problem reading from the data stream
095         */
096        public void readNext(JCalDataStreamListener listener) throws IOException {
097                if (parser == null) {
098                        JsonFactory factory = new JsonFactory();
099                        parser = factory.createParser(reader);
100                }
101
102                if (parser.isClosed()) {
103                        return;
104                }
105
106                this.listener = listener;
107
108                //find the next iCalendar object
109                JsonToken prev = parser.getCurrentToken();
110                JsonToken cur;
111                while ((cur = parser.nextToken()) != null) {
112                        if (prev == JsonToken.START_ARRAY && cur == JsonToken.VALUE_STRING && VCALENDAR_COMPONENT_NAME.equals(parser.getValueAsString())) {
113                                //found
114                                break;
115                        }
116
117                        if (strict) {
118                                //the parser was expecting the jCal to be there 
119                                if (prev != JsonToken.START_ARRAY) {
120                                        throw new JCalParseException(JsonToken.START_ARRAY, prev);
121                                }
122
123                                if (cur != JsonToken.VALUE_STRING) {
124                                        throw new JCalParseException(JsonToken.VALUE_STRING, cur);
125                                }
126
127                                throw new JCalParseException("Invalid value for first token: expected \"vcalendar\" , was \"" + parser.getValueAsString() + "\"", JsonToken.VALUE_STRING, cur);
128                        }
129
130                        prev = cur;
131                }
132
133                if (cur == null) {
134                        //EOF
135                        eof = true;
136                        return;
137                }
138
139                parseComponent(new ArrayList<String>());
140        }
141
142        private void parseComponent(List<String> components) throws IOException {
143                checkCurrent(JsonToken.VALUE_STRING);
144                String componentName = parser.getValueAsString();
145                listener.readComponent(components, componentName);
146                components.add(componentName);
147
148                //start properties array
149                checkNext(JsonToken.START_ARRAY);
150
151                //read properties
152                while (parser.nextToken() != JsonToken.END_ARRAY) { //until we reach the end properties array
153                        checkCurrent(JsonToken.START_ARRAY);
154                        parser.nextToken();
155                        parseProperty(components);
156                }
157
158                //start sub-components array
159                checkNext(JsonToken.START_ARRAY);
160
161                //read sub-components
162                while (parser.nextToken() != JsonToken.END_ARRAY) { //until we reach the end sub-components array
163                        checkCurrent(JsonToken.START_ARRAY);
164                        parser.nextToken();
165                        parseComponent(new ArrayList<String>(components));
166                }
167
168                //read the end of the component array (e.g. the last bracket in this example: ["comp", [ /* props */ ], [ /* comps */] ])
169                checkNext(JsonToken.END_ARRAY);
170        }
171
172        private void parseProperty(List<String> components) throws IOException {
173                //get property name
174                checkCurrent(JsonToken.VALUE_STRING);
175                String propertyName = parser.getValueAsString().toLowerCase();
176
177                ICalParameters parameters = parseParameters();
178
179                //get data type
180                checkNext(JsonToken.VALUE_STRING);
181                String dataTypeStr = parser.getText();
182                ICalDataType dataType = "unknown".equals(dataTypeStr) ? null : ICalDataType.get(dataTypeStr);
183
184                //get property value(s)
185                List<JsonValue> values = parseValues();
186
187                JCalValue value = new JCalValue(values);
188                listener.readProperty(components, propertyName, parameters, dataType, value);
189        }
190
191        private ICalParameters parseParameters() throws IOException {
192                checkNext(JsonToken.START_OBJECT);
193
194                ICalParameters parameters = new ICalParameters();
195                while (parser.nextToken() != JsonToken.END_OBJECT) {
196                        String parameterName = parser.getText();
197
198                        if (parser.nextToken() == JsonToken.START_ARRAY) {
199                                //multi-valued parameter
200                                while (parser.nextToken() != JsonToken.END_ARRAY) {
201                                        parameters.put(parameterName, parser.getText());
202                                }
203                        } else {
204                                parameters.put(parameterName, parser.getValueAsString());
205                        }
206                }
207
208                return parameters;
209        }
210
211        private List<JsonValue> parseValues() throws IOException {
212                List<JsonValue> values = new ArrayList<JsonValue>();
213                while (parser.nextToken() != JsonToken.END_ARRAY) { //until we reach the end of the property array
214                        JsonValue value = parseValue();
215                        values.add(value);
216                }
217                return values;
218        }
219
220        private Object parseValueElement() throws IOException {
221                switch (parser.getCurrentToken()) {
222                case VALUE_FALSE:
223                case VALUE_TRUE:
224                        return parser.getBooleanValue();
225                case VALUE_NUMBER_FLOAT:
226                        return parser.getDoubleValue();
227                case VALUE_NUMBER_INT:
228                        return parser.getLongValue();
229                case VALUE_NULL:
230                        return null;
231                default:
232                        return parser.getText();
233                }
234        }
235
236        private List<JsonValue> parseValueArray() throws IOException {
237                List<JsonValue> array = new ArrayList<JsonValue>();
238
239                while (parser.nextToken() != JsonToken.END_ARRAY) {
240                        JsonValue value = parseValue();
241                        array.add(value);
242                }
243
244                return array;
245        }
246
247        private Map<String, JsonValue> parseValueObject() throws IOException {
248                Map<String, JsonValue> object = new HashMap<String, JsonValue>();
249
250                parser.nextToken();
251                while (parser.getCurrentToken() != JsonToken.END_OBJECT) {
252                        checkCurrent(JsonToken.FIELD_NAME);
253
254                        String key = parser.getText();
255                        parser.nextToken();
256                        JsonValue value = parseValue();
257                        object.put(key, value);
258
259                        parser.nextToken();
260                }
261
262                return object;
263        }
264
265        private JsonValue parseValue() throws IOException {
266                switch (parser.getCurrentToken()) {
267                case START_ARRAY:
268                        return new JsonValue(parseValueArray());
269                case START_OBJECT:
270                        return new JsonValue(parseValueObject());
271                default:
272                        return new JsonValue(parseValueElement());
273                }
274        }
275
276        private void checkNext(JsonToken expected) throws IOException {
277                JsonToken actual = parser.nextToken();
278                check(expected, actual);
279        }
280
281        private void checkCurrent(JsonToken expected) throws JCalParseException {
282                JsonToken actual = parser.getCurrentToken();
283                check(expected, actual);
284        }
285
286        private void check(JsonToken expected, JsonToken actual) throws JCalParseException {
287                if (actual != expected) {
288                        throw new JCalParseException(expected, actual);
289                }
290        }
291
292        /**
293         * Determines whether the end of the data stream has been reached.
294         * @return true if the end has been reached, false if not
295         */
296        public boolean eof() {
297                return eof;
298        }
299
300        /**
301         * Handles the iCalendar data as it is read off the data stream.
302         * @author Michael Angstadt
303         */
304        public interface JCalDataStreamListener {
305                /**
306                 * Called when the parser begins to read a component.
307                 * @param parentHierarchy the component's parent components
308                 * @param componentName the component name (e.g. "vevent")
309                 */
310                void readComponent(List<String> parentHierarchy, String componentName);
311
312                /**
313                 * Called when a property is read.
314                 * @param componentHierarchy the hierarchy of components that the
315                 * property belongs to
316                 * @param propertyName the property name (e.g. "summary")
317                 * @param parameters the parameters
318                 * @param dataType the data type (e.g. "text")
319                 * @param value the property value
320                 */
321                void readProperty(List<String> componentHierarchy, String propertyName, ICalParameters parameters, ICalDataType dataType, JCalValue value);
322        }
323
324        /**
325         * Closes the underlying {@link Reader} object.
326         */
327        public void close() throws IOException {
328                if (parser != null) {
329                        parser.close();
330                }
331                if (reader != null) {
332                        reader.close();
333                }
334        }
335}