Kalenderspielereien mit Java – iCalendar

„Das klingt ja ganz spannend, aber Ich brauche eher ein Tool, damit ich die Feiertage alle in meinen Kalender importieren kann.“

Ein Kollege

Neue Anforderungen und Anwendungsfälle für selbstgeschrieben Bibliotheken ergeben sich häufig in Gesprächen mit Kollegen. In diesem Fall benötigte der Kollege eine Feiertagsliste für seinen Kalender. Die Bibliothek zur Feiertagsberechnung war für sein Problem unzureichend.

Termine in einen elektronischen Kalender einzufügen, ist schon vor langer Zeit mit dem RFC 5545 standardisiert worden. Dieser RFC definiert ein zeilenbasiertes Format, in dem einzelne Zeilen Attribut-Wert Paare darstellen, die zu Objekten zusammengefasst werden können.

Im folgenden Beispiel ist ein Termin dargestellt, dessen Attribute durch BEGIN:VEVENT und END:VEVENT in einem VEVENT Objekt zusammengefasst sind.

BEGIN:VEVENT
SUMMARY:1. Weihnachtstag
DESCRIPTION:Gesetzlicher Feiertag in Deutschland
UID:de-schegge-holiday-de-DE-20201225
CREATED:20200326T052849
LAST-MODIFIED:20200326T052849
DTSTART:20201225
DTEND:20201226
CATEGORIES:Feiertage
TRANSP:TRANSPARENT
CLASS:PUBLIC
END:VEVENT

Das Attribut SUMMARY enthält den Titel des Termins, DESCRIPTION einen Erläuterungstest und DTSTART, DTEND das Anfangs- und Endedatum des Termins. Für Feiertage sind dies immer das Datum des Feiertags und das Datum des Folgetags. Die Attribute UID, CATEGORIES, TRANSP, CLASS, CREATED und LAST-MODIFIED dienen der Verwaltung der Termine.

Eine Reihe von Terminen können in einem VCALENDAR Objekt zusammengefasst werden.

BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//schegge.de//NONSGML Holidays v0.1.5//EN
BEGIN:VEVENT
...
END:VEVENT
...
END:VCALENDAR

Um einen RFC 5545 konformen Kalender für deutsche Feiertage zu erstellen benötigen wir also nur eine Liste der Feiertage und schreiben diese in der oben dargestellten Notation in einen Outputstream.

Map<LocalDate, CalendarDay> calendarDays = collectAllGermanHolidays(year);
VCalendar calendar = calendarDays.entrySet().stream().map(this::createHolidayEvent).collect(
        collectingAndThen(toList(), VCalendar::new));
System.out.println(calendar);

In der ersten Zeile werden alle deutschen Feiertage für das angegebene Jahr in einer HashMap gespeichert. Der Typ CalendarDay ist ein POJO, der uns für diesen Tag speichert, in welchen Bundesländern und in welchen Orten (Augsburg) dieser Tag gefeiert wird. Diese Informationen werden benötigt, um etwas für Pfingstsonntag die Beschreibung „Gesetzlicher Feiertag in Brandenburg und Hessen“ zu erzeugen oder für Ostermontag „Gesetzlicher Feiertag in Deutschland“.

In der zweiten Zeile werden für die CalendarDay Instanzen entsprechende VEvent Instanzen erzeugt, in einer Liste gesammelt und damit eine VCalendar Instanz erzeugt. Die Java Stream API liefert mit toList und collectingAndThen Methoden, mit denen der eigene Code äußerst kompakt zu formulieren ist.

In der dritten Zeile wird das Ergebnis auf den Standard Outputstream geschrieben. Dafür wurde die toString Methode der VCalendar Klasse überschrieben. Für Kalendar, die nur wenige Feiertage besitzen ein statthafter Weg.

public final class VCalendar {
  private static final String PREFIX = "BEGIN:VCALENDAR\nVERSION:2.0\nPRODID:-//schegge.de//NONSGML Holidays v0.1.5//EN\n";
  private static final String SUFFIX = "\nEND:VCALENDAR\n";

