overhauled API:

- wrote openapi schema
- re-implemented api endpoint following openapi schema
- intensified and improved working with Result objects

Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
This commit is contained in:
2024-12-31 00:02:14 +01:00
parent 10ad309d8c
commit 4822320961
28 changed files with 874 additions and 542 deletions
+1 -1
View File
@@ -4,7 +4,7 @@ dependencies {
implementation(project(":de.srsoftware.cal.api"))
implementation("de.srsoftware:tools.optionals:1.0.0")
implementation("de.srsoftware:tools.util:1.2.3")
implementation("de.srsoftware:tools.util:1.3.0")
implementation("de.srsoftware:tools.web:1.3.8")
implementation("org.json:json:20240303")
}
@@ -123,6 +123,11 @@ public class BaseAppointment implements Appointment {
return nullable(end);
}
@Override
public String ical() {
return "converting event (%s) to ical not implemented".formatted(title);
}
@Override
public long id() {
return id;
@@ -140,7 +145,7 @@ public class BaseAppointment implements Appointment {
json.put("start", start().format(DATE_TIME));
json.put("tags", tags());
json.put("title", title());
json.put("links", urls().stream().map(Link::json).toList());
json.put("links", links().stream().map(Link::json).toList());
return json;
}
@@ -191,7 +196,7 @@ public class BaseAppointment implements Appointment {
}
@Override
public Set<Link> urls() {
public Set<Link> links() {
return links;
}
}
@@ -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);
}
}
}
@@ -1,23 +1,24 @@
/* © SRSoftware 2024 */
package de.srsoftware.cal;
import static de.srsoftware.tools.Error.error;
import de.srsoftware.cal.api.Coords;
import de.srsoftware.tools.Error;
import de.srsoftware.tools.Payload;
import de.srsoftware.tools.Result;
public class Util {
public static Result<Coords> extractCoords(String coords) {
if (coords == null) return de.srsoftware.tools.Error.of("Argument is null");
if (coords.isBlank()) return de.srsoftware.tools.Error.of("Argument is blank");
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 de.srsoftware.tools.Error.format("Argument has invalid format: %s", coords);
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(lon, lat));
} catch (NumberFormatException nfe) {
return Error.of("Failed to parse coords from %s".formatted(coords), nfe);
return error(nfe, "Failed to parse coords from %s", coords);
}
}
}