macdevcenter.com
oreilly.comSafari Books Online.Conferences.

advertisement

AddThis Social Bookmark Button

Transforming iCal Calendars with Java
Pages: 1, 2, 3

Reading your calendar from disk

If you are comfortable reading files from disk, you can skip this section.



The class ICalendarToString is fairly lightweight. Here's what it looks like to an outsider.

package ical;

public class ICalendarToString {
  public static String getCalendarAsString(String calendarName)
                       throws CalendarNotFoundException {}
}

It has a single static method, getCalendarAsString, that takes the name of the calendar as a String and returns the contents of the file in a String. You invoke it like this:

ICalendarToString.getCalendarAsString("example");

In the code listing below you can see the details. The body of the getICalendarAsString() method shows that three private methods are called. You can put everything into a single method, but that makes your code harder to understand. Note that when you read the body of this method, you get a high level view of what this class does. It locates the calendar file using the name it received, sets up a file reader that is used to read the contents of the file, and returns a representation of that calendar as a String. The method also throws a CalendarNotFoundException. It is important to use exceptions that help the developer know what has gone wrong in using your class.

package ical;

import java.io.FileNotFoundException;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;


public class ICalendarToString {
  private static final String CALENDAR_HOME = "Calendar/";
  private static File inputFile;
  private static FileReader calendarReader;

  public static String getCalendarAsString(String calendarName)
                          throws CalendarNotFoundException {
    findCalendarFile(calendarName);
    setUpFileReader();
    return representCalendarAsString();
  }

  private static void findCalendarFile(String calendarName)
                          throws CalendarNotFoundException {
    inputFile = new File(CALENDAR_HOME + calendarName + ".ics");
    if (!inputFile.exists())  throw new CalendarNotFoundException(
         "There is no file with the name " + inputFile.getAbsolutePath());
  }

  private static void setUpFileReader() throws CalendarNotFoundException {
    try {
      calendarReader = new FileReader(inputFile);
    } catch (FileNotFoundException e) {
      throw new CalendarNotFoundException(e.getMessage(), e.getCause());
    }
  }

  private static String representCalendarAsString()
                                 throws CalendarNotFoundException {
    try {
      char[] contentCharacterArray = new char[(int) inputFile.length()];
      calendarReader.read(contentCharacterArray);
      calendarReader.close();
      return new String(contentCharacterArray);
    } catch (IOException e) {
      throw new CalendarNotFoundException(e.getMessage(), e.getCause());
    }
  }
}

The awkward part of the code is found in the representCalendarAsString() method. You create a character array with enough slots to read in the contents of the file. You then use the calendarReader to read the file into the array, remembering to close the reader, and finally construct a String from this array.

The findCalendarFile method tries to find the calendar file in the directory that holds your calendars (you need to alter this to match your setup) and appends ".ics" to the end of the name entered. It may not be clear whether the user is to enter the calendar name as "example" or "example.ics".

If I were writing this application for distribution I would probably choose a strategy that took the complete file name as input and didn't add the suffix. In any case, you can see that the CalendarNotFoundException is thrown with a message that details the presumed location of the file. A developer that gets an exception, that "example.ics.ics" was not found, can quickly see that the calendar should be passed in as "example". In the other two methods, the exceptions are constructed to pass on the message and cause from the exception that triggered the condition. This uses another new facility from Java 1.4, chained exceptions. Here's the CalendarNotFoundException class.

package ical;

public class CalendarNotFoundException extends Exception {
  public CalendarNotFoundException(String description) {
    super(description);
  }

  public CalendarNotFoundException(String description, Throwable cause) {
    super(description, cause);
  }
}

Next stop is manipulating the calendar using regular expressions.

Much ToDo about Nothing

The idea is to use regular expressions to identify a todo item from our calendar and then replace the todo so that it can be recognized as an event. We could look at this all at once and use this as the search string.

BEGIN:VTODO(.*?)SUMMARY:(.*?)DTSTART(.*?)\n(.*?)END:VTODO

You could then use this as the replacement string.

BEGIN:VEVENT$1SUMMARY:TODO $2DTSTART;VALUE=DATE:20030520\nDURATION:P1D\n$4END:VEVENT

If you are comfortable with these type of expressions, then the code presented in this section will point you at how to accomplish this using the Java APIs. Our actual code may appear overly verbose to those already comfortable with regexen. The JavaDocs for these two classes contain a lot of information, but if you are going to end up doing a lot of regex work, you should pick up a copy of Jeffrey Friedl's book, Mastering Regular Expressions, 2nd Edition.

Consider this first pass at the code.

 public static String convert( String contents){
    Matcher todoMatcher = getTodoMatcher(contents);
    return todoMatcher.replaceAll(setTodoRegEx());
  }

  private static Matcher getTodoMatcher(String contents){
    Pattern todoPattern = Pattern.compile(getTodoRegEx(),Pattern.DOTALL);
    return todoPattern.matcher(contents);
  }
  private static String getTodoRegEx(){
    return "BEGIN:VTODO(.*?)END:VTODO";
  }

  private static String setTodoRegEx(){
    return "BEGIN:VEVENT$1END:VEVENT";
  }

