001    package ezvcard.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 com.fasterxml.jackson.core.JsonFactory;
012    import com.fasterxml.jackson.core.JsonParseException;
013    import com.fasterxml.jackson.core.JsonParser;
014    import com.fasterxml.jackson.core.JsonToken;
015    
016    import ezvcard.VCardDataType;
017    import ezvcard.parameter.VCardParameters;
018    
019    /*
020     Copyright (c) 2013, 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
048     * href="http://tools.ietf.org/html/draft-kewisch-vcard-in-json-04">jCard
049     * draft</a>
050     */
051    public class JCardRawReader implements Closeable {
052            private final Reader reader;
053            private JsonParser jp;
054            private boolean eof = false;
055            private JCardDataStreamListener listener;
056    
057            /**
058             * Creates a new reader.
059             * @param reader the reader to the data stream
060             */
061            public JCardRawReader(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 (jp == null) ? 0 : jp.getCurrentLocation().getLineNr();
071            }
072    
073            /**
074             * Reads the next vCard from the jCard data stream.
075             * @param listener handles the vCard data as it is read off the wire
076             * @throws JCardParseException if the jCard syntax is incorrect (the JSON
077             * syntax may be valid, but it is not in the correct jCard 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(JCardDataStreamListener listener) throws IOException {
082                    if (jp == null) {
083                            JsonFactory factory = new JsonFactory();
084                            jp = factory.createJsonParser(reader);
085                    } else if (jp.isClosed()) {
086                            return;
087                    }
088    
089                    this.listener = listener;
090    
091                    //find the next vCard object
092                    JsonToken prev = null;
093                    JsonToken cur;
094                    while ((cur = jp.nextToken()) != null) {
095                            if (prev == JsonToken.START_ARRAY && cur == JsonToken.VALUE_STRING && "vcard".equals(jp.getValueAsString())) {
096                                    break;
097                            }
098                            prev = cur;
099                    }
100                    if (cur == null) {
101                            //EOF
102                            eof = true;
103                            return;
104                    }
105    
106                    listener.beginVCard();
107                    parseProperties();
108            }
109    
110            private void parseProperties() throws IOException {
111                    //start properties array
112                    if (jp.nextToken() != JsonToken.START_ARRAY) {
113                            throw new JCardParseException(JsonToken.START_ARRAY, jp.getCurrentToken());
114                    }
115    
116                    //read properties
117                    while (jp.nextToken() != JsonToken.END_ARRAY) { //until we reach the end properties array
118                            if (jp.getCurrentToken() != JsonToken.START_ARRAY) {
119                                    throw new JCardParseException(JsonToken.START_ARRAY, jp.getCurrentToken());
120                            }
121                            jp.nextToken();
122                            parseProperty();
123                    }
124            }
125    
126            private void parseProperty() throws IOException {
127                    //get property name
128                    if (jp.getCurrentToken() != JsonToken.VALUE_STRING) {
129                            throw new JCardParseException(JsonToken.VALUE_STRING, jp.getCurrentToken());
130                    }
131                    String propertyName = jp.getValueAsString().toLowerCase();
132    
133                    //get parameters
134                    VCardParameters parameters = parseParameters();
135    
136                    //get group
137                    List<String> removed = parameters.removeAll("group");
138                    String group = removed.isEmpty() ? null : removed.get(0);
139    
140                    //get data type
141                    if (jp.nextToken() != JsonToken.VALUE_STRING) {
142                            throw new JCardParseException(JsonToken.VALUE_STRING, jp.getCurrentToken());
143                    }
144                    String dataTypeStr = jp.getText().toLowerCase();
145                    VCardDataType dataType = "unknown".equals(dataTypeStr) ? null : VCardDataType.get(dataTypeStr);
146    
147                    //get property value(s)
148                    List<JsonValue> values = parseValues();
149    
150                    JCardValue value = new JCardValue(values);
151                    listener.readProperty(group, propertyName, parameters, dataType, value);
152            }
153    
154            private VCardParameters parseParameters() throws IOException {
155                    if (jp.nextToken() != JsonToken.START_OBJECT) {
156                            throw new JCardParseException(JsonToken.START_OBJECT, jp.getCurrentToken());
157                    }
158    
159                    VCardParameters parameters = new VCardParameters();
160                    while (jp.nextToken() != JsonToken.END_OBJECT) {
161                            String parameterName = jp.getText();
162    
163                            if (jp.nextToken() == JsonToken.START_ARRAY) {
164                                    //multi-valued parameter
165                                    while (jp.nextToken() != JsonToken.END_ARRAY) {
166                                            parameters.put(parameterName, jp.getText());
167                                    }
168                            } else {
169                                    parameters.put(parameterName, jp.getValueAsString());
170                            }
171                    }
172    
173                    return parameters;
174            }
175    
176            private List<JsonValue> parseValues() throws IOException {
177                    List<JsonValue> values = new ArrayList<JsonValue>();
178                    while (jp.nextToken() != JsonToken.END_ARRAY) { //until we reach the end of the property array
179                            JsonValue value = parseValue();
180                            values.add(value);
181                    }
182                    return values;
183            }
184    
185            private Object parseValueElement() throws IOException {
186                    switch (jp.getCurrentToken()) {
187                    case VALUE_FALSE:
188                    case VALUE_TRUE:
189                            return jp.getBooleanValue();
190                    case VALUE_NUMBER_FLOAT:
191                            return jp.getDoubleValue();
192                    case VALUE_NUMBER_INT:
193                            return jp.getLongValue();
194                    case VALUE_NULL:
195                            return null;
196                    default:
197                            return jp.getText();
198                    }
199            }
200    
201            private List<JsonValue> parseValueArray() throws IOException {
202                    List<JsonValue> array = new ArrayList<JsonValue>();
203    
204                    while (jp.nextToken() != JsonToken.END_ARRAY) {
205                            JsonValue value = parseValue();
206                            array.add(value);
207                    }
208    
209                    return array;
210            }
211    
212            private Map<String, JsonValue> parseValueObject() throws IOException {
213                    Map<String, JsonValue> object = new HashMap<String, JsonValue>();
214    
215                    jp.nextToken();
216                    while (jp.getCurrentToken() != JsonToken.END_OBJECT) {
217                            if (jp.getCurrentToken() != JsonToken.FIELD_NAME) {
218                                    throw new JCardParseException(JsonToken.FIELD_NAME, jp.getCurrentToken());
219                            }
220    
221                            String key = jp.getText();
222                            jp.nextToken();
223                            JsonValue value = parseValue();
224                            object.put(key, value);
225    
226                            jp.nextToken();
227                    }
228    
229                    return object;
230            }
231    
232            private JsonValue parseValue() throws IOException {
233                    switch (jp.getCurrentToken()) {
234                    case START_ARRAY:
235                            return new JsonValue(parseValueArray());
236                    case START_OBJECT:
237                            return new JsonValue(parseValueObject());
238                    default:
239                            return new JsonValue(parseValueElement());
240                    }
241            }
242    
243            /**
244             * Determines whether the end of the data stream has been reached.
245             * @return true if the end has been reached, false if not
246             */
247            public boolean eof() {
248                    return eof;
249            }
250    
251            /**
252             * Handles the vCard data as it is read off the data stream.
253             * @author Michael Angstadt
254             */
255            public static interface JCardDataStreamListener {
256                    /**
257                     * Called when a vCard has been found in the stream.
258                     */
259                    void beginVCard();
260    
261                    /**
262                     * Called when a property is read.
263                     * @param group the group or null if there is not group
264                     * @param propertyName the property name (e.g. "summary")
265                     * @param parameters the parameters
266                     * @param dataType the data type or null for "unknown"
267                     * @param value the property value
268                     */
269                    void readProperty(String group, String propertyName, VCardParameters parameters, VCardDataType dataType, JCardValue value);
270            }
271    
272            /**
273             * Closes the underlying {@link Reader} object.
274             */
275            public void close() throws IOException {
276                    reader.close();
277            }
278    }