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