  private final List<VEvent> events;

  public VCalendar(List<VEvent> events) {
    this.events = events;
  }

  public String toString() {
    return events.stream().map(String::valueOf).collect(joining("\n", PREFIX, SUFFIX));
  }
}

Die Darstellung der einzelnen Events wird in der toString der VEvent Klasse erstellt. Da hier mehrere Attribute ausgegeben werden müssen, ist diese Klasse etwas aufwendiger ausgefallen.

public class VEvent {
  private static final String PREFIX = "BEGIN:VEVENT\n";
  private static final String SUFFIX = "\nEND:VEVENT";

  public static final PrintAttributeVisitor PRINT_ATTRIBUTE_VISITOR_VISITOR = new PrintAttributeVisitor();

  private final Map<Attribute, Object> attributes = new EnumMap<>(Attribute.class);

  private VEvent(Map<Attribute, Object> attributes) {
    this.attributes.putAll(attributes);
  }

  public String toString() {
    return attributes.entrySet().stream().filter(a -> Objects.nonNull(a.getValue()))
        .map(a -> a.getKey().accept(PRINT_ATTRIBUTE_VISITOR_VISITOR, a.getValue()))
        .collect(joining("\n", PREFIX, SUFFIX));
  }
}

Die Werte der Attribute werden in einer Map gespeichert. Dabei wird das Enum Attribute für den Typ des Attributes als Key verwendet. Da die Keys in der Map Enums sind, wird statt einer HashMap eine für Enums optimierte Map Implementierung verwendet. Ein weiterer kleiner Vorteil der EnumMap ist es, dass die Verarbeitung der Map Einträge der Reihenfolge der Enum Konstanten folgt.

In der toString Methode wird die Map durchlaufen und für alle nicht null Werte die accept Methode eines Visitors aufgerufen. Das Visitor Pattern für Enums wurde hier bereits in den Beiträgen Zu Besuch bei den Enums und Noch mehr Besucher thematisiert.

Der hier verwendete Visitor implementiert das Interface AttributVisitor mit den generischen Typen R und P für das Ergebnis und einen zusätzlichen Parameter.

interface AttributeVisitor<R, P> {
  R visitString(Attribute attribute, P param);

  R visitDate(Attribute attribute, P param);

  R visitTimestamp(Attribute attribute, P param);
}

Für jedes Attribute wird eine der drei Methoden aufgerufen. Welche Methode es ist, entscheiden die jeweilige Enum Konstante in ihrer accept Methode.

class enum Attribute {
  DTEND {
    @Override
    <V, M> V accept(AttributeVisitor<V, M> visitor, M param) {
      return visitor.visitDate(this, param);
    }
  }
}

Der zur Ausgabe verwendet Vistor erzeugt String Darstellungen aus dem Attribute und dem dazugehörigen Wert. Dazu implementiert er das Interface AttributeVisitor<String, Object>. In den Methoden wird der Wert entsprechend gecastet und der String zusammengefügt.

private static class PrintAttributeVisitor implements AttributeVisitor<String, Object> {

  public static final DateTimeFormatter TIMESTAMP_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMdd'T'hhmmss");
  public static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMdd");

  @Override
  public String visitString(Attribute attribute, Object value) {
    return attribute.name() + ":" + value;
  }

  @Override
  public String visitDate(Attribute attribute, Object value) {
    return attribute.name() + ":" + DATE_FORMATTER.format((LocalDate) value);
  }

  @Override
  public String visitTimestamp(Attribute attribute, Object value) {
    return attribute.name() + ":" + TIMESTAMP_FORMATTER.format((ZonedDateTime) value);
  }
}

Damit ist die Generierung der Feiertags Einträge für einen RFC 5545 konformen Kalender auch schon beendet.

Die Datei FeierTageIndeutschland.ics enthält die deutschen Feiertage 2020 für ihren elektronischen Kalendar. Dafür bitte einen neuen Kalender in ihrer App erstellen und die Datei in diesen importieren. So können die Feiertage ohne großen Aufwand wieder gelöscht werden.