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