Heute Nacht wollte ich eigentlich planen, welche Sessions ich auf der Agile 2009 besuchen möchte, die nächste Woche in Chicago stattfindet. I freue mich schon sehr auf die Konferenz und kann mich nur schwer zwischen den vielen Sessions entscheiden, weil ich so viele verpassen werde! Planung erfordert natürlich ein mindestmaß an Organisation. Auf der Homepage der Konferenz kann man sich durch das Importieren von iCalendar-Dateien die Termine der Sessions in den Kalender einpflegen. Eine großartige Idee … die leider nicht funktioniert, wenn man in einer anderen Zeitzone als Chicago lebt, oder Outlook als seinen Kalender benutzt. Meine Vermutung ist, dass eines der beiden Kriterien auf die meisten Benutzer zutrifft. Deshalb frage ich mich ernsthaft wie gut das Feature auf der Webseite getestet wurde. Naja, egal, mir hat es in den Fingern gekribbelt und ich habe einen Blick in dieSpezifikationen von iCalendar geworfen und ein kleines Groovy-Skript fabriziert, dass die bestehenden iCalendar-Dateien konvertiert.
>> Alle sessions sind konvertiert und können hier heruntergeladen werden. Fertig um in den Kalender importiert zu werden – auch nach Outlook.
Es gab mit den originären Dateien zwei Probleme. Hier ist eine Beispieldatei:
BEGIN:VCALENDAR
VERSION:2.0
PRODID:Agile2009
METHOD:PUBLISH
CALSCALE:GREGORIAN
X-WR-CALNAME:Agile2009
X-WR-CALDESC:Agile2009
BEGIN:VEVENT
UID:1220
DTSTART:20090825T140000
DTEND:20090825T173000
SUMMARY:User Stories for Agile Requirements
LOCATION:Columbus IJ
URL;VALUE=URI:http://agile2009.agilealliance.org/node/1220
DTSTAMP:20090209T022940Z
LAST-UPDATED:20090723T173948Z
END:VEVENT
END:VCALENDAR |
BEGIN:VCALENDAR
VERSION:2.0
PRODID:Agile2009
METHOD:PUBLISH
CALSCALE:GREGORIAN
X-WR-CALNAME:Agile2009
X-WR-CALDESC:Agile2009
BEGIN:VEVENT
UID:1220
DTSTART:20090825T140000
DTEND:20090825T173000
SUMMARY:User Stories for Agile Requirements
LOCATION:Columbus IJ
URL;VALUE=URI:http://agile2009.agilealliance.org/node/1220
DTSTAMP:20090209T022940Z
LAST-UPDATED:20090723T173948Z
END:VEVENT
END:VCALENDAR
Zeitzonen
Die Start- und Endzeit beinhalten keine Information über die Zeitzone, also wird die lokale Zeit angenommen. iCalender spezifiziert, dass die Zeit UTC ist, wenn ein „Z“ angehangen wird. Die Transformation ist also sehr einfach: fünf Stunden draufzählen um von Chicago zu UTC zukommen, und ein „Z“ anhängen.
Outlook
Das zweite Problem wird offenbar, wenn man versucht mehr als eine Session in Outlook zu importieren. Beim ersten Mal erstellt Outlook automatisch einen neuen Kalender „Agile2009“. Cool. Bei der zweiten Session landet diese aber wieder in einem neuen Kalender „Agile2009 (1)“. Extrem uncool. Um das zu beheben, müssen die beiden Properties X-WR-CALNAME und X-WR-CALDESC entfernt werden. Dies scheint leider ein bekanntes Problem mit Outlook zu sein. Zum Glück lässt sich das ebenfalls einfach beheben.
Groovy
Um die Konvertierung zu automatisieren, habe ich ein kleines Groovy-Skript geschrieben, welches sich erst alle gültigen Session-IDs besorgt, dann die iCalendar-Dateien herunterlädt und die beiden Fehler behebt, und mir auch noch den Text ausgibt, den ich in unser WordPress kopieren muss. Das Skript kann mit Sicherheit noch verbessert werden, aber es tut erstmal seinen Job. Was ich wirklich liebe an Groovy, ist die Möglichkeit Strings wie ein Array zu zerschneiden, und dabei mit negativen Indexen auch von hinten zählen zu können, sowie die Möglichkeiten durch das regex-Handling, das macht Aufgaben wie diese um ein Vielfaches einfacher 🙂
public class ConvertCalendar{
private static final String VCAL_TIME_FMT = "yyyyMMdd'T'HHmmss";
public static void main(def args){
// use the smartphone page to get all valid session numbers
// http://agile2009.pairwith.us/sessions
InputStream sessionStream = new URL("http://agile2009.pairwith.us/sessions").openConnection().inputStream
String sessions = org.apache.commons.io.IOUtils.toString(sessionStream);
List calList = new ArrayList();
// href="/sessions/5107"
sessions.eachMatch(/href="\/sessions\/\d{1,4}"/) {
InputStream calendarStream = new URL("http://agile2009.agilealliance.org/session_ical/" + it[16 .. -2]).openConnection().inputStream
String calendar = org.apache.commons.io.IOUtils.toString(calendarStream);
File calendarFile = new File("./cals/temp - calendar.ics");
def calendarFileWriter = new FileWriter(calendarFile)
def calendarFileName = "";
calendar.eachLine {
def vCalLine = it;
if (vCalLine ==~ /^X-WR-CALNAME:.*/ || vCalLine ==~ /^X-WR-CALDESC:.*/) {
// ignore that line
} else if (vCalLine ==~ /^DTSTART:.*/ ) {
def origDate = Date.parse(VCAL_TIME_FMT, vCalLine[-15..-1])
vCalLine = vCalLine[0..-16] + DateUtils.addHours(origDate,5).format(VCAL_TIME_FMT) + "Z";
calendarFileWriter.write("${vCalLine}\n")
calendarFileName = origDate.format("yyyy-MM-dd'T'HHmm");
} else if (vCalLine ==~ /^DTEND:.*/) {
def origDate = Date.parse(VCAL_TIME_FMT, vCalLine[-15..-1])
vCalLine = vCalLine[0..-16] + DateUtils.addHours(origDate,5).format(VCAL_TIME_FMT) + "Z";
calendarFileWriter.write("${vCalLine}\n")
} else if (vCalLine ==~ /^SUMMARY:.*/) {
calendarFileName = calendarFileName + " " + vCalLine[8..-1].replaceAll("[^\\w\\s]", "")
println "Processing " + calendarFileName
calendarFileWriter.write("${vCalLine}\n")
} else {
calendarFileWriter.write("${vCalLine}\n")
}
}
calendarFileWriter.close();
calendarFile.renameTo(new File("./cals/"+calendarFileName+".ics"))
calList.add(calendarFileName+".ics")
calendarStream.close();
}
println "Text to paste into Wordpress after uploading: "
println "<ul>"
Collections.sort(calList)
calList.each {
println "<li><a href="https://blog.codecentric.de/en/2009/08/english-international-calendar-file-processing-its-groovy-2/">${it}</a></li>"
}
println "</ul>"
}} |
public class ConvertCalendar{ private static final String VCAL_TIME_FMT = "yyyyMMdd'T'HHmmss"; public static void main(def args){
// use the smartphone page to get all valid session numbers
// http://agile2009.pairwith.us/sessions
InputStream sessionStream = new URL("http://agile2009.pairwith.us/sessions").openConnection().inputStream String sessions = org.apache.commons.io.IOUtils.toString(sessionStream); List calList = new ArrayList(); // href="/sessions/5107"
sessions.eachMatch(/href="\/sessions\/\d{1,4}"/) {
InputStream calendarStream = new URL("http://agile2009.agilealliance.org/session_ical/" + it[16 .. -2]).openConnection().inputStream
String calendar = org.apache.commons.io.IOUtils.toString(calendarStream); File calendarFile = new File("./cals/temp - calendar.ics");
def calendarFileWriter = new FileWriter(calendarFile)
def calendarFileName = "";
calendar.eachLine {
def vCalLine = it;
if (vCalLine ==~ /^X-WR-CALNAME:.*/ || vCalLine ==~ /^X-WR-CALDESC:.*/) {
// ignore that line
} else if (vCalLine ==~ /^DTSTART:.*/ ) {
def origDate = Date.parse(VCAL_TIME_FMT, vCalLine[-15..-1])
vCalLine = vCalLine[0..-16] + DateUtils.addHours(origDate,5).format(VCAL_TIME_FMT) + "Z";
calendarFileWriter.write("${vCalLine}\n")
calendarFileName = origDate.format("yyyy-MM-dd'T'HHmm");
} else if (vCalLine ==~ /^DTEND:.*/) {
def origDate = Date.parse(VCAL_TIME_FMT, vCalLine[-15..-1])
vCalLine = vCalLine[0..-16] + DateUtils.addHours(origDate,5).format(VCAL_TIME_FMT) + "Z";
calendarFileWriter.write("${vCalLine}\n")
} else if (vCalLine ==~ /^SUMMARY:.*/) {
calendarFileName = calendarFileName + " " + vCalLine[8..-1].replaceAll("[^\\w\\s]", "")
println "Processing " + calendarFileName
calendarFileWriter.write("${vCalLine}\n")
} else {
calendarFileWriter.write("${vCalLine}\n")
}
}
calendarFileWriter.close();
calendarFile.renameTo(new File("./cals/"+calendarFileName+".ics")) calList.add(calendarFileName+".ics")
calendarStream.close();
} println "Text to paste into Wordpress after uploading: "
println "<ul>"
Collections.sort(calList)
calList.each {
println "<li><a href="https://blog.codecentric.de/en/2009/08/english-international-calendar-file-processing-its-groovy-2/">${it}</a></li>"
}
println "</ul>" }}