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