The two classes used for regular expressions are in the java.util package. You create the patterns to match on using the Pattern class and do most of the matching and replacement work with the Matcher class. In the getTodoMatcher() method you create an instance of Matcher using the compile static method from the Pattern class. Then in the convert() method this matcher is used to replace all of the found matches with the replacement expression.

The expression in the getTodoRegEx() method will match a string that begins with BEGIN:VTODO and ends with END:VTODO. In between the dot will match almost any character and the star means match as many of those characters as you can. Ordinarily this would only apply to characters on the same line so this pattern would look for a string with that pattern that is contained in a single line. To enable the search to take place in multiple lines we have set the Pattern,DOTALL flag in the second parameter of the call to thecompile() method. This flag specifies that the dot should match any character including new line characters. Now the pattern can search across multiple lines.

That explains the dot and the star and the interpretation of the dot in the portion of the expression given by (.*?). If we didn't include the question mark we would be performing a greedy search. The effect would be that we would locate a string containing the first occurrence of BEGIN:VTODO and the last occurrence of END:VTODO. If you look back at our sample calendar, this would capture both todos in a single string. Worse, if there was an event between them it would capture that as well. To search for the first occurrence of the END:VTODO, we insert the question mark after the star. That leaves the parentheses. If we later want to refer to a bit of the found string we enclose what we want to refer to in parentheses. Now we can refer back to it. In this case the replacement string BEGIN:VEVENT$1END:VEVENT results in the following:

  • Replace BEGIN:VTODO with BEGIN:VEVENT
  • Replace END:VTODO with END:VEVENT
  • Everything identified by (.*?) stays where it was because it is referred to by $1

The first note is that you don't really get an event by switching the todos to events. We'll need to change the insides as well. We need to set the date of the newly created event and a duration. We'll set the date as today and the duration as a full day. For the most part you could use the search and replacement strings given at the top of this section although the date is hardcoded there. I find the following version easier to read and more modifiable and maintainable.

package ical;

import java.util.regex.Pattern;
import java.util.regex.Matcher;
import java.util.Calendar;

public class ICalendarTodoConverter {

  private static final String SAVED_PORTION = "(.*?)";

  public static String convert(String contents) {
    Matcher todoMatcher = getTodoMatcher(contents);
    return todoMatcher.replaceAll(setTodoRegEx());
  }

  private static Matcher getTodoMatcher(String contents) {
    Pattern todoPattern = Pattern.compile(getTodoRegEx(), Pattern.DOTALL);
    return todoPattern.matcher(contents);
  }

  private static String getTodoRegEx() {
    return "BEGIN:VTODO" + getTodoContentsRegEx() + "END:VTODO";
  }

  private static String setTodoRegEx() {
    return "BEGIN:VEVENT" + setTodoContentsRegEx() + "END:VEVENT";
  }

  private static String getTodoContentsRegEx() {
    return SAVED_PORTION // $1
        + getSummaryRegEx()
        + getDateRegEx()
        + SAVED_PORTION; //$4
  }

  private static String setTodoContentsRegEx() {
    return "$1"
        + setSummaryRegEx()
        + setDateRegEx()
        + "$4";
  }

  private static String getSummaryRegEx() {
    return "SUMMARY:"
        + SAVED_PORTION; // $2
  }

  private static String setSummaryRegEx() {
    return "SUMMARY:TODO "
        + "$2";
  }

  private static String getDateRegEx() {
    return "DTSTART"
        + SAVED_PORTION //$3
        + "\n";
  }

  private static String setDateRegEx() {
    return "DTSTART;VALUE=DATE:"
        + getCurrentDateAsString()
        + "\nDURATION:P1D\n";
  }

  private static String getCurrentDateAsString() {
    Calendar now = Calendar.getInstance();
    String rightNow = now.get(now.YEAR)
          + formatDateComponent(now.get(now.MONTH) + 1)
          + formatDateComponent(now.get(now.DAY_OF_MONTH));
    return rightNow;
  }

  private static String formatDateComponent(int i) {
    if (i < 10)
      return "0" + i;
    else
      return "" + i;
  }
}

The expression SAVED_PORTION is used for (.*?). Also you can follow the getxxx() methods to see how the calendar is pulled apart and compare to the setxxx() methods to see how it will be reassembled. For example you can see that we are inserting the words "TODO" as part of the summary. In get/setSummaryRegEx() methods you can see that the string SUMMARY: is going to be replaced by the string SUMMARY: TODO . You can use the Calendar class to format today's date but remember that the number corresponding to the month starts with 0 instead of the expected value of 1. Also, you will need to convert one digit months such as 5 for May to two digits 05 so that iCal can correctly parse the date.

Pages: 1, 2, 3

Next Pagearrow