completed CRUD operations
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
This commit is contained in:
@@ -70,6 +70,7 @@ public abstract class BaseImporter implements Importer {
|
|||||||
return Error.of("not implemented");
|
return Error.of("not implemented");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
protected Result<LocalDateTime> extractEnd(Tag eventTag) {
|
protected Result<LocalDateTime> extractEnd(Tag eventTag) {
|
||||||
Result<Tag> endTag = extractEndTag(eventTag);
|
Result<Tag> endTag = extractEndTag(eventTag);
|
||||||
if (endTag.optional().isEmpty()) return transform(endTag);
|
if (endTag.optional().isEmpty()) return transform(endTag);
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
/* © SRSoftware 2024 */
|
||||||
|
package de.srsoftware.cal;
|
||||||
|
|
||||||
|
import de.srsoftware.cal.api.Coords;
|
||||||
|
import de.srsoftware.tools.Error;
|
||||||
|
import de.srsoftware.tools.Payload;
|
||||||
|
import de.srsoftware.tools.Result;
|
||||||
|
|
||||||
|
public class Util {
|
||||||
|
public static Result<Coords> extractCoords(String coords) {
|
||||||
|
if (coords == null) return de.srsoftware.tools.Error.of("Argument is null");
|
||||||
|
if (coords.isBlank()) return de.srsoftware.tools.Error.of("Argument is blank");
|
||||||
|
var parts = coords.split(",");
|
||||||
|
if (parts.length != 2) return de.srsoftware.tools.Error.format("Argument has invalid format: %s", coords);
|
||||||
|
try {
|
||||||
|
var lat = Double.parseDouble(parts[0].trim());
|
||||||
|
var lon = Double.parseDouble(parts[1].trim());
|
||||||
|
return Payload.of(new Coords(lon, lat));
|
||||||
|
} catch (NumberFormatException nfe) {
|
||||||
|
return Error.of("Failed to parse coords from %s".formatted(coords), nfe);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,7 +4,7 @@ dependencies {
|
|||||||
implementation(project(":de.srsoftware.cal.api"))
|
implementation(project(":de.srsoftware.cal.api"))
|
||||||
implementation(project(":de.srsoftware.cal.base"))
|
implementation(project(":de.srsoftware.cal.base"))
|
||||||
|
|
||||||
implementation("de.srsoftware:tools.jdbc:1.1.2")
|
implementation("de.srsoftware:tools.jdbc:1.1.3")
|
||||||
implementation("de.srsoftware:tools.optionals:1.0.0")
|
implementation("de.srsoftware:tools.optionals:1.0.0")
|
||||||
implementation("de.srsoftware:tools.util:1.2.3")
|
implementation("de.srsoftware:tools.util:1.2.3")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,10 +2,13 @@
|
|||||||
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.Error;
|
||||||
import de.srsoftware.tools.Result;
|
import de.srsoftware.tools.Result;
|
||||||
import java.sql.SQLException;
|
import java.sql.SQLException;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Optional;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -19,6 +22,8 @@ public interface Database {
|
|||||||
*/
|
*/
|
||||||
public Result<Appointment> add(Appointment appointment);
|
public Result<Appointment> add(Appointment appointment);
|
||||||
|
|
||||||
|
Result<List<String>> findTags(String infix);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* list appointments unfiltered
|
* list appointments unfiltered
|
||||||
* @param count the maximum number of appointments to return
|
* @param count the maximum number of appointments to return
|
||||||
@@ -50,7 +55,7 @@ public interface Database {
|
|||||||
|
|
||||||
Result<Appointment> loadEvent(String location, LocalDateTime start);
|
Result<Appointment> loadEvent(String location, LocalDateTime start);
|
||||||
|
|
||||||
Result<List<String>> findTags(String infix);
|
Result<Long> removeAppointment(long id);
|
||||||
|
|
||||||
Result<Appointment> update(Appointment baseAppointment);
|
Result<Appointment> update(Appointment baseAppointment);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import static de.srsoftware.tools.jdbc.Query.*;
|
|||||||
import static java.lang.System.Logger.Level.*;
|
import static java.lang.System.Logger.Level.*;
|
||||||
|
|
||||||
import de.srsoftware.cal.BaseAppointment;
|
import de.srsoftware.cal.BaseAppointment;
|
||||||
|
import de.srsoftware.cal.Util;
|
||||||
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;
|
||||||
@@ -251,17 +252,25 @@ public class MariaDB implements Database {
|
|||||||
|
|
||||||
private Result<BaseAppointment> createAppointmentOf(ResultSet results) throws SQLException {
|
private Result<BaseAppointment> createAppointmentOf(ResultSet results) throws SQLException {
|
||||||
var id = results.getInt(AID);
|
var id = results.getInt(AID);
|
||||||
var title = results.getString("title");
|
var title = results.getString(TITLE);
|
||||||
var description = results.getString("description");
|
var description = results.getString(DESCRIPTION);
|
||||||
if (allEmpty(title, description)) return Error.format("Title and Description of appointment %s are empty", id);
|
if (allEmpty(title, description)) return Error.format("Title and Description of appointment %s are empty", id);
|
||||||
var start = results.getTimestamp("start").toLocalDateTime();
|
var start = results.getTimestamp(START).toLocalDateTime();
|
||||||
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 appointment = new BaseAppointment(id, title, description, start, end, location);
|
var appointment = new BaseAppointment(id, title, description, start, end, location);
|
||||||
|
|
||||||
|
try {
|
||||||
|
Util.extractCoords(results.getString(COORDS)).optional().ifPresent(appointment::coords);
|
||||||
|
} catch (SQLException e) {
|
||||||
|
LOG.log(WARNING, "Failed to read coordinates from database!");
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
var tags = nullIfEmpty(results.getString("tags"));
|
var tags = nullIfEmpty(results.getString("tags"));
|
||||||
if (tags != null) appointment.tags(tags.split(","));
|
if (tags != null) appointment.tags(tags.split(","));
|
||||||
} catch (SQLException e) {
|
} catch (SQLException e) {
|
||||||
|
LOG.log(WARNING, "Failed to read tags from database!");
|
||||||
}
|
}
|
||||||
return Payload.of(appointment);
|
return Payload.of(appointment);
|
||||||
}
|
}
|
||||||
@@ -290,6 +299,19 @@ public class MariaDB implements Database {
|
|||||||
return List.of();
|
return List.of();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Result<Long> removeAppointment(long id) {
|
||||||
|
try {
|
||||||
|
Query.delete().from(APPOINTMENTS).where(AID,equal(id)).execute(connection);
|
||||||
|
Query.delete().from(APPOINTMENT_TAGS).where(AID,equal(id)).execute(connection);
|
||||||
|
Query.delete().from(APPOINTMENT_ATTACHMENTS).where(AID,equal(id)).execute(connection);
|
||||||
|
Query.delete().from(APPOINTMENT_URLS).where(AID,equal(id)).execute(connection);
|
||||||
|
return Payload.of(id);
|
||||||
|
} catch (SQLException e) {
|
||||||
|
return Error.of("Failed to delete event %s".formatted(id),e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Result<Appointment> update(Appointment event) {
|
public Result<Appointment> update(Appointment event) {
|
||||||
var end = event.end().map(Timestamp::valueOf).orElse(null);
|
var end = event.end().map(Timestamp::valueOf).orElse(null);
|
||||||
|
|||||||
@@ -11,10 +11,8 @@ import com.sun.net.httpserver.HttpExchange;
|
|||||||
import de.srsoftware.cal.api.Appointment;
|
import de.srsoftware.cal.api.Appointment;
|
||||||
import de.srsoftware.cal.api.Link;
|
import de.srsoftware.cal.api.Link;
|
||||||
import de.srsoftware.cal.db.Database;
|
import de.srsoftware.cal.db.Database;
|
||||||
|
import de.srsoftware.tools.*;
|
||||||
import de.srsoftware.tools.Error;
|
import de.srsoftware.tools.Error;
|
||||||
import de.srsoftware.tools.PathHandler;
|
|
||||||
import de.srsoftware.tools.Payload;
|
|
||||||
import de.srsoftware.tools.Result;
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.sql.SQLException;
|
import java.sql.SQLException;
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
@@ -22,6 +20,7 @@ import java.time.LocalDateTime;
|
|||||||
import java.time.format.DateTimeFormatter;
|
import java.time.format.DateTimeFormatter;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
import org.json.JSONObject;
|
import org.json.JSONObject;
|
||||||
|
|
||||||
public class ApiHandler extends PathHandler {
|
public class ApiHandler extends PathHandler {
|
||||||
@@ -44,12 +43,42 @@ public class ApiHandler extends PathHandler {
|
|||||||
case "/tags" -> listTags(ex,params);
|
case "/tags" -> listTags(ex,params);
|
||||||
default -> PathHandler.notFound(ex);
|
default -> PathHandler.notFound(ex);
|
||||||
};
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean doPost(String path, HttpExchange ex) throws IOException {
|
public boolean doDelete(String path, HttpExchange ex) throws IOException {
|
||||||
|
var params = queryParam(ex);
|
||||||
return switch (path) {
|
return switch (path) {
|
||||||
|
case "/event" -> delete(ex,params);
|
||||||
|
default -> PathHandler.notFound(ex);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean delete(HttpExchange ex, Map<String,String> params) throws IOException {
|
||||||
|
var aid = params.get(AID);
|
||||||
|
if (aid == null) return sendContent(ex,Error.of("Missing appointment id"));
|
||||||
|
long id = 0;
|
||||||
|
try {
|
||||||
|
id = Long.parseLong(aid);
|
||||||
|
} catch (Exception e){
|
||||||
|
return sendContent(ex,Error.format("%s is not a valid id!",aid));
|
||||||
|
}
|
||||||
|
var json = json(ex);
|
||||||
|
var title = json.has(TITLE) ? nullIfEmpty(json.getString(TITLE)) :
|
||||||
|
null;
|
||||||
|
if (title == null) return sendContent(ex, Error.of("Missing appointment title"));
|
||||||
|
var res = db.loadEvent(id);
|
||||||
|
if (res.optional().isEmpty()) return sendContent(ex, res);
|
||||||
|
var event = res.optional().get();
|
||||||
|
if (!title.equals(event.title())) return sendContent(ex, Error.of("Title mismatch!"));
|
||||||
|
var del = db.removeAppointment(id);
|
||||||
|
if (del.optional().isEmpty()) return sendContent(ex,del);
|
||||||
|
return sendContent(ex,Map.of("deleted",del.optional().get()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean doPost(String path, HttpExchange ex) throws IOException {
|
||||||
|
return switch (path) {
|
||||||
case "/event/edit" -> editEvent(ex);
|
case "/event/edit" -> editEvent(ex);
|
||||||
default -> PathHandler.notFound(ex);
|
default -> PathHandler.notFound(ex);
|
||||||
};
|
};
|
||||||
@@ -58,129 +87,135 @@ public class ApiHandler extends PathHandler {
|
|||||||
// spotless:off
|
// spotless:off
|
||||||
private boolean editEvent(HttpExchange ex) throws IOException {
|
private boolean editEvent(HttpExchange ex) throws IOException {
|
||||||
var json = json(ex);
|
var json = json(ex);
|
||||||
var location = json.has(LOCATION) ? json.getString(LOCATION) : null;
|
Optional<Appointment> existingAppointment = Optional.empty();
|
||||||
var start = json.has(START) ? LocalDateTime.parse(json.getString(START)) : null;
|
Long aid = json.has(AID) ? json.getLong(AID) : null;
|
||||||
if (allSet(location, start)) {
|
if (aid != null) { // load appointment by aid
|
||||||
var existingAppointment = db.loadEvent(location, start).optional();
|
existingAppointment = db.loadEvent(aid).optional();
|
||||||
if (existingAppointment.isPresent()) {
|
} else { // try to load appointment by location @ time
|
||||||
var event = existingAppointment.get();
|
var location = json.has(LOCATION) ? json.getString(LOCATION) : null;
|
||||||
json.put(AID,event.id());
|
var start = json.has(START) ? LocalDateTime.parse(json.getString(START)) : null;
|
||||||
return update(ex,toEvent(json));
|
if (allSet(location, start)) {
|
||||||
|
existingAppointment = db.loadEvent(location, start).optional();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (existingAppointment.isPresent()) {
|
||||||
|
var event = existingAppointment.get();
|
||||||
|
json.put(AID,event.id());
|
||||||
|
return update(ex,toEvent(json));
|
||||||
|
}
|
||||||
return createEvent(ex, json);
|
return createEvent(ex, json);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// spotless:on
|
// spotless:on
|
||||||
|
|
||||||
private Result<BaseAppointment> toEvent(JSONObject json) {
|
private Result<BaseAppointment> toEvent(JSONObject json) {
|
||||||
var description = json.has(DESCRIPTION) ? nullIfEmpty(json.getString(DESCRIPTION)) : null;
|
var description = json.has(DESCRIPTION) ? nullIfEmpty(json.getString(DESCRIPTION)) : null;
|
||||||
var title = json.has(TITLE) ? nullIfEmpty(json.getString(TITLE)) : null;
|
var title = json.has(TITLE) ? nullIfEmpty(json.getString(TITLE)) : null;
|
||||||
if (title == null) return Error.of("title missing");
|
if (title == null) return Error.of("title missing");
|
||||||
var start = json.has(START) ? nullIfEmpty(json.getString(START)) : null;
|
var start = json.has(START) ? nullIfEmpty(json.getString(START)) : null;
|
||||||
if (start == null) return Error.of("start missing");
|
if (start == null) return Error.of("start missing");
|
||||||
var startDate = nullable(start).map(dt -> LocalDateTime.parse(dt, ISO_DATE_TIME)).orElse(null);
|
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 end = json.has(END) ? nullIfEmpty(json.getString(END)) : null;
|
||||||
var endDate = nullable(end).map(dt -> LocalDateTime.parse(dt, ISO_DATE_TIME)).orElse(null);
|
var endDate = nullable(end).map(dt -> LocalDateTime.parse(dt, ISO_DATE_TIME)).orElse(null);
|
||||||
var location = json.has(LOCATION) ? json.getString(LOCATION) : null;
|
var location = json.has(LOCATION) ? json.getString(LOCATION) : null;
|
||||||
if (location == null) return Error.of("location missing");
|
if (location == null) return Error.of("location missing");
|
||||||
var aid = json.has(AID) ? json.getLong(AID) : 0;
|
var aid = json.has(AID) ? json.getLong(AID) : 0;
|
||||||
var event = new BaseAppointment(aid, title, description, startDate, endDate, location);
|
var event = new BaseAppointment(aid, title, description, startDate, endDate, location);
|
||||||
if (json.has(ATTACHMENTS)) {
|
if (json.has(ATTACHMENTS)) {
|
||||||
json.getJSONArray(ATTACHMENTS).forEach(att -> {
|
json.getJSONArray(ATTACHMENTS).forEach(att -> {
|
||||||
Payload //
|
Payload //
|
||||||
.of(att.toString())
|
.of(att.toString())
|
||||||
.map(BaseImporter::url)
|
.map(BaseImporter::url)
|
||||||
.map(BaseImporter::toAttachment)
|
.map(BaseImporter::toAttachment)
|
||||||
.optional()
|
.optional()
|
||||||
.ifPresent(event::add);
|
.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 boolean createEvent(HttpExchange ex, JSONObject json) throws IOException {
|
|
||||||
var eventRes = toEvent(json);
|
|
||||||
if (eventRes.optional().isPresent()) {
|
|
||||||
return sendContent(ex, db.add(eventRes.optional().get()).map(ApiHandler::toJson));
|
|
||||||
}
|
|
||||||
return sendContent(ex, eventRes);
|
|
||||||
}
|
|
||||||
|
|
||||||
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.of("Failed to create link from %s".formatted(json), e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean update(HttpExchange ex, Result<BaseAppointment> event) throws IOException {
|
|
||||||
if (event.optional().isPresent()) return sendContent(ex, db.update(event.optional().get()).map(ApiHandler::toJson));
|
|
||||||
return sendContent(ex, event);
|
|
||||||
}
|
|
||||||
|
|
||||||
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"));
|
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<JSONObject> toJson(Result<Appointment> res) {
|
private boolean createEvent(HttpExchange ex, JSONObject json) throws IOException {
|
||||||
var opt = res.optional();
|
var eventRes = toEvent(json);
|
||||||
if (opt.isEmpty()) return Result.transform(res);
|
if (eventRes.optional().isPresent()) {
|
||||||
return Payload.of(opt.get().json());
|
return sendContent(ex, db.add(eventRes.optional().get()).map(ApiHandler::toJson));
|
||||||
}
|
}
|
||||||
|
return sendContent(ex, eventRes);
|
||||||
|
}
|
||||||
|
|
||||||
|
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.of("Failed to create link from %s".formatted(json), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean update(HttpExchange ex, Result<BaseAppointment> event) throws IOException {
|
||||||
|
if (event.optional().isPresent()) return sendContent(ex, db.update(event.optional().get()).map(ApiHandler::toJson));
|
||||||
|
return sendContent(ex, event);
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|||||||
@@ -9,9 +9,13 @@
|
|||||||
<script src="/static/script/leaflet.js"></script>
|
<script src="/static/script/leaflet.js"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<h1>Create new event</h1>
|
<h1>Create/Edit event</h1>
|
||||||
<button id="save" disabled>save</button>
|
<span id="buttons">
|
||||||
|
<button id="save" disabled>save</button>
|
||||||
|
<button id="cancel" onclick="location.href = location.href.replace('/static/edit','').replace('copy=','id=')">cancel</button>
|
||||||
|
</span>
|
||||||
<div class="form">
|
<div class="form">
|
||||||
|
<input type="hidden" id="aid" />
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<legend>basic data</legend>
|
<legend>basic data</legend>
|
||||||
<table>
|
<table>
|
||||||
@@ -113,5 +117,6 @@
|
|||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
<script>document.addEventListener("DOMContentLoaded", loadEvent);</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@@ -5,20 +5,37 @@
|
|||||||
<link rel="stylesheet" href="occ.css" />
|
<link rel="stylesheet" href="occ.css" />
|
||||||
<script src="/static/script/common.js"></script>
|
<script src="/static/script/common.js"></script>
|
||||||
<script src="/static/script/event.js"></script>
|
<script src="/static/script/event.js"></script>
|
||||||
|
<link rel="stylesheet" href="/static/leaflet.css" />
|
||||||
|
<script src="/static/script/leaflet.js"></script>
|
||||||
</head>
|
</head>
|
||||||
<body class="event">
|
<body class="event">
|
||||||
<nav />
|
<nav />
|
||||||
<h1>Loading…</h1>
|
<h1 id="title">Loading…</h1>
|
||||||
|
<span id="buttons">
|
||||||
|
<button title="create a copy of this event" onclick="window.top.location.href = location.href.replace('/static/event','/static/edit').replace('id=','copy=')">⧉ copy</button>
|
||||||
|
<button title="edit the details of this event" onclick="window.top.location.href = location.href.replace('/static/event','/static/edit')">✍ edit</button>
|
||||||
|
<button title="delete this event" onclick="confirmDelete()">🗑 delete</button>
|
||||||
|
</span>
|
||||||
<div id="time">Loading time…</div>
|
<div id="time">Loading time…</div>
|
||||||
<div id="description">Loading description…</div>
|
<div id="description">Loading description…</div>
|
||||||
<div id="links">Loading links…</div>
|
<div id="links">Loading links…</div>
|
||||||
<div id="attachments">Loading attachments…</div>
|
<div id="attachments">Loading attachments…</div>
|
||||||
<div id="tags">Loading tags…</div>
|
<div id="tags">Loading tags…</div>
|
||||||
|
<div id="map" style="width: 100%; height: 500px;"></div>
|
||||||
<script>
|
<script>
|
||||||
|
|
||||||
|
var map = L.map('map').setView([51.160556, 10.4425], 9);
|
||||||
|
L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||||
|
maxZoom: 19,
|
||||||
|
attribution: '© <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>'
|
||||||
|
}).addTo(map);
|
||||||
|
|
||||||
document.addEventListener("DOMContentLoaded", function(event){
|
document.addEventListener("DOMContentLoaded", function(event){
|
||||||
console.log("page loaded…");
|
console.log("page loaded…");
|
||||||
loadEventData();
|
loadEventData();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@@ -27,16 +27,14 @@
|
|||||||
|
|
||||||
</th>
|
</th>
|
||||||
<th>End</th>
|
<th>End</th>
|
||||||
<th>Location</th>
|
|
||||||
<th>Title</th>
|
<th>Title</th>
|
||||||
|
<th>Location</th>
|
||||||
<th>Tags <span id="tag_selection"></span></th>
|
<th>Tags <span id="tag_selection"></span></th>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
<script>
|
<script>
|
||||||
document.addEventListener("DOMContentLoaded", function(event){
|
document.addEventListener("DOMContentLoaded", loadCurrentEvents);
|
||||||
loadCurrentEvents();
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
<div id="overlay">
|
<div id="overlay">
|
||||||
|
|
||||||
|
|||||||
@@ -48,14 +48,15 @@ body.event {
|
|||||||
position: fixed;
|
position: fixed;
|
||||||
top: 20px;
|
top: 20px;
|
||||||
right: 20px;
|
right: 20px;
|
||||||
height: 20px;
|
height: 25px;
|
||||||
width: 20px;
|
width: 25px;
|
||||||
background: chocolate;
|
background: chocolate;
|
||||||
z-index: 1000;
|
z-index: 1000;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
border: 1px solid yellow;
|
border: 1px solid yellow;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
font-size: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#tag_selection {
|
#tag_selection {
|
||||||
@@ -127,15 +128,22 @@ table#eventlist{
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
#save {
|
#buttons {
|
||||||
min-width: 100px;
|
|
||||||
min-height: 50px;
|
|
||||||
font-size: 18px;
|
|
||||||
font-weight: bold;
|
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 15px;
|
display: block;
|
||||||
left: 50%;
|
text-align: center;
|
||||||
|
top: 10px;
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#cancel,
|
||||||
|
#save {
|
||||||
|
min-width: 80px;
|
||||||
|
min-height: 30px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
.error {
|
.error {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 0;
|
left: 0;
|
||||||
|
|||||||
@@ -155,8 +155,10 @@ async function handleSave(response){
|
|||||||
if (response.ok){
|
if (response.ok){
|
||||||
var json = await response.json();
|
var json = await response.json();
|
||||||
var url = window.location.href.replace('/static/edit','')
|
var url = window.location.href.replace('/static/edit','')
|
||||||
if (json.id) url+='?highlight='+json.id;
|
let pos = url.indexOf('?');
|
||||||
window.location.href = url; // TODO: add highlight on new event
|
if (pos>0) url = url.substring(0,pos);
|
||||||
|
if (json.id) url+='?id='+json.id;
|
||||||
|
window.location.href = url;
|
||||||
} else {
|
} else {
|
||||||
var json = await response.json();
|
var json = await response.json();
|
||||||
if (json.error) showError(json.error);
|
if (json.error) showError(json.error);
|
||||||
@@ -169,6 +171,7 @@ async function saveEvent(){
|
|||||||
var location = element('location').value;
|
var location = element('location').value;
|
||||||
var start = element('start').value;
|
var start = element('start').value;
|
||||||
var event = {
|
var event = {
|
||||||
|
aid : element('aid').value,
|
||||||
title : element('title').value,
|
title : element('title').value,
|
||||||
description : element('description').value,
|
description : element('description').value,
|
||||||
location : element('location').value,
|
location : element('location').value,
|
||||||
@@ -186,4 +189,53 @@ async function saveEvent(){
|
|||||||
'Content-Type' : 'appication/json'
|
'Content-Type' : 'appication/json'
|
||||||
}
|
}
|
||||||
}).then(handleSave);
|
}).then(handleSave);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function distributeEventData(response){
|
||||||
|
if (response.ok){
|
||||||
|
let event = await response.json();
|
||||||
|
console.log({'event-data':event});
|
||||||
|
var keys = ['title','location','description'];
|
||||||
|
if (element('aid').value) {
|
||||||
|
keys.push('start');
|
||||||
|
keys.push('end');
|
||||||
|
}
|
||||||
|
for (var key of keys){
|
||||||
|
if (event[key]) element(key).value = event[key];
|
||||||
|
}
|
||||||
|
if (event.coords){
|
||||||
|
element('coords').value = event.coords.lat + ', ' + event.coords.lon;
|
||||||
|
marker.setLatLng([event.coords.lat, event.coords.lon]);
|
||||||
|
map.setView([event.coords.lat, event.coords.lon], 17);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
var linkList = element('link-list');
|
||||||
|
|
||||||
|
for (var link of event.links){
|
||||||
|
var span = document.createElement('span');
|
||||||
|
span.innerHTML = `<a href="${link.url}">${link.description}</a><a onclick="removeParent(this)">❌</a> `;
|
||||||
|
linkList.appendChild(span);
|
||||||
|
}
|
||||||
|
|
||||||
|
var imgList = element('attachment-list');
|
||||||
|
for (var image of event.attachments){
|
||||||
|
var span = document.createElement('span');
|
||||||
|
span.innerHTML = `<img src="${image.url}" /><a onclick="removeParent(this)">❌</a> `;
|
||||||
|
imgList.appendChild(span);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.tags){
|
||||||
|
for (var tag of event.tags) addTag(tag);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadEvent(){
|
||||||
|
let params = new URLSearchParams(window.location.search);
|
||||||
|
var aid = params.get('id');
|
||||||
|
if (aid){
|
||||||
|
element('aid').value = aid;
|
||||||
|
} else aid = params.get('copy');
|
||||||
|
if (aid) fetch('/api/event?id='+aid).then(distributeEventData);
|
||||||
}
|
}
|
||||||
@@ -17,17 +17,21 @@ function attachmentList(json){
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function handleEventData(response){
|
async function handleEventData(response){
|
||||||
if (response.ok){
|
if (response.ok){
|
||||||
var json = await response.json();
|
var event = await response.json();
|
||||||
document.getElementsByTagName('h1')[0].innerHTML = json.title ? json.title : '';
|
document.getElementsByTagName('h1')[0].innerHTML = event.title ? event.title : '';
|
||||||
document.getElementById('time').innerHTML = json.start + (json.end ? '…'+json.end : '');
|
document.getElementById('time').innerHTML = event.start + (event.end ? '…'+event.end : '');
|
||||||
addDescription(json);
|
addDescription(event);
|
||||||
document.getElementById('tags').innerHTML = "Tags: "+json.tags.join(" ");
|
document.getElementById('tags').innerHTML = "Tags: "+event.tags.join(" ");
|
||||||
var links = document.getElementById('links');
|
var links = document.getElementById('links');
|
||||||
links.innerHTML = "";
|
links.innerHTML = "";
|
||||||
links.appendChild(linkList(json.links));
|
links.appendChild(linkList(event.links));
|
||||||
attachmentList(json.attachments);
|
attachmentList(event.attachments);
|
||||||
}
|
if (event.coords){
|
||||||
|
L.marker([event.coords.lat, event.coords.lon]).addTo(map);
|
||||||
|
map.setView([event.coords.lat, event.coords.lon], 17);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function linkList(json){
|
function linkList(json){
|
||||||
@@ -50,3 +54,28 @@ function loadEventData(){
|
|||||||
if (id) fetch('/api/event?id='+id).then(handleEventData);
|
if (id) fetch('/api/event?id='+id).then(handleEventData);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function handleDeleted(response){
|
||||||
|
if (response.ok){
|
||||||
|
console.log(location.href);
|
||||||
|
var url = location.href.replace('/static/event','/');
|
||||||
|
console.log(url);
|
||||||
|
var pos = url.indexOf('?');
|
||||||
|
if (pos>0) url = url.substring(0,pos);
|
||||||
|
console.log(url);
|
||||||
|
window.top.location.href = url;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function confirmDelete(){
|
||||||
|
var title = element('title').innerHTML;
|
||||||
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
|
var id = urlParams.get('id');
|
||||||
|
if (confirm(`Do you really want to delete "${title}"?`)){
|
||||||
|
fetch('/api/event?aid='+id,{
|
||||||
|
method: 'DELETE',
|
||||||
|
body: JSON.stringify({
|
||||||
|
title: title
|
||||||
|
})
|
||||||
|
}).then(handleDeleted);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -23,8 +23,8 @@ function addRow(json){
|
|||||||
addCell(row,json.id,json.id);
|
addCell(row,json.id,json.id);
|
||||||
addCell(row,json.start,json.id);
|
addCell(row,json.start,json.id);
|
||||||
addCell(row,json.end,json.id);
|
addCell(row,json.end,json.id);
|
||||||
addCell(row,json.location,json.id);
|
|
||||||
addCell(row,json.title,json.id);
|
addCell(row,json.title,json.id);
|
||||||
|
addCell(row,json.location,json.id);
|
||||||
row.appendChild(createTags(json.tags));
|
row.appendChild(createTags(json.tags));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -80,15 +80,17 @@ async function handleEvents(response){
|
|||||||
updateTagVisibility();
|
updateTagVisibility();
|
||||||
if (highlight){
|
if (highlight){
|
||||||
var row = element(highlight);
|
var row = element(highlight);
|
||||||
row.classList.add('highlight');
|
if (row) {
|
||||||
row.scrollIntoView({behavior: 'smooth',block: 'center'});
|
row.classList.add('highlight');
|
||||||
|
row.scrollIntoView({behavior: 'smooth',block: 'center'});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadCurrentEvents(){
|
function loadCurrentEvents(){
|
||||||
let params = new URLSearchParams(location.search);
|
let params = new URLSearchParams(location.search);
|
||||||
highlight = params.get('highlight');
|
highlight = params.get('id');
|
||||||
if (start == null){
|
if (start == null){
|
||||||
var now = new Date();
|
var now = new Date();
|
||||||
var year = now.getFullYear();
|
var year = now.getFullYear();
|
||||||
@@ -106,10 +108,11 @@ function showOverlay(url){
|
|||||||
div.appendChild(iframe);
|
div.appendChild(iframe);
|
||||||
div.style.display = 'block';
|
div.style.display = 'block';
|
||||||
div.onclick = e => div.style.display = 'none';
|
div.onclick = e => div.style.display = 'none';
|
||||||
var btn = document.createElement('div');
|
var closeBtn = document.createElement('div');
|
||||||
btn.id = 'close_overlay';
|
closeBtn.id = 'close_overlay';
|
||||||
btn.innerHTML = '✖';
|
closeBtn.innerHTML = '✖';
|
||||||
div.appendChild(btn);
|
closeBtn.title = 'close overlay';
|
||||||
|
div.appendChild(closeBtn);
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleTag(tag){
|
function toggleTag(tag){
|
||||||
|
|||||||
Reference in New Issue
Block a user