diff --git a/de.srsoftware.cal.api/build.gradle.kts b/de.srsoftware.cal.api/build.gradle.kts index cd74b64..871543f 100644 --- a/de.srsoftware.cal.api/build.gradle.kts +++ b/de.srsoftware.cal.api/build.gradle.kts @@ -1,4 +1,5 @@ description = "OpenCloudCal : API" dependencies { + implementation("de.srsoftware:tools.util:1.1.1") } diff --git a/de.srsoftware.cal.api/src/main/java/de/srsoftware/cal/api/Appointment.java b/de.srsoftware.cal.api/src/main/java/de/srsoftware/cal/api/Appointment.java index 0517ae8..00add6c 100644 --- a/de.srsoftware.cal.api/src/main/java/de/srsoftware/cal/api/Appointment.java +++ b/de.srsoftware.cal.api/src/main/java/de/srsoftware/cal/api/Appointment.java @@ -60,6 +60,13 @@ public interface Appointment { */ Set tags(); + + /** + * the title of the appointment + * @return the title + */ + String title(); + /** * set of Links that point to related information * @return set of links diff --git a/de.srsoftware.cal.api/src/main/java/de/srsoftware/cal/api/Importer.java b/de.srsoftware.cal.api/src/main/java/de/srsoftware/cal/api/Importer.java index ab98ae3..463eda4 100644 --- a/de.srsoftware.cal.api/src/main/java/de/srsoftware/cal/api/Importer.java +++ b/de.srsoftware.cal.api/src/main/java/de/srsoftware/cal/api/Importer.java @@ -1,8 +1,9 @@ /* © SRSoftware 2024 */ package de.srsoftware.cal.api; +import de.srsoftware.tools.Result; import java.io.IOException; -import java.util.List; +import java.util.stream.Stream; /** * Instances of this class can be used to read Appointments from various sources @@ -18,5 +19,5 @@ public interface Importer { * get the list of appointments from the source associated with this importer * @return a list of appointments */ - List fetch() throws IOException; + Stream> fetch() throws IOException; } diff --git a/de.srsoftware.cal.app/build.gradle.kts b/de.srsoftware.cal.app/build.gradle.kts index 01c6283..935fcef 100644 --- a/de.srsoftware.cal.app/build.gradle.kts +++ b/de.srsoftware.cal.app/build.gradle.kts @@ -3,4 +3,5 @@ description = "OpenCloudCal : Application" dependencies { implementation(project(":de.srsoftware.cal.api")) implementation(project(":de.srsoftware.cal.importer")) + implementation("de.srsoftware:tools.util:1.1.1") } diff --git a/de.srsoftware.cal.app/src/main/java/de/srsoftware/cal/app/Application.java b/de.srsoftware.cal.app/src/main/java/de/srsoftware/cal/app/Application.java index 9729a43..2848d4f 100644 --- a/de.srsoftware.cal.app/src/main/java/de/srsoftware/cal/app/Application.java +++ b/de.srsoftware.cal.app/src/main/java/de/srsoftware/cal/app/Application.java @@ -1,12 +1,23 @@ /* © SRSoftware 2024 */ package de.srsoftware.cal.app; +import de.srsoftware.cal.api.Appointment; import de.srsoftware.cal.importer.JenaRosenkeller; +import de.srsoftware.tools.Payload; import java.io.IOException; public class Application { public static void main(String[] args) throws IOException { - var rosenkeller = new JenaRosenkeller(); - rosenkeller.fetch(); + var rosenkeller = new JenaRosenkeller(); + var appointments = rosenkeller.fetch(); + appointments.forEach(res -> { + System.out.printf("class: %s%n", res.getClass()); + if (res instanceof Payload payload) { + System.out.printf("payload: %s%n", payload.get().getClass()); + System.out.println(payload.get()); + } else { + System.err.println(res); + } + }); } } diff --git a/de.srsoftware.cal.importer/build.gradle.kts b/de.srsoftware.cal.importer/build.gradle.kts index 1456989..91dda1d 100644 --- a/de.srsoftware.cal.importer/build.gradle.kts +++ b/de.srsoftware.cal.importer/build.gradle.kts @@ -3,6 +3,6 @@ description = "OpenCloudCal : Importers" dependencies { implementation(project(":de.srsoftware.cal.api")) implementation("de.srsoftware:tools.optionals:1.0.0") - implementation("de.srsoftware:tools.util:1.1.0") - implementation("de.srsoftware:tools.web:1.2.1") + implementation("de.srsoftware:tools.util:1.1.1") + implementation("de.srsoftware:tools.web:1.2.2") } diff --git a/de.srsoftware.cal.importer/src/main/java/de/srsoftware/cal/importer/JenaRosenkeller.java b/de.srsoftware.cal.importer/src/main/java/de/srsoftware/cal/importer/JenaRosenkeller.java index 5304393..c310974 100644 --- a/de.srsoftware.cal.importer/src/main/java/de/srsoftware/cal/importer/JenaRosenkeller.java +++ b/de.srsoftware.cal.importer/src/main/java/de/srsoftware/cal/importer/JenaRosenkeller.java @@ -1,59 +1,192 @@ /* © SRSoftware 2024 */ package de.srsoftware.cal.importer; -import static de.srsoftware.tools.Optionals.nullable; import static java.util.Optional.empty; import static java.util.function.Predicate.not; -import de.srsoftware.cal.api.Appointment; -import de.srsoftware.cal.api.Importer; +import de.srsoftware.cal.api.*; +import de.srsoftware.tools.Error; import de.srsoftware.tools.Payload; +import de.srsoftware.tools.Result; import de.srsoftware.tools.Tag; import de.srsoftware.tools.XMLParser; 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.time.LocalDateTime; import java.util.List; import java.util.Optional; +import java.util.Set; import java.util.stream.Stream; public class JenaRosenkeller implements Importer { + private static class EmptyAppointment implements Appointment { + @Override + public Set attachments() { + return Set.of(); + } + + @Override + public Optional coords() { + return empty(); + } + + @Override + public String description() { + return null; + } + + @Override + public Optional end() { + return empty(); + } + + @Override + public long id() { + return 0; + } + + @Override + public String location() { + return null; + } + + @Override + public LocalDateTime start() { + return null; + } + + @Override + public Set tags() { + return Set.of(); + } + + @Override + public String title() { + return null; + } + + @Override + public String toString() { + return "%s (%s)".formatted(title(), EmptyAppointment.class.getSimpleName()); + } + + @Override + public Set urls() { + return Set.of(); + } + } + @Override public String description() { return "Events von der Seite rosenkeller.org importieren"; } @Override - public List fetch() throws IOException { - var url = "https://rosenkeller.org/de/programm"; - var listUrl = url(url); - if (listUrl.isEmpty()) throw new RuntimeException("Failed to fetch %s".formatted(url)); - - var input = listUrl.get().openConnection().getInputStream(); - input = XMLParser.preload(input); - var result = XMLParser.parse(input); - input.close(); - List appointments = nullable(result) // - .filter(o -> o instanceof Payload) - .map(o -> (Payload)o) - .map(Payload::get) - .stream() - .flatMap(JenaRosenkeller::findEventUrls) - .map(JenaRosenkeller::url) - .filter(Optional::isPresent) - .map(Optional::get) - .map(JenaRosenkeller::loadEvent) - .filter(Optional::isPresent) - .map(Optional::get) - .toList(); - - System.out.println(appointments); - return appointments; + public Stream> fetch() throws IOException { + var url = Payload.of("https://rosenkeller.org/de/programm"); + Stream> stream = url(url) + .map(JenaRosenkeller::open) // + .map(JenaRosenkeller::preload) + .map(JenaRosenkeller::parse) + .map(JenaRosenkeller::findEventUrls) + .stream(); + return stream // + .map(JenaRosenkeller::url) + .map(JenaRosenkeller::open) + .map(JenaRosenkeller::preload) + .map(JenaRosenkeller::parse) + .map(JenaRosenkeller::loadEvent); + } + + private static Result> findEventUrls(Result tagResult) { + return switch (tagResult) { + case Payload payload -> { + List urls = payload // find tag with event-id + .get() + .find("id", val -> val.startsWith("event-")) + .stream() + .map(t -> t.find("class", "ect-event-url"::equals)) + .flatMap(List::stream) + .map(t -> t.get("href")) + .toList(); + yield Payload.of(urls); + } + case Error error -> error.transform(); + default -> Error.format("Invalid parameter: %s", tagResult.getClass().getSimpleName()); + }; + } + + private static Result parse(Result inputStream) { + return switch (inputStream) { + case Payload payload -> XMLParser.parse(payload.get()); + case Error error -> error.transform(); + default -> Error.of("Invalid parameter: %s".formatted(inputStream.getClass().getSimpleName())); + }; + } + + private static Result preload(Result inputStream) { + switch (inputStream){ + case Payload 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 error: + return error.transform(); + default: + return Error.format("Invalid parameter: %s", inputStream.getClass().getSimpleName()); + } } - private static Optional loadEvent(URL url) { + private static Result open(Result url) { + switch (url) { + case Payload payload: + try { + return Payload.of(payload.get().openConnection().getInputStream()); + } catch (IOException e) { + return Error.of("Failed to open %s".formatted(payload), e); + } + case Error error: + return error.transform(); + default: + return Error.format("Invalid parameter: %s", url.getClass().getSimpleName()); + } + } + + private static Result loadEvent(Result domResult) { + switch (domResult) { + case Payload payload: + var tag = payload.get(); + Optional title = tag.find("class", s -> s.endsWith("single-event-title")) // + .stream() + .map(Tag::children) + .filter(not(List::isEmpty)) + .map(List::getFirst) + .map(Tag::toString) + .findAny(); + if (title.isPresent()) { + var appointment = new EmptyAppointment() { + @Override + public String title() { + return title.get(); + } + }; + return Payload.of(appointment); + } + return Error.of("Could not find appointment title"); + case Error err: + return err.transform(); + default: + return Error.format("Invalid parameter: %s", domResult.getClass().getSimpleName()); + } + } + + private static Optional nope(URL url) { try { var input = url.openConnection().getInputStream(); input = XMLParser.preload(input); @@ -76,16 +209,19 @@ public class JenaRosenkeller implements Importer { } } - private static Stream findEventUrls(Tag tag) { - return tag.find("id", val -> val.startsWith("event-")).stream().map(t -> t.find("class", "ect-event-url" ::equals)).flatMap(List::stream).map(t -> t.get("href")); - } - - private static Optional url(String url) { - try { - return Optional.of(new URI(url).toURL()); - } catch (MalformedURLException | URISyntaxException e) { - e.printStackTrace(); - return empty(); + private static Result url(Result urls) { + switch (urls) { + case Payload payload: + var url = payload.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); + } + case Error err: + return err.transform(); + default: + return Error.format("Invalid parameter: %s", urls.getClass().getSimpleName()); } } } \ No newline at end of file diff --git a/doc/database.dia b/doc/database.dia index be90534..a85241f 100644 --- a/doc/database.dia +++ b/doc/database.dia @@ -88,6 +88,7 @@ + @@ -181,13 +182,13 @@ - + - + - + @@ -213,7 +214,7 @@ - + @@ -226,13 +227,13 @@ - + - + - + @@ -258,7 +259,7 @@ - + @@ -271,13 +272,13 @@ - + - + - + @@ -303,7 +304,7 @@ - + @@ -316,13 +317,13 @@ - + - + - + @@ -348,7 +349,7 @@ - + @@ -361,13 +362,13 @@ - + - + - + @@ -393,7 +394,7 @@ - + @@ -1596,16 +1597,16 @@ - + - + - + - + @@ -1684,5 +1685,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + #title# + + + + + + + + + + + + + + + + + + +