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}