preparing save/update of appointment

Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
This commit is contained in:
2024-12-28 00:55:49 +01:00
parent 9ffa40fd39
commit b7944613b3
6 changed files with 169 additions and 59 deletions
@@ -1,9 +1,11 @@
/* © SRSoftware 2024 */
package de.srsoftware.cal;
import static de.srsoftware.tools.Optionals.nullIfEmpty;
import static de.srsoftware.tools.Optionals.nullable;
import static java.lang.System.*;
import static java.lang.System.Logger.Level.WARNING;
import static java.time.format.DateTimeFormatter.ISO_DATE_TIME;
import com.sun.net.httpserver.HttpExchange;
import de.srsoftware.cal.api.Appointment;
@@ -22,7 +24,13 @@ import java.util.Map;
import org.json.JSONObject;
public class ApiHandler extends PathHandler {
private static final Logger LOG = getLogger(ApiHandler.class.getSimpleName());
private static final Logger LOG = getLogger(ApiHandler.class.getSimpleName());
private static final String SLUG = "slug";
private static final String DESCRIPTION = "description";
private static final String TITLE = "title";
private static final String START = "start";
private static final String END = "end";
private static final String LOCATION = "location";
private final Database db;
public ApiHandler(Database db) {
@@ -41,58 +49,106 @@ public class ApiHandler extends PathHandler {
}
private boolean listTags(HttpExchange ex, Map<String, String> params) throws IOException {
var infix = params.get("infix");
if (infix == null)return sendContent(ex,Error.of("No infix set in method call parameters"));
var res = db.findTags(infix).map(ApiHandler::sortTags);
return sendContent(ex,res);
@Override
public boolean doPost(String path, HttpExchange ex) throws IOException {
return switch (path) {
case "/event/edit" -> editEvent(ex);
default -> PathHandler.notFound(ex);
};
}
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;
private boolean editEvent(HttpExchange ex) throws IOException {
var json = json(ex);
// spotless:off
var slug = json.has(SLUG) ? nullIfEmpty(json.getString(SLUG)) : null;
// spotless:on
if (slug == null) sendContent(ex, Error.of("No slug value in appointment"));
var existingAppointment = db.loadEvent(slug);
return switch (existingAppointment) {
case Payload<Appointment> payload //
-> update(ex, payload.get(), json);
case Error<Appointment> err //
-> err.toString().startsWith("Failed to find appointment with slug") ? createEvent(ex, json):
sendContent(ex, err);
default -> serverError(ex, existingAppointment);
};
}
return Payload.of(list);
}
private boolean listEvents(HttpExchange ex, Map<String, String> params) throws IOException {
var start = nullable(params.get("start")).map(ApiHandler::toLocalDateTime).orElse(null);
var end = nullable(params.get("end")).map(ApiHandler::toLocalDateTime).orElse(null);
try {
return PathHandler.sendContent(ex,db.list(start, end).stream().map(Appointment::json).toList());
} catch (SQLException e) {
LOG.log(WARNING,"Failed to fetch events (start = {0}, end = {1}!",start,end,e);
private boolean createEvent(HttpExchange ex, JSONObject json) throws IOException {
var description = json.has(DESCRIPTION) ? nullIfEmpty(json.getString(DESCRIPTION)) : null;
var title = json.has(TITLE) ? nullIfEmpty(json.getString(TITLE)) : null;
if (title == null) return sendContent(ex, Error.of("title missing"));
var start = json.has(START) ? nullIfEmpty(json.getString(START)) : null;
if (start == null) return sendContent(ex, Error.of("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 sendContent(ex, Error.of("location missing"));
var clientSlug = json.has(SLUG) ? json.getString(SLUG) : null;
var serverSlug = Database.slug(location, startDate);
if (!serverSlug.equals(clientSlug)) return sendContent(ex, Error.of("Slug mismatch!"));
var event = new BaseAppointment(0, title, description, startDate, endDate, location, serverSlug);
db.add(event);
return sendContent(ex, Error.of("createEvent not implemented"));
}
return PathHandler.notFound(ex);
}
private boolean loadEvent(HttpExchange ex, Map<String, String> params) throws IOException {
var id = params.get("id");
if (id != null) try {
return PathHandler.sendContent(ex,db.loadEvent(Long.parseLong(id)).map(ApiHandler::toJson));
} catch (NumberFormatException | IOException nfe){
return PathHandler.sendContent(ex, Error.format("%s is not a numeric event id!",id));
private boolean update(HttpExchange ex, Appointment event, JSONObject json) throws IOException {
return sendContent(ex, Error.of("update not implemented"));
}
return PathHandler.sendContent(ex,Error.of("ID missing"));
}
private static Result<JSONObject> toJson(Result<Appointment> res) {
var opt = res.optional();
if (opt.isEmpty()) return Result.transform(res);
return Payload.of(opt.get().json());
}
private boolean listTags(HttpExchange ex, Map<String, String> params) throws IOException {
var infix = params.get("infix");
if (infix == null) return sendContent(ex, Error.of("No infix set in method call parameters"));
var res = db.findTags(infix).map(ApiHandler::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);
}
private boolean listEvents(HttpExchange ex, Map<String, String> params) throws IOException {
var start = nullable(params.get("start")).map(ApiHandler::toLocalDateTime).orElse(null);
var end = nullable(params.get("end")).map(ApiHandler::toLocalDateTime).orElse(null);
try {
return PathHandler.sendContent(ex, db.list(start, end).stream().map(Appointment::json).toList());
} catch (SQLException e) {
LOG.log(WARNING, "Failed to fetch events (start = {0}, end = {1}!", start, end, e);
}
return PathHandler.notFound(ex);
}
private boolean loadEvent(HttpExchange ex, Map<String, String> params) throws IOException {
var id = params.get("id");
if (id != null) try {
return PathHandler.sendContent(ex, db.loadEvent(Long.parseLong(id)).map(ApiHandler::toJson));
} catch (NumberFormatException | IOException nfe) {
return PathHandler.sendContent(ex, Error.format("%s is not a numeric event id!", id));
}
return PathHandler.sendContent(ex, Error.of("ID missing"));
}
private static Result<JSONObject> toJson(Result<Appointment> res) {
var opt = res.optional();
if (opt.isEmpty()) return Result.transform(res);
return Payload.of(opt.get().json());
}
private static LocalDateTime toLocalDateTime(String dateString) {
try {
return LocalDate.parse(dateString + "-01", DateTimeFormatter.ISO_LOCAL_DATE).atTime(0, 0);
} catch (Exception e) {
return null;
private static LocalDateTime toLocalDateTime(String dateString) {
try {
return LocalDate.parse(dateString + "-01", DateTimeFormatter.ISO_LOCAL_DATE).atTime(0, 0);
} catch (Exception e) {
return null;
}
}
}
}
@@ -34,10 +34,7 @@ function addImage(evt){
async function handleTags(response){
if (response.ok){
var tags = await response.json();
if (tags.length == 0){
console.log("no proposals!");
return;
}
if (tags.length == 0) return;
var select = element('proposals');
var input = element('tags-input');
if (!select) return;
@@ -78,7 +75,6 @@ function tagKeyPress(e){
var btn = element('save');
btn.removeAttribute("disabled");
btn.onclick= function(){ saveEvent(); };
console.log(btn);
} else {
if (keyTimer != null) clearTimeout(keyTimer);
setTimeout(() => fetchTags(input.value),500);
@@ -117,7 +113,36 @@ function getAttachments(){
return urls;
}
function saveEvent(){
function camelCase(text) {
if (text == null) {
return null;
} else {
var slug = '';
var upper = false;
for(var i = 0; i < text.length; i++) {
var c = text.charAt(i);
if (c == ' ') {
upper = true;
} else {
slug += upper ? c.toUpperCase() : c;
upper = false;
}
}
return slug;
}
}
async function slug(start,location){
const hash = await crypto.subtle.digest("SHA-256", (new TextEncoder()).encode(`${start}@${location}`));
return btoa(String.fromCharCode(...new Uint8Array(hash)));
}
async function saveEvent(){
var location = element('location').value;
var start = element('start').value;
var slugVal = await slug(start,location);
var event = {
title : element('title').value,
description : element('description').value,
@@ -127,7 +152,14 @@ function saveEvent(){
tags: getTags(),
links: getLinks(),
coords: element('coords').value,
attachments: getAttachments()
attachments: getAttachments(),
slug: slugVal
};
console.log(event);
fetch('/api/event/edit',{
method: 'POST',
body: JSON.stringify(event),
headers: {
'Content-Type' : 'appication/json'
}
});
}