The Android Open Source Project | 9066cfe | 2009-03-03 19:31:44 -0800 | [diff] [blame] | 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); |
Alon Albert | 06912bd | 2011-02-17 18:10:14 -0800 | [diff] [blame] | 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; |
The Android Open Source Project | 9066cfe | 2009-03-03 19:31:44 -0800 | [diff] [blame] | 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 | } |