/* * Copyright 1990-2009 Sun Microsystems, Inc. All Rights Reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License version * 2 only, as published by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * General Public License version 2 for more details (a copy is * included at /legal/license.txt). * * You should have received a copy of the GNU General Public License * version 2 along with this work; if not, write to the Free Software * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA * * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa * Clara, CA 95054 or visit www.sun.com if you need additional * information or have any questions. */ package com.sun.j2me.pim.formats; import com.sun.j2me.pim.AbstractPIMItem; import com.sun.j2me.pim.AbstractPIMList; import com.sun.j2me.pim.EventImpl; import com.sun.j2me.pim.LineReader; import com.sun.j2me.pim.PIMFormat; import com.sun.j2me.pim.PIMHandler; import com.sun.j2me.pim.ToDoImpl; import com.sun.j2me.pim.UnsupportedPIMFormatException; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.io.Writer; import java.util.Date; import java.util.Enumeration; import java.util.Vector; import javax.microedition.pim.*; import com.sun.j2me.jsr75.StringUtil; /** * Implementation of PIMEncoding for VCalendar/1.0. * */ public class VCalendar10Format extends EndMatcher implements PIMFormat { /** List of the weekday masks from RepeatRule */ private static final int[] DAYS_OF_WEEK = { RepeatRule.SUNDAY, RepeatRule.MONDAY, RepeatRule.TUESDAY, RepeatRule.WEDNESDAY, RepeatRule.THURSDAY, RepeatRule.FRIDAY, RepeatRule.SATURDAY }; /** * List of vCalendar weekday codes. This sequence is parallel to * DAYS_OF_WEEK. */ private static final String[] DAYS_OF_WEEK_CODES = { "SU", "MO", "TU", "WE", "TH", "FR", "SA" }; /** List of the week in month masks from RepeatRule */ private static final int[] WEEKS_OF_MONTH = { RepeatRule.FIRST, RepeatRule.SECOND, RepeatRule.THIRD, RepeatRule.FOURTH, RepeatRule.FIFTH, RepeatRule.LAST, RepeatRule.SECONDLAST, RepeatRule.THIRDLAST, RepeatRule.FOURTHLAST, RepeatRule.FIFTHLAST }; /** * List of vCalendar week of month codes. This sequence is * parallel to WEEKS_OF_MONTH. */ private static final String[] WEEKS_OF_MONTH_CODES = { "1+", "2+", "3+", "4+", "5+", "1-", "2-", "3-", "4-", "5-", }; /** List of month in year masks from RepeatRule */ private static final int[] MONTHS_IN_YEAR = { 0, RepeatRule.JANUARY, RepeatRule.FEBRUARY, RepeatRule.MARCH, RepeatRule.APRIL, RepeatRule.MAY, RepeatRule.JUNE, RepeatRule.JULY, RepeatRule.AUGUST, RepeatRule.SEPTEMBER, RepeatRule.OCTOBER, RepeatRule.NOVEMBER, RepeatRule.DECEMBER }; /** * VCalendar 1.0 formatter. */ public VCalendar10Format() { super("VCALENDAR"); } /** * Gets the code name of this encoding (e.g. "VCARD/2.1"). * @return the encoding name */ public String getName() { return "VCALENDAR/1.0"; } /** * Checks to see if a given PIM list type is supported by this encoding. * @param pimListType int representing the PIM list type to check * @return true if the type can be read and written by this encoding, * false otherwise */ public boolean isTypeSupported(int pimListType) { switch (pimListType) { case PIM.EVENT_LIST: case PIM.TODO_LIST: return FormatSupport.isListTypeSupported(pimListType); } return false; } /** * Serializes a PIMItem. * @param out Stream to which serialized data is written * @param encoding Character encoding to use for serialized data * @param pimItem The item to write to the stream * @throws IOException if a write error occurs */ public void encode(OutputStream out, String encoding, PIMItem pimItem) throws IOException { Writer w = new OutputStreamWriter(out, encoding); w.write("BEGIN:VCALENDAR\r\n"); w.write("VERSION:1.0\r\n"); if (pimItem instanceof Event) { encode(w, (Event) pimItem); } else if (pimItem instanceof ToDo) { encode(w, (ToDo) pimItem); } w.write("END:VCALENDAR\r\n"); w.flush(); } /** * Serializes a vEvent. * @param w output stream * @param event the event to be encoded * @throws IOException if an error occurs while encoding */ private void encode(Writer w, Event event) throws IOException { w.write("BEGIN:VEVENT\r\n"); // write known fields int[] fields = event.getFields(); for (int i = 0; i < fields.length; i++) { int valueCount = event.countValues(fields[i]); for (int j = 0; j < valueCount; j++) { writeValue(w, event, fields[i], j); } } // write categories String categories = StringUtil.join(event.getCategories(), ","); if (categories.length() > 0) { w.write("CATEGORIES:"); w.write(categories); w.write("\r\n"); } // write repeat rule RepeatRule rule = event.getRepeat(); if (rule != null) { String s = encodeRepeatRule(rule, 0); if (s != null) { w.write("RRULE:"); w.write(s); w.write("\r\n"); } Enumeration exDates = rule.getExceptDates(); if (exDates.hasMoreElements()) { w.write("EXDATE;VALUE=DATE:"); while (exDates.hasMoreElements()) { long time = ((Date) exDates.nextElement()).getTime(); w.write(PIMHandler.getInstance().composeDate1(time)); if (exDates.hasMoreElements()) { w.write(","); } } w.write("\r\n"); } } w.write("END:VEVENT\r\n"); } /** * Serializes a vToDo. * @param w output stream target * @param todo item to be encoded * @throws IOException if an error occurs while encoding */ private void encode(Writer w, ToDo todo) throws IOException { w.write("BEGIN:VTODO\r\n"); // write known fields int[] fields = todo.getFields(); for (int i = 0; i < fields.length; i++) { int valueCount = todo.countValues(fields[i]); for (int j = 0; j < valueCount; j++) { writeValue(w, todo, fields[i], j); } } // write categories String categories = StringUtil.join(todo.getCategories(), ","); if (categories.length() > 0) { w.write("CATEGORIES:"); w.write(categories); w.write("\r\n"); } w.write("END:VTODO\r\n"); } /** * Serializes one line of a vEvent. * @param w output stream target * @param event element to be processed * @param field component of element to output * @param index offset in field to process * @throws IOException if an error occurs while writing */ private void writeValue(Writer w, Event event, int field, int index) throws IOException { switch (field) { case Event.CLASS: { int iValue = event.getInt(field, index); String sValue = VEventSupport.getClassType(iValue); if (sValue != null) { w.write("CLASS:"); w.write(sValue); w.write("\r\n"); } break; } case Event.ALARM: { int iValue = event.getInt(field, index); // subtract Event.ALARM from Event.START try { long startTime = event.getDate(Event.START, 0); w.write("DALARM:"); w.write(PIMHandler.getInstance() .composeDateTime(startTime - iValue * 1000)); w.write("\r\n"); } catch (IOException e) { // don't write a DALARM field } break; } case Event.LOCATION: case Event.NOTE: case Event.SUMMARY: case Event.UID: { String sValue = event.getString(field, index); if (sValue != null) { String property = VEventSupport.getFieldLabel(field); w.write(property); w.write(":"); w.write(sValue); w.write("\r\n"); } break; } case Event.END: case Event.REVISION: case Event.START: { long date = event.getDate(field, index); w.write(VEventSupport.getFieldLabel(field)); w.write(":"); w.write(PIMHandler.getInstance().composeDateTime(date)); w.write("\r\n"); break; } } } /** * Serializes one line of a vToDo. * @param w output stream target * @param todo element to be processed * @param field component of element to output * @param index offset in field to process * @throws IOException if an error occurs while writing * */ private void writeValue(Writer w, ToDo todo, int field, int index) throws IOException { switch (field) { case ToDo.CLASS: { int iValue = todo.getInt(field, index); String sValue = VToDoSupport.getClassType(iValue); if (sValue != null) { w.write("CLASS:"); w.write(sValue); w.write("\r\n"); } break; } case ToDo.NOTE: case ToDo.SUMMARY: case ToDo.UID: { String sValue = todo.getString(field, index); if (sValue != null) { String property = VToDoSupport.getFieldLabel(field); w.write(property); w.write(":"); w.write(sValue); w.write("\r\n"); } break; } case ToDo.DUE: case ToDo.COMPLETION_DATE: case ToDo.REVISION: { long date = todo.getDate(field, index); w.write(VToDoSupport.getFieldLabel(field)); w.write(":"); w.write(PIMHandler.getInstance().composeDateTime(date)); w.write("\r\n"); break; } case ToDo.COMPLETED: { w.write("STATUS:COMPLETED\r\n"); break; } case ToDo.PRIORITY: { w.write(VToDoSupport.getFieldLabel(field)); w.write(":"); w.write(String.valueOf(todo.getInt(field, index))); w.write("\r\n"); break; } } } /** * Returns a VCalendar representation of a repeating rule, or * null if the rule cannot be encoded. * * For more details please see The Electronic Calendaring and Scheduling * Exchange Format Version 1.0 * * @param rule data to be encoded * @param startFreq start frequency value (0 on start) * @return encoded rule */ private String encodeRepeatRule(RepeatRule rule, int startFreq) { StringBuffer sb = new StringBuffer(); int[] fields = rule.getFields(); FormatSupport.sort(fields); if (!FormatSupport.contains(fields, RepeatRule.FREQUENCY)) { return null; } int frequency; if (startFreq != 0) { frequency = startFreq; } else { frequency = rule.getInt(RepeatRule.FREQUENCY); } int interval = 1; // default value according to JSR75 spec if (FormatSupport.contains(fields, RepeatRule.INTERVAL) && (startFreq == 0)) { interval = rule.getInt(RepeatRule.INTERVAL); } String encodedCount = " #0"; // forever if (FormatSupport.contains(fields, RepeatRule.COUNT) && (startFreq == 0)) { encodedCount = " #" + rule.getInt(RepeatRule.COUNT); } // enddate - ISO 8601 clause 5.4.1 String encodedEndDate = ""; if (FormatSupport.contains(fields, RepeatRule.END)) { encodedEndDate = " " + PIMHandler.getInstance().composeDateTime( rule.getDate(RepeatRule.END)); } switch (frequency) { case RepeatRule.DAILY: { // D<interval> [<duration>] sb.append(FormatSupport.DAILY); sb.append(interval); sb.append(encodedCount); break; } case RepeatRule.WEEKLY: { // W<interval> <weekday> [<duration>] sb.append(FormatSupport.WEEKLY); sb.append(interval); if (FormatSupport.contains(fields, RepeatRule.DAY_IN_WEEK)) { sb.append(encodeRepeatRuleDaysInWeek(rule)); } sb.append(encodedCount); break; } case RepeatRule.MONTHLY: { sb.append(FormatSupport.MONTHLY); if (FormatSupport.contains(fields, RepeatRule.DAY_IN_MONTH)) { // MD<interval> <daynumber> [<duration>] sb.append(FormatSupport.DAY_IN_MONTH); sb.append(interval); sb.append(" "); sb.append(rule.getInt(RepeatRule.DAY_IN_MONTH)); sb.append(encodedCount); } else if (FormatSupport.contains(fields, RepeatRule.WEEK_IN_MONTH)) { // MP<interval> {<1>|<2>}{<+>|<->} [<duration>] [weekly|daily] sb.append(FormatSupport.WEEK_IN_MONTH); sb.append(interval); sb.append(encodeRepeatRuleWeeksInMonth(fields, rule)); sb.append(encodedCount); if (FormatSupport.contains(fields, RepeatRule.DAY_IN_WEEK)) { sb.append(" " + encodeRepeatRule(rule, RepeatRule.WEEKLY)); } } break; } case RepeatRule.YEARLY: { sb.append(FormatSupport.YEARLY); if (FormatSupport.contains(fields, RepeatRule.DAY_IN_YEAR)) { sb.append(FormatSupport.DAY_IN_YEAR); sb.append(interval); sb.append(" "); sb.append(rule.getInt(RepeatRule.DAY_IN_YEAR)); sb.append(encodedCount); } else if (FormatSupport.contains(fields, RepeatRule.MONTH_IN_YEAR)) { sb.append(FormatSupport.MONTH_IN_YEAR); sb.append(interval); sb.append(encodeRepeatRuleMonthsInYear(fields, rule)); sb.append(encodedCount); if (FormatSupport.contains(fields, RepeatRule.DAY_IN_MONTH) || FormatSupport.contains(fields, RepeatRule.WEEK_IN_MONTH)) { sb.append(" " + encodeRepeatRule(rule, RepeatRule.MONTHLY)); } } break; } default: return null; } if (startFreq == 0) { sb.append(encodedEndDate); } return sb.toString(); } /** * Returns a string representation of a weekly rule. * @param rule data to be encoded * @return encoded rule */ private String encodeRepeatRuleDaysInWeek(RepeatRule rule) { StringBuffer sb = new StringBuffer(); int daysInWeek = rule.getInt(RepeatRule.DAY_IN_WEEK); for (int i = 0; i < DAYS_OF_WEEK.length; i++) { if ((daysInWeek & DAYS_OF_WEEK[i]) != 0) { sb.append(" "); sb.append(DAYS_OF_WEEK_CODES[i]); } } return sb.toString(); } /** * Returns a string representation of a monthly rule with a weekly * parameter. * @param fields data to be processed * @param rule to encode * @return encoded rule */ private String encodeRepeatRuleWeeksInMonth(int[] fields, RepeatRule rule) { StringBuffer sb = new StringBuffer(); int weeksInMonth = rule.getInt(RepeatRule.WEEK_IN_MONTH); for (int i = 0; i < WEEKS_OF_MONTH.length; i++) { if ((weeksInMonth & WEEKS_OF_MONTH[i]) != 0) { sb.append(" "); sb.append(WEEKS_OF_MONTH_CODES[i]); } } return sb.toString(); } /** * Returns a string representation of a yearly rule with a monthly * parameter. * @param fields data to be processed * @param rule to encode * @return encoded rule */ private String encodeRepeatRuleMonthsInYear(int[] fields, RepeatRule rule) { StringBuffer sb = new StringBuffer(); int monthsInYear = rule.getInt(RepeatRule.MONTH_IN_YEAR); for (int i = 0; i < MONTHS_IN_YEAR.length; i++) { if ((monthsInYear & MONTHS_IN_YEAR[i]) != 0) { sb.append(" "); sb.append(i); } } return sb.toString(); } /** * Constructs one or more PIMItems from serialized data. * @param in Stream containing serialized data * @param encoding Character encoding of the stream * @param list PIMList to which items should be added, or null if the items * should not be part of a list * @throws UnsupportedPIMFormatException if the serialized * data cannot be interpreted by this encoding. * @return a non-empty array of PIMItems containing the objects described * in the serialized data, or null if no items are available * @throws IOException if a read error occurs */ public PIMItem[] decode(InputStream in, String encoding, PIMList list) throws IOException { LineReader r = new LineReader(in, encoding, this); String line = r.readLine(); if (line == null) { return null; } if (!line.toUpperCase().equals("BEGIN:VCALENDAR")) { throw new UnsupportedPIMFormatException("Not a vCalendar object"); } Vector items = new Vector(); for (AbstractPIMItem item; (item = decode(r, list)) != null; ) { items.addElement(item); } if (items.size() == 0) { return null; } AbstractPIMItem[] a = new AbstractPIMItem[items.size()]; items.copyInto(a); return a; } /** * Constructs a single PIMItem from serialized data. * @param in LineReader containing serialized data * @param list PIM list to which the item belongs * @throws UnsupportedPIMFormatException if the serialized data cannot be * interpreted by this encoding. * @return an unserialized Event, or null if no data was available */ private AbstractPIMItem decode(LineReader in, PIMList list) throws IOException { while (true) { String line = in.readLine(); if (line == null) { return null; } FormatSupport.DataElement element = FormatSupport.parseObjectLine(line); if (element.propertyName.equals("BEGIN")) { if (element.data.toUpperCase().equals("VEVENT")) { if (isTypeSupported(PIM.EVENT_LIST)) { return decodeEvent(in, list); } throw new UnsupportedPIMFormatException( "Events list is not supported"); } else if (element.data.toUpperCase().equals("VTODO")) { if (isTypeSupported(PIM.TODO_LIST)) { return decodeToDo(in, list); } throw new UnsupportedPIMFormatException( "ToDo list is not supported"); } else { throw new UnsupportedPIMFormatException( "Bad argument to BEGIN: " + element.data); } } else if (element.propertyName.equals("END")) { if (element.data.toUpperCase().equals("VCALENDAR")) { return null; } else { throw new UnsupportedPIMFormatException( "Bad argument to END: " + element.data); } } else if (element.propertyName.equals("PRODID")) { // ignore product ID } else if (element.propertyName.equals("VERSION")) { // check version, then keep reading if (!element.data.equals("1.0")) { throw new UnsupportedPIMFormatException("vCalendar version '" + element.data + "' is not supported"); } } else if (element.propertyName.equals("CATEGORIES")) { // what should I do with this? this seems to be the wrong place // to put the field. } else { throw new UnsupportedPIMFormatException("Unrecognized item: " + line); } } } /** * Reads and decodes a single vEvent. * @param in encoded event reader stream * @param list PIM list to which the item belongs * @return event reader implementation handle * @throws IOException if a reading error occurs */ private EventImpl decodeEvent(LineReader in, PIMList list) throws IOException { EventImpl event = new EventImpl((AbstractPIMList)list); String line; while ((line = in.readLine()) != null) { FormatSupport.DataElement element = FormatSupport.parseObjectLine(line); if (element.propertyName.equals("END")) { // patch DALARM values try { int alarmValues = event.countValues(Event.ALARM); if (alarmValues > 0 && event.countValues(Event.START) > 0) { int startTime = (int) (event.getDate(Event.START, 0) / 1000); for (int i = 0, j = 0; i < alarmValues; i++, j++) { int alarmTime = event.getInt(Event.ALARM, i); if (alarmTime * 1000 < startTime) { event.setInt(Event.ALARM, i, Event.ATTR_NONE, startTime - alarmTime); } else { event.removeValue(Event.ALARM, i); alarmValues --; i --; } } } } catch (UnsupportedFieldException ufe) { // Nothing to do if ALARM is not supported } return event; } else if (element.propertyName.equals("VERSION")) { if (!element.data.equals("1.0")) { throw new UnsupportedPIMFormatException("Version " + element.data + " is not supported"); } } else if (element.propertyName.equals("CATEGORIES")) { String[] categories = StringUtil.split(element.data, ',', 0); for (int j = 0; j < categories.length; j++) { try { event.addToCategory(categories[j]); } catch (PIMException e) { // cannot add item } } } else if (element.propertyName.equals("RRULE")) { RepeatRule rule = new RepeatRule(); if (!decodeRepeatRule(rule, element.data, true)) { throw new IOException( "Empty or invalid RepeatRule data"); } event.setRepeat(rule); } else if (element.propertyName.equals("EXDATE")) { RepeatRule rule = event.getRepeat(); if (rule != null) { decodeExDates(rule, element.data); event.setRepeat(rule); } } else { importData(event, element.propertyName, element.attributes, element.data); } } throw new IOException("Unterminated vEvent"); } /** * Decodes except dates. * * For more details please see The Electronic Calendaring and Scheduling * Exchange Format Version 1.0 * * @param rule repeat rule instance * @param data string contains encoded dates * separated by "," * */ private void decodeExDates(RepeatRule rule, String data) throws IOException { Parser parser = new Parser(data); long date; while (parser.hasNextDate()) { int dateLen = parser.getEndDate().length(); // end date is either date in yyyyMMdd format or // date/time in yyyymmddThhmmss(Z). date = (dateLen < 15) ? PIMHandler.getInstance().parseDate(parser.getEndDate()) : PIMHandler.getInstance().parseDateTime(parser.getEndDate()); parser.setPos(parser.getPos() + parser.getEndDate().length()); rule.addExceptDate(date); if (!parser.hasMoreChars()) { break; } parser.matchSkip(','); // separator } } /** * Decodes repeat rule. * * For more details please see The Electronic Calendaring and Scheduling * Exchange Format Version 1.0 * * @param rule repeat rule instance * @param data string contains encoded repeat rule * @param isTop true on top recursive level else false * @return true if repeat rule was successfully decoded, false otherwise */ private boolean decodeRepeatRule(RepeatRule rule, String data, boolean isTop) { boolean res = true; Parser parser = new Parser(data); char sym; try { parser.skipBlank(); sym = parser.readChar(); int interval; switch (sym) { case FormatSupport.DAILY: // D<interval> [<duration>] interval = parser.readInt(); if (isTop) { rule.setInt(RepeatRule.FREQUENCY, RepeatRule.DAILY); rule.setInt(RepeatRule.INTERVAL, interval); } setRepeatRuleCount(parser, rule, isTop); break; case FormatSupport.WEEKLY: // W<interval> <weekday> [<duration>] interval = parser.readInt(); if (isTop) { rule.setInt(RepeatRule.FREQUENCY, RepeatRule.WEEKLY); rule.setInt(RepeatRule.INTERVAL, interval); } rule.setInt(RepeatRule.DAY_IN_WEEK, decodeDaysInWeek(parser)); setRepeatRuleCount(parser, rule, isTop); break; case FormatSupport.MONTHLY: if (isTop) { rule.setInt(RepeatRule.FREQUENCY, RepeatRule.MONTHLY); } sym = parser.readChar(); switch (sym) { case FormatSupport.DAY_IN_MONTH: // MD<interval> <daynumber> [<duration>] interval = parser.readInt(); if (isTop) { rule.setInt(RepeatRule.INTERVAL, interval); } parser.skipBlank(); rule.setInt(RepeatRule.DAY_IN_MONTH, parser.readInt()); setRepeatRuleCount(parser, rule, isTop); break; case FormatSupport.WEEK_IN_MONTH: interval = parser.readInt(); if (isTop) { rule.setInt(RepeatRule.INTERVAL, interval); } parser.skipBlank(); rule.setInt(RepeatRule.WEEK_IN_MONTH, decodeWeeksInMonth(parser, rule)); setRepeatRuleCount(parser, rule, isTop); parser.skipBlank(); if (parser.hasMoreChars()) { // parse next rule res = decodeRepeatRule(rule, parser.getRemainder(), false); } break; default: res = false; } break; case FormatSupport.YEARLY: if (isTop) { rule.setInt(RepeatRule.FREQUENCY, RepeatRule.YEARLY); } sym = parser.readChar(); switch (sym) { case FormatSupport.DAY_IN_YEAR: interval = parser.readInt(); if (isTop) { rule.setInt(RepeatRule.INTERVAL, interval); } parser.skipBlank(); rule.setInt(RepeatRule.DAY_IN_YEAR, parser.readInt()); setRepeatRuleCount(parser, rule, isTop); break; case FormatSupport.MONTH_IN_YEAR: interval = parser.readInt(); if (isTop) { rule.setInt(RepeatRule.INTERVAL, interval); } rule.setInt(RepeatRule.MONTH_IN_YEAR, decodeMonthsInYear(parser, rule)); setRepeatRuleCount(parser, rule, isTop); parser.skipBlank(); if (parser.hasMoreChars()) { // parse next rule res = decodeRepeatRule(rule, parser.getRemainder(), false); } break; default: res = false; } } } catch (IOException ex) { res = false; } catch (NumberFormatException ex) { res = false; } catch (IllegalArgumentException ex) { res = false; } catch (FieldEmptyException ex) { res = false; } return res; } /** * Decodes days in a week. * * @param parser input data parser * @return day-in-a-week value * @throws IOException if a reading error occurs or wrong format */ private int decodeDaysInWeek(Parser parser) throws IOException { int daysInWeek = 0; while (true) { // decode days parser.skipBlank(); if (!parser.isNextMatchStr(DAYS_OF_WEEK_CODES)) { break; } String dayStr = parser.readId(); int i; for (i = 0; i < DAYS_OF_WEEK_CODES.length; i++) { if (DAYS_OF_WEEK_CODES[i].equals(dayStr)) { daysInWeek |= DAYS_OF_WEEK[i]; break; } } if (i == DAYS_OF_WEEK_CODES.length) { throw new IOException("Wrong format"); // not found } } return daysInWeek; } /** * Decodes weeks in a month. * * @param parser input data parser * @param rule RepeatRule for setting fields * @return week-in-a-month value * @throws IOException if a reading error occurs or wrong format */ private int decodeWeeksInMonth(Parser parser, RepeatRule rule) throws IOException { int weeksInMonth = 0; while (true) { // decode weeks parser.skipBlank(); if (!parser.isNextMatchStr(WEEKS_OF_MONTH_CODES)) { break; } String weekStr = parser.readId(); int i; for (i = 0; i < WEEKS_OF_MONTH_CODES.length; i++) { if (WEEKS_OF_MONTH_CODES[i].equals(weekStr)) { weeksInMonth |= WEEKS_OF_MONTH[i]; break; } } if (i == WEEKS_OF_MONTH_CODES.length) { throw new IOException("Wrong format"); // not found } } return weeksInMonth; } /** * Decodes months in a year. * * @param parser input data parser * @param rule RepeatRule for setting fields * @return month-in-a-year value * @throws IOException if a reading error occurs or wrong format */ private int decodeMonthsInYear(Parser parser, RepeatRule rule) throws IOException { int monthsInYear = 0; int monthNum; while (true) { // decode monthes parser.skipBlank(); if (!parser.isNextInt()) { break; } monthNum = parser.readInt(); if (monthNum >= MONTHS_IN_YEAR.length) { throw new IOException("Wrong month number"); } monthsInYear |= MONTHS_IN_YEAR[monthNum]; } return monthsInYear; } /** * Puts duration (#value) and end date (yyyymmddThhmmss(Z)/yyyyMMdd) * to COUNT and END fields of the repeat rule. * * @param parser input data parser * @param rule repeat rule object for setting * @param isSetCount set COUNT field else don't set * @throws IOException if a reading error occurs or wrong format */ private void setRepeatRuleCount(Parser parser, RepeatRule rule, boolean isSetCount) throws IOException { // parse duration parser.skipBlank(); if (parser.match('#')) { parser.skip(); int count = parser.readInt(); if (isSetCount && count > 0) { rule.setInt(RepeatRule.COUNT, count); } } // parse end date parser.skipBlank(); if (parser.hasNextDate()) { int dateLen = parser.getEndDate().length(); // end date is either date in yyyyMMdd format or // date/time in yyyymmddThhmmss(Z). long date = (dateLen < 15) ? PIMHandler.getInstance().parseDate(parser.getEndDate()) : PIMHandler.getInstance().parseDateTime(parser.getEndDate()); rule.setDate(RepeatRule.END, date); parser.setPos(parser.getPos() + dateLen); } } /** * Decodes one line of a vEvent. * @param event data to be processed * @param propertyName property key name * @param attributes fields to be processed * @param data string to be processed * * @throws IOException if a read error occured */ private void importData(Event event, String propertyName, String[] attributes, String data) throws IOException { int field = VEventSupport.getFieldCode(propertyName); if (event instanceof EventImpl) { EventImpl eventImpl = (EventImpl)event; if (!PIMHandler.getInstance().isSupportedField( eventImpl.getPIMListHandle(), field)) { // ignore unsupported fields return; } } switch (field) { case Event.SUMMARY: case Event.LOCATION: case Event.NOTE: case Event.UID: { String sdata = FormatSupport.parseString(attributes, data); event.addString(field, Event.ATTR_NONE, sdata); break; } case Event.END: case Event.REVISION: case Event.START: { long date = PIMHandler.getInstance().parseDateTime(data); event.addDate(field, Event.ATTR_NONE, date); break; } case Event.CLASS: { String sdata = FormatSupport.parseString(attributes, data); if (attributes != null && attributes.length != 0) { throw new IOException("Invalid parameter for field CLASS"); } int c = VEventSupport.getClassCode(sdata); event.addInt(Event.CLASS, Event.ATTR_NONE, c); break; } case Event.ALARM: { String[] s = FormatSupport.parseStringArray(attributes, data); if (s.length > 0) { long alarmTime = PIMHandler.getInstance().parseDateTime(s[0]); event.addInt(Event.ALARM, Event.ATTR_NONE, (int) (alarmTime / 1000)); } break; } } } /** * Reads and decodes a single vToDo. * @param in input stream * @param list PIM list to which the item belongs * @throws IOException if an error occurs reading * @return ToDo implementation handler */ private ToDoImpl decodeToDo(LineReader in, PIMList list) throws IOException { ToDoImpl todo = new ToDoImpl((AbstractPIMList)list); String line; while ((line = in.readLine()) != null) { FormatSupport.DataElement element = FormatSupport.parseObjectLine(line); if (element.propertyName.equals("END")) { return todo; } else if (element.propertyName.equals("VERSION")) { if (!element.data.equals("1.0")) { throw new UnsupportedPIMFormatException("Version " + element.data + " is not supported"); } } else if (element.propertyName.equals("CATEGORIES")) { String[] categories = StringUtil.split(element.data, ',', 0); for (int j = 0; j < categories.length; j++) { try { todo.addToCategory(categories[j]); } catch (PIMException e) { // cannot add item to category } } } else { importData(todo, element.propertyName, element.attributes, element.data); } } throw new IOException("Unterminated vToDo"); } /** * Decodes one line of a vToDo. * @param todo element to fill * @param propertyName key to property value * @param attributes fields to import * @param data string containing input data */ private void importData(ToDo todo, String propertyName, String[] attributes, String data) { int field = VToDoSupport.getFieldCode(propertyName); switch (field) { case ToDo.SUMMARY: case ToDo.NOTE: case ToDo.UID: { String sdata = FormatSupport.parseString(attributes, data); todo.addString(field, ToDo.ATTR_NONE, sdata); break; } case ToDo.COMPLETION_DATE: todo.addBoolean(ToDo.COMPLETED, ToDo.ATTR_NONE, true); // fall through case ToDo.DUE: case ToDo.REVISION: { long date = PIMHandler.getInstance().parseDateTime(data); todo.addDate(field, ToDo.ATTR_NONE, date); break; } case ToDo.CLASS: { String sdata = FormatSupport.parseString(attributes, data); int c = VToDoSupport.getClassCode(sdata); todo.addInt(ToDo.CLASS, ToDo.ATTR_NONE, c); break; } case ToDo.PRIORITY: { try { int i = Integer.parseInt(data); todo.addInt(ToDo.PRIORITY, ToDo.ATTR_NONE, i); } catch (NumberFormatException e) { // ignore this field } break; } } } } /** * A simple parser for encoded RepeatRule data. */ class Parser { /** Source data buffer. */ String s; /** Current position in source data buffer. */ private int index; /** Saved end date. */ private String endDate = ""; /** * Constructor. * * @param s string for parsing */ Parser(String s) { this.s = s; index = 0; } /** * Checks that current position is valid. * @throws IOException if position is out of buffer */ private void checkBound() throws IOException { checkBound(0); } /** * Checks that input position is valid. * @param off offset from current position * @throws IOException if position + offset is out of buffer */ private void checkBound(int off) throws IOException { int bound = index + off; if (bound < 0 || bound >= s.length()) { throw new IOException("Out of bound"); } } /** * Reads next char from buffer. * @return next char from buffer * @throws IOException if position is out of buffer */ char readChar() throws IOException { checkBound(); return s.charAt(index++); } /** * Reads next char from buffer without changing the pointer. * @return next char from buffer * @throws IOException if position is out of buffer */ char nextChar() throws IOException { return nextChar(0); } /** * Reads next char by given position * from buffer without changing the pointer. * @param off offset from current position * @return next char from buffer * @throws IOException if position + offset is out of buffer */ char nextChar(int off) throws IOException { checkBound(off); return s.charAt(index + off); } /** * Checks that next chars contain a date * in format yyyymmddThhmmss. * * For more details please see RFC 2445 * * @return true when next chars contain a date else false * If the date exists then it is saved in endDate member */ boolean hasNextDate() { // Date format yyyymmddThhmmss endDate = ""; if (!hasMoreChars()) { return false; } String strDate = getRemainder(); int dateLength = 8; // yyyymmdd if (strDate.length() < dateLength) { return false; } if (strDate.length() > 14 && strDate.charAt(8) == 'T') { dateLength = 15; // yyyymmddThhmmss // yyyymmddThhmmssZ - absolute time if (strDate.length() > 15 && strDate.charAt(15) == 'Z') { dateLength = 16; } } strDate = strDate.substring(0,dateLength); try { int year = Integer.parseInt(strDate.substring(0, 4)); if (year < 1970) { return false; } int month = Integer.parseInt(strDate.substring(4, 6)); if ((month < 1) || (month > 12)) { return false; } int day = Integer.parseInt(strDate.substring(6, 8)); if ((day < 1) || (day > 31)) { return false; } // Don't check time } catch (NumberFormatException ex) { return false; } endDate = strDate; return true; } /** * Gets a date that saved by hasNextDate method. * * @return saved date */ String getEndDate() { return endDate; } /** * Reads next integer value from buffer. * * @return integer value * @throws IOException if chars from current position don't contain * integer value */ int readInt() throws IOException { checkBound(); StringBuffer sb = new StringBuffer(); for( ; index < s.length() && Character.isDigit(s.charAt(index)) ; index++) { sb.append(s.charAt(index)); } if (sb.length() == 0) { throw new IOException("No digital chars"); } return Integer.parseInt(sb.toString()); } /** * Checks that the next char is equal to given char. * @param sym char for comparing * @return true when next char is equal to given char */ boolean match(char sym) { if (!hasMoreChars()) { return false; } return sym == s.charAt(index); } /** * Checks that the buffer contains chars from current * position. * @return true when buffer has unparsed chars */ boolean hasMoreChars() { return index < s.length(); } /** * Skips the current position. */ void skip() { index++; } /** * Skips next spaces and tabs. */ void skipBlank() { while (hasMoreChars() && (match(' ') || match('\t'))) { skip(); } } /** * Checks that the next symbol is equal to given one. * * @throws IOException if next symbol is different from input one * @param sym the symbol to be checked */ void matchSkip(char sym) throws IOException { if (!match(sym)) { throw new IOException("No symbol " + sym); } index++; } /** * Gets the remainder of parsed buffer. * @return part of buffer from current position */ String getRemainder() { String retValue = null; if (index < s.length()) { retValue = s.substring(index); } return retValue; } /** * Gets the current position. * @return current position */ int getPos() { return index; } /** * Sets the given position. * @param pos position for setting */ void setPos(int pos) { index = pos; } /** * Checks that next ID from buffer could be * found in input string array. * @param arrStr array for searching * @return true when next ID is found in the input array else false */ boolean isNextMatchStr(String[] arrStr) { if (!hasMoreChars()) { return false; } int pos = index; String nextId; try { nextId = readId(); } catch (IOException ex) { return false; } index = pos; if ((nextId == null) || (nextId.length() == 0)) { return false; } for (int i = 0; i < arrStr.length; i++) { if (arrStr[i].equals(nextId)) { return true; } } return false; } /** * Checks that next ID from buffer contains * digit symbols only. * @return true when next ID contains digit symbols only */ boolean isNextInt() { if (!hasMoreChars()) { return false; } int pos = index; String nextId; try { nextId = readId(); } catch (IOException ex) { return false; } index = pos; if ((nextId == null) || (nextId.length() == 0)) { return false; } return Character.isDigit(nextId.charAt(0)); } /** * Gets next ID from buffer. * @return next ID * @throws IOException if position is out of buffer bounds */ String readId() throws IOException { checkBound(); String id = getRemainder(); int index = id.indexOf(' '); if (index > -1) { id = id.substring(0, index); } setPos(getPos() + id.length()); return id; } }