re-ordered methods
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
This commit is contained in:
@@ -11,6 +11,7 @@ spotless {
|
||||
target("**/src/**/java/**/*.java")
|
||||
removeUnusedImports()
|
||||
importOrder()
|
||||
//cleanthat()
|
||||
clangFormat("18.1.8").style("file:config/clang-format")
|
||||
licenseHeader("/* © SRSoftware 2024 */")
|
||||
toggleOffOn()
|
||||
|
||||
@@ -44,6 +44,46 @@ public class ApiEndpoint extends PathHandler {
|
||||
this.db = db;
|
||||
}
|
||||
|
||||
private Result<JSONObject> createEvent(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.add(opt.get()).map(ApiEndpoint::toJson);
|
||||
} catch (IOException e) {
|
||||
return error(e, "Failed to read event data from request body");
|
||||
}
|
||||
}
|
||||
|
||||
private Result<Long> delete(String 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);
|
||||
} catch (Exception e) {
|
||||
return HttpError.of(400, "%s is not a valid appointment id!", id);
|
||||
}
|
||||
var opt = getHeader(ex, "title");
|
||||
if (opt.isEmpty()) return HttpError.of(412, "title missing");
|
||||
var title = opt.get();
|
||||
return db.loadEvent(aid).map(event -> deleteEvent(event, title)).map(ApiEndpoint::httpError);
|
||||
}
|
||||
|
||||
private Result<Long> deleteEvent(Result<Appointment> eventResult, String title) {
|
||||
var opt = eventResult.optional();
|
||||
if (opt.isEmpty()) return transform(eventResult);
|
||||
var event = opt.get();
|
||||
if (!title.equals(event.title())) return HttpError.of(412, "title mismatch");
|
||||
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));
|
||||
@@ -61,26 +101,6 @@ public class ApiEndpoint extends PathHandler {
|
||||
};
|
||||
}
|
||||
|
||||
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 static Result<List<String>> sortTags(Result<List<String>> listResult) {
|
||||
if (listResult.optional().isEmpty()) return listResult;
|
||||
List<String> 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);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean doPatch(String path, HttpExchange ex) throws IOException {
|
||||
if ("/event".equals(path)) return sendContent(ex, updateEvent(ex));
|
||||
@@ -89,146 +109,48 @@ public class ApiEndpoint extends PathHandler {
|
||||
|
||||
@Override
|
||||
public boolean doPost(String path, HttpExchange ex) throws IOException {
|
||||
if ("/event".equals(path)) return sendContent(ex, newEvent(ex));
|
||||
if ("/event".equals(path)) return sendContent(ex, createEvent(ex));
|
||||
return unknownPath(ex, path);
|
||||
}
|
||||
|
||||
private Result<JSONObject> newEvent(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.add(opt.get()).map(ApiEndpoint::toJson);
|
||||
} catch (IOException e) {
|
||||
return error(e,"Failed to read event data from request body");
|
||||
}
|
||||
}
|
||||
private Result<List<Appointment>> 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();
|
||||
|
||||
private Result<JSONObject> 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");
|
||||
}
|
||||
}
|
||||
Result<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();
|
||||
var startDate = (start == null) ? null:
|
||||
start.optional().orElse(null);
|
||||
Integer offset = null;
|
||||
|
||||
private Result<Appointment> 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 Result<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");
|
||||
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);
|
||||
}
|
||||
|
||||
protected static Result<Link> 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 static Result<String> toIcal(Result<List<Appointment>> 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<List<JSONObject>> toJsonList(Result<List<Appointment>> res) {
|
||||
var opt = res.optional();
|
||||
if (opt.isEmpty()) return transform(res);
|
||||
var list = opt.get().stream().map(Appointment::json).toList();
|
||||
return Payload.of(list);
|
||||
}
|
||||
|
||||
private static Result<JSONObject> toJson(Result<? extends Appointment> res) {
|
||||
var opt = res.optional();
|
||||
if (opt.isEmpty()) return transform(res);
|
||||
return Payload.of(opt.get().json());
|
||||
}
|
||||
|
||||
private Result<List<Appointment>> 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<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();
|
||||
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);
|
||||
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);
|
||||
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));
|
||||
return db.list(startDate, null, count, offset).map(res -> filterByTags(res, tags));
|
||||
}
|
||||
|
||||
|
||||
private Result<List<Appointment>> filterByTags(Result<List<Appointment>> res, List<String> tags) {
|
||||
if (tags == null || tags.isEmpty()) return res;
|
||||
var opt = res.optional();
|
||||
@@ -237,10 +159,48 @@ public class ApiEndpoint extends PathHandler {
|
||||
return Payload.of(list);
|
||||
}
|
||||
|
||||
private static boolean tagsMatch(Appointment event, List<String> tags) {
|
||||
return new HashSet<>(event.tags().stream().map(String::toLowerCase).toList()).containsAll(tags);
|
||||
|
||||
private Result<Appointment> 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 <T> Result<T> httpError(Result<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 = 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);
|
||||
}
|
||||
|
||||
public static Result<LocalDateTime> 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);
|
||||
}
|
||||
|
||||
|
||||
public static Result<LocalDateTime> parsePast(String s) {
|
||||
var start = LocalDateTime.now().withDayOfMonth(1).withHour(0).withMinute(0).withSecond(0).withNano(0);
|
||||
if (s == null) return Payload.of(start);
|
||||
@@ -264,51 +224,103 @@ public class ApiEndpoint extends PathHandler {
|
||||
return error("invalid past format: %s", s);
|
||||
}
|
||||
|
||||
public static Result<LocalDateTime> 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));
|
||||
private static Result<List<String>> sortTags(Result<List<String>> listResult) {
|
||||
if (listResult.optional().isEmpty()) return listResult;
|
||||
List<String> 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 error("invalid date time format: %s", s);
|
||||
return Payload.of(list);
|
||||
}
|
||||
|
||||
private Result<Long> delete(String path, HttpExchange ex) {
|
||||
var params = queryParam(ex);
|
||||
var id = params.get(ID);
|
||||
if (id == null) return HttpError.of(400, "Id missing");
|
||||
long aid = 0;
|
||||
|
||||
private static boolean tagsMatch(Appointment event, List<String> tags) {
|
||||
return new HashSet<>(event.tags().stream().map(String::toLowerCase).toList()).containsAll(tags);
|
||||
}
|
||||
|
||||
|
||||
private Result<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");
|
||||
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<String> toIcal(Result<List<Appointment>> 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<JSONObject> toJson(Result<? extends Appointment> res) {
|
||||
var opt = res.optional();
|
||||
if (opt.isEmpty()) return transform(res);
|
||||
return Payload.of(opt.get().json());
|
||||
}
|
||||
|
||||
private static Result<List<JSONObject>> toJsonList(Result<List<Appointment>> 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<Link> toLink(JSONObject json) {
|
||||
try {
|
||||
aid = Long.parseLong(id);
|
||||
var description = json.getString(DESCRIPTION);
|
||||
return Payload.of(json.getString(URL)).map(BaseImporter::url).map(url -> BaseImporter.link(url, description));
|
||||
|
||||
} catch (Exception e) {
|
||||
return HttpError.of(400, "%s is not a valid appointment id!", id);
|
||||
return error(e, "Failed to create link from %s", json);
|
||||
}
|
||||
var opt = getHeader(ex, "title");
|
||||
if (opt.isEmpty()) return HttpError.of(412, "title missing");
|
||||
var title = opt.get();
|
||||
return db.loadEvent(aid).map(event -> deleteEvent(event, title)).map(ApiEndpoint::httpError);
|
||||
}
|
||||
|
||||
private Result<Long> deleteEvent(Result<Appointment> eventResult, String title) {
|
||||
var opt = eventResult.optional();
|
||||
if (opt.isEmpty()) return transform(eventResult);
|
||||
var event = opt.get();
|
||||
if (!title.equals(event.title())) return HttpError.of(412, "title mismatch");
|
||||
return db.removeAppointment(event.id());
|
||||
}
|
||||
|
||||
private boolean unknownPath(HttpExchange ex, String path) throws IOException {
|
||||
return sendContent(ex, HttpError.of(404, "%s is not known to this API", path));
|
||||
}
|
||||
|
||||
private static <T> Result<T> httpError(Result<T> res) {
|
||||
if (res instanceof NotFound<T> notFound) return HttpError.of(404, notFound.message());
|
||||
return res;
|
||||
|
||||
private Result<JSONObject> 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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user