blob: 9c4eaf4f5765abc95571aa9a1a372af132e8ace8 [file] [log] [blame]
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001/*
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
17package android.pim;
18
19import android.util.Log;
20import android.util.Config;
21
22import java.util.LinkedHashMap;
23import java.util.LinkedList;
24import java.util.List;
25import java.util.Set;
26import java.util.ArrayList;
27
28/**
29 * Parses RFC 2445 iCalendar objects.
30 */
31public 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 Albert06912bd2011-02-17 18:10:14 -0800581 } 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 Project9066cfe2009-03-03 19:31:44 -0800598 }
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}