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