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