0001: /*
0002: *
0003: *
0004: * Copyright 1990-2007 Sun Microsystems, Inc. All Rights Reserved.
0005: * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER
0006: *
0007: * This program is free software; you can redistribute it and/or
0008: * modify it under the terms of the GNU General Public License version
0009: * 2 only, as published by the Free Software Foundation.
0010: *
0011: * This program is distributed in the hope that it will be useful, but
0012: * WITHOUT ANY WARRANTY; without even the implied warranty of
0013: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
0014: * General Public License version 2 for more details (a copy is
0015: * included at /legal/license.txt).
0016: *
0017: * You should have received a copy of the GNU General Public License
0018: * version 2 along with this work; if not, write to the Free Software
0019: * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
0020: * 02110-1301 USA
0021: *
0022: * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa
0023: * Clara, CA 95054 or visit www.sun.com if you need additional
0024: * information or have any questions.
0025: */
0026:
0027: package com.sun.kvem.midp.pim.formats;
0028:
0029: import com.sun.kvem.midp.pim.AbstractPIMItem;
0030: import com.sun.kvem.midp.pim.AbstractPIMList;
0031: import com.sun.kvem.midp.pim.EventImpl;
0032: import com.sun.kvem.midp.pim.LineReader;
0033: import com.sun.kvem.midp.pim.PIMFormat;
0034: import com.sun.kvem.midp.pim.PIMHandler;
0035: import com.sun.kvem.midp.pim.ToDoImpl;
0036: import com.sun.kvem.midp.pim.UnsupportedPIMFormatException;
0037: import java.io.IOException;
0038: import java.io.InputStream;
0039: import java.io.OutputStream;
0040: import java.io.OutputStreamWriter;
0041: import java.io.Writer;
0042: import java.util.Date;
0043: import java.util.Enumeration;
0044: import java.util.Vector;
0045: import javax.microedition.pim.*;
0046:
0047: /**
0048: * Implementation of PIMEncoding for VCalendar/1.0.
0049: *
0050: */
0051: public class VCalendar10Format extends EndMatcher implements PIMFormat {
0052:
0053: /** List of the weekday masks from RepeatRule */
0054: private static final int[] DAYS_OF_WEEK = { RepeatRule.SUNDAY,
0055: RepeatRule.MONDAY, RepeatRule.TUESDAY,
0056: RepeatRule.WEDNESDAY, RepeatRule.THURSDAY,
0057: RepeatRule.FRIDAY, RepeatRule.SATURDAY };
0058:
0059: /**
0060: * List of vCalendar weekday codes. This sequence is parallel to
0061: * DAYS_OF_WEEK.
0062: */
0063: private static final String[] DAYS_OF_WEEK_CODES = { "SU", "MO",
0064: "TU", "WE", "TH", "FR", "SA" };
0065:
0066: /** List of the week in month masks from RepeatRule */
0067: private static final int[] WEEKS_OF_MONTH = { RepeatRule.FIRST,
0068: RepeatRule.SECOND, RepeatRule.THIRD, RepeatRule.FOURTH,
0069: RepeatRule.FIFTH, RepeatRule.LAST, RepeatRule.SECONDLAST,
0070: RepeatRule.THIRDLAST, RepeatRule.FOURTHLAST,
0071: RepeatRule.FIFTHLAST };
0072:
0073: /**
0074: * List of vCalendar week of month codes. This sequence is
0075: * parallel to WEEKS_OF_MONTH.
0076: */
0077: private static final String[] WEEKS_OF_MONTH_CODES = { "1+", "2+",
0078: "3+", "4+", "5+", "1-", "2-", "3-", "4-", "5-", };
0079:
0080: /** List of month in year masks from RepeatRule */
0081: private static final int[] MONTHS_IN_YEAR = { 0,
0082: RepeatRule.JANUARY, RepeatRule.FEBRUARY, RepeatRule.MARCH,
0083: RepeatRule.APRIL, RepeatRule.MAY, RepeatRule.JUNE,
0084: RepeatRule.JULY, RepeatRule.AUGUST, RepeatRule.SEPTEMBER,
0085: RepeatRule.OCTOBER, RepeatRule.NOVEMBER,
0086: RepeatRule.DECEMBER };
0087:
0088: /**
0089: * VCalendar 1.0 formatter.
0090: */
0091: public VCalendar10Format() {
0092: super ("VCALENDAR");
0093: }
0094:
0095: /**
0096: * Gets the code name of this encoding (e.g. "VCARD/2.1").
0097: * @return the encoding name
0098: */
0099: public String getName() {
0100: return "VCALENDAR/1.0";
0101: }
0102:
0103: /**
0104: * Checks to see if a given PIM list type is supported by this encoding.
0105: * @param pimListType int representing the PIM list type to check
0106: * @return true if the type can be read and written by this encoding,
0107: * false otherwise
0108: */
0109: public boolean isTypeSupported(int pimListType) {
0110: return pimListType == PIM.TODO_LIST
0111: || pimListType == PIM.EVENT_LIST;
0112: }
0113:
0114: /**
0115: * Serializes a PIMItem.
0116: * @param out Stream to which serialized data is written
0117: * @param encoding Character encoding to use for serialized data
0118: * @param pimItem The item to write to the stream
0119: * @throws IOException if a write error occurs
0120: */
0121: public void encode(OutputStream out, String encoding,
0122: PIMItem pimItem) throws IOException {
0123:
0124: Writer w = new OutputStreamWriter(out, encoding);
0125: w.write("BEGIN:VCALENDAR\r\n");
0126: w.write("VERSION:1.0\r\n");
0127: if (pimItem instanceof Event) {
0128: encode(w, (Event) pimItem);
0129: } else if (pimItem instanceof ToDo) {
0130: encode(w, (ToDo) pimItem);
0131: }
0132: w.write("END:VCALENDAR\r\n");
0133: w.flush();
0134: }
0135:
0136: /**
0137: * Serializes a vEvent.
0138: * @param w output stream
0139: * @param event the event to be encoded
0140: * @throws IOException if an error occurs while encoding
0141: */
0142: private void encode(Writer w, Event event) throws IOException {
0143: w.write("BEGIN:VEVENT\r\n");
0144: // write known fields
0145: int[] fields = event.getFields();
0146: for (int i = 0; i < fields.length; i++) {
0147: int valueCount = event.countValues(fields[i]);
0148: for (int j = 0; j < valueCount; j++) {
0149: writeValue(w, event, fields[i], j);
0150: }
0151: }
0152: // write categories
0153: String categories = FormatSupport.join(event.getCategories(),
0154: ",");
0155: if (categories.length() > 0) {
0156: w.write("CATEGORIES:");
0157: w.write(categories);
0158: w.write("\r\n");
0159: }
0160: // write repeat rule
0161: RepeatRule rule = event.getRepeat();
0162: if (rule != null) {
0163: String s = encodeRepeatRule(rule, 0);
0164: if (s != null) {
0165: w.write("RRULE:");
0166: w.write(s);
0167: w.write("\r\n");
0168: }
0169: Enumeration exDates = rule.getExceptDates();
0170: if (exDates.hasMoreElements()) {
0171: w.write("EXDATE;VALUE=DATE:");
0172: while (exDates.hasMoreElements()) {
0173: long time = ((Date) exDates.nextElement())
0174: .getTime();
0175: w
0176: .write(PIMHandler.getInstance()
0177: .composeDate1(time));
0178: if (exDates.hasMoreElements()) {
0179: w.write(",");
0180: }
0181: }
0182: w.write("\r\n");
0183: }
0184: }
0185: w.write("END:VEVENT\r\n");
0186: }
0187:
0188: /**
0189: * Serializes a vToDo.
0190: * @param w output stream target
0191: * @param todo item to be encoded
0192: * @throws IOException if an error occurs while encoding
0193: */
0194: private void encode(Writer w, ToDo todo) throws IOException {
0195: w.write("BEGIN:VTODO\r\n");
0196: // write known fields
0197: int[] fields = todo.getFields();
0198: for (int i = 0; i < fields.length; i++) {
0199: int valueCount = todo.countValues(fields[i]);
0200: for (int j = 0; j < valueCount; j++) {
0201: writeValue(w, todo, fields[i], j);
0202: }
0203: }
0204: // write categories
0205: String categories = FormatSupport.join(todo.getCategories(),
0206: ",");
0207: if (categories.length() > 0) {
0208: w.write("CATEGORIES:");
0209: w.write(categories);
0210: w.write("\r\n");
0211: }
0212: w.write("END:VTODO\r\n");
0213: }
0214:
0215: /**
0216: * Serializes one line of a vEvent.
0217: * @param w output stream target
0218: * @param event element to be processed
0219: * @param field component of element to output
0220: * @param index offset in field to process
0221: * @throws IOException if an error occurs while writing
0222: */
0223: private void writeValue(Writer w, Event event, int field, int index)
0224: throws IOException {
0225:
0226: switch (field) {
0227: case Event.CLASS: {
0228: int iValue = event.getInt(field, index);
0229: String sValue = VEventSupport.getClassType(iValue);
0230: if (sValue != null) {
0231: w.write("CLASS:");
0232: w.write(sValue);
0233: w.write("\r\n");
0234: }
0235: break;
0236: }
0237: case Event.ALARM: {
0238: int iValue = event.getInt(field, index);
0239: // subtract Event.ALARM from Event.START
0240: try {
0241: long startTime = event.getDate(Event.START, 0);
0242: w.write("DALARM:");
0243: w.write(PIMHandler.getInstance().composeDateTime(
0244: startTime - iValue * 1000));
0245: w.write("\r\n");
0246: } catch (IOException e) {
0247: // don't write a DALARM field
0248: }
0249: break;
0250: }
0251: case Event.LOCATION:
0252: case Event.NOTE:
0253: case Event.SUMMARY:
0254: case Event.UID: {
0255: String sValue = event.getString(field, index);
0256: if (sValue != null) {
0257: String property = VEventSupport.getFieldLabel(field);
0258: w.write(property);
0259: w.write(":");
0260: w.write(sValue);
0261: w.write("\r\n");
0262: }
0263: break;
0264: }
0265: case Event.END:
0266: case Event.REVISION:
0267: case Event.START: {
0268: long date = event.getDate(field, index);
0269: w.write(VEventSupport.getFieldLabel(field));
0270: w.write(":");
0271: w.write(PIMHandler.getInstance().composeDateTime(date));
0272: w.write("\r\n");
0273: break;
0274: }
0275: }
0276:
0277: }
0278:
0279: /**
0280: * Serializes one line of a vToDo.
0281: * @param w output stream target
0282: * @param todo element to be processed
0283: * @param field component of element to output
0284: * @param index offset in field to process
0285: * @throws IOException if an error occurs while writing
0286: *
0287: */
0288: private void writeValue(Writer w, ToDo todo, int field, int index)
0289: throws IOException {
0290:
0291: switch (field) {
0292: case ToDo.CLASS: {
0293: int iValue = todo.getInt(field, index);
0294: String sValue = VToDoSupport.getClassType(iValue);
0295: if (sValue != null) {
0296: w.write("CLASS:");
0297: w.write(sValue);
0298: w.write("\r\n");
0299: }
0300: break;
0301: }
0302: case ToDo.NOTE:
0303: case ToDo.SUMMARY:
0304: case ToDo.UID: {
0305: String sValue = todo.getString(field, index);
0306: if (sValue != null) {
0307: String property = VToDoSupport.getFieldLabel(field);
0308: w.write(property);
0309: w.write(":");
0310: w.write(sValue);
0311: w.write("\r\n");
0312: }
0313: break;
0314: }
0315: case ToDo.DUE:
0316: case ToDo.COMPLETION_DATE:
0317: case ToDo.REVISION: {
0318: long date = todo.getDate(field, index);
0319: w.write(VToDoSupport.getFieldLabel(field));
0320: w.write(":");
0321: w.write(PIMHandler.getInstance().composeDateTime(date));
0322: w.write("\r\n");
0323: break;
0324: }
0325: case ToDo.COMPLETED: {
0326: w.write("STATUS:COMPLETED\r\n");
0327: break;
0328: }
0329: case ToDo.PRIORITY: {
0330: w.write(VToDoSupport.getFieldLabel(field));
0331: w.write(":");
0332: w.write(String.valueOf(todo.getInt(field, index)));
0333: w.write("\r\n");
0334: break;
0335: }
0336: }
0337:
0338: }
0339:
0340: /**
0341: * Returns a VCalendar representation of a repeating rule, or
0342: * null if the rule cannot be encoded.
0343: *
0344: * For more details please see The Electronic Calendaring and Scheduling
0345: * Exchange Format Version 1.0
0346: *
0347: * @param rule data to be encoded
0348: * @param startFreq start frequency value (0 on start)
0349: * @return encoded rule
0350: */
0351: private String encodeRepeatRule(RepeatRule rule, int startFreq) {
0352: StringBuffer sb = new StringBuffer();
0353: int[] fields = rule.getFields();
0354: FormatSupport.sort(fields);
0355: if (!FormatSupport.contains(fields, RepeatRule.FREQUENCY)) {
0356: return null;
0357: }
0358: int frequency;
0359: if (startFreq != 0) {
0360: frequency = startFreq;
0361: } else {
0362: frequency = rule.getInt(RepeatRule.FREQUENCY);
0363: }
0364: int interval = 1; // default value according to JSR75 spec
0365: if (FormatSupport.contains(fields, RepeatRule.INTERVAL)
0366: && (startFreq == 0)) {
0367: interval = rule.getInt(RepeatRule.INTERVAL);
0368: }
0369: String encodedCount = " #0"; // forever
0370: if (FormatSupport.contains(fields, RepeatRule.COUNT)
0371: && (startFreq == 0)) {
0372: encodedCount = " #" + rule.getInt(RepeatRule.COUNT);
0373: }
0374: // enddate - ISO 8601 clause 5.4.1
0375: String encodedEndDate = "";
0376: if (FormatSupport.contains(fields, RepeatRule.END)) {
0377: encodedEndDate = " "
0378: + PIMHandler.getInstance().composeDateTime(
0379: rule.getDate(RepeatRule.END));
0380: }
0381: switch (frequency) {
0382: case RepeatRule.DAILY: {
0383: // D<interval> [<duration>]
0384: sb.append(FormatSupport.DAILY);
0385: sb.append(interval);
0386: sb.append(encodedCount);
0387: break;
0388: }
0389: case RepeatRule.WEEKLY: {
0390: // W<interval> <weekday> [<duration>]
0391: sb.append(FormatSupport.WEEKLY);
0392: sb.append(interval);
0393: if (FormatSupport.contains(fields, RepeatRule.DAY_IN_WEEK)) {
0394: sb.append(encodeRepeatRuleDaysInWeek(rule));
0395: }
0396: sb.append(encodedCount);
0397: break;
0398: }
0399: case RepeatRule.MONTHLY: {
0400: sb.append(FormatSupport.MONTHLY);
0401: if (FormatSupport.contains(fields, RepeatRule.DAY_IN_MONTH)) {
0402: // MD<interval> <daynumber> [<duration>]
0403: sb.append(FormatSupport.DAY_IN_MONTH);
0404: sb.append(interval);
0405: sb.append(" ");
0406: sb.append(rule.getInt(RepeatRule.DAY_IN_MONTH));
0407: sb.append(encodedCount);
0408: } else if (FormatSupport.contains(fields,
0409: RepeatRule.WEEK_IN_MONTH)) {
0410: // MP<interval> {<1>|<2>}{<+>|<->} [<duration>] [weekly|daily]
0411: sb.append(FormatSupport.WEEK_IN_MONTH);
0412: sb.append(interval);
0413: sb.append(encodeRepeatRuleWeeksInMonth(fields, rule));
0414: sb.append(encodedCount);
0415: if (FormatSupport.contains(fields,
0416: RepeatRule.DAY_IN_WEEK)) {
0417: sb
0418: .append(" "
0419: + encodeRepeatRule(rule,
0420: RepeatRule.WEEKLY));
0421: }
0422: }
0423: break;
0424: }
0425: case RepeatRule.YEARLY: {
0426: sb.append(FormatSupport.YEARLY);
0427: if (FormatSupport.contains(fields, RepeatRule.DAY_IN_YEAR)) {
0428: sb.append(FormatSupport.DAY_IN_YEAR);
0429: sb.append(interval);
0430: sb.append(" ");
0431: sb.append(rule.getInt(RepeatRule.DAY_IN_YEAR));
0432: sb.append(encodedCount);
0433: } else if (FormatSupport.contains(fields,
0434: RepeatRule.MONTH_IN_YEAR)) {
0435: sb.append(FormatSupport.MONTH_IN_YEAR);
0436: sb.append(interval);
0437: sb.append(encodeRepeatRuleMonthsInYear(fields, rule));
0438: sb.append(encodedCount);
0439: if (FormatSupport.contains(fields,
0440: RepeatRule.DAY_IN_MONTH)
0441: || FormatSupport.contains(fields,
0442: RepeatRule.WEEK_IN_MONTH)) {
0443: sb
0444: .append(" "
0445: + encodeRepeatRule(rule,
0446: RepeatRule.MONTHLY));
0447: }
0448: }
0449: break;
0450: }
0451: default:
0452: return null;
0453: }
0454: if (startFreq == 0) {
0455: sb.append(encodedEndDate);
0456: }
0457: return sb.toString();
0458: }
0459:
0460: /**
0461: * Returns a string representation of a weekly rule.
0462: * @param rule data to be encoded
0463: * @return encoded rule
0464: */
0465: private String encodeRepeatRuleDaysInWeek(RepeatRule rule) {
0466: StringBuffer sb = new StringBuffer();
0467: int daysInWeek = rule.getInt(RepeatRule.DAY_IN_WEEK);
0468: for (int i = 0; i < DAYS_OF_WEEK.length; i++) {
0469: if ((daysInWeek & DAYS_OF_WEEK[i]) != 0) {
0470: sb.append(" ");
0471: sb.append(DAYS_OF_WEEK_CODES[i]);
0472: }
0473: }
0474: return sb.toString();
0475: }
0476:
0477: /**
0478: * Returns a string representation of a monthly rule with a weekly
0479: * parameter.
0480: * @param fields data to be processed
0481: * @param rule to encode
0482: * @return encoded rule
0483: */
0484: private String encodeRepeatRuleWeeksInMonth(int[] fields,
0485: RepeatRule rule) {
0486: StringBuffer sb = new StringBuffer();
0487: int weeksInMonth = rule.getInt(RepeatRule.WEEK_IN_MONTH);
0488: for (int i = 0; i < WEEKS_OF_MONTH.length; i++) {
0489: if ((weeksInMonth & WEEKS_OF_MONTH[i]) != 0) {
0490: sb.append(" ");
0491: sb.append(WEEKS_OF_MONTH_CODES[i]);
0492: }
0493: }
0494: return sb.toString();
0495: }
0496:
0497: /**
0498: * Returns a string representation of a yearly rule with a monthly
0499: * parameter.
0500: * @param fields data to be processed
0501: * @param rule to encode
0502: * @return encoded rule
0503: */
0504: private String encodeRepeatRuleMonthsInYear(int[] fields,
0505: RepeatRule rule) {
0506: StringBuffer sb = new StringBuffer();
0507: int monthsInYear = rule.getInt(RepeatRule.MONTH_IN_YEAR);
0508: for (int i = 0; i < MONTHS_IN_YEAR.length; i++) {
0509: if ((monthsInYear & MONTHS_IN_YEAR[i]) != 0) {
0510: sb.append(" ");
0511: sb.append(i);
0512: }
0513: }
0514: return sb.toString();
0515: }
0516:
0517: /**
0518: * Constructs one or more PIMItems from serialized data.
0519: * @param in Stream containing serialized data
0520: * @param encoding Character encoding of the stream
0521: * @param list PIMList to which items should be added, or null if the items
0522: * should not be part of a list
0523: * @throws UnsupportedPIMFormatException if the serialized
0524: * data cannot be interpreted by this encoding.
0525: * @return a non-empty array of PIMItems containing the objects described
0526: * in the serialized data, or null if no items are available
0527: * @throws IOException if a read error occurs
0528: */
0529: public PIMItem[] decode(InputStream in, String encoding,
0530: PIMList list) throws IOException {
0531:
0532: LineReader r = new LineReader(in, encoding, this );
0533: String line = r.readLine();
0534: if (line == null) {
0535: return null;
0536: }
0537: if (!line.toUpperCase().equals("BEGIN:VCALENDAR")) {
0538: throw new UnsupportedPIMFormatException(
0539: "Not a vCalendar object");
0540: }
0541: Vector items = new Vector();
0542: for (AbstractPIMItem item; (item = decode(r, list)) != null;) {
0543: items.addElement(item);
0544: }
0545: if (items.size() == 0) {
0546: return null;
0547: }
0548: AbstractPIMItem[] a = new AbstractPIMItem[items.size()];
0549: items.copyInto(a);
0550: return a;
0551: }
0552:
0553: /**
0554: * Constructs a single PIMItem from serialized data.
0555: * @param in LineReader containing serialized data
0556: * @param list PIM list to which the item belongs
0557: * @throws UnsupportedPIMFormatException if the serialized data cannot be
0558: * interpreted by this encoding.
0559: * @return an unserialized Event, or null if no data was available
0560: */
0561: private AbstractPIMItem decode(LineReader in, PIMList list)
0562: throws IOException {
0563:
0564: while (true) {
0565: String line = in.readLine();
0566: if (line == null) {
0567: return null;
0568: }
0569: FormatSupport.DataElement element = FormatSupport
0570: .parseObjectLine(line);
0571: if (element.propertyName.equals("BEGIN")) {
0572: if (element.data.toUpperCase().equals("VEVENT")) {
0573: return decodeEvent(in, list);
0574: } else if (element.data.toUpperCase().equals("VTODO")) {
0575: return decodeToDo(in, list);
0576: } else {
0577: throw new UnsupportedPIMFormatException(
0578: "Bad argument to BEGIN: " + element.data);
0579: }
0580: } else if (element.propertyName.equals("END")) {
0581: if (element.data.toUpperCase().equals("VCALENDAR")) {
0582: return null;
0583: } else {
0584: throw new UnsupportedPIMFormatException(
0585: "Bad argument to END: " + element.data);
0586: }
0587: } else if (element.propertyName.equals("PRODID")) {
0588: // ignore product ID
0589: } else if (element.propertyName.equals("VERSION")) {
0590: // check version, then keep reading
0591: if (!element.data.equals("1.0")) {
0592: throw new UnsupportedPIMFormatException(
0593: "vCalendar version '" + element.data
0594: + "' is not supported");
0595: }
0596: } else if (element.propertyName.equals("CATEGORIES")) {
0597: // what should I do with this? this seems to be the wrong place
0598: // to put the field.
0599: } else {
0600: throw new UnsupportedPIMFormatException(
0601: "Unrecognized item: " + line);
0602: }
0603: }
0604: }
0605:
0606: /**
0607: * Reads and decodes a single vEvent.
0608: * @param in encoded event reader stream
0609: * @param list PIM list to which the item belongs
0610: * @return event reader implementation handle
0611: * @throws IOException if a reading error occurs
0612: */
0613: private EventImpl decodeEvent(LineReader in, PIMList list)
0614: throws IOException {
0615: EventImpl event = new EventImpl((AbstractPIMList) list);
0616: String line;
0617: while ((line = in.readLine()) != null) {
0618: FormatSupport.DataElement element = FormatSupport
0619: .parseObjectLine(line);
0620: if (element.propertyName.equals("END")) {
0621: // patch DALARM values
0622: int alarmValues = event.countValues(Event.ALARM);
0623: if (alarmValues > 0
0624: && event.countValues(Event.START) > 0) {
0625: int startTime = (int) (event
0626: .getDate(Event.START, 0) / 1000);
0627: for (int i = 0, j = 0; i < alarmValues; i++, j++) {
0628: int alarmTime = event.getInt(Event.ALARM, i);
0629: if (alarmTime * 1000 < startTime) {
0630: event.setInt(Event.ALARM, i,
0631: Event.ATTR_NONE, startTime
0632: - alarmTime);
0633: } else {
0634: event.removeValue(Event.ALARM, i);
0635: alarmValues--;
0636: i--;
0637: }
0638: }
0639: }
0640: return event;
0641: } else if (element.propertyName.equals("VERSION")) {
0642: if (!element.data.equals("1.0")) {
0643: throw new UnsupportedPIMFormatException("Version "
0644: + element.data + " is not supported");
0645: }
0646: } else if (element.propertyName.equals("CATEGORIES")) {
0647: String[] categories = FormatSupport.split(element.data,
0648: ',', 0);
0649: for (int j = 0; j < categories.length; j++) {
0650: try {
0651: event.addToCategory(categories[j]);
0652: } catch (PIMException e) {
0653: // cannot add item
0654: }
0655: }
0656: } else if (element.propertyName.equals("RRULE")) {
0657: RepeatRule rule = new RepeatRule();
0658: if (!decodeRepeatRule(rule, element.data, true)) {
0659: throw new IOException(
0660: "Empty or invalid RepeatRule data");
0661: }
0662: event.setRepeat(rule);
0663: } else if (element.propertyName.equals("EXDATE")) {
0664: RepeatRule rule = event.getRepeat();
0665: if (rule != null) {
0666: decodeExDates(rule, element.data);
0667: event.setRepeat(rule);
0668: }
0669: } else {
0670: importData(event, element.propertyName,
0671: element.attributes, element.data);
0672: }
0673: }
0674: throw new IOException("Unterminated vEvent");
0675: }
0676:
0677: /**
0678: * Decodes except dates.
0679: *
0680: * For more details please see The Electronic Calendaring and Scheduling
0681: * Exchange Format Version 1.0
0682: *
0683: * @param rule repeat rule instance
0684: * @param data string contains encoded dates
0685: * separated by ","
0686: *
0687: */
0688: private void decodeExDates(RepeatRule rule, String data)
0689: throws IOException {
0690: Parser parser = new Parser(data);
0691: long date;
0692: while (parser.hasNextDate()) {
0693: date = PIMHandler.getInstance().parseDate(
0694: parser.getEndDate());
0695: parser.setPos(parser.getPos()
0696: + parser.getEndDate().length());
0697: rule.addExceptDate(date);
0698: if (!parser.hasMoreChars()) {
0699: break;
0700: }
0701: parser.matchSkip(','); // separator
0702: }
0703: }
0704:
0705: /**
0706: * Decodes repeat rule.
0707: *
0708: * For more details please see The Electronic Calendaring and Scheduling
0709: * Exchange Format Version 1.0
0710: *
0711: * @param rule repeat rule instance
0712: * @param data string contains encoded repeat rule
0713: * @param isTop true on top recursive level else false
0714: * @return true if repeat rule was successfully decoded, false otherwise
0715: */
0716: private boolean decodeRepeatRule(RepeatRule rule, String data,
0717: boolean isTop) {
0718: boolean res = true;
0719: Parser parser = new Parser(data);
0720:
0721: char sym;
0722: try {
0723: parser.skipBlank();
0724: sym = parser.readChar();
0725: int interval;
0726: switch (sym) {
0727: case FormatSupport.DAILY:
0728: // D<interval> [<duration>]
0729: interval = parser.readInt();
0730: if (isTop) {
0731: rule.setInt(RepeatRule.FREQUENCY, RepeatRule.DAILY);
0732: rule.setInt(RepeatRule.INTERVAL, interval);
0733: }
0734: setRepeatRuleCount(parser, rule, isTop);
0735: break;
0736: case FormatSupport.WEEKLY:
0737: // W<interval> <weekday> [<duration>]
0738: interval = parser.readInt();
0739: if (isTop) {
0740: rule
0741: .setInt(RepeatRule.FREQUENCY,
0742: RepeatRule.WEEKLY);
0743: rule.setInt(RepeatRule.INTERVAL, interval);
0744: }
0745: rule.setInt(RepeatRule.DAY_IN_WEEK,
0746: decodeDaysInWeek(parser));
0747: setRepeatRuleCount(parser, rule, isTop);
0748: break;
0749: case FormatSupport.MONTHLY:
0750: if (isTop) {
0751: rule.setInt(RepeatRule.FREQUENCY,
0752: RepeatRule.MONTHLY);
0753: }
0754: sym = parser.readChar();
0755: switch (sym) {
0756: case FormatSupport.DAY_IN_MONTH:
0757: // MD<interval> <daynumber> [<duration>]
0758: interval = parser.readInt();
0759: if (isTop) {
0760: rule.setInt(RepeatRule.INTERVAL, interval);
0761: }
0762: parser.skipBlank();
0763: rule.setInt(RepeatRule.DAY_IN_MONTH, parser
0764: .readInt());
0765: setRepeatRuleCount(parser, rule, isTop);
0766: break;
0767: case FormatSupport.WEEK_IN_MONTH:
0768: interval = parser.readInt();
0769: if (isTop) {
0770: rule.setInt(RepeatRule.INTERVAL, interval);
0771: }
0772: parser.skipBlank();
0773: rule.setInt(RepeatRule.WEEK_IN_MONTH,
0774: decodeWeeksInMonth(parser, rule));
0775: setRepeatRuleCount(parser, rule, isTop);
0776: parser.skipBlank();
0777: if (parser.hasMoreChars()) { // parse next rule
0778: res = decodeRepeatRule(rule, parser
0779: .getRemainder(), false);
0780: }
0781: break;
0782: default:
0783: res = false;
0784: }
0785: break;
0786: case FormatSupport.YEARLY:
0787: if (isTop) {
0788: rule
0789: .setInt(RepeatRule.FREQUENCY,
0790: RepeatRule.YEARLY);
0791: }
0792: sym = parser.readChar();
0793: switch (sym) {
0794: case FormatSupport.DAY_IN_YEAR:
0795: interval = parser.readInt();
0796: if (isTop) {
0797: rule.setInt(RepeatRule.INTERVAL, interval);
0798: }
0799: parser.skipBlank();
0800: rule.setInt(RepeatRule.DAY_IN_YEAR, parser
0801: .readInt());
0802: setRepeatRuleCount(parser, rule, isTop);
0803: break;
0804: case FormatSupport.MONTH_IN_YEAR:
0805: interval = parser.readInt();
0806: if (isTop) {
0807: rule.setInt(RepeatRule.INTERVAL, interval);
0808: }
0809: rule.setInt(RepeatRule.MONTH_IN_YEAR,
0810: decodeMonthsInYear(parser, rule));
0811: setRepeatRuleCount(parser, rule, isTop);
0812: parser.skipBlank();
0813: if (parser.hasMoreChars()) { // parse next rule
0814: res = decodeRepeatRule(rule, parser
0815: .getRemainder(), false);
0816: }
0817: break;
0818: default:
0819: res = false;
0820: }
0821: }
0822: } catch (IOException ex) {
0823: res = false;
0824: } catch (NumberFormatException ex) {
0825: res = false;
0826: } catch (IllegalArgumentException ex) {
0827: res = false;
0828: } catch (FieldEmptyException ex) {
0829: res = false;
0830: }
0831:
0832: return res;
0833: }
0834:
0835: /**
0836: * Decodes days in a week.
0837: *
0838: * @param parser input data parser
0839: * @return day-in-a-week value
0840: * @throws IOException if a reading error occurs or wrong format
0841: */
0842: private int decodeDaysInWeek(Parser parser) throws IOException {
0843: int daysInWeek = 0;
0844: while (true) { // decode days
0845: parser.skipBlank();
0846: if (!parser.isNextMatchStr(DAYS_OF_WEEK_CODES)) {
0847: break;
0848: }
0849: String dayStr = parser.readId();
0850: int i;
0851: for (i = 0; i < DAYS_OF_WEEK_CODES.length; i++) {
0852: if (DAYS_OF_WEEK_CODES[i].equals(dayStr)) {
0853: daysInWeek |= DAYS_OF_WEEK[i];
0854: break;
0855: }
0856: }
0857: if (i == DAYS_OF_WEEK_CODES.length) {
0858: throw new IOException("Wrong format"); // not found
0859: }
0860: }
0861: return daysInWeek;
0862: }
0863:
0864: /**
0865: * Decodes weeks in a month.
0866: *
0867: * @param parser input data parser
0868: * @param rule RepeatRule for setting fields
0869: * @return week-in-a-month value
0870: * @throws IOException if a reading error occurs or wrong format
0871: */
0872: private int decodeWeeksInMonth(Parser parser, RepeatRule rule)
0873: throws IOException {
0874: int weeksInMonth = 0;
0875: while (true) { // decode weeks
0876: parser.skipBlank();
0877: if (!parser.isNextMatchStr(WEEKS_OF_MONTH_CODES)) {
0878: break;
0879: }
0880: String weekStr = parser.readId();
0881: int i;
0882: for (i = 0; i < WEEKS_OF_MONTH_CODES.length; i++) {
0883: if (WEEKS_OF_MONTH_CODES[i].equals(weekStr)) {
0884: weeksInMonth |= WEEKS_OF_MONTH[i];
0885: break;
0886: }
0887: }
0888: if (i == WEEKS_OF_MONTH_CODES.length) {
0889: throw new IOException("Wrong format"); // not found
0890: }
0891: }
0892: return weeksInMonth;
0893: }
0894:
0895: /**
0896: * Decodes months in a year.
0897: *
0898: * @param parser input data parser
0899: * @param rule RepeatRule for setting fields
0900: * @return month-in-a-year value
0901: * @throws IOException if a reading error occurs or wrong format
0902: */
0903: private int decodeMonthsInYear(Parser parser, RepeatRule rule)
0904: throws IOException {
0905: int monthsInYear = 0;
0906: int monthNum;
0907: while (true) { // decode monthes
0908: parser.skipBlank();
0909: if (!parser.isNextInt()) {
0910: break;
0911: }
0912: monthNum = parser.readInt();
0913: if (monthNum >= MONTHS_IN_YEAR.length) {
0914: throw new IOException("Wrong month number");
0915: }
0916: monthsInYear |= MONTHS_IN_YEAR[monthNum];
0917: }
0918: return monthsInYear;
0919: }
0920:
0921: /**
0922: * Puts duration (#value) and end date (yyyymmddThhmmss(Z)/yyyyMMdd)
0923: * to COUNT and END fields of the repeat rule.
0924: *
0925: * @param parser input data parser
0926: * @param rule repeat rule object for setting
0927: * @param isSetCount set COUNT field else don't set
0928: * @throws IOException if a reading error occurs or wrong format
0929: */
0930: private void setRepeatRuleCount(Parser parser, RepeatRule rule,
0931: boolean isSetCount) throws IOException {
0932: // parse duration
0933: parser.skipBlank();
0934: if (parser.match('#')) {
0935: parser.skip();
0936: int count = parser.readInt();
0937: if (isSetCount && count > 0) {
0938: rule.setInt(RepeatRule.COUNT, count);
0939: }
0940: }
0941: // parse end date
0942: parser.skipBlank();
0943: if (parser.hasNextDate()) {
0944: int dateLen = parser.getEndDate().length();
0945: // end date is either date in yyyyMMdd format or
0946: // date/time in yyyymmddThhmmss(Z).
0947: long date = (dateLen < 15) ? PIMHandler.getInstance()
0948: .parseDate(parser.getEndDate()) : PIMHandler
0949: .getInstance().parseDateTime(parser.getEndDate());
0950: rule.setDate(RepeatRule.END, date);
0951: parser.setPos(parser.getPos() + dateLen);
0952: }
0953: }
0954:
0955: /**
0956: * Decodes one line of a vEvent.
0957: * @param event data to be processed
0958: * @param propertyName property key name
0959: * @param attributes fields to be processed
0960: * @param data string to be processed
0961: */
0962: private void importData(Event event, String propertyName,
0963: String[] attributes, String data) {
0964: int field = VEventSupport.getFieldCode(propertyName);
0965: switch (field) {
0966: case Event.SUMMARY:
0967: case Event.LOCATION:
0968: case Event.NOTE:
0969: case Event.UID: {
0970: String sdata = FormatSupport.parseString(attributes, data);
0971: event.addString(field, Event.ATTR_NONE, sdata);
0972: break;
0973: }
0974: case Event.END:
0975: case Event.REVISION:
0976: case Event.START: {
0977: long date = PIMHandler.getInstance().parseDateTime(data);
0978: event.addDate(field, Event.ATTR_NONE, date);
0979: break;
0980: }
0981: case Event.CLASS: {
0982: String sdata = FormatSupport.parseString(attributes, data);
0983: int c = VEventSupport.getClassCode(sdata);
0984: event.addInt(Event.CLASS, Event.ATTR_NONE, c);
0985: break;
0986: }
0987: case Event.ALARM: {
0988: String[] s = FormatSupport.parseStringArray(attributes,
0989: data);
0990: if (s.length > 0) {
0991: long alarmTime = PIMHandler.getInstance()
0992: .parseDateTime(s[0]);
0993: event.addInt(Event.ALARM, Event.ATTR_NONE,
0994: (int) (alarmTime / 1000));
0995: }
0996: break;
0997: }
0998: }
0999: }
1000:
1001: /**
1002: * Reads and decodes a single vToDo.
1003: * @param in input stream
1004: * @param list PIM list to which the item belongs
1005: * @throws IOException if an error occurs reading
1006: * @return ToDo implementation handler
1007: */
1008: private ToDoImpl decodeToDo(LineReader in, PIMList list)
1009: throws IOException {
1010: ToDoImpl todo = new ToDoImpl((AbstractPIMList) list);
1011: String line;
1012: while ((line = in.readLine()) != null) {
1013: FormatSupport.DataElement element = FormatSupport
1014: .parseObjectLine(line);
1015: if (element.propertyName.equals("END")) {
1016: return todo;
1017: } else if (element.propertyName.equals("VERSION")) {
1018: if (!element.data.equals("1.0")) {
1019: throw new UnsupportedPIMFormatException("Version "
1020: + element.data + " is not supported");
1021: }
1022: } else if (element.propertyName.equals("CATEGORIES")) {
1023: String[] categories = FormatSupport.split(element.data,
1024: ',', 0);
1025: for (int j = 0; j < categories.length; j++) {
1026: try {
1027: todo.addToCategory(categories[j]);
1028: } catch (PIMException e) {
1029: // cannot add item to category
1030: }
1031: }
1032: } else {
1033: importData(todo, element.propertyName,
1034: element.attributes, element.data);
1035: }
1036: }
1037: throw new IOException("Unterminated vToDo");
1038: }
1039:
1040: /**
1041: * Decodes one line of a vToDo.
1042: * @param todo element to fill
1043: * @param propertyName key to property value
1044: * @param attributes fields to import
1045: * @param data string containing input data
1046: */
1047: private void importData(ToDo todo, String propertyName,
1048: String[] attributes, String data) {
1049:
1050: int field = VToDoSupport.getFieldCode(propertyName);
1051: switch (field) {
1052: case ToDo.SUMMARY:
1053: case ToDo.NOTE:
1054: case ToDo.UID: {
1055: String sdata = FormatSupport.parseString(attributes, data);
1056: todo.addString(field, ToDo.ATTR_NONE, sdata);
1057: break;
1058: }
1059: case ToDo.COMPLETION_DATE:
1060: todo.addBoolean(ToDo.COMPLETED, ToDo.ATTR_NONE, true);
1061: // fall through
1062: case ToDo.DUE:
1063: case ToDo.REVISION: {
1064: long date = PIMHandler.getInstance().parseDateTime(data);
1065: todo.addDate(field, ToDo.ATTR_NONE, date);
1066: break;
1067: }
1068: case ToDo.CLASS: {
1069: String sdata = FormatSupport.parseString(attributes, data);
1070: int c = VToDoSupport.getClassCode(sdata);
1071: todo.addInt(ToDo.CLASS, ToDo.ATTR_NONE, c);
1072: break;
1073: }
1074: case ToDo.PRIORITY: {
1075: try {
1076: int i = Integer.parseInt(data);
1077: todo.addInt(ToDo.PRIORITY, ToDo.ATTR_NONE, i);
1078: } catch (NumberFormatException e) {
1079: // ignore this field
1080: }
1081: break;
1082: }
1083: }
1084: }
1085:
1086: }
1087:
1088: /**
1089: * A simple parser for encoded RepeatRule data.
1090: */
1091: class Parser {
1092:
1093: /** Source data buffer. */
1094: String s;
1095:
1096: /** Current position in source data buffer. */
1097: private int index;
1098:
1099: /** Saved end date. */
1100: private String endDate = "";
1101:
1102: /**
1103: * Constructor.
1104: *
1105: * @param s string for parsing
1106: */
1107: Parser(String s) {
1108: this .s = s;
1109: index = 0;
1110: }
1111:
1112: /**
1113: * Checks that current position is valid.
1114: * @throws IOException if position is out of buffer
1115: */
1116: private void checkBound() throws IOException {
1117: checkBound(0);
1118: }
1119:
1120: /**
1121: * Checks that input position is valid.
1122: * @param off offset from current position
1123: * @throws IOException if position + offset is out of buffer
1124: */
1125: private void checkBound(int off) throws IOException {
1126: int bound = index + off;
1127: if (bound < 0 || bound >= s.length()) {
1128: throw new IOException("Out of bound");
1129: }
1130: }
1131:
1132: /**
1133: * Reads next char from buffer.
1134: * @return next char from buffer
1135: * @throws IOException if position is out of buffer
1136: */
1137: char readChar() throws IOException {
1138: checkBound();
1139: return s.charAt(index++);
1140: }
1141:
1142: /**
1143: * Reads next char from buffer without changing the pointer.
1144: * @return next char from buffer
1145: * @throws IOException if position is out of buffer
1146: */
1147: char nextChar() throws IOException {
1148: return nextChar(0);
1149: }
1150:
1151: /**
1152: * Reads next char by given position
1153: * from buffer without changing the pointer.
1154: * @param off offset from current position
1155: * @return next char from buffer
1156: * @throws IOException if position + offset is out of buffer
1157: */
1158: char nextChar(int off) throws IOException {
1159: checkBound(off);
1160: return s.charAt(index + off);
1161: }
1162:
1163: /**
1164: * Checks that next chars contain a date
1165: * in format yyyymmddThhmmss.
1166: *
1167: * For more details please see RFC 2445
1168: *
1169: * @return true when next chars contain a date else false
1170: * If the date exists then it is saved in endDate member
1171: */
1172: boolean hasNextDate() { // Date format yyyymmddThhmmss
1173: endDate = "";
1174: if (!hasMoreChars()) {
1175: return false;
1176: }
1177: String strDate = getRemainder();
1178: int dateLength = 8; // yyyymmdd
1179: if (strDate.length() < dateLength) {
1180: return false;
1181: }
1182: if (strDate.length() > 14 && strDate.charAt(8) == 'T') {
1183: dateLength = 15; // yyyymmddThhmmss
1184: // yyyymmddThhmmssZ - absolute time
1185: if (strDate.length() > 15 && strDate.charAt(15) == 'Z') {
1186: dateLength = 16;
1187: }
1188: }
1189: strDate = strDate.substring(0, dateLength);
1190: try {
1191: int year = Integer.parseInt(strDate.substring(0, 4));
1192: if (year < 1970) {
1193: return false;
1194: }
1195: int month = Integer.parseInt(strDate.substring(4, 6));
1196: if ((month < 1) || (month > 12)) {
1197: return false;
1198: }
1199: int day = Integer.parseInt(strDate.substring(6, 8));
1200: if ((day < 1) || (day > 31)) {
1201: return false;
1202: }
1203: // Don't check time
1204: } catch (NumberFormatException ex) {
1205: return false;
1206: }
1207: endDate = strDate;
1208: return true;
1209: }
1210:
1211: /**
1212: * Gets a date that saved by hasNextDate method.
1213: *
1214: * @return saved date
1215: */
1216: String getEndDate() {
1217: return endDate;
1218: }
1219:
1220: /**
1221: * Reads next integer value from buffer.
1222: *
1223: * @return integer value
1224: * @throws IOException if chars from current position don't contain
1225: * integer value
1226: */
1227: int readInt() throws IOException {
1228: checkBound();
1229: StringBuffer sb = new StringBuffer();
1230: for (; index < s.length() && Character.isDigit(s.charAt(index)); index++) {
1231: sb.append(s.charAt(index));
1232: }
1233: if (sb.length() == 0) {
1234: throw new IOException("No digital chars");
1235: }
1236: return Integer.parseInt(sb.toString());
1237: }
1238:
1239: /**
1240: * Checks that the next char is equal to given char.
1241: * @param sym char for comparing
1242: * @return true when next char is equal to given char
1243: */
1244: boolean match(char sym) {
1245: if (!hasMoreChars()) {
1246: return false;
1247: }
1248: return sym == s.charAt(index);
1249: }
1250:
1251: /**
1252: * Checks that the buffer contains chars from current
1253: * position.
1254: * @return true when buffer has unparsed chars
1255: */
1256: boolean hasMoreChars() {
1257: return index < s.length();
1258: }
1259:
1260: /**
1261: * Skips the current position.
1262: */
1263: void skip() {
1264: index++;
1265: }
1266:
1267: /**
1268: * Skips next spaces and tabs.
1269: */
1270: void skipBlank() {
1271: while (hasMoreChars() && (match(' ') || match('\t'))) {
1272: skip();
1273: }
1274: }
1275:
1276: /**
1277: * Checks that the next symbol is equal to given one.
1278: *
1279: * @throws IOException if next symbol is different from input one
1280: * @param sym the symbol to be checked
1281: */
1282: void matchSkip(char sym) throws IOException {
1283: if (!match(sym)) {
1284: throw new IOException("No symbol " + sym);
1285: }
1286: index++;
1287: }
1288:
1289: /**
1290: * Gets the remainder of parsed buffer.
1291: * @return part of buffer from current position
1292: */
1293: String getRemainder() {
1294: String retValue = null;
1295: if (index < s.length()) {
1296: retValue = s.substring(index);
1297: }
1298: return retValue;
1299: }
1300:
1301: /**
1302: * Gets the current position.
1303: * @return current position
1304: */
1305: int getPos() {
1306: return index;
1307: }
1308:
1309: /**
1310: * Sets the given position.
1311: * @param pos position for setting
1312: */
1313: void setPos(int pos) {
1314: index = pos;
1315: }
1316:
1317: /**
1318: * Checks that next ID from buffer could be
1319: * found in input string array.
1320: * @param arrStr array for searching
1321: * @return true when next ID is found in the input array else false
1322: */
1323: boolean isNextMatchStr(String[] arrStr) {
1324: if (!hasMoreChars()) {
1325: return false;
1326: }
1327: int pos = index;
1328: String nextId;
1329: try {
1330: nextId = readId();
1331: } catch (IOException ex) {
1332: return false;
1333: }
1334: index = pos;
1335: if ((nextId == null) || (nextId.length() == 0)) {
1336: return false;
1337: }
1338: for (int i = 0; i < arrStr.length; i++) {
1339: if (arrStr[i].equals(nextId)) {
1340: return true;
1341: }
1342: }
1343: return false;
1344: }
1345:
1346: /**
1347: * Checks that next ID from buffer contains
1348: * digit symbols only.
1349: * @return true when next ID contains digit symbols only
1350: */
1351: boolean isNextInt() {
1352: if (!hasMoreChars()) {
1353: return false;
1354: }
1355: int pos = index;
1356: String nextId;
1357: try {
1358: nextId = readId();
1359: } catch (IOException ex) {
1360: return false;
1361: }
1362: index = pos;
1363: if ((nextId == null) || (nextId.length() == 0)) {
1364: return false;
1365: }
1366: return Character.isDigit(nextId.charAt(0));
1367: }
1368:
1369: /**
1370: * Gets next ID from buffer.
1371: * @return next ID
1372: * @throws IOException if position is out of buffer bounds
1373: */
1374: String readId() throws IOException {
1375: checkBound();
1376: String id = getRemainder();
1377: int index = id.indexOf(' ');
1378: if (index > -1) {
1379: id = id.substring(0, index);
1380: }
1381: setPos(getPos() + id.length());
1382: return id;
1383: }
1384: }
|