implemented ical exports

Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
This commit is contained in:
2024-12-31 15:16:53 +01:00
parent f60bd90283
commit 82b4b47a37
12 changed files with 212 additions and 21 deletions
@@ -1,6 +1,7 @@
/* © SRSoftware 2024 */
package de.srsoftware.cal;
import static de.srsoftware.cal.Util.*;
import static de.srsoftware.tools.Optionals.nullable;
import de.srsoftware.cal.api.Appointment;
@@ -124,10 +125,24 @@ public class BaseAppointment implements Appointment {
}
@Override
public String ical() { // TODO: implement
return "converting event (%s) to ical not implemented".formatted(title);
public String ical(String calendarIdentifier) { // TODO: implement
var sb = new StringBuilder();
sb.append(contentLine(BEGIN,VEVENT));
if (calendarIdentifier != null) sb.append(contentLine(UID,"%s@%s".formatted(id(),calendarIdentifier)));
sb.append(contentLine(DTSTART,start().format(ICAL_DATE_FORMAT).replace(" ","T")));
end().map(end -> contentLine(DTEND,end.format(ICAL_DATE_FORMAT).replace(" ","T"))).ifPresent(sb::append);
sb.append(contentLine(SUMMARY,title()));
sb.append(contentLine(DESCRIPTION,description()));
coords().map(Coords::icalFormat).map(geo -> contentLine(GEO,geo)).ifPresent(sb::append);
if (!location().isBlank()) sb.append(contentLine(LOCATION,location()));
for (var attachment : attachments()) sb.append(contentLine("ATTACH;FMTYPE=%s".formatted(attachment.mime()),attachment.url().toString()));
for (var link : links) sb.append(contentLine("ATTACH;TITLE="+paramText(link.desciption()),link.url().toString()));
sb.append(contentLine("CLASS","PUBLIC"));
sb.append(contentLine(END,VEVENT));
return sb.toString();
}
@Override
public long id() {
return id;
@@ -6,8 +6,51 @@ import static de.srsoftware.tools.Error.error;
import de.srsoftware.cal.api.Coords;
import de.srsoftware.tools.Payload;
import de.srsoftware.tools.Result;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
public class Util {
public static final String BEGIN = "BEGIN";
public static final String END = "END";
public static final String DESCRIPTION = "DESCRIPTION";
public static final String DTEND = "DTEND";
public static final String DTSTAMP = "DTSTAMP";
public static final String DTSTART = "DTSTART";
public static final String GEO = "GEO";
public static final DateTimeFormatter ICAL_DATE_FORMAT = DateTimeFormatter.ofPattern("yyyyMMdd HHmmss");
public static final String LOCATION = "LOCATION";
public static final String PRODID = "PRODID";
public static final String SUMMARY = "SUMMARY";
public static final String UID = "UID";
public static final String VERSION = "VERSION";
public static final String VEVENT = "VEVENT";
public static final String VCALENDAR = "VCALENDAR";
private Util(){}
/**
* formats a content line as defined in <a href="https://datatracker.ietf.org/doc/html/rfc5545#section-3.1">iCalendar spec</a>
* @param key the content line key
* @param value the content line value
* @return content line formatted as described in the spec
*/
public static String contentLine(String key, String value){
var contentLine = "%s:%s".formatted(key,value).trim();
// escape line breaks
contentLine = contentLine.replace("\\n","\\\\n").replace("\r\n","\\n").replace("\r","\\n").replace("\n","\\n");
var lines = new ArrayList<String>();
while (contentLine.length()>70){
var pos = contentLine.lastIndexOf(" ",70);
if (pos < 10) pos = 70;
var dummy = contentLine.substring(0,pos);
lines.add(dummy);
contentLine = '\t'+contentLine.substring(pos);
}
lines.add(contentLine);
lines.add("");
return String.join("\r\n",lines);
}
public static Result<Coords> extractCoords(String coords) {
if (coords == null) return error("Argument is null");
if (coords.isBlank()) return error("Argument is blank");
@@ -21,4 +64,35 @@ public class Util {
return error(nfe, "Failed to parse coords from %s", coords);
}
}
public static String paramText(String param) {
return param
.replace("\n","\\n")
.replace("\"","''")
.replace(";","/")
.replace(",","/")
.replace(":","/");
}
/**
* wraps a text (list of vevents in a vcalendar, as described in th <a href="https://datatracker.ietf.org/doc/html/rfc5545#section-3.4">iCalendar spec</a>
* @param ical the vevents list
* @param prodId the producer id of this icalendar
* @return the completed ical string
*/
public static Result<String> wrapIcal(Result<String> ical, String prodId) {
if (ical instanceof Payload<String> payload){
var calendar = new StringBuilder();
calendar.append(contentLine(BEGIN,VCALENDAR));
calendar.append(contentLine(VERSION,"2.0"));
calendar.append(contentLine(PRODID,prodId));
calendar.append(payload.get());
calendar.append(contentLine(END,VCALENDAR));
return Payload.of(calendar.toString());
}
return ical;
}
}
@@ -0,0 +1,25 @@
/* © SRSoftware 2024 */
package de.srsoftware.cal;
import static org.junit.jupiter.api.Assertions.assertEquals;
import org.junit.jupiter.api.Test;
public class UtilTest {
@Test
public void testContentLine(){
var longText = """
This text block, spanning several long lines, should be distributed over several lines, when passing it to the contentLine function.
Let`s see what happens… New line breaks should be introduced at roughly every 70 characters,
while the existing line breaks should be converted to '\n' escapes.""";
var expected = """
Test:This text block, spanning several long lines, should be\r
\t distributed over several lines, when passing it to the contentLine\r
\t function.\\nLet`s see what happens… New line breaks should be\r
\t introduced at roughly every 70 characters,\\nwhile the existing line\r
\t breaks should be converted to '\\n' escapes.\r\n""";
var result = Util.contentLine("Test",longText);
assertEquals(expected,result);
}
}