“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.
2 thoughts on “Kalenderspielereien mit Java – iCalendar”
Comments are closed.