275 lines
10 KiB
Java
275 lines
10 KiB
Java
/* © SRSoftware 2026 */
|
|
package de.srsoftware.cal;
|
|
|
|
import static de.srsoftware.tools.Tag.STYLE;
|
|
import static de.srsoftware.tools.container.Container.transform;
|
|
import static de.srsoftware.tools.container.Error.error;
|
|
|
|
import static java.lang.System.Logger.Level.DEBUG;
|
|
import static java.lang.System.Logger.Level.WARNING;
|
|
|
|
import de.srsoftware.cal.api.Attachment;
|
|
import de.srsoftware.cal.api.Coords;
|
|
import de.srsoftware.tools.*;
|
|
import de.srsoftware.tools.container.Container;
|
|
import de.srsoftware.tools.container.Payload;
|
|
|
|
import java.io.IOException;
|
|
import java.io.InputStream;
|
|
import java.net.HttpURLConnection;
|
|
import java.net.URI;
|
|
import java.net.URL;
|
|
import java.nio.file.Files;
|
|
import java.nio.file.Path;
|
|
import java.time.LocalDate;
|
|
import java.time.LocalDateTime;
|
|
import java.time.LocalTime;
|
|
import java.time.format.DateTimeFormatter;
|
|
import java.util.ArrayList;
|
|
import java.util.regex.Pattern;
|
|
|
|
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 URL = "URL";
|
|
public static final String VERSION = "VERSION";
|
|
public static final String VEVENT = "VEVENT";
|
|
public static final String VCALENDAR = "VCALENDAR";
|
|
|
|
public static final Pattern GERMAN_DATE_PATTERN = Pattern.compile("^\\D*(\\d\\d?)\\.(\\d\\d?)\\.(\\d{4})\\D");
|
|
public static final Pattern GERMAN_DATE_WITHOUT_YEAR = Pattern.compile("(\\d\\d?)\\.(\\d\\d?)");
|
|
public static final Pattern GERMAN_DATE_PATTERN_LONG = Pattern.compile("(\\d\\d?)\\.?\\s*(\\w+)\\s+(\\d{4})\\D");
|
|
public static final Pattern GERMAN_TIME_PATTERN = Pattern.compile("(\\d\\d?):(\\d\\d?)(:(\\d\\d?))?\\D");
|
|
private static final Pattern BG_IMAGE_URL = Pattern.compile("background(-image)?:\\surl\\('?([^)]+)'?\\)");
|
|
private static final System.Logger LOG = System.getLogger(Util.class.getSimpleName());
|
|
|
|
private Util(){}
|
|
|
|
|
|
public static Container<LocalDateTime> combine(Container<LocalDate> date, Container<LocalTime> time) {
|
|
if (date.optional().isEmpty())return transform(date);
|
|
if (time.optional().isEmpty())return transform(time);
|
|
return Payload.of(LocalDateTime.of(date.optional().get(),time.optional().get()));
|
|
}
|
|
|
|
/**
|
|
* 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 void dump(Tag tag){
|
|
try {
|
|
Files.writeString(Path.of("/tmp/dump.txt"),tag.toString(4));
|
|
} catch (IOException e) {
|
|
throw new RuntimeException(e);
|
|
}
|
|
}
|
|
|
|
public static Container<Coords> extractCoords(String coords) {
|
|
if (coords == null) return error("Argument is null");
|
|
if (coords.isBlank()) return error("Argument is blank");
|
|
var parts = coords.split(",");
|
|
if (parts.length != 2) return error("Argument has invalid format: %s", coords);
|
|
try {
|
|
var lat = Double.parseDouble(parts[0].trim());
|
|
var lon = Double.parseDouble(parts[1].trim());
|
|
return Payload.of(new Coords(lat, lon));
|
|
} catch (NumberFormatException nfe) {
|
|
return error(nfe, "Failed to parse coords from %s", coords);
|
|
}
|
|
}
|
|
|
|
public static Container<InputStream> open(Container<URL> url) {
|
|
var opt = url.optional();
|
|
if (opt.isEmpty()) return transform(url);
|
|
try {
|
|
var conn = (HttpURLConnection) opt.get().openConnection();
|
|
conn.setRequestProperty("Accept","*/*");
|
|
conn.setRequestProperty("Host",opt.get().getHost());
|
|
conn.setRequestProperty("User-Agent","OpenCloudCal/0.1");
|
|
return Payload.of(conn.getInputStream());
|
|
} catch (IOException e) {
|
|
return error(e, "Failed to open %s", url, e);
|
|
}
|
|
}
|
|
|
|
public static String paramText(String param) {
|
|
return param
|
|
.replace("\n","\\n")
|
|
.replace("\"","''")
|
|
.replace(";","/")
|
|
.replace(",","/")
|
|
.replace(":","/");
|
|
}
|
|
|
|
public static Container<LocalDate> parseGermanDate(String s){
|
|
var match = GERMAN_DATE_PATTERN.matcher(" "+s.trim()+" ");
|
|
if (match.find()){
|
|
var day = Integer.parseInt(match.group(1));
|
|
var month = Integer.parseInt(match.group(2));
|
|
var year = Integer.parseInt(match.group(3));
|
|
return Payload.of(LocalDate.of(year,month,day));
|
|
}
|
|
return error("Failed to find date");
|
|
}
|
|
|
|
public static Container<LocalDate> parseLongGermanDate(String s){
|
|
var match = GERMAN_DATE_PATTERN_LONG.matcher(" "+s+" ");
|
|
if (match.find()){
|
|
var day = Integer.parseInt(match.group(1));
|
|
var res = toNumericMonth(match.group(2));
|
|
var month = res.optional();
|
|
if (month.isEmpty()) return transform(res);
|
|
var year = Integer.parseInt(match.group(3));
|
|
return Payload.of(LocalDate.of(year,month.get(),day));
|
|
}
|
|
return error("Failed to find date");
|
|
}
|
|
|
|
public static Container<LocalTime> parseGermanTime(String s){
|
|
var match = GERMAN_TIME_PATTERN.matcher(" "+s.trim()+" ");
|
|
if (match.find()){
|
|
var hour = Integer.parseInt(match.group(1));
|
|
var minute = Integer.parseInt(match.group(2));
|
|
var sec = match.group(4);
|
|
var second = sec == null ? 0 : Integer.parseInt(sec);
|
|
return Payload.of(LocalTime.of(hour,minute,second));
|
|
}
|
|
return error("Failed to find time");
|
|
}
|
|
|
|
public static Container<Tag> parseXML(Container<InputStream> inputStream) {
|
|
var opt = inputStream.optional();
|
|
return opt.isEmpty() ? transform((inputStream)) : XMLParser.parse(opt.get());
|
|
}
|
|
|
|
public static Container<InputStream> preload(Container<InputStream> inputStream) {
|
|
var opt = inputStream.optional();
|
|
if (opt.isEmpty()) return transform(inputStream);
|
|
try {
|
|
return Payload.of(XMLParser.preload(opt.get()));
|
|
} catch (IOException e) {
|
|
return error(e, "Failed to buffer data from %s", inputStream);
|
|
}
|
|
}
|
|
|
|
public static Container<Integer> toNumericMonth(String month) {
|
|
month = month.toLowerCase();
|
|
if (month.startsWith("ja")) return Payload.of(1);
|
|
if (month.startsWith("f")) return Payload.of(2);
|
|
if ("may".equals(month) || "mai".equals(month)) return Payload.of(5);
|
|
if (month.startsWith("m")) return Payload.of(3);
|
|
if (month.startsWith("ap")) return Payload.of(4);
|
|
if (month.startsWith("jun")) return Payload.of(6);
|
|
if (month.startsWith("jul")) return Payload.of(7);
|
|
if (month.startsWith("au")) return Payload.of(8);
|
|
if (month.startsWith("s")) return Payload.of(9);
|
|
if (month.startsWith("o")) return Payload.of(10);
|
|
if (month.startsWith("n")) return Payload.of(11);
|
|
if (month.startsWith("d")) return Payload.of(12);
|
|
return error("Failed to recognize \"%s\" as a month!", month);
|
|
}
|
|
|
|
/**
|
|
* 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 Container<String> wrapIcal(Container<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;
|
|
}
|
|
|
|
public static Container<String> extractBackgroundImage(Tag tag, String baseUrl) {
|
|
var style = tag.get(STYLE);
|
|
if (style != null){
|
|
var matcher = BG_IMAGE_URL.matcher(style);
|
|
if (matcher.find()) {
|
|
var link = matcher.group(2);
|
|
return Payload.of(link.contains("://") ? link : baseUrl+link);
|
|
}
|
|
}
|
|
return error("Failed to findbackground image url in %s",tag);
|
|
}
|
|
|
|
public static Container<Attachment> toAttachment(Container<URL> urlResult) {
|
|
var opt = urlResult.optional();
|
|
if (opt.isEmpty()) return transform(urlResult);
|
|
try {
|
|
var mime = MimeType.guessMime(opt.get());
|
|
return Payload.of(new Attachment(opt.get(), mime));
|
|
} catch (Exception e) {
|
|
LOG.log(WARNING, "Failed to read mime type of {0}", opt.get());
|
|
return error("Failed to read mime type of %s", opt.get());
|
|
}
|
|
}
|
|
|
|
public static Container<Integer> parseInt(String s){
|
|
try {
|
|
return Payload.of(Integer.parseInt(s));
|
|
} catch (NumberFormatException e){
|
|
return error(e,"Failed to parse %s as integer!",s);
|
|
}
|
|
}
|
|
|
|
public static Container<URL> url(Container<String> urlResult) {
|
|
if (urlResult.optional().isEmpty()) return transform(urlResult);
|
|
var url = urlResult.optional().get();
|
|
try {
|
|
return Payload.of(new URI(url).toURL());
|
|
} catch (Exception e) {
|
|
return error(e, "Failed to create URL of %s", url);
|
|
}
|
|
}
|
|
|
|
public static Container<LocalDate> parseGermanDateWithoutYear(String string) {
|
|
var matcher = GERMAN_DATE_WITHOUT_YEAR.matcher(string);
|
|
if (matcher.find()){
|
|
int day = Integer.parseInt(matcher.group(1));
|
|
int mon = Integer.parseInt(matcher.group(2));
|
|
var now = LocalDate.now();
|
|
var date = LocalDate.of(now.getYear(),mon,day);
|
|
if (date.isBefore(now)) date = date.withYear(now.getYear()+1);
|
|
return Payload.of(date);
|
|
}
|
|
return error("Failed to parse date from %s",string);
|
|
}
|
|
}
|