preparing save/update of appointment
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
This commit is contained in:
@@ -13,6 +13,7 @@ spotless {
|
|||||||
importOrder()
|
importOrder()
|
||||||
clangFormat("18.1.8").style("file:config/clang-format")
|
clangFormat("18.1.8").style("file:config/clang-format")
|
||||||
licenseHeader("/* © SRSoftware 2024 */")
|
licenseHeader("/* © SRSoftware 2024 */")
|
||||||
|
toggleOffOn()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,9 @@
|
|||||||
package de.srsoftware.cal.db;
|
package de.srsoftware.cal.db;
|
||||||
|
|
||||||
import de.srsoftware.cal.api.Appointment;
|
import de.srsoftware.cal.api.Appointment;
|
||||||
|
import de.srsoftware.tools.Calc;
|
||||||
import de.srsoftware.tools.Result;
|
import de.srsoftware.tools.Result;
|
||||||
|
import de.srsoftware.tools.Strings;
|
||||||
import java.sql.SQLException;
|
import java.sql.SQLException;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -48,5 +50,11 @@ public interface Database {
|
|||||||
|
|
||||||
Result<Appointment> loadEvent(long id);
|
Result<Appointment> loadEvent(long id);
|
||||||
|
|
||||||
|
Result<Appointment> loadEvent(String slug);
|
||||||
|
|
||||||
Result<List<String>> findTags(String infix);
|
Result<List<String>> findTags(String infix);
|
||||||
|
|
||||||
|
public static String slug(String location, LocalDateTime start) {
|
||||||
|
return Calc.sha256(start + "@" + location).map(Strings::base64).orElse(null);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
/* © SRSoftware 2024 */
|
/* © SRSoftware 2024 */
|
||||||
package de.srsoftware.cal.db;
|
package de.srsoftware.cal.db;
|
||||||
|
|
||||||
|
import static de.srsoftware.cal.db.Database.slug;
|
||||||
|
import static de.srsoftware.tools.NotImplemented.notImplemented;
|
||||||
import static de.srsoftware.tools.Optionals.*;
|
import static de.srsoftware.tools.Optionals.*;
|
||||||
import static de.srsoftware.tools.Result.transform;
|
import static de.srsoftware.tools.Result.transform;
|
||||||
import static de.srsoftware.tools.Strings.camelCase;
|
|
||||||
import static de.srsoftware.tools.jdbc.Condition.*;
|
import static de.srsoftware.tools.jdbc.Condition.*;
|
||||||
import static de.srsoftware.tools.jdbc.Query.MARK;
|
import static de.srsoftware.tools.jdbc.Query.MARK;
|
||||||
import static java.lang.System.Logger.Level.*;
|
import static java.lang.System.Logger.Level.*;
|
||||||
@@ -12,7 +13,6 @@ import de.srsoftware.cal.BaseAppointment;
|
|||||||
import de.srsoftware.cal.api.Appointment;
|
import de.srsoftware.cal.api.Appointment;
|
||||||
import de.srsoftware.cal.api.Attachment;
|
import de.srsoftware.cal.api.Attachment;
|
||||||
import de.srsoftware.cal.api.Link;
|
import de.srsoftware.cal.api.Link;
|
||||||
import de.srsoftware.tools.Calc;
|
|
||||||
import de.srsoftware.tools.Error;
|
import de.srsoftware.tools.Error;
|
||||||
import de.srsoftware.tools.Payload;
|
import de.srsoftware.tools.Payload;
|
||||||
import de.srsoftware.tools.Result;
|
import de.srsoftware.tools.Result;
|
||||||
@@ -40,6 +40,7 @@ public class MariaDB implements Database {
|
|||||||
private static final String MIME = "mime";
|
private static final String MIME = "mime";
|
||||||
private static final String APPOINTMENT_ATTACHMENTS = "appointment_attachments";
|
private static final String APPOINTMENT_ATTACHMENTS = "appointment_attachments";
|
||||||
private static final String TAGS = "tags";
|
private static final String TAGS = "tags";
|
||||||
|
private static final String SLUG = "slug";
|
||||||
private static Connection connection;
|
private static Connection connection;
|
||||||
|
|
||||||
private MariaDB(Connection conn) throws SQLException {
|
private MariaDB(Connection conn) throws SQLException {
|
||||||
@@ -82,10 +83,9 @@ public class MariaDB implements Database {
|
|||||||
LOG.log(TRACE, () -> "%s: %s".formatted(id, title));
|
LOG.log(TRACE, () -> "%s: %s".formatted(id, title));
|
||||||
var descr = rs.getString("description");
|
var descr = rs.getString("description");
|
||||||
if (allEmpty(title, descr)) continue;
|
if (allEmpty(title, descr)) continue;
|
||||||
var start = nullable(rs.getTimestamp("start"));
|
var start = nullable(rs.getTimestamp("start")).map(Timestamp::toLocalDateTime);
|
||||||
if (start.isEmpty()) continue;
|
if (start.isEmpty()) continue;
|
||||||
var slug = "%s@%s".formatted(start.get().toLocalDateTime(), camelCase(location.get().replace(",", "")));
|
var slug = slug(location.get(), start.get());
|
||||||
if (slug.length() > 250) slug = slug.substring(0, 250);
|
|
||||||
slugMap.put(id, slug);
|
slugMap.put(id, slug);
|
||||||
}
|
}
|
||||||
rs.close();
|
rs.close();
|
||||||
@@ -107,7 +107,7 @@ public class MariaDB implements Database {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Database add(Appointment appointment) {
|
public Database add(Appointment appointment) {
|
||||||
return null;
|
throw notImplemented(this, "add(Appointment)");
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Database connect(String jdbc, String user, String pass) throws SQLException {
|
public static Database connect(String jdbc, String user, String pass) throws SQLException {
|
||||||
@@ -148,6 +148,18 @@ public class MariaDB implements Database {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Result<Appointment> loadEvent(String slug) {
|
||||||
|
try {
|
||||||
|
var rs = Query.select(ALL).from(APPOINTMENTS).where(SLUG, equal(slug)).exec(connection);
|
||||||
|
Result<Appointment> result = rs.next() ? createAppointmentOf(rs).map(MariaDB::loadExtra) : Error.format("Failed to find appointment with slug %s", slug);
|
||||||
|
rs.close();
|
||||||
|
return result;
|
||||||
|
} catch (SQLException e) {
|
||||||
|
return Error.of("Failed to load appointment with slug = %s".formatted(slug), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static Result<Appointment> loadExtra(Result<BaseAppointment> res) {
|
private static Result<Appointment> loadExtra(Result<BaseAppointment> res) {
|
||||||
return loadTags(res).map(MariaDB::loadLinks).map(MariaDB::loadAttachments);
|
return loadTags(res).map(MariaDB::loadLinks).map(MariaDB::loadAttachments);
|
||||||
}
|
}
|
||||||
@@ -221,7 +233,7 @@ public class MariaDB implements Database {
|
|||||||
var end = nullable(results.getTimestamp("end")).map(Timestamp::toLocalDateTime).orElse(null);
|
var end = nullable(results.getTimestamp("end")).map(Timestamp::toLocalDateTime).orElse(null);
|
||||||
var location = results.getString("location");
|
var location = results.getString("location");
|
||||||
var slug = results.getString("slug");
|
var slug = results.getString("slug");
|
||||||
if (slug == null) slug = Calc.hash(start + "@" + location).orElse(null);
|
if (slug == null) slug = slug(location, start);
|
||||||
var appointment = new BaseAppointment(id, title, description, start, end, location, slug);
|
var appointment = new BaseAppointment(id, title, description, start, end, location, slug);
|
||||||
try {
|
try {
|
||||||
var tags = nullIfEmpty(results.getString("tags"));
|
var tags = nullIfEmpty(results.getString("tags"));
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ description = "OpenCloudCal : Web"
|
|||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation(project(":de.srsoftware.cal.api"))
|
implementation(project(":de.srsoftware.cal.api"))
|
||||||
|
implementation(project(":de.srsoftware.cal.base"))
|
||||||
implementation(project(":de.srsoftware.cal.db"))
|
implementation(project(":de.srsoftware.cal.db"))
|
||||||
|
|
||||||
implementation("de.srsoftware:tools.http:1.0.4")
|
implementation("de.srsoftware:tools.http:1.0.4")
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
/* © SRSoftware 2024 */
|
/* © SRSoftware 2024 */
|
||||||
package de.srsoftware.cal;
|
package de.srsoftware.cal;
|
||||||
|
|
||||||
|
import static de.srsoftware.tools.Optionals.nullIfEmpty;
|
||||||
import static de.srsoftware.tools.Optionals.nullable;
|
import static de.srsoftware.tools.Optionals.nullable;
|
||||||
import static java.lang.System.*;
|
import static java.lang.System.*;
|
||||||
import static java.lang.System.Logger.Level.WARNING;
|
import static java.lang.System.Logger.Level.WARNING;
|
||||||
|
import static java.time.format.DateTimeFormatter.ISO_DATE_TIME;
|
||||||
|
|
||||||
import com.sun.net.httpserver.HttpExchange;
|
import com.sun.net.httpserver.HttpExchange;
|
||||||
import de.srsoftware.cal.api.Appointment;
|
import de.srsoftware.cal.api.Appointment;
|
||||||
@@ -22,7 +24,13 @@ import java.util.Map;
|
|||||||
import org.json.JSONObject;
|
import org.json.JSONObject;
|
||||||
|
|
||||||
public class ApiHandler extends PathHandler {
|
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;
|
private final Database db;
|
||||||
|
|
||||||
public ApiHandler(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 {
|
@Override
|
||||||
var infix = params.get("infix");
|
public boolean doPost(String path, HttpExchange ex) throws IOException {
|
||||||
if (infix == null)return sendContent(ex,Error.of("No infix set in method call parameters"));
|
return switch (path) {
|
||||||
var res = db.findTags(infix).map(ApiHandler::sortTags);
|
case "/event/edit" -> editEvent(ex);
|
||||||
return sendContent(ex,res);
|
default -> PathHandler.notFound(ex);
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Result<List<String>> sortTags(Result<List<String>> listResult) {
|
private boolean editEvent(HttpExchange ex) throws IOException {
|
||||||
if (listResult.optional().isEmpty()) return listResult;
|
var json = json(ex);
|
||||||
List<String> list = listResult.optional().get();
|
// spotless:off
|
||||||
while (list.size() > 15) {
|
var slug = json.has(SLUG) ? nullIfEmpty(json.getString(SLUG)) : null;
|
||||||
int longest = list.stream().map(String::length).reduce(0, Integer::max);
|
// spotless:on
|
||||||
var subset = list.stream().filter(s -> s.length() < longest).toList();
|
if (slug == null) sendContent(ex, Error.of("No slug value in appointment"));
|
||||||
if (subset.size()<3) return Payload.of(list);
|
var existingAppointment = db.loadEvent(slug);
|
||||||
list = subset;
|
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 {
|
private boolean createEvent(HttpExchange ex, JSONObject json) throws IOException {
|
||||||
var start = nullable(params.get("start")).map(ApiHandler::toLocalDateTime).orElse(null);
|
var description = json.has(DESCRIPTION) ? nullIfEmpty(json.getString(DESCRIPTION)) : null;
|
||||||
var end = nullable(params.get("end")).map(ApiHandler::toLocalDateTime).orElse(null);
|
var title = json.has(TITLE) ? nullIfEmpty(json.getString(TITLE)) : null;
|
||||||
try {
|
if (title == null) return sendContent(ex, Error.of("title missing"));
|
||||||
return PathHandler.sendContent(ex,db.list(start, end).stream().map(Appointment::json).toList());
|
var start = json.has(START) ? nullIfEmpty(json.getString(START)) : null;
|
||||||
} catch (SQLException e) {
|
if (start == null) return sendContent(ex, Error.of("start missing"));
|
||||||
LOG.log(WARNING,"Failed to fetch events (start = {0}, end = {1}!",start,end,e);
|
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 {
|
private boolean update(HttpExchange ex, Appointment event, JSONObject json) throws IOException {
|
||||||
var id = params.get("id");
|
return sendContent(ex, Error.of("update not implemented"));
|
||||||
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) {
|
private boolean listTags(HttpExchange ex, Map<String, String> params) throws IOException {
|
||||||
var opt = res.optional();
|
var infix = params.get("infix");
|
||||||
if (opt.isEmpty()) return Result.transform(res);
|
if (infix == null) return sendContent(ex, Error.of("No infix set in method call parameters"));
|
||||||
return Payload.of(opt.get().json());
|
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) {
|
private static LocalDateTime toLocalDateTime(String dateString) {
|
||||||
try {
|
try {
|
||||||
return LocalDate.parse(dateString + "-01", DateTimeFormatter.ISO_LOCAL_DATE).atTime(0, 0);
|
return LocalDate.parse(dateString + "-01", DateTimeFormatter.ISO_LOCAL_DATE).atTime(0, 0);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
return null;
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|||||||
@@ -34,10 +34,7 @@ function addImage(evt){
|
|||||||
async function handleTags(response){
|
async function handleTags(response){
|
||||||
if (response.ok){
|
if (response.ok){
|
||||||
var tags = await response.json();
|
var tags = await response.json();
|
||||||
if (tags.length == 0){
|
if (tags.length == 0) return;
|
||||||
console.log("no proposals!");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
var select = element('proposals');
|
var select = element('proposals');
|
||||||
var input = element('tags-input');
|
var input = element('tags-input');
|
||||||
if (!select) return;
|
if (!select) return;
|
||||||
@@ -78,7 +75,6 @@ function tagKeyPress(e){
|
|||||||
var btn = element('save');
|
var btn = element('save');
|
||||||
btn.removeAttribute("disabled");
|
btn.removeAttribute("disabled");
|
||||||
btn.onclick= function(){ saveEvent(); };
|
btn.onclick= function(){ saveEvent(); };
|
||||||
console.log(btn);
|
|
||||||
} else {
|
} else {
|
||||||
if (keyTimer != null) clearTimeout(keyTimer);
|
if (keyTimer != null) clearTimeout(keyTimer);
|
||||||
setTimeout(() => fetchTags(input.value),500);
|
setTimeout(() => fetchTags(input.value),500);
|
||||||
@@ -117,7 +113,36 @@ function getAttachments(){
|
|||||||
return urls;
|
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 = {
|
var event = {
|
||||||
title : element('title').value,
|
title : element('title').value,
|
||||||
description : element('description').value,
|
description : element('description').value,
|
||||||
@@ -127,7 +152,14 @@ function saveEvent(){
|
|||||||
tags: getTags(),
|
tags: getTags(),
|
||||||
links: getLinks(),
|
links: getLinks(),
|
||||||
coords: element('coords').value,
|
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'
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user