first implementation of cal.db, that
- successfully connects to db - updated db sheme - succeeds in listing appointments Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
This commit is contained in:
@@ -1,175 +0,0 @@
|
||||
/* © SRSoftware 2024 */
|
||||
package de.srsoftware.cal.importer;
|
||||
|
||||
import static de.srsoftware.tools.Optionals.nullable;
|
||||
|
||||
import de.srsoftware.cal.api.Appointment;
|
||||
import de.srsoftware.cal.api.Attachment;
|
||||
import de.srsoftware.cal.api.Coords;
|
||||
import de.srsoftware.cal.api.Link;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* basic class for Appointments
|
||||
*/
|
||||
public class BaseAppointment implements Appointment {
|
||||
private final long id;
|
||||
private final String title, description;
|
||||
private final LocalDateTime end, start;
|
||||
private final String hash;
|
||||
private Coords coords = null;
|
||||
private final Set<Attachment> attachments = new HashSet<>();
|
||||
private final Set<String> tags = new HashSet<>();
|
||||
private final Set<Link> links = new HashSet<>();
|
||||
private final String location;
|
||||
|
||||
/**
|
||||
* create a new appointment
|
||||
* @param id set the id
|
||||
* @param title set the title
|
||||
* @param description set the description
|
||||
* @param start set the start date
|
||||
* @param end set the end date
|
||||
* @param location set the location
|
||||
*/
|
||||
public BaseAppointment(long id, String title, String description, LocalDateTime start, LocalDateTime end, String location, String hash) {
|
||||
this.description = description;
|
||||
this.end = end;
|
||||
this.hash = hash;
|
||||
this.id = id;
|
||||
this.location = location;
|
||||
this.start = start;
|
||||
this.title = title;
|
||||
}
|
||||
|
||||
/**
|
||||
* adds attachments
|
||||
* @param newAttachments the attachments to add to the appointment
|
||||
* @return the appointment
|
||||
*/
|
||||
public BaseAppointment add(Attachment... newAttachments) {
|
||||
Collections.addAll(attachments, newAttachments);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* adds attachments
|
||||
* @param newAttachments the attachments to add to the appointment
|
||||
* @return the appointment
|
||||
*/
|
||||
public BaseAppointment add(Collection<Attachment> newAttachments) {
|
||||
attachments.addAll(newAttachments);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* adds links
|
||||
* @param newLinks the links to add to the appointment
|
||||
* @return the appointment
|
||||
*/
|
||||
public BaseAppointment addLinks(Link... newLinks) {
|
||||
Collections.addAll(links, newLinks);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* adds links
|
||||
* @param newLinks the links to add to the appointment
|
||||
* @return the appointment
|
||||
*/
|
||||
public BaseAppointment addLinks(Collection<Link> newLinks) {
|
||||
links.addAll(newLinks);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* adds tag
|
||||
* @param newTags the tag to add to the appointment
|
||||
* @return the appointment
|
||||
*/
|
||||
public BaseAppointment tags(String... newTags) {
|
||||
Collections.addAll(tags, newTags);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* adds tag
|
||||
* @param newTags the tag to add to the appointment
|
||||
* @return the appointment
|
||||
*/
|
||||
public BaseAppointment tags(Collection<String> newTags) {
|
||||
tags.addAll(newTags);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* set the coordinates of the attachments
|
||||
* @param newCoords the coordinates to apply
|
||||
* @return the appointment
|
||||
*/
|
||||
public BaseAppointment coords(Coords newCoords) {
|
||||
coords = newCoords;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<Attachment> attachments() {
|
||||
return Set.of();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<Coords> coords() {
|
||||
return nullable(coords);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String description() {
|
||||
return description;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<LocalDateTime> end() {
|
||||
return nullable(end);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String hash() {
|
||||
return hash;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long id() {
|
||||
return id;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String location() {
|
||||
return location;
|
||||
}
|
||||
|
||||
@Override
|
||||
public LocalDateTime start() {
|
||||
return start;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<String> tags() {
|
||||
return Set.of();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String title() {
|
||||
return title;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "%s (%s)".formatted(title, start);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<Link> urls() {
|
||||
return Set.of();
|
||||
}
|
||||
}
|
||||
@@ -1,330 +0,0 @@
|
||||
/* © SRSoftware 2024 */
|
||||
package de.srsoftware.cal.importer;
|
||||
|
||||
import static de.srsoftware.tools.Strings.hex;
|
||||
import static de.srsoftware.tools.TagFilter.ofType;
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
|
||||
import de.srsoftware.cal.api.*;
|
||||
import de.srsoftware.tools.*;
|
||||
import de.srsoftware.tools.Error;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.net.URL;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
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;
|
||||
|
||||
protected BaseImporter() throws NoSuchAlgorithmException {
|
||||
digest = MessageDigest.getInstance(SHA256);
|
||||
}
|
||||
|
||||
protected abstract String baseUrl();
|
||||
|
||||
@Override
|
||||
public String description() {
|
||||
return "abstract base class to create other importers on";
|
||||
}
|
||||
|
||||
protected List<Attachment> extractAttachments(Tag eventTag) {
|
||||
return extractAttachmentsTag(eventTag) //
|
||||
.optional()
|
||||
.stream()
|
||||
.flatMap(tag -> tag.find(ofType("img")).stream())
|
||||
.map(tag -> tag.get("src"))
|
||||
.filter(Objects::nonNull)
|
||||
.map(Payload::of)
|
||||
.map(this::url)
|
||||
.map(this::toAttachment)
|
||||
.map(Result::optional)
|
||||
.flatMap(Optional::stream)
|
||||
.toList();
|
||||
}
|
||||
|
||||
protected Result<Tag> extractAttachmentsTag(Tag eventTag) {
|
||||
return extractDescriptionTag(eventTag);
|
||||
}
|
||||
|
||||
|
||||
protected Result<String> extractDescription(Tag eventTag) {
|
||||
Result<Tag> titleTag = extractDescriptionTag(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 description found");
|
||||
}
|
||||
|
||||
protected abstract Result<Tag> extractDescriptionTag(Tag eventTag);
|
||||
|
||||
protected Result<Coords> extractCoords(Tag eventTag) {
|
||||
return Error.of("not implemented");
|
||||
}
|
||||
|
||||
protected Result<LocalDateTime> extractEnd(Tag eventTag) {
|
||||
Result<Tag> endTag = extractEndTag(eventTag);
|
||||
if (endTag.optional().isEmpty()) return transform(endTag);
|
||||
return parseEndDate(endTag.optional().get().toString(0));
|
||||
}
|
||||
|
||||
protected abstract Result<Tag> extractEndTag(Tag eventTag);
|
||||
|
||||
protected Result<Appointment> extractEvent(Tag eventTag, Link eventPage) {
|
||||
long id = 0;
|
||||
|
||||
var titleResult = extractTitle(eventTag);
|
||||
if (titleResult.optional().isEmpty()) return transform(titleResult);
|
||||
String title = titleResult.optional().get();
|
||||
|
||||
var descriptionResult = extractDescription(eventTag);
|
||||
if (descriptionResult.optional().isEmpty()) return transform(descriptionResult);
|
||||
var description = descriptionResult.optional().get();
|
||||
|
||||
var startResult = extractStart(eventTag);
|
||||
if (startResult.optional().isEmpty()) return transform(startResult);
|
||||
var start = startResult.optional().get();
|
||||
|
||||
var endResult = extractEnd(eventTag);
|
||||
var end = endResult.optional().orElse(null);
|
||||
|
||||
var locationResult = extractLocation(eventTag);
|
||||
if (locationResult.optional().isEmpty()) return transform(locationResult);
|
||||
var location = locationResult.optional().get();
|
||||
|
||||
var hash = hash("%s@%s".formatted(start, location));
|
||||
|
||||
var event = new BaseAppointment(id, title, description, start, end, location, hash) //
|
||||
.add(extractAttachments(eventTag))
|
||||
.addLinks(extractLinks(eventTag))
|
||||
.tags(extractTags(eventTag));
|
||||
|
||||
extractCoords(eventTag).optional().ifPresent(event::coords);
|
||||
|
||||
return Payload.of(event);
|
||||
}
|
||||
|
||||
|
||||
private Result<Appointment> extractEvent(Result<Tag> domResult, Link eventPage) {
|
||||
return switch (domResult) {
|
||||
case Payload<Tag> payload -> extractEvent(payload.get(), eventPage);
|
||||
case Error<Tag> err -> err.transform();
|
||||
default -> invalidParameter(domResult);
|
||||
};
|
||||
}
|
||||
|
||||
protected abstract Result<Tag> extractEventTag(Result<Tag> pageResult);
|
||||
|
||||
protected abstract Result<List<String>> extractEventUrls(Result<Tag> programPage);
|
||||
|
||||
|
||||
protected List<Link> extractLinks(Tag appointmentTag) {
|
||||
var links = new ArrayList<Link>();
|
||||
|
||||
extractLinksTag(appointmentTag) //
|
||||
.map(this::extractLinkAnchors)
|
||||
.optional()
|
||||
.stream()
|
||||
.flatMap(List::stream).forEach(anchor -> {
|
||||
var href = anchor.get("href");
|
||||
if (href == null) return;
|
||||
if (!href.contains("://")) href = baseUrl() + href;
|
||||
var text = anchor.inner(0).orElse(href);
|
||||
Payload //
|
||||
.of(href)
|
||||
.map(this::url)
|
||||
.optional()
|
||||
.map(url -> new Link(url, text))
|
||||
.ifPresent(links::add);
|
||||
});
|
||||
return links;
|
||||
}
|
||||
|
||||
public abstract Result<List<Tag>> extractLinkAnchors(Result<Tag> tagResult);
|
||||
|
||||
protected Result<Tag> extractLinksTag(Tag eventTag) {
|
||||
return extractDescriptionTag(eventTag);
|
||||
}
|
||||
|
||||
protected Result<String> extractLocation(Tag eventTag) {
|
||||
Result<Tag> locationTag = extractLocationTag(eventTag);
|
||||
if (locationTag.optional().isEmpty()) return transform(locationTag);
|
||||
return Payload.of(locationTag.optional().get().toString(2));
|
||||
}
|
||||
|
||||
protected abstract Result<Tag> extractLocationTag(Tag eventTag);
|
||||
|
||||
|
||||
protected Result<LocalDateTime> extractStart(Tag eventTag) {
|
||||
Result<Tag> startTag = extractStartTag(eventTag);
|
||||
if (startTag.optional().isEmpty()) return transform(startTag);
|
||||
return parseStartDate(startTag.optional().get().strip());
|
||||
}
|
||||
|
||||
protected abstract Result<Tag> extractStartTag(Tag eventTag);
|
||||
|
||||
|
||||
protected abstract List<String> extractTags(Tag eventTag);
|
||||
|
||||
protected Result<String> extractTitle(Tag eventTag) {
|
||||
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(this::url)
|
||||
.map(this::loadEvent)
|
||||
.peek(e -> {
|
||||
if (e instanceof Error<Appointment> err) System.err.println(err);
|
||||
})
|
||||
.flatMap(result -> result.optional().stream());
|
||||
}
|
||||
|
||||
/**
|
||||
* create a hash from a text
|
||||
* @param plain the plain text
|
||||
* @return the hash of the plain text
|
||||
*/
|
||||
protected String hash(String plain){
|
||||
return hex(digest.digest(plain.getBytes(UTF_8)));
|
||||
}
|
||||
|
||||
protected static <T> Result<T> invalidParameter(Result<?> result) {
|
||||
return Error.format("Invalid parameter: %s", result.getClass().getSimpleName());
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
};
|
||||
}
|
||||
|
||||
protected Result<InputStream> preload(Result<InputStream> inputStream) {
|
||||
switch (inputStream) {
|
||||
case Payload<InputStream> payload:
|
||||
try {
|
||||
return Payload.of(XMLParser.preload(payload.get()));
|
||||
} catch (IOException e) {
|
||||
return Error.of("Failed to buffer data from %s".formatted(payload), e);
|
||||
}
|
||||
case Error<InputStream> error:
|
||||
return error.transform();
|
||||
default:
|
||||
return invalidParameter(inputStream);
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract String programURL();
|
||||
|
||||
protected Result<Attachment> toAttachment(Result<URL> urlResult) {
|
||||
switch (urlResult) {
|
||||
case Payload<URL> payload:
|
||||
try {
|
||||
var mime = payload.get().openConnection().getContentType();
|
||||
return Payload.of(new Attachment(payload.get(), mime));
|
||||
} catch (Exception e) {
|
||||
return Error.format("Failed to read mime type of %s", payload);
|
||||
}
|
||||
case Error<URL> err:
|
||||
return err.transform();
|
||||
default:
|
||||
return invalidParameter(urlResult);
|
||||
}
|
||||
}
|
||||
|
||||
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 <T> Result<T> transform(Result<?> result) {
|
||||
if (result instanceof Error<?> err) return err.transform();
|
||||
return invalidParameter(result);
|
||||
}
|
||||
|
||||
protected Result<URL> url(Result<String> urlResult) {
|
||||
switch (urlResult) {
|
||||
case Payload<String> payload:
|
||||
var url = payload.get();
|
||||
try {
|
||||
return Payload.of(new URI(url).toURL());
|
||||
} catch (MalformedURLException | URISyntaxException e) {
|
||||
return de.srsoftware.tools.Error.of("Failed to create URL of %s".formatted(url), e);
|
||||
}
|
||||
case de.srsoftware.tools.Error<String> err:
|
||||
return err.transform();
|
||||
default:
|
||||
return invalidParameter(urlResult);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@ package de.srsoftware.cal.importer.jena;
|
||||
|
||||
import static de.srsoftware.tools.TagFilter.*;
|
||||
|
||||
import de.srsoftware.cal.importer.BaseImporter;
|
||||
import de.srsoftware.cal.BaseImporter;
|
||||
import de.srsoftware.tools.*;
|
||||
import de.srsoftware.tools.Error;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
|
||||
@@ -4,7 +4,7 @@ package de.srsoftware.cal.importer.jena;
|
||||
import static de.srsoftware.tools.Optionals.nullable;
|
||||
import static de.srsoftware.tools.TagFilter.*;
|
||||
|
||||
import de.srsoftware.cal.importer.BaseImporter;
|
||||
import de.srsoftware.cal.BaseImporter;
|
||||
import de.srsoftware.tools.Error;
|
||||
import de.srsoftware.tools.Payload;
|
||||
import de.srsoftware.tools.Result;
|
||||
|
||||
Reference in New Issue
Block a user