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:
2024-12-22 20:32:38 +01:00
parent edfe423621
commit e4b8bcb99a
12 changed files with 214 additions and 29 deletions

View File

@@ -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();
}
}

View File

@@ -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);
}
}
}

View File

@@ -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;

View File

@@ -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;