|
|
|
@@ -1,7 +1,9 @@
|
|
|
|
|
/* © SRSoftware 2024 */
|
|
|
|
|
package de.srsoftware.cal;
|
|
|
|
|
|
|
|
|
|
import static de.srsoftware.tools.Error.error;
|
|
|
|
|
import static de.srsoftware.tools.Result.transform;
|
|
|
|
|
import static java.lang.System.Logger.Level.WARNING;
|
|
|
|
|
|
|
|
|
|
import de.srsoftware.cal.api.*;
|
|
|
|
|
import de.srsoftware.tools.*;
|
|
|
|
@@ -22,8 +24,9 @@ import java.util.Optional;
|
|
|
|
|
import java.util.stream.Stream;
|
|
|
|
|
|
|
|
|
|
public abstract class BaseImporter implements Importer {
|
|
|
|
|
private static final String SHA256 = "SHA-256";
|
|
|
|
|
private final MessageDigest digest;
|
|
|
|
|
private static final System.Logger LOG = System.getLogger(BaseImporter.class.getSimpleName());
|
|
|
|
|
private static final String SHA256 = "SHA-256";
|
|
|
|
|
private final MessageDigest digest;
|
|
|
|
|
|
|
|
|
|
protected BaseImporter() throws NoSuchAlgorithmException {
|
|
|
|
|
digest = MessageDigest.getInstance(SHA256);
|
|
|
|
@@ -61,13 +64,13 @@ public abstract class BaseImporter implements Importer {
|
|
|
|
|
if (titleTag.optional().isEmpty()) return transform(titleTag);
|
|
|
|
|
var inner = titleTag.optional().flatMap(tag -> tag.inner(2));
|
|
|
|
|
if (inner.isPresent()) return Payload.of(inner.get());
|
|
|
|
|
return Error.of("No description found");
|
|
|
|
|
return error("No description found");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected abstract Result<Tag> extractDescriptionTag(Tag eventTag);
|
|
|
|
|
|
|
|
|
|
protected Result<Coords> extractCoords(Tag eventTag) {
|
|
|
|
|
return Error.of("not implemented");
|
|
|
|
|
return error("not implemented");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -177,73 +180,75 @@ public abstract class BaseImporter implements Importer {
|
|
|
|
|
Result<Tag> titleTag = extractTitleTag(eventTag);
|
|
|
|
|
if (titleTag.optional().isEmpty()) return transform(titleTag);
|
|
|
|
|
var inner = titleTag.optional().flatMap(tag -> tag.inner(2));
|
|
|
|
|
if (inner.isPresent()) return Payload.of(inner.get());
|
|
|
|
|
return Error.of("No title found");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected abstract Result<Tag> extractTitleTag(Tag eventTag);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
public Stream<Appointment> fetch() {
|
|
|
|
|
var url = Payload.of(programURL());
|
|
|
|
|
Stream<Result<String>> stream = url(url)
|
|
|
|
|
.map(this::open) //
|
|
|
|
|
.map(this::preload)
|
|
|
|
|
.map(this::parseXML)
|
|
|
|
|
.map(this::extractEventUrls)
|
|
|
|
|
.stream();
|
|
|
|
|
return stream //
|
|
|
|
|
.map(BaseImporter::url)
|
|
|
|
|
.map(this::loadEvent)
|
|
|
|
|
.peek(e -> {
|
|
|
|
|
if (e instanceof Error<Appointment> err) System.err.println(err);
|
|
|
|
|
})
|
|
|
|
|
.flatMap(result -> result.optional().stream());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected static <T> Result<T> invalidParameter(Result<?> result) {
|
|
|
|
|
return Error.format("Invalid parameter: %s", result.getClass().getSimpleName());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected static Result<Link> link(Result<URL> url, String text) {
|
|
|
|
|
var opt = url.optional();
|
|
|
|
|
if (opt.isEmpty()) return transform(url);
|
|
|
|
|
return Payload.of(new Link(opt.get(),text));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected Result<Appointment> loadEvent(Result<URL> urlResult) {
|
|
|
|
|
var link = urlResult //
|
|
|
|
|
.optional().map(url -> new Link(url, "Event-Seite")).orElse(null);
|
|
|
|
|
return urlResult //
|
|
|
|
|
.map(this::open)
|
|
|
|
|
.map(this::preload)
|
|
|
|
|
.map(this::parseXML)
|
|
|
|
|
.map(this::extractEventTag)
|
|
|
|
|
.map(tagResult -> extractEvent(tagResult, link));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected Result<InputStream> open(Result<URL> url) {
|
|
|
|
|
switch (url) {
|
|
|
|
|
case Payload<URL> payload:
|
|
|
|
|
try {
|
|
|
|
|
return Payload.of(payload.get().openConnection().getInputStream());
|
|
|
|
|
} catch (IOException e) {
|
|
|
|
|
return Error.of("Failed to open %s".formatted(payload), e);
|
|
|
|
|
}
|
|
|
|
|
case Error<URL> error:
|
|
|
|
|
return error.transform();
|
|
|
|
|
default:
|
|
|
|
|
return invalidParameter(url);
|
|
|
|
|
return inner.isPresent() ? Payload.of(inner.get()) :
|
|
|
|
|
error("No title found");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected abstract Result<LocalDateTime> parseEndDate(String string);
|
|
|
|
|
protected abstract Result<Tag> extractTitleTag(Tag eventTag);
|
|
|
|
|
|
|
|
|
|
protected abstract Result<LocalDateTime> parseStartDate(String string);
|
|
|
|
|
|
|
|
|
|
protected Result<Tag> parseXML(Result<InputStream> inputStream) {
|
|
|
|
|
return switch (inputStream) {
|
|
|
|
|
@Override
|
|
|
|
|
public Stream<Appointment> fetch() {
|
|
|
|
|
var url = Payload.of(programURL());
|
|
|
|
|
Stream<Result<String>> stream = url(url)
|
|
|
|
|
.map(this::open) //
|
|
|
|
|
.map(this::preload)
|
|
|
|
|
.map(this::parseXML)
|
|
|
|
|
.map(this::extractEventUrls)
|
|
|
|
|
.stream();
|
|
|
|
|
return stream //
|
|
|
|
|
.map(BaseImporter::url)
|
|
|
|
|
.map(this::loadEvent)
|
|
|
|
|
.peek(e -> {
|
|
|
|
|
if (e instanceof Error<Appointment> err) System.err.println(err);
|
|
|
|
|
})
|
|
|
|
|
.flatMap(result -> result.optional().stream());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected static <T> Result<T> invalidParameter(Result<?> result) {
|
|
|
|
|
return error("Invalid parameter: %s", result.getClass().getSimpleName());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected static Result<Link> link(Result<URL> url, String text) {
|
|
|
|
|
var opt = url.optional();
|
|
|
|
|
if (opt.isEmpty()) return transform(url);
|
|
|
|
|
return Payload.of(new Link(opt.get(), text));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected Result<Appointment> loadEvent(Result<URL> urlResult) {
|
|
|
|
|
var link = urlResult //
|
|
|
|
|
.optional()
|
|
|
|
|
.map(url -> new Link(url, "Event-Seite"))
|
|
|
|
|
.orElse(null);
|
|
|
|
|
return urlResult //
|
|
|
|
|
.map(this::open)
|
|
|
|
|
.map(this::preload)
|
|
|
|
|
.map(this::parseXML)
|
|
|
|
|
.map(this::extractEventTag)
|
|
|
|
|
.map(tagResult -> extractEvent(tagResult, link));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected Result<InputStream> open(Result<URL> url) {
|
|
|
|
|
switch (url) {
|
|
|
|
|
case Payload<URL> payload:
|
|
|
|
|
try {
|
|
|
|
|
return Payload.of(payload.get().openConnection().getInputStream());
|
|
|
|
|
} catch (IOException e) {
|
|
|
|
|
return error(e, "Failed to open %s", payload, e);
|
|
|
|
|
}
|
|
|
|
|
case Error<URL> error:
|
|
|
|
|
return error.transform();
|
|
|
|
|
default:
|
|
|
|
|
return invalidParameter(url);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected abstract Result<LocalDateTime> parseEndDate(String string);
|
|
|
|
|
|
|
|
|
|
protected abstract Result<LocalDateTime> parseStartDate(String string);
|
|
|
|
|
|
|
|
|
|
protected Result<Tag> parseXML(Result<InputStream> inputStream) {
|
|
|
|
|
return switch (inputStream) {
|
|
|
|
|
case Payload<InputStream> payload -> XMLParser.parse(payload.get());
|
|
|
|
|
case Error<InputStream> error -> error.transform();
|
|
|
|
|
default -> invalidParameter(inputStream);
|
|
|
|
@@ -256,55 +261,55 @@ public abstract class BaseImporter implements Importer {
|
|
|
|
|
try {
|
|
|
|
|
return Payload.of(XMLParser.preload(payload.get()));
|
|
|
|
|
} catch (IOException e) {
|
|
|
|
|
return Error.of("Failed to buffer data from %s".formatted(payload), e);
|
|
|
|
|
return error(e, "Failed to buffer data from %s", payload);
|
|
|
|
|
}
|
|
|
|
|
case Error<InputStream> error:
|
|
|
|
|
return error.transform();
|
|
|
|
|
default:
|
|
|
|
|
return invalidParameter(inputStream);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected abstract String programURL();
|
|
|
|
|
|
|
|
|
|
protected static Result<Attachment> toAttachment(Result<URL> urlResult) {
|
|
|
|
|
var opt = urlResult.optional();
|
|
|
|
|
if (opt.isEmpty()) return transform(urlResult);
|
|
|
|
|
try {
|
|
|
|
|
var mime = opt.get().openConnection().getContentType();
|
|
|
|
|
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());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
protected static Result<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);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
protected static Result<URL> url(Result<String> urlResult) {
|
|
|
|
|
if (urlResult.optional().isEmpty()) return transform(urlResult);
|
|
|
|
|
var url = urlResult.optional().get();
|
|
|
|
|
try {
|
|
|
|
|
return Payload.of(new URI(url).toURL());
|
|
|
|
|
} catch (MalformedURLException | URISyntaxException e) {
|
|
|
|
|
return error(e, "Failed to create URL of %s", url);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected abstract String programURL();
|
|
|
|
|
|
|
|
|
|
protected static Result<Attachment> toAttachment(Result<URL> urlResult) {
|
|
|
|
|
var opt = urlResult.optional();
|
|
|
|
|
if (opt.isEmpty()) return transform(urlResult);
|
|
|
|
|
try {
|
|
|
|
|
var mime = opt.get().openConnection().getContentType();
|
|
|
|
|
return Payload.of(new Attachment(opt.get(), mime));
|
|
|
|
|
} catch (Exception e) {
|
|
|
|
|
e.printStackTrace();
|
|
|
|
|
return Error.format("Failed to read mime type of %s", opt.get());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
protected static Result<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.format("Failed to recognize \"%s\" as a month!", month);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
protected static Result<URL> url(Result<String> urlResult) {
|
|
|
|
|
if (urlResult.optional().isEmpty()) return transform(urlResult);
|
|
|
|
|
var url = urlResult.optional().get();
|
|
|
|
|
try {
|
|
|
|
|
return Payload.of(new URI(url).toURL());
|
|
|
|
|
} catch (MalformedURLException | URISyntaxException e) {
|
|
|
|
|
return Error.of("Failed to create URL of %s".formatted(url), e);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|