1 /* 2 * Copyright (C) 2007 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package android.pim; 18 19 import android.util.Log; 20 import android.util.Config; 21 22 import java.util.LinkedHashMap; 23 import java.util.LinkedList; 24 import java.util.List; 25 import java.util.Set; 26 import java.util.ArrayList; 27 28 /** 29 * Parses RFC 2445 iCalendar objects. 30 */ 31 public class ICalendar { 32 33 private static final String TAG = "Sync"; 34 35 // TODO: keep track of VEVENT, VTODO, VJOURNAL, VFREEBUSY, VTIMEZONE, VALARM 36 // components, by type field or by subclass? subclass would allow us to 37 // enforce grammars. 38 39 /** 40 * Exception thrown when an iCalendar object has invalid syntax. 41 */ 42 public static class FormatException extends Exception { 43 public FormatException() { 44 super(); 45 } 46 47 public FormatException(String msg) { 48 super(msg); 49 } 50 51 public FormatException(String msg, Throwable cause) { 52 super(msg, cause); 53 } 54 } 55 56 /** 57 * A component within an iCalendar (VEVENT, VTODO, VJOURNAL, VFEEBUSY, 58 * VTIMEZONE, VALARM). 59 */ 60 public static class Component { 61 62 // components 63 private static final String BEGIN = "BEGIN"; 64 private static final String END = "END"; 65 private static final String NEWLINE = "\n"; 66 public static final String VCALENDAR = "VCALENDAR"; 67 public static final String VEVENT = "VEVENT"; 68 public static final String VTODO = "VTODO"; 69 public static final String VJOURNAL = "VJOURNAL"; 70 public static final String VFREEBUSY = "VFREEBUSY"; 71 public static final String VTIMEZONE = "VTIMEZONE"; 72 public static final String VALARM = "VALARM"; 73 74 private final String mName; 75 private final Component mParent; // see if we can get rid of this 76 private LinkedList<Component> mChildren = null; 77 private final LinkedHashMap<String, ArrayList<Property>> mPropsMap = 78 new LinkedHashMap<String, ArrayList<Property>>(); 79 80 /** 81 * Creates a new component with the provided name. 82 * @param name The name of the component. 83 */ 84 public Component(String name, Component parent) { 85 mName = name; 86 mParent = parent; 87 } 88 89 /** 90 * Returns the name of the component. 91 * @return The name of the component. 92 */ 93 public String getName() { 94 return mName; 95 } 96 97 /** 98 * Returns the parent of this component. 99 * @return The parent of this component. 100 */ 101 public Component getParent() { 102 return mParent; 103 } 104 105 /** 106 * Helper that lazily gets/creates the list of children. 107 * @return The list of children. 108 */ 109 protected LinkedList<Component> getOrCreateChildren() { 110 if (mChildren == null) { 111 mChildren = new LinkedList<Component>(); 112 } 113 return mChildren; 114 } 115 116 /** 117 * Adds a child component to this component. 118 * @param child The child component. 119 */ 120 public void addChild(Component child) { 121 getOrCreateChildren().add(child); 122 } 123 124 /** 125 * Returns a list of the Component children of this component. May be 126 * null, if there are no children. 127 * 128 * @return A list of the children. 129 */ 130 public List<Component> getComponents() { 131 return mChildren; 132 } 133 134 /** 135 * Adds a Property to this component. 136 * @param prop 137 */ 138 public void addProperty(Property prop) { 139 String name= prop.getName(); 140 ArrayList<Property> props = mPropsMap.get(name); 141 if (props == null) { 142 props = new ArrayList<Property>(); 143 mPropsMap.put(name, props); 144 } 145 props.add(prop); 146 } 147 148 /** 149 * Returns a set of the property names within this component. 150 * @return A set of property names within this component. 151 */ 152 public Set<String> getPropertyNames() { 153 return mPropsMap.keySet(); 154 } 155 156 /** 157 * Returns a list of properties with the specified name. Returns null 158 * if there are no such properties. 159 * @param name The name of the property that should be returned. 160 * @return A list of properties with the requested name. 161 */ 162 public List<Property> getProperties(String name) { 163 return mPropsMap.get(name); 164 } 165 166 /** 167 * Returns the first property with the specified name. Returns null 168 * if there is no such property. 169 * @param name The name of the property that should be returned. 170 * @return The first property with the specified name. 171 */ 172 public Property getFirstProperty(String name) { 173 List<Property> props = mPropsMap.get(name); 174 if (props == null || props.size() == 0) { 175 return null; 176 } 177 return props.get(0); 178 } 179 180 @Override 181 public String toString() { 182 StringBuilder sb = new StringBuilder(); 183 toString(sb); 184 sb.append(NEWLINE); 185 return sb.toString(); 186 } 187 188 /** 189 * Helper method that appends this component to a StringBuilder. The 190 * caller is responsible for appending a newline at the end of the 191 * component. 192 */ 193 public void toString(StringBuilder sb) { 194 sb.append(BEGIN); 195 sb.append(":"); 196 sb.append(mName); 197 sb.append(NEWLINE); 198 199 // append the properties 200 for (String propertyName : getPropertyNames()) { 201 for (Property property : getProperties(propertyName)) { 202 property.toString(sb); 203 sb.append(NEWLINE); 204 } 205 } 206 207 // append the sub-components 208 if (mChildren != null) { 209 for (Component component : mChildren) { 210 component.toString(sb); 211 sb.append(NEWLINE); 212 } 213 } 214 215 sb.append(END); 216 sb.append(":"); 217 sb.append(mName); 218 } 219 } 220 221 /** 222 * A property within an iCalendar component (e.g., DTSTART, DTEND, etc., 223 * within a VEVENT). 224 */ 225 public static class Property { 226 // properties 227 // TODO: do we want to list these here? the complete list is long. 228 public static final String DTSTART = "DTSTART"; 229 public static final String DTEND = "DTEND"; 230 public static final String DURATION = "DURATION"; 231 public static final String RRULE = "RRULE"; 232 public static final String RDATE = "RDATE"; 233 public static final String EXRULE = "EXRULE"; 234 public static final String EXDATE = "EXDATE"; 235 // ... need to add more. 236 237 private final String mName; 238 private LinkedHashMap<String, ArrayList<Parameter>> mParamsMap = 239 new LinkedHashMap<String, ArrayList<Parameter>>(); 240 private String mValue; // TODO: make this final? 241 242 /** 243 * Creates a new property with the provided name. 244 * @param name The name of the property. 245 */ 246 public Property(String name) { 247 mName = name; 248 } 249 250 /** 251 * Creates a new property with the provided name and value. 252 * @param name The name of the property. 253 * @param value The value of the property. 254 */ 255 public Property(String name, String value) { 256 mName = name; 257 mValue = value; 258 } 259 260 /** 261 * Returns the name of the property. 262 * @return The name of the property. 263 */ 264 public String getName() { 265 return mName; 266 } 267 268 /** 269 * Returns the value of this property. 270 * @return The value of this property. 271 */ 272 public String getValue() { 273 return mValue; 274 } 275 276 /** 277 * Sets the value of this property. 278 * @param value The desired value for this property. 279 */ 280 public void setValue(String value) { 281 mValue = value; 282 } 283 284 /** 285 * Adds a {@link Parameter} to this property. 286 * @param param The parameter that should be added. 287 */ 288 public void addParameter(Parameter param) { 289 ArrayList<Parameter> params = mParamsMap.get(param.name); 290 if (params == null) { 291 params = new ArrayList<Parameter>(); 292 mParamsMap.put(param.name, params); 293 } 294 params.add(param); 295 } 296 297 /** 298 * Returns the set of parameter names for this property. 299 * @return The set of parameter names for this property. 300 */ 301 public Set<String> getParameterNames() { 302 return mParamsMap.keySet(); 303 } 304 305 /** 306 * Returns the list of parameters with the specified name. May return 307 * null if there are no such parameters. 308 * @param name The name of the parameters that should be returned. 309 * @return The list of parameters with the specified name. 310 */ 311 public List<Parameter> getParameters(String name) { 312 return mParamsMap.get(name); 313 } 314 315 /** 316 * Returns the first parameter with the specified name. May return 317 * nll if there is no such parameter. 318 * @param name The name of the parameter that should be returned. 319 * @return The first parameter with the specified name. 320 */ 321 public Parameter getFirstParameter(String name) { 322 ArrayList<Parameter> params = mParamsMap.get(name); 323 if (params == null || params.size() == 0) { 324 return null; 325 } 326 return params.get(0); 327 } 328 329 @Override 330 public String toString() { 331 StringBuilder sb = new StringBuilder(); 332 toString(sb); 333 return sb.toString(); 334 } 335 336 /** 337 * Helper method that appends this property to a StringBuilder. The 338 * caller is responsible for appending a newline after this property. 339 */ 340 public void toString(StringBuilder sb) { 341 sb.append(mName); 342 Set<String> parameterNames = getParameterNames(); 343 for (String parameterName : parameterNames) { 344 for (Parameter param : getParameters(parameterName)) { 345 sb.append(";"); 346 param.toString(sb); 347 } 348 } 349 sb.append(":"); 350 sb.append(mValue); 351 } 352 } 353 354 /** 355 * A parameter defined for an iCalendar property. 356 */ 357 // TODO: make this a proper class rather than a struct? 358 public static class Parameter { 359 public String name; 360 public String value; 361 362 /** 363 * Creates a new empty parameter. 364 */ 365 public Parameter() { 366 } 367 368 /** 369 * Creates a new parameter with the specified name and value. 370 * @param name The name of the parameter. 371 * @param value The value of the parameter. 372 */ 373 public Parameter(String name, String value) { 374 this.name = name; 375 this.value = value; 376 } 377 378 @Override 379 public String toString() { 380 StringBuilder sb = new StringBuilder(); 381 toString(sb); 382 return sb.toString(); 383 } 384 385 /** 386 * Helper method that appends this parameter to a StringBuilder. 387 */ 388 public void toString(StringBuilder sb) { 389 sb.append(name); 390 sb.append("="); 391 sb.append(value); 392 } 393 } 394 395 private static final class ParserState { 396 // public int lineNumber = 0; 397 public String line; // TODO: just point to original text 398 public int index; 399 } 400 401 // use factory method 402 private ICalendar() { 403 } 404 405 // TODO: get rid of this -- handle all of the parsing in one pass through 406 // the text. 407 private static String normalizeText(String text) { 408 // it's supposed to be \r\n, but not everyone does that 409 text = text.replaceAll("\r\n", "\n"); 410 text = text.replaceAll("\r", "\n"); 411 412 // we deal with line folding, by replacing all "\n " strings 413 // with nothing. The RFC specifies "\r\n " to be folded, but 414 // we handle "\n " and "\r " too because we can get those. 415 text = text.replaceAll("\n ", ""); 416 417 return text; 418 } 419 420 /** 421 * Parses text into an iCalendar component. Parses into the provided 422 * component, if not null, or parses into a new component. In the latter 423 * case, expects a BEGIN as the first line. Returns the provided or newly 424 * created top-level component. 425 */ 426 // TODO: use an index into the text, so we can make this a recursive 427 // function? 428 private static Component parseComponentImpl(Component component, 429 String text) 430 throws FormatException { 431 Component current = component; 432 ParserState state = new ParserState(); 433 state.index = 0; 434 435 // split into lines 436 String[] lines = text.split("\n"); 437 438 // each line is of the format: 439 // name *(";" param) ":" value 440 for (String line : lines) { 441 try { 442 current = parseLine(line, state, current); 443 // if the provided component was null, we will return the root 444 // NOTE: in this case, if the first line is not a BEGIN, a 445 // FormatException will get thrown. 446 if (component == null) { 447 component = current; 448 } 449 } catch (FormatException fe) { 450 if (Config.LOGV) { 451 Log.v(TAG, "Cannot parse " + line, fe); 452 } 453 // for now, we ignore the parse error. Google Calendar seems 454 // to be emitting some misformatted iCalendar objects. 455 } 456 continue; 457 } 458 return component; 459 } 460 461 /** 462 * Parses a line into the provided component. Creates a new component if 463 * the line is a BEGIN, adding the newly created component to the provided 464 * parent. Returns whatever component is the current one (to which new 465 * properties will be added) in the parse. 466 */ 467 private static Component parseLine(String line, ParserState state, 468 Component component) 469 throws FormatException { 470 state.line = line; 471 int len = state.line.length(); 472 473 // grab the name 474 char c = 0; 475 for (state.index = 0; state.index < len; ++state.index) { 476 c = line.charAt(state.index); 477 if (c == ';' || c == ':') { 478 break; 479 } 480 } 481 String name = line.substring(0, state.index); 482 483 if (component == null) { 484 if (!Component.BEGIN.equals(name)) { 485 throw new FormatException("Expected BEGIN"); 486 } 487 } 488 489 Property property; 490 if (Component.BEGIN.equals(name)) { 491 // start a new component 492 String componentName = extractValue(state); 493 Component child = new Component(componentName, component); 494 if (component != null) { 495 component.addChild(child); 496 } 497 return child; 498 } else if (Component.END.equals(name)) { 499 // finish the current component 500 String componentName = extractValue(state); 501 if (component == null || 502 !componentName.equals(component.getName())) { 503 throw new FormatException("Unexpected END " + componentName); 504 } 505 return component.getParent(); 506 } else { 507 property = new Property(name); 508 } 509 510 if (c == ';') { 511 Parameter parameter = null; 512 while ((parameter = extractParameter(state)) != null) { 513 property.addParameter(parameter); 514 } 515 } 516 String value = extractValue(state); 517 property.setValue(value); 518 component.addProperty(property); 519 return component; 520 } 521 522 /** 523 * Extracts the value ":..." on the current line. The first character must 524 * be a ':'. 525 */ 526 private static String extractValue(ParserState state) 527 throws FormatException { 528 String line = state.line; 529 if (state.index >= line.length() || line.charAt(state.index) != ':') { 530 throw new FormatException("Expected ':' before end of line in " 531 + line); 532 } 533 String value = line.substring(state.index + 1); 534 state.index = line.length() - 1; 535 return value; 536 } 537 538 /** 539 * Extracts the next parameter from the line, if any. If there are no more 540 * parameters, returns null. 541 */ 542 private static Parameter extractParameter(ParserState state) 543 throws FormatException { 544 String text = state.line; 545 int len = text.length(); 546 Parameter parameter = null; 547 int startIndex = -1; 548 int equalIndex = -1; 549 while (state.index < len) { 550 char c = text.charAt(state.index); 551 if (c == ':') { 552 if (parameter != null) { 553 if (equalIndex == -1) { 554 throw new FormatException("Expected '=' within " 555 + "parameter in " + text); 556 } 557 parameter.value = text.substring(equalIndex + 1, 558 state.index); 559 } 560 return parameter; // may be null 561 } else if (c == ';') { 562 if (parameter != null) { 563 if (equalIndex == -1) { 564 throw new FormatException("Expected '=' within " 565 + "parameter in " + text); 566 } 567 parameter.value = text.substring(equalIndex + 1, 568 state.index); 569 return parameter; 570 } else { 571 parameter = new Parameter(); 572 startIndex = state.index; 573 } 574 } else if (c == '=') { 575 equalIndex = state.index; 576 if ((parameter == null) || (startIndex == -1)) { 577 throw new FormatException("Expected ';' before '=' in " 578 + text); 579 } 580 parameter.name = text.substring(startIndex + 1, equalIndex); 581 } else if (c == '"') { 582 if (parameter == null) { 583 throw new FormatException("Expected parameter before '\"' in " + text); 584 } 585 if (equalIndex == -1) { 586 throw new FormatException("Expected '=' within parameter in " + text); 587 } 588 if (state.index > equalIndex + 1) { 589 throw new FormatException("Parameter value cannot contain a '\"' in " + text); 590 } 591 final int endQuote = text.indexOf('"', state.index + 1); 592 if (endQuote < 0) { 593 throw new FormatException("Expected closing '\"' in " + text); 594 } 595 parameter.value = text.substring(state.index + 1, endQuote); 596 state.index = endQuote + 1; 597 return parameter; 598 } 599 ++state.index; 600 } 601 throw new FormatException("Expected ':' before end of line in " + text); 602 } 603 604 /** 605 * Parses the provided text into an iCalendar object. The top-level 606 * component must be of type VCALENDAR. 607 * @param text The text to be parsed. 608 * @return The top-level VCALENDAR component. 609 * @throws FormatException Thrown if the text could not be parsed into an 610 * iCalendar VCALENDAR object. 611 */ 612 public static Component parseCalendar(String text) throws FormatException { 613 Component calendar = parseComponent(null, text); 614 if (calendar == null || !Component.VCALENDAR.equals(calendar.getName())) { 615 throw new FormatException("Expected " + Component.VCALENDAR); 616 } 617 return calendar; 618 } 619 620 /** 621 * Parses the provided text into an iCalendar event. The top-level 622 * component must be of type VEVENT. 623 * @param text The text to be parsed. 624 * @return The top-level VEVENT component. 625 * @throws FormatException Thrown if the text could not be parsed into an 626 * iCalendar VEVENT. 627 */ 628 public static Component parseEvent(String text) throws FormatException { 629 Component event = parseComponent(null, text); 630 if (event == null || !Component.VEVENT.equals(event.getName())) { 631 throw new FormatException("Expected " + Component.VEVENT); 632 } 633 return event; 634 } 635 636 /** 637 * Parses the provided text into an iCalendar component. 638 * @param text The text to be parsed. 639 * @return The top-level component. 640 * @throws FormatException Thrown if the text could not be parsed into an 641 * iCalendar component. 642 */ 643 public static Component parseComponent(String text) throws FormatException { 644 return parseComponent(null, text); 645 } 646 647 /** 648 * Parses the provided text, adding to the provided component. 649 * @param component The component to which the parsed iCalendar data should 650 * be added. 651 * @param text The text to be parsed. 652 * @return The top-level component. 653 * @throws FormatException Thrown if the text could not be parsed as an 654 * iCalendar object. 655 */ 656 public static Component parseComponent(Component component, String text) 657 throws FormatException { 658 text = normalizeText(text); 659 return parseComponentImpl(component, text); 660 } 661 } 662