|
|
|
@@ -4,10 +4,10 @@ package de.srsoftware.cal;
|
|
|
|
|
import static de.srsoftware.cal.Util.extractCoords;
|
|
|
|
|
import static de.srsoftware.cal.db.Fields.*;
|
|
|
|
|
import static de.srsoftware.cal.db.Fields.AID;
|
|
|
|
|
import static de.srsoftware.tools.Error.error;
|
|
|
|
|
import static de.srsoftware.tools.container.Error.error;
|
|
|
|
|
import static de.srsoftware.tools.Optionals.nullIfEmpty;
|
|
|
|
|
import static de.srsoftware.tools.Optionals.nullable;
|
|
|
|
|
import static de.srsoftware.tools.Result.transform;
|
|
|
|
|
import static de.srsoftware.tools.container.Container.transform;
|
|
|
|
|
import static java.time.format.DateTimeFormatter.ISO_DATE_TIME;
|
|
|
|
|
|
|
|
|
|
import com.sun.net.httpserver.HttpExchange;
|
|
|
|
@@ -16,9 +16,10 @@ import de.srsoftware.cal.api.Link;
|
|
|
|
|
import de.srsoftware.cal.db.Database;
|
|
|
|
|
import de.srsoftware.cal.db.NotFound;
|
|
|
|
|
import de.srsoftware.tools.HttpError;
|
|
|
|
|
import de.srsoftware.tools.Path;
|
|
|
|
|
import de.srsoftware.tools.PathHandler;
|
|
|
|
|
import de.srsoftware.tools.Payload;
|
|
|
|
|
import de.srsoftware.tools.Result;
|
|
|
|
|
import de.srsoftware.tools.container.Payload;
|
|
|
|
|
import de.srsoftware.tools.container.Container;
|
|
|
|
|
import java.io.IOException;
|
|
|
|
|
import java.time.LocalDateTime;
|
|
|
|
|
import java.util.Arrays;
|
|
|
|
@@ -45,7 +46,7 @@ public class ApiEndpoint extends PathHandler {
|
|
|
|
|
this.db = db;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private Result<JSONObject> createEvent(HttpExchange ex) {
|
|
|
|
|
private Container<JSONObject> createEvent(HttpExchange ex) {
|
|
|
|
|
try {
|
|
|
|
|
var res = toEvent(json(ex));
|
|
|
|
|
var opt = res.optional();
|
|
|
|
@@ -62,13 +63,13 @@ public class ApiEndpoint extends PathHandler {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private Result<Long> delete(String path, HttpExchange ex) {
|
|
|
|
|
private Container<Long> delete(Path path, HttpExchange ex) {
|
|
|
|
|
var params = queryParam(ex);
|
|
|
|
|
var id = params.get(ID);
|
|
|
|
|
if (id == null) return HttpError.of(400, "Id missing");
|
|
|
|
|
long aid = 0;
|
|
|
|
|
try {
|
|
|
|
|
aid = Long.parseLong(id);
|
|
|
|
|
aid = Long.parseLong(""+id);
|
|
|
|
|
} catch (Exception e) {
|
|
|
|
|
return HttpError.of(404, "%s is not a valid appointment id!", id);
|
|
|
|
|
}
|
|
|
|
@@ -78,7 +79,7 @@ public class ApiEndpoint extends PathHandler {
|
|
|
|
|
return db.loadEvent(aid).map(event -> deleteEvent(event, title)).map(ApiEndpoint::httpError);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private Result<Long> deleteEvent(Result<Appointment> eventResult, String title) {
|
|
|
|
|
private Container<Long> deleteEvent(Container<Appointment> eventResult, String title) {
|
|
|
|
|
var opt = eventResult.optional();
|
|
|
|
|
if (opt.isEmpty()) return transform(eventResult);
|
|
|
|
|
var event = opt.get();
|
|
|
|
@@ -87,57 +88,58 @@ public class ApiEndpoint extends PathHandler {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
public boolean doDelete(String path, HttpExchange ex) throws IOException {
|
|
|
|
|
if ("/event".equals(path)) return sendContent(ex, delete(path, ex));
|
|
|
|
|
public boolean doDelete(Path path, HttpExchange ex) throws IOException {
|
|
|
|
|
if (path.equals("/event")) return sendContent(ex, delete(path, ex));
|
|
|
|
|
return unknownPath(ex, path);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
public boolean doGet(String path, HttpExchange ex) throws IOException {
|
|
|
|
|
public boolean doGet(Path p, HttpExchange ex) throws IOException {
|
|
|
|
|
String hostname = hostname(ex);
|
|
|
|
|
String urlTemplate = hostname+"/static/event?id={}";
|
|
|
|
|
String prodId = "OpenCloudCal@"+hostname.split("://",2)[1];
|
|
|
|
|
ex.getResponseHeaders().add("Access-Control-Allow-Origin","*");
|
|
|
|
|
var path = p.toString();
|
|
|
|
|
return switch (path) {
|
|
|
|
|
case "/event" -> sendContent(ex,getEvent(ex).map(event -> toJson(event,urlTemplate)).map(ApiEndpoint::httpError));
|
|
|
|
|
case "/event/ical"-> sendContent(ex,getEvent(ex).map(event -> toIcal(event, hostname, urlTemplate)).map(ical -> Util.wrapIcal(ical,prodId)).map(ApiEndpoint::httpError));
|
|
|
|
|
case "/events/ical"-> sendContent(ex,eventList(ex).map(list -> toIcalList(list, hostname, urlTemplate)).map(ical -> Util.wrapIcal(ical,prodId)).map(ApiEndpoint::httpError));
|
|
|
|
|
case "/events/json" -> sendContent(ex,eventList(ex).map(list -> toJsonList(list,urlTemplate)).map(ApiEndpoint::httpError));
|
|
|
|
|
case "/tags" -> listTags(ex);
|
|
|
|
|
default -> unknownPath(ex, path);
|
|
|
|
|
case "event" -> sendContent(ex,(Container<?>)getEvent(ex).map(event -> toJson(event, urlTemplate)).map(ApiEndpoint::httpError));
|
|
|
|
|
case "event/ical"-> sendContent(ex,(Container<?>)getEvent(ex).map(event -> toIcal(event, hostname, urlTemplate)).map(ical -> Util.wrapIcal(ical,prodId)).map(ApiEndpoint::httpError));
|
|
|
|
|
case "events/ical"-> sendContent(ex,(Container<?>)eventList(ex).map(list -> toIcalList(list, hostname, urlTemplate)).map(ical -> Util.wrapIcal(ical,prodId)).map(ApiEndpoint::httpError));
|
|
|
|
|
case "events/json" -> sendContent(ex,(Container<?>)eventList(ex).map(list -> toJsonList(list,urlTemplate)).map(ApiEndpoint::httpError));
|
|
|
|
|
case "tags" -> listTags(ex);
|
|
|
|
|
default -> unknownPath(ex, p);
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
public boolean doPatch(String path, HttpExchange ex) throws IOException {
|
|
|
|
|
if ("/event".equals(path)) return sendContent(ex, updateEvent(ex));
|
|
|
|
|
public boolean doPatch(Path path, HttpExchange ex) throws IOException {
|
|
|
|
|
if (path.equals("/event")) 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));
|
|
|
|
|
public boolean doPost(Path path, HttpExchange ex) throws IOException {
|
|
|
|
|
if (path.equals("/event")) return sendContent(ex, createEvent(ex));
|
|
|
|
|
return unknownPath(ex, path);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private Result<List<Appointment>> eventList(HttpExchange ex) {
|
|
|
|
|
private Container<List<Appointment>> eventList(HttpExchange ex) {
|
|
|
|
|
var param = queryParam(ex);
|
|
|
|
|
var tags = nullable(param.get(TAGS)).stream()
|
|
|
|
|
var tags = nullable((String)param.get(TAGS)).stream()
|
|
|
|
|
.flatMap(s -> Arrays.stream(s.split(",")))
|
|
|
|
|
.map(s -> s.trim().toLowerCase())
|
|
|
|
|
.toList();
|
|
|
|
|
|
|
|
|
|
Result<LocalDateTime> start = parseDate(param.get(START));
|
|
|
|
|
Container<LocalDateTime> start = parseDate(param.get(START));
|
|
|
|
|
if (start == null) start = parsePast(param.get(PAST));
|
|
|
|
|
if (start instanceof de.srsoftware.tools.Error<LocalDateTime> err) return err.transform();
|
|
|
|
|
if (start instanceof de.srsoftware.tools.container.Error<LocalDateTime> 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);
|
|
|
|
|
if (o instanceof String s) try {
|
|
|
|
|
offset = Integer.parseInt(s);
|
|
|
|
|
} catch (NumberFormatException e) {
|
|
|
|
|
return HttpError.of(400,"Offset (offset=%s) is not a number!", o);
|
|
|
|
|
}
|
|
|
|
@@ -145,19 +147,19 @@ public class ApiEndpoint extends PathHandler {
|
|
|
|
|
|
|
|
|
|
Integer count = null;
|
|
|
|
|
o = param.get(COUNT);
|
|
|
|
|
if (o != null) try {
|
|
|
|
|
count = Integer.parseInt(o);
|
|
|
|
|
if (o instanceof String s) try {
|
|
|
|
|
count = Integer.parseInt(s);
|
|
|
|
|
} catch (NumberFormatException e) {
|
|
|
|
|
return HttpError.of(400,"Count (count=%s) is not a number!", o);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Result<LocalDateTime> end = parseDate(param.get(END));
|
|
|
|
|
Container<LocalDateTime> end = parseDate(param.get(END));
|
|
|
|
|
var endDate = (end == null) ? null: end.optional().orElse(null);
|
|
|
|
|
|
|
|
|
|
return db.list(startDate, endDate, count, offset, tags).map(res -> filterByTags(res, tags));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private Result<List<Appointment>> filterByTags(Result<List<Appointment>> res, List<String> tags) {
|
|
|
|
|
private Container<List<Appointment>> filterByTags(Container<List<Appointment>> res, List<String> tags) {
|
|
|
|
|
if (tags == null || tags.isEmpty()) return res;
|
|
|
|
|
var opt = res.optional();
|
|
|
|
|
if (opt.isEmpty()) return transform(res);
|
|
|
|
@@ -165,32 +167,32 @@ public class ApiEndpoint extends PathHandler {
|
|
|
|
|
return Payload.of(list);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private Result<Appointment> getEvent(HttpExchange ex) {
|
|
|
|
|
private Container<Appointment> getEvent(HttpExchange ex) {
|
|
|
|
|
var params = queryParam(ex);
|
|
|
|
|
var o = params.get(ID);
|
|
|
|
|
if (o == null) return HttpError.of(400,"id parameter missing!");
|
|
|
|
|
if (!(o instanceof String s)) return HttpError.of(400,"id parameter missing!");
|
|
|
|
|
try {
|
|
|
|
|
return db.loadEvent(Long.parseLong(o));
|
|
|
|
|
return db.loadEvent(Long.parseLong(s));
|
|
|
|
|
} catch (NumberFormatException e) {
|
|
|
|
|
return HttpError.of(400,e, "Illegal format for id parameter (%s)", o);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static <T> Result<T> httpError(Result<T> res) {
|
|
|
|
|
private static <T> Container<T> httpError(Container<T> res) {
|
|
|
|
|
if (res instanceof NotFound<T> 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"));
|
|
|
|
|
var infix = nullIfEmpty((String)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<LocalDateTime> parseDate(String s) {
|
|
|
|
|
if (s == null) return null;
|
|
|
|
|
public static Container<LocalDateTime> parseDate(Object o) {
|
|
|
|
|
if (!(o instanceof String s)) return null;
|
|
|
|
|
var matcher = DATE_TIME.matcher(s);
|
|
|
|
|
if (matcher.find()) {
|
|
|
|
|
int year = Integer.parseInt(matcher.group(1));
|
|
|
|
@@ -204,9 +206,9 @@ public class ApiEndpoint extends PathHandler {
|
|
|
|
|
return error("invalid date time format: %s", s);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static Result<LocalDateTime> parsePast(String s) {
|
|
|
|
|
public static Container<LocalDateTime> parsePast(Object o) {
|
|
|
|
|
var start = LocalDateTime.now().withDayOfMonth(1).withHour(0).withMinute(0).withSecond(0).withNano(0);
|
|
|
|
|
if (s == null) return Payload.of(start);
|
|
|
|
|
if (!(o instanceof String s)) return Payload.of(start);
|
|
|
|
|
var matcher = PAST_PATTERN.matcher(s);
|
|
|
|
|
if (matcher.find()) {
|
|
|
|
|
int num = nullable(matcher.group(1)).map(Integer::parseInt).orElse(0);
|
|
|
|
@@ -226,7 +228,7 @@ public class ApiEndpoint extends PathHandler {
|
|
|
|
|
return error("invalid past format: %s", s);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static Result<List<String>> sortTags(Result<List<String>> listResult) {
|
|
|
|
|
private static Container<List<String>> sortTags(Container<List<String>> listResult) {
|
|
|
|
|
if (listResult.optional().isEmpty()) return listResult;
|
|
|
|
|
List<String> list = listResult.optional().get();
|
|
|
|
|
while (list.size() > 15) {
|
|
|
|
@@ -242,7 +244,7 @@ public class ApiEndpoint extends PathHandler {
|
|
|
|
|
return new HashSet<>(event.tags().stream().map(String::toLowerCase).toList()).containsAll(tags);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private Result<BaseAppointment> toEvent(JSONObject json) {
|
|
|
|
|
private Container<BaseAppointment> 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");
|
|
|
|
@@ -274,31 +276,31 @@ public class ApiEndpoint extends PathHandler {
|
|
|
|
|
return Payload.of(event);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static Result<String> toIcal(Result<Appointment> res, String hostname, String urlTemplate) {
|
|
|
|
|
private static Container<String> toIcal(Container<Appointment> res, String hostname, String urlTemplate) {
|
|
|
|
|
var opt = res.optional();
|
|
|
|
|
return opt.isEmpty() ? transform(res) : Payload.of(opt.get().ical(hostname, urlTemplate));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static Result<String> toIcalList(Result<List<Appointment>> res, String hostname, String urlTemplate) {
|
|
|
|
|
private static Container<String> toIcalList(Container<List<Appointment>> res, String hostname, String urlTemplate) {
|
|
|
|
|
var opt = res.optional();
|
|
|
|
|
if (opt.isEmpty()) return transform(res);
|
|
|
|
|
var list = opt.get().stream().map(event -> event.ical(hostname, urlTemplate)).collect(Collectors.joining("\n"));
|
|
|
|
|
return Payload.of(list);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static Result<JSONObject> toJson(Result<? extends Appointment> res, String urlTemplate) {
|
|
|
|
|
private static Container<JSONObject> toJson(Container<? extends Appointment> res, String urlTemplate) {
|
|
|
|
|
var opt = res.optional();
|
|
|
|
|
return opt.isEmpty() ? transform(res) : Payload.of(opt.get().json(urlTemplate));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static Result<List<JSONObject>> toJsonList(Result<List<Appointment>> res, String urlTemplate) {
|
|
|
|
|
private static Container<List<JSONObject>> toJsonList(Container<List<Appointment>> res, String urlTemplate) {
|
|
|
|
|
var opt = res.optional();
|
|
|
|
|
if (opt.isEmpty()) return transform(res);
|
|
|
|
|
var list = opt.get().stream().map(event -> event.json(urlTemplate)).toList();
|
|
|
|
|
return Payload.of(list);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected static Result<Link> toLink(JSONObject json) {
|
|
|
|
|
protected static Container<Link> toLink(JSONObject json) {
|
|
|
|
|
try {
|
|
|
|
|
var description = json.getString(DESCRIPTION);
|
|
|
|
|
return Payload.of(json.getString(URL))
|
|
|
|
@@ -309,11 +311,11 @@ public class ApiEndpoint extends PathHandler {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private boolean unknownPath(HttpExchange ex, String path) throws IOException {
|
|
|
|
|
private boolean unknownPath(HttpExchange ex, Path path) throws IOException {
|
|
|
|
|
return sendContent(ex, HttpError.of(404, "%s is not known to this API", path));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private Result<JSONObject> updateEvent(HttpExchange ex) {
|
|
|
|
|
private Container<JSONObject> updateEvent(HttpExchange ex) {
|
|
|
|
|
try {
|
|
|
|
|
var res = toEvent(json(ex));
|
|
|
|
|
var opt = res.optional();
|
|
|
|
|