diff --git a/build.gradle.kts b/build.gradle.kts index f42cd89..2ff669d 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -11,8 +11,6 @@ spotless { target("**/src/**/java/**/*.java") removeUnusedImports() importOrder() - //cleanthat() - clangFormat("18.1.8").style("file:config/clang-format") licenseHeader("/* © SRSoftware 2024 */") toggleOffOn() } 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 d31dd1f..424078b 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 @@ -30,21 +30,18 @@ public interface Appointment { */ String description(); - /** * The date and time when the appointment is scheduled to end * @return an optionals LocalDateTime */ Optional end(); - /** * represent this event as ical entry * @return the ical string */ String ical(); - /** * ID of the appointment – unique within this system * @return the appointment`s id @@ -75,14 +72,12 @@ public interface Appointment { */ LocalDateTime start(); - /** * tags i.e. keywords may be used to filter appointments * @return the set of tags associated with the current appointment */ Set tags(); - /** * the title of the appointment * @return the title 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 92abf54..af8fc92 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 @@ -60,7 +60,6 @@ public class Application { server.start(); // TODO: schedule imports - // TODO: update URL, when tags are selected // TODO: allow list start time as URL param // TODO: provide ICAL and WEBDAV links } diff --git a/de.srsoftware.cal.base/src/main/java/de/srsoftware/cal/BaseAppointment.java b/de.srsoftware.cal.base/src/main/java/de/srsoftware/cal/BaseAppointment.java index d3cfa0d..602626b 100644 --- a/de.srsoftware.cal.base/src/main/java/de/srsoftware/cal/BaseAppointment.java +++ b/de.srsoftware.cal.base/src/main/java/de/srsoftware/cal/BaseAppointment.java @@ -124,7 +124,7 @@ public class BaseAppointment implements Appointment { } @Override - public String ical() { + public String ical() { // TODO: implement return "converting event (%s) to ical not implemented".formatted(title); } @@ -199,4 +199,4 @@ public class BaseAppointment implements Appointment { public Set links() { return links; } -} \ No newline at end of file +} diff --git a/de.srsoftware.cal.base/src/main/java/de/srsoftware/cal/BaseImporter.java b/de.srsoftware.cal.base/src/main/java/de/srsoftware/cal/BaseImporter.java index 89c8183..8937b82 100644 --- a/de.srsoftware.cal.base/src/main/java/de/srsoftware/cal/BaseImporter.java +++ b/de.srsoftware.cal.base/src/main/java/de/srsoftware/cal/BaseImporter.java @@ -58,7 +58,6 @@ public abstract class BaseImporter implements Importer { return extractDescriptionTag(eventTag); } - protected Result extractDescription(Tag eventTag) { Result titleTag = extractDescriptionTag(eventTag); if (titleTag.optional().isEmpty()) return transform(titleTag); @@ -73,7 +72,6 @@ public abstract class BaseImporter implements Importer { return error("not implemented"); } - protected Result extractEnd(Tag eventTag) { Result endTag = extractEndTag(eventTag); if (endTag.optional().isEmpty()) return transform(endTag); @@ -114,79 +112,75 @@ public abstract class BaseImporter implements Importer { return Payload.of(event); } - private Result extractEvent(Result domResult, Link eventPage) { return switch (domResult) { - case Payload payload -> extractEvent(payload.get(), eventPage); - case Error err -> err.transform(); - default -> invalidParameter(domResult); - }; - } - - protected abstract Result extractEventTag(Result pageResult); - - protected abstract Result> extractEventUrls(Result programPage); - - - protected List extractLinks(Tag appointmentTag) { - var links = new ArrayList(); - - 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(BaseImporter::url) - .map(url -> link(url,text)) - .optional() - .ifPresent(links::add); - }); - return links; - } - - public abstract Result> extractLinkAnchors(Result tagResult); - - protected Result extractLinksTag(Tag eventTag) { - return extractDescriptionTag(eventTag); - } - - protected Result extractLocation(Tag eventTag) { - Result locationTag = extractLocationTag(eventTag); - if (locationTag.optional().isEmpty()) return transform(locationTag); - return Payload.of(locationTag.optional().get().toString(2)); - } - - protected abstract Result extractLocationTag(Tag eventTag); - - - protected Result extractStart(Tag eventTag) { - Result startTag = extractStartTag(eventTag); - if (startTag.optional().isEmpty()) return transform(startTag); - return parseStartDate(startTag.optional().get().strip()); - } - - protected abstract Result extractStartTag(Tag eventTag); - - - protected abstract List extractTags(Tag eventTag); - - protected Result extractTitle(Tag eventTag) { - Result titleTag = extractTitleTag(eventTag); - if (titleTag.optional().isEmpty()) return transform(titleTag); - var inner = titleTag.optional().flatMap(tag -> tag.inner(2)); - return inner.isPresent() ? Payload.of(inner.get()) : - error("No title found"); + case Payload payload -> extractEvent(payload.get(), eventPage); + case Error err -> err.transform(); + default -> invalidParameter(domResult); + }; + } + + protected abstract Result extractEventTag(Result pageResult); + + protected abstract Result> extractEventUrls(Result programPage); + + protected List extractLinks(Tag appointmentTag) { + var links = new ArrayList(); + + 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(BaseImporter::url) + .map(url -> link(url, text)) + .optional() + .ifPresent(links::add); + }); + return links; + } + + public abstract Result> extractLinkAnchors(Result tagResult); + + protected Result extractLinksTag(Tag eventTag) { + return extractDescriptionTag(eventTag); + } + + protected Result extractLocation(Tag eventTag) { + Result locationTag = extractLocationTag(eventTag); + if (locationTag.optional().isEmpty()) return transform(locationTag); + return Payload.of(locationTag.optional().get().toString(2)); + } + + protected abstract Result extractLocationTag(Tag eventTag); + + protected Result extractStart(Tag eventTag) { + Result startTag = extractStartTag(eventTag); + if (startTag.optional().isEmpty()) return transform(startTag); + return parseStartDate(startTag.optional().get().strip()); + } + + protected abstract Result extractStartTag(Tag eventTag); + + protected abstract List extractTags(Tag eventTag); + + protected Result extractTitle(Tag eventTag) { + Result titleTag = extractTitleTag(eventTag); + if (titleTag.optional().isEmpty()) return transform(titleTag); + var inner = titleTag.optional().flatMap(tag -> tag.inner(2)); + return inner.isPresent() ? Payload.of(inner.get()) : + error("No title found"); } protected abstract Result extractTitleTag(Tag eventTag); - @Override public Stream fetch() { var url = Payload.of(programURL()); @@ -249,24 +243,24 @@ public abstract class BaseImporter implements Importer { protected Result parseXML(Result inputStream) { return switch (inputStream) { - case Payload payload -> XMLParser.parse(payload.get()); - case Error error -> error.transform(); - default -> invalidParameter(inputStream); - }; - } - - protected Result preload(Result inputStream) { - switch (inputStream) { - case Payload payload: - try { - return Payload.of(XMLParser.preload(payload.get())); - } catch (IOException e) { - return error(e, "Failed to buffer data from %s", payload); - } - case Error error: - return error.transform(); - default: - return invalidParameter(inputStream); + case Payload payload -> XMLParser.parse(payload.get()); + case Error error -> error.transform(); + default -> invalidParameter(inputStream); + }; + } + + protected Result preload(Result inputStream) { + switch (inputStream) { + case Payload payload: + try { + return Payload.of(XMLParser.preload(payload.get())); + } catch (IOException e) { + return error(e, "Failed to buffer data from %s", payload); + } + case Error error: + return error.transform(); + default: + return invalidParameter(inputStream); } } @@ -284,7 +278,6 @@ public abstract class BaseImporter implements Importer { } } - protected static Result toNumericMonth(String month) { month = month.toLowerCase(); if (month.startsWith("ja")) return Payload.of(1); @@ -302,7 +295,6 @@ public abstract class BaseImporter implements Importer { return error("Failed to recognize \"%s\" as a month!", month); } - protected static Result url(Result urlResult) { if (urlResult.optional().isEmpty()) return transform(urlResult); var url = urlResult.optional().get(); diff --git a/de.srsoftware.cal.db/src/main/java/de/srsoftware/cal/db/Database.java b/de.srsoftware.cal.db/src/main/java/de/srsoftware/cal/db/Database.java index b15a462..f4a1c0f 100644 --- a/de.srsoftware.cal.db/src/main/java/de/srsoftware/cal/db/Database.java +++ b/de.srsoftware.cal.db/src/main/java/de/srsoftware/cal/db/Database.java @@ -16,7 +16,7 @@ public interface Database { * @param appointment the appointment to store * @return a clone of the provided appointment with id field set */ - public Result add(Appointment appointment); + Result add(Appointment appointment); Result> findTags(String infix); @@ -28,7 +28,7 @@ public interface Database { * @param till restrict appointments to times before this date time * @return list of appointments in this time span */ - public Result> list(LocalDateTime from, LocalDateTime till, Integer count, Integer offset); + Result> list(LocalDateTime from, LocalDateTime till, Integer count, Integer offset); /** * list appointments @@ -37,7 +37,7 @@ public interface Database { * @param offset the number of appointments to skip * @return the list of appointments fetched from the db */ - public List listByTags(Set tags, Integer count, Integer offset); + List listByTags(Set tags, Integer count, Integer offset); Result loadEvent(long id); diff --git a/de.srsoftware.cal.db/src/main/java/de/srsoftware/cal/db/MariaDB.java b/de.srsoftware.cal.db/src/main/java/de/srsoftware/cal/db/MariaDB.java index 74de756..dcd4592 100644 --- a/de.srsoftware.cal.db/src/main/java/de/srsoftware/cal/db/MariaDB.java +++ b/de.srsoftware.cal.db/src/main/java/de/srsoftware/cal/db/MariaDB.java @@ -58,7 +58,6 @@ public class MariaDB implements Database { throw new RuntimeException("%s.createTables() not implemented!"); } - @Override public Result add(Appointment appointment) { try { @@ -290,7 +289,6 @@ public class MariaDB implements Database { return Payload.of(appointment); } - @Override public List listByTags(Set tags, Integer count, Integer offset) { return List.of(); diff --git a/de.srsoftware.cal.db/src/main/java/de/srsoftware/cal/db/NotFound.java b/de.srsoftware.cal.db/src/main/java/de/srsoftware/cal/db/NotFound.java index 25646cf..52ff210 100644 --- a/de.srsoftware.cal.db/src/main/java/de/srsoftware/cal/db/NotFound.java +++ b/de.srsoftware.cal.db/src/main/java/de/srsoftware/cal/db/NotFound.java @@ -14,7 +14,6 @@ public class NotFound extends Error { return new NotFound<>(message.formatted(fills), null, null); } - @Override public NotFound transform() { return new NotFound<>(message(), data(), exceptions()); diff --git a/de.srsoftware.cal.web/src/main/java/de/srsoftware/cal/ApiEndpoint.java b/de.srsoftware.cal.web/src/main/java/de/srsoftware/cal/ApiEndpoint.java index a87af7d..80aa0a3 100644 --- a/de.srsoftware.cal.web/src/main/java/de/srsoftware/cal/ApiEndpoint.java +++ b/de.srsoftware.cal.web/src/main/java/de/srsoftware/cal/ApiEndpoint.java @@ -51,8 +51,8 @@ public class ApiEndpoint extends PathHandler { if (opt.isEmpty()) return transform(res); var event = opt.get(); var title = getHeader(ex, TITLE); - if (title.isEmpty()) return error("Missing title header"); - if (!title.get().equals(event.title())) return error("Title mismatch!"); + if (title.isEmpty()) return HttpError.of(400,"Missing title header"); + if (!title.get().equals(event.title())) return HttpError.of(400,"Title mismatch!"); return db.add(opt.get()).map(ApiEndpoint::toJson); } catch (IOException e) { return error(e, "Failed to read event data from request body"); @@ -67,7 +67,7 @@ public class ApiEndpoint extends PathHandler { try { aid = Long.parseLong(id); } catch (Exception e) { - return HttpError.of(400, "%s is not a valid appointment id!", id); + return HttpError.of(404, "%s is not a valid appointment id!", id); } var opt = getHeader(ex, "title"); if (opt.isEmpty()) return HttpError.of(412, "title missing"); @@ -83,7 +83,6 @@ public class ApiEndpoint extends PathHandler { return db.removeAppointment(event.id()); } - @Override public boolean doDelete(String path, HttpExchange ex) throws IOException { if ("/event".equals(path)) return sendContent(ex, delete(path, ex)); @@ -94,233 +93,219 @@ public class ApiEndpoint extends PathHandler { public boolean doGet(String path, HttpExchange ex) throws IOException { return switch (path) { case "/event" -> sendContent(ex,getEvent(ex).map(ApiEndpoint::toJson).map(ApiEndpoint::httpError)); - case "/events/json" -> sendContent(ex,eventList(ex).map(ApiEndpoint::toJsonList).map(ApiEndpoint::httpError)); case "/events/ical"-> sendContent(ex,eventList(ex).map(ApiEndpoint::toIcal).map(ApiEndpoint::httpError)); - case "/tags" -> listTags(ex); - default -> unknownPath(ex,path); - }; - } - - @Override - public boolean doPatch(String path, HttpExchange ex) throws IOException { - if ("/event".equals(path)) return sendContent(ex, updateEvent(ex)); - return unknownPath(ex, path); - } - - @Override - public boolean doPost(String path, HttpExchange ex) throws IOException { - if ("/event".equals(path)) return sendContent(ex, createEvent(ex)); - return unknownPath(ex, path); - } - - private Result> eventList(HttpExchange ex) { - var param = queryParam(ex); - var tags = nullable(param.get(TAGS)) // - .stream() - .flatMap(s -> Arrays.stream(s.split(","))) - .map(s -> s.trim().toLowerCase()) - .toList(); - - Result start = parseDate(param.get(START)); - if (start == null) start = parsePast(param.get(PAST)); - if (start instanceof de.srsoftware.tools.Error err) return err.transform(); - var startDate = (start == null) ? null: - start.optional().orElse(null); - Integer offset = null; - - var o = param.get(OFFSET); - if (o != null) { - try { - offset = Integer.parseInt(o); - } catch (NumberFormatException e) { - return error("Offset (offset=%s) is not a number!", o); - } - } - - Integer count = null; - o = param.get(COUNT); - if (o != null) { - try { - count = Integer.parseInt(o); - } catch (NumberFormatException e) { - return error("Count (count=%s) is not a number!", o); - } - } - - return db.list(startDate, null, count, offset).map(res -> filterByTags(res, tags)); + case "/events/json" -> sendContent(ex,eventList(ex).map(ApiEndpoint::toJsonList).map(ApiEndpoint::httpError)); + case "/tags" -> listTags(ex); + default -> unknownPath(ex, path); + }; + } + + @Override + public boolean doPatch(String path, HttpExchange ex) throws IOException { + if ("/event".equals(path)) return sendContent(ex, updateEvent(ex)); + return unknownPath(ex, path); + } + + @Override + public boolean doPost(String path, HttpExchange ex) throws IOException { + if ("/event".equals(path)) return sendContent(ex, createEvent(ex)); + return unknownPath(ex, path); + } + + private Result> eventList(HttpExchange ex) { + var param = queryParam(ex); + var tags = nullable(param.get(TAGS)).stream() + .flatMap(s -> Arrays.stream(s.split(","))) + .map(s -> s.trim().toLowerCase()) + .toList(); + + Result start = parseDate(param.get(START)); + if (start == null) start = parsePast(param.get(PAST)); + if (start instanceof de.srsoftware.tools.Error err) return err.transform(); + var startDate = (start == null) ? null: start.optional().orElse(null); + Integer offset = null; + + var o = param.get(OFFSET); + if (o != null) try { + offset = Integer.parseInt(o); + } catch (NumberFormatException e) { + return HttpError.of(400,"Offset (offset=%s) is not a number!", o); } - private Result> filterByTags(Result> res, List tags) { - if (tags == null || tags.isEmpty()) return res; - var opt = res.optional(); - if (opt.isEmpty()) return transform(res); - var list = opt.get().stream().filter(event -> tagsMatch(event, tags)).toList(); - return Payload.of(list); + Integer count = null; + o = param.get(COUNT); + if (o != null) try { + count = Integer.parseInt(o); + } catch (NumberFormatException e) { + return HttpError.of(400,"Count (count=%s) is not a number!", o); } + return db.list(startDate, null, count, offset).map(res -> filterByTags(res, tags)); + } - private Result getEvent(HttpExchange ex) { - var params = queryParam(ex); - var o = params.get(ID); - if (o == null) return error("id parameter missing!"); - try { - long id = Long.parseLong(o); - return db.loadEvent(id); - } catch (NumberFormatException e) { - return error(e, "Illegal format for id parameter (%s)", o); - } - } - - private static Result httpError(Result res) { - if (res instanceof NotFound notFound) return HttpError.of(404, notFound.message()); - return res; - } + private Result> filterByTags(Result> res, List tags) { + if (tags == null || tags.isEmpty()) return res; + var opt = res.optional(); + if (opt.isEmpty()) return transform(res); + var list = opt.get().stream().filter(event -> tagsMatch(event, tags)).toList(); + return Payload.of(list); + } - private boolean listTags(HttpExchange ex) throws IOException { - var params = queryParam(ex); - var infix = params.get("infix"); - if (infix == null) return sendContent(ex, error("No infix set in method call parameters")); - var res = db.findTags(infix).map(ApiEndpoint::sortTags); - return sendContent(ex, res); + private Result getEvent(HttpExchange ex) { + var params = queryParam(ex); + var o = params.get(ID); + if (o == null) return HttpError.of(400,"id parameter missing!"); + try { + return db.loadEvent(Long.parseLong(o)); + } catch (NumberFormatException e) { + return HttpError.of(400,e, "Illegal format for id parameter (%s)", o); } + } - public static Result parseDate(String s) { - if (s == null) return null; - var matcher = DATE_TIME.matcher(s); - if (matcher.find()) { - int year = Integer.parseInt(matcher.group(1)); - int month = Integer.parseInt(matcher.group(2)); - int day = nullable(matcher.group(4)).map(Integer::parseInt).orElse(1); - int hour = nullable(matcher.group(6)).map(Integer::parseInt).orElse(0); - int minute = nullable(matcher.group(7)).map(Integer::parseInt).orElse(0); - int second = nullable(matcher.group(9)).map(Integer::parseInt).orElse(0); - return Payload.of(LocalDateTime.of(year, month, day, hour, minute, second)); - } - return error("invalid date time format: %s", s); - } + private static Result httpError(Result res) { + if (res instanceof NotFound notFound) return HttpError.of(404, notFound.message()); + return res; + } + private boolean listTags(HttpExchange ex) throws IOException { + var params = queryParam(ex); + var infix = nullIfEmpty(params.get("infix")); + if (infix == null) return sendContent(ex, HttpError.of(400,"No infix set in method call parameters")); + var res = db.findTags(infix).map(ApiEndpoint::sortTags); + return sendContent(ex, res); + } - public static Result parsePast(String s) { - var start = LocalDateTime.now().withDayOfMonth(1).withHour(0).withMinute(0).withSecond(0).withNano(0); - if (s == null) return Payload.of(start); - var matcher = PAST_PATTERN.matcher(s); - if (matcher.find()) { - for (int i = 0; i <= matcher.groupCount(); i++) System.out.printf("%s: %s\n", i, matcher.group(i)); - - int num = nullable(matcher.group(1)).map(Integer::parseInt).orElse(0); - switch (nullable(matcher.group(2)).orElse("")) { - case "m": - start = start.minusMonths(num); - break; - case "y": - start = start.minusYears(num); - break; - } - var all = matcher.group(3); - if ("all".equals(all)) return null; - return Payload.of(start); - } - return error("invalid past format: %s", s); + public static Result parseDate(String s) { + if (s == null) return null; + var matcher = DATE_TIME.matcher(s); + if (matcher.find()) { + int year = Integer.parseInt(matcher.group(1)); + int month = Integer.parseInt(matcher.group(2)); + int day = nullable(matcher.group(4)).map(Integer::parseInt).orElse(1); + int hour = nullable(matcher.group(6)).map(Integer::parseInt).orElse(0); + int minute = nullable(matcher.group(7)).map(Integer::parseInt).orElse(0); + int second = nullable(matcher.group(9)).map(Integer::parseInt).orElse(0); + return Payload.of(LocalDateTime.of(year, month, day, hour, minute, second)); } + return error("invalid date time format: %s", s); + } - private static Result> sortTags(Result> listResult) { - if (listResult.optional().isEmpty()) return listResult; - List list = listResult.optional().get(); - while (list.size() > 15) { - int longest = list.stream().map(String::length).reduce(0, Integer::max); - var subset = list.stream().filter(s -> s.length() < longest).toList(); - if (subset.size() < 3) return Payload.of(list); - list = subset; + public static Result parsePast(String s) { + var start = LocalDateTime.now().withDayOfMonth(1).withHour(0).withMinute(0).withSecond(0).withNano(0); + if (s == null) return Payload.of(start); + var matcher = PAST_PATTERN.matcher(s); + if (matcher.find()) { + int num = nullable(matcher.group(1)).map(Integer::parseInt).orElse(0); + var g2 = nullable(matcher.group(2)).orElse(""); + switch (g2) { + case "m": + start = start.minusMonths(num); + break; + case "y": + start = start.minusYears(num); + break; } - return Payload.of(list); + var all = matcher.group(3); + if ("all".equals(all)) return null; + return Payload.of(start); } + return error("invalid past format: %s", s); + } - - private static boolean tagsMatch(Appointment event, List tags) { - return new HashSet<>(event.tags().stream().map(String::toLowerCase).toList()).containsAll(tags); + private static Result> sortTags(Result> listResult) { + if (listResult.optional().isEmpty()) return listResult; + List list = listResult.optional().get(); + while (list.size() > 15) { + int longest = list.stream().map(String::length).reduce(0, Integer::max); + var subset = list.stream().filter(s -> s.length() < longest).toList(); + if (subset.size() < 3) return Payload.of(list); + list = subset; } + return Payload.of(list); + } + private static boolean tagsMatch(Appointment event, List tags) { + return new HashSet<>(event.tags().stream().map(String::toLowerCase).toList()).containsAll(tags); + } - private Result toEvent(JSONObject json) { - var description = json.has(DESCRIPTION) ? nullIfEmpty(json.getString(DESCRIPTION)) : null; - var title = json.has(TITLE) ? nullIfEmpty(json.getString(TITLE)) : null; - if (title == null) return error("title missing"); - var start = json.has(START) ? nullIfEmpty(json.getString(START)) : null; - if (start == null) return error("start missing"); - var startDate = nullable(start).map(dt -> LocalDateTime.parse(dt, ISO_DATE_TIME)).orElse(null); - var end = json.has(END) ? nullIfEmpty(json.getString(END)) : null; - var endDate = nullable(end).map(dt -> LocalDateTime.parse(dt, ISO_DATE_TIME)).orElse(null); - var location = json.has(LOCATION) ? json.getString(LOCATION) : null; - if (location == null) return error("location missing"); - var aid = json.has(AID) ? json.getLong(AID) : 0; - var event = new BaseAppointment(aid, title, description, startDate, endDate, location); - if (json.has(ATTACHMENTS)) { - json.getJSONArray(ATTACHMENTS).forEach(att -> { - Payload // - .of(att.toString()) - .map(BaseImporter::url) - .map(BaseImporter::toAttachment) - .optional() - .ifPresent(event::add); - }); - } - if (json.has(LINKS)) { - json.getJSONArray(LINKS).forEach(o -> { - if (o instanceof JSONObject j) toLink(j).optional().ifPresent(event::addLinks); - }); - } - if (json.has(TAGS)) json.getJSONArray(TAGS).forEach(o -> event.tags(o.toString())); - return Payload.of(event); + private Result toEvent(JSONObject json) { + var description = json.has(DESCRIPTION) ? nullIfEmpty(json.getString(DESCRIPTION)) : null; + var title = json.has(TITLE) ? nullIfEmpty(json.getString(TITLE)) : null; + if (title == null) return error("title missing"); + var start = json.has(START) ? nullIfEmpty(json.getString(START)) : null; + if (start == null) return error("start missing"); + var startDate = nullable(start).map(dt -> LocalDateTime.parse(dt, ISO_DATE_TIME)).orElse(null); + var end = json.has(END) ? nullIfEmpty(json.getString(END)) : null; + var endDate = nullable(end).map(dt -> LocalDateTime.parse(dt, ISO_DATE_TIME)).orElse(null); + var location = json.has(LOCATION) ? json.getString(LOCATION) : null; + if (location == null) return error("location missing"); + var aid = json.has(AID) ? json.getLong(AID) : 0; + var event = new BaseAppointment(aid, title, description, startDate, endDate, location); + if (json.has(ATTACHMENTS)) { + json.getJSONArray(ATTACHMENTS).forEach(att -> { + Payload // + .of(att.toString()) + .map(BaseImporter::url) + .map(BaseImporter::toAttachment) + .optional() + .ifPresent(event::add); + }); } + if (json.has(LINKS)) json.getJSONArray(LINKS).forEach(o -> { + if (o instanceof JSONObject j) toLink(j).optional().ifPresent(event::addLinks); + }); + if (json.has(TAGS)) json.getJSONArray(TAGS).forEach(o -> event.tags(o.toString())); + return Payload.of(event); + } - private static Result toIcal(Result> res) { - var opt = res.optional(); - if (opt.isEmpty()) return transform(res); - var list = opt.get().stream().map(Appointment::ical).collect(Collectors.joining("\n")); - return Payload.of(list); - } - + private static Result toIcal(Result> res) { + var opt = res.optional(); + if (opt.isEmpty()) return transform(res); + var list = opt.get().stream().map(Appointment::ical).collect(Collectors.joining("\n")); + return Payload.of(list); + } - private static Result toJson(Result res) { - var opt = res.optional(); - if (opt.isEmpty()) return transform(res); - return Payload.of(opt.get().json()); - } + private static Result toJson(Result res) { + var opt = res.optional(); + return opt.isEmpty() ? transform(res) : Payload.of(opt.get().json()); + } - private static Result> toJsonList(Result> res) { - var opt = res.optional(); - if (opt.isEmpty()) return transform(res); - var list = opt.get().stream().map(Appointment::json).toList(); - return Payload.of(list); - } - protected static Result toLink(JSONObject json) { - try { - var description = json.getString(DESCRIPTION); - return Payload.of(json.getString(URL)).map(BaseImporter::url).map(url -> BaseImporter.link(url, description)); + private static Result> toJsonList(Result> res) { + var opt = res.optional(); + if (opt.isEmpty()) return transform(res); + var list = opt.get().stream().map(Appointment::json).toList(); + return Payload.of(list); + } - } catch (Exception e) { - return error(e, "Failed to create link from %s", json); - } - } - private boolean unknownPath(HttpExchange ex, String path) throws IOException { - return sendContent(ex, HttpError.of(404, "%s is not known to this API", path)); + protected static Result toLink(JSONObject json) { + try { + var description = json.getString(DESCRIPTION); + return Payload.of(json.getString(URL)) + .map(BaseImporter::url) + .map(url -> BaseImporter.link(url, description)); + } catch (Exception e) { + return error(e, "Failed to create link from %s", json); } + } + private boolean unknownPath(HttpExchange ex, String path) throws IOException { + return sendContent(ex, HttpError.of(404, "%s is not known to this API", path)); + } - private Result updateEvent(HttpExchange ex) { - try { - var res = toEvent(json(ex)); - var opt = res.optional(); - if (opt.isEmpty()) return transform(res); - var event = opt.get(); - var title = getHeader(ex, TITLE); - if (title.isEmpty()) return error("Missing title header"); - if (!title.get().equals(event.title())) return error("Title mismatch!"); - return db.update(opt.get()).map(ApiEndpoint::toJson); - } catch (IOException e) { - return error(e, "Failed to read event data from request body"); - } + private Result updateEvent(HttpExchange ex) { + try { + var res = toEvent(json(ex)); + var opt = res.optional(); + if (opt.isEmpty()) return transform(res); + var event = opt.get(); + var title = getHeader(ex, TITLE); + if (title.isEmpty()) return HttpError.of(400,"Missing title header"); + if (!title.get().equals(event.title())) return HttpError.of(400,"Title mismatch!"); + return db.update(opt.get()).map(ApiEndpoint::toJson); + } catch (IOException e) { + return error(e, "Failed to read event data from request body"); } } +} diff --git a/de.srsoftware.cal.web/src/main/java/de/srsoftware/cal/IndexHandler.java b/de.srsoftware.cal.web/src/main/java/de/srsoftware/cal/IndexHandler.java index 7da488b..b05c6b7 100644 --- a/de.srsoftware.cal.web/src/main/java/de/srsoftware/cal/IndexHandler.java +++ b/de.srsoftware.cal.web/src/main/java/de/srsoftware/cal/IndexHandler.java @@ -7,6 +7,7 @@ import java.io.IOException; public class IndexHandler extends PathHandler { PathHandler staticPages; + public IndexHandler(PathHandler staticPages) { this.staticPages = staticPages; } @@ -14,10 +15,9 @@ public class IndexHandler extends PathHandler { @Override public boolean doGet(String path, HttpExchange ex) throws IOException { return switch (path) { - case "/" -> staticPages.doGet("/index", ex); - case "/favicon.ico" -> staticPages.doGet("/images/%s".formatted(path), ex); - default -> super.doGet(path, ex); - }; - - } + case "/" -> staticPages.doGet("/index", ex); + case "/favicon.ico" -> staticPages.doGet("/images/%s".formatted(path), ex); + default -> super.doGet(path, ex); + }; + } } diff --git a/de.srsoftware.cal.web/src/main/java/de/srsoftware/cal/StaticHandler.java b/de.srsoftware.cal.web/src/main/java/de/srsoftware/cal/StaticHandler.java index ae9c1b6..677bda9 100644 --- a/de.srsoftware.cal.web/src/main/java/de/srsoftware/cal/StaticHandler.java +++ b/de.srsoftware.cal.web/src/main/java/de/srsoftware/cal/StaticHandler.java @@ -8,7 +8,6 @@ import java.io.IOException; import java.nio.file.Path; import java.util.Optional; - public class StaticHandler extends PathHandler { private final Optional staticPath; diff --git a/de.srsoftware.cal.web/src/main/resources/script/index.js b/de.srsoftware.cal.web/src/main/resources/script/index.js index ecc8f16..fb46b76 100644 --- a/de.srsoftware.cal.web/src/main/resources/script/index.js +++ b/de.srsoftware.cal.web/src/main/resources/script/index.js @@ -118,7 +118,7 @@ function openIcal(){ var pos = url.indexOf('?'); if (pos>1) url = url.substring(0,pos); if (!url.endsWith('/')) url += '/'; - url += 'api/ical'+query; + url += 'api/events/ical'+query; var elem = create('a'); elem.setAttribute('href',url); elem.setAttribute('download','calendar.ical'); diff --git a/doc/openapi3_0.yaml b/doc/openapi3_0.yaml index 8f4125f..1bcd0b2 100644 --- a/doc/openapi3_0.yaml +++ b/doc/openapi3_0.yaml @@ -266,6 +266,14 @@ paths: items: type: string type: array + '400': + description: invalid input + content: + application/json: + schema: + type: string + example: {"message":"No infix set in method call parameters"} + components: schemas: Appointment: