working on calendar application
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
This commit is contained in:
@@ -2,4 +2,5 @@ description = "OpenCloudCal : API"
|
|||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation("de.srsoftware:tools.util:1.2.1")
|
implementation("de.srsoftware:tools.util:1.2.1")
|
||||||
|
implementation("org.json:json:20240303")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ package de.srsoftware.cal.api;
|
|||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
import org.json.JSONObject;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This is the central object of the calendar: An appointment
|
* This is the central object of the calendar: An appointment
|
||||||
@@ -41,6 +42,12 @@ public interface Appointment {
|
|||||||
long id();
|
long id();
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* get a json representation of this Appointment
|
||||||
|
* @return a JSON Object
|
||||||
|
*/
|
||||||
|
JSONObject json();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* descriptive text of the location, e.g. address
|
* descriptive text of the location, e.g. address
|
||||||
* @return location text
|
* @return location text
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
package de.srsoftware.cal.api;
|
package de.srsoftware.cal.api;
|
||||||
|
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
|
import java.util.Map;
|
||||||
|
import org.json.JSONObject;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* an attachment for appointments
|
* an attachment for appointments
|
||||||
@@ -9,4 +11,7 @@ import java.net.URL;
|
|||||||
* @param mime the mime type of the attached document
|
* @param mime the mime type of the attached document
|
||||||
*/
|
*/
|
||||||
public record Attachment(URL url, String mime) {
|
public record Attachment(URL url, String mime) {
|
||||||
|
public JSONObject json() {
|
||||||
|
return new JSONObject(Map.of("url", url.toString(), "mime", mime));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,16 @@
|
|||||||
/* © SRSoftware 2024 */
|
/* © SRSoftware 2024 */
|
||||||
package de.srsoftware.cal.api;
|
package de.srsoftware.cal.api;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
import org.json.JSONObject;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* cartesian coords
|
* cartesian coords
|
||||||
* @param longitude the longitude
|
* @param longitude the longitude
|
||||||
* @param latitude the latitude
|
* @param latitude the latitude
|
||||||
*/
|
*/
|
||||||
public record Coords(double longitude, double latitude) {
|
public record Coords(double longitude, double latitude) {
|
||||||
|
public JSONObject json() {
|
||||||
|
return new JSONObject(Map.of("lon", longitude, "lat", latitude));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
package de.srsoftware.cal.api;
|
package de.srsoftware.cal.api;
|
||||||
|
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
|
import java.util.Map;
|
||||||
|
import org.json.JSONObject;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Links are additional content that may be added to appointments
|
* Links are additional content that may be added to appointments
|
||||||
@@ -9,4 +11,7 @@ import java.net.URL;
|
|||||||
* @param desciption some information about the target of the link
|
* @param desciption some information about the target of the link
|
||||||
*/
|
*/
|
||||||
public record Link(URL url, String desciption) {
|
public record Link(URL url, String desciption) {
|
||||||
|
public JSONObject json() {
|
||||||
|
return new JSONObject(Map.of("description", desciption(), "url", url().toString()));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,8 +7,11 @@ dependencies {
|
|||||||
|
|
||||||
implementation("de.srsoftware:configuration.api:1.0.0")
|
implementation("de.srsoftware:configuration.api:1.0.0")
|
||||||
implementation("de.srsoftware:configuration.json:1.0.0")
|
implementation("de.srsoftware:configuration.json:1.0.0")
|
||||||
implementation("de.srsoftware:tools.http:1.0.1")
|
implementation("de.srsoftware:tools.http:1.0.3")
|
||||||
implementation("de.srsoftware:tools.logging:1.0.1")
|
implementation("de.srsoftware:tools.logging:1.0.1")
|
||||||
|
implementation("de.srsoftware:tools.optionals:1.0.0")
|
||||||
implementation("de.srsoftware:tools.util:1.2.1")
|
implementation("de.srsoftware:tools.util:1.2.1")
|
||||||
implementation("de.srsoftware:tools.web:1.3.4")
|
implementation("de.srsoftware:tools.web:1.3.8")
|
||||||
implementation("com.mysql:mysql-connector-j:9.1.0")}
|
implementation("com.mysql:mysql-connector-j:9.1.0")
|
||||||
|
implementation("org.json:json:20240303")
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,81 @@
|
|||||||
|
/* © SRSoftware 2024 */
|
||||||
|
package de.srsoftware.cal.app;
|
||||||
|
|
||||||
|
import static de.srsoftware.tools.Optionals.nullable;
|
||||||
|
import static java.lang.System.Logger;
|
||||||
|
import static java.lang.System.Logger.Level.WARNING;
|
||||||
|
import static java.lang.System.getLogger;
|
||||||
|
|
||||||
|
import com.sun.net.httpserver.HttpExchange;
|
||||||
|
import de.srsoftware.cal.api.Appointment;
|
||||||
|
import de.srsoftware.cal.db.Database;
|
||||||
|
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.sql.SQLException;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import java.util.Map;
|
||||||
|
import org.json.JSONObject;
|
||||||
|
|
||||||
|
public class ApiHandler extends PathHandler {
|
||||||
|
private static final Logger LOG = getLogger(ApiHandler.class.getSimpleName());
|
||||||
|
private final Database db;
|
||||||
|
|
||||||
|
public ApiHandler(Database db) {
|
||||||
|
this.db = db;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean doGet(String path, HttpExchange ex) throws IOException {
|
||||||
|
var params = queryParam(ex);
|
||||||
|
return switch (path) {
|
||||||
|
case "/event" -> loadEvent(ex,params);
|
||||||
|
case "/events/list" -> listEvents(ex,params);
|
||||||
|
default -> notFound(ex);
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
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 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 notFound(ex);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean loadEvent(HttpExchange ex, Map<String, String> params) throws IOException {
|
||||||
|
var id = params.get("id");
|
||||||
|
if (id != null) try {
|
||||||
|
return sendContent(ex,db.loadEvent(Long.parseLong(id)).map(ApiHandler::toJson));
|
||||||
|
} catch (NumberFormatException | IOException nfe){
|
||||||
|
return sendContent(ex, Error.format("%s is not a numeric event id!",id));
|
||||||
|
}
|
||||||
|
return sendContent(ex,Error.of("ID missing"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Result<JSONObject> toJson(Result<Appointment> appointmentResult) {
|
||||||
|
var opt = appointmentResult.optional();
|
||||||
|
return opt.isEmpty() ? transform(appointmentResult) :
|
||||||
|
Payload.of(opt.get().json());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static <T> Result<T> transform(Result<?> res) {
|
||||||
|
return res instanceof Error<?> err ? err.transform() : Error.format("Invalid parameter: %s", res.getClass().getSimpleName());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static LocalDateTime toLocalDateTime(String dateString) {
|
||||||
|
try {
|
||||||
|
return LocalDate.parse(dateString + "-01", DateTimeFormatter.ISO_LOCAL_DATE).atTime(0, 0);
|
||||||
|
} catch (Exception e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,14 +1,16 @@
|
|||||||
/* © SRSoftware 2024 */
|
/* © SRSoftware 2024 */
|
||||||
package de.srsoftware.cal.app;
|
package de.srsoftware.cal.app;
|
||||||
|
|
||||||
|
import static java.lang.System.Logger.Level.*;
|
||||||
|
|
||||||
import com.sun.net.httpserver.HttpServer;
|
import com.sun.net.httpserver.HttpServer;
|
||||||
import de.srsoftware.cal.db.Database;
|
import de.srsoftware.cal.db.Database;
|
||||||
import de.srsoftware.cal.db.MariaDB;
|
import de.srsoftware.cal.db.MariaDB;
|
||||||
import de.srsoftware.configuration.Configuration;
|
import de.srsoftware.configuration.Configuration;
|
||||||
import de.srsoftware.configuration.JsonConfig;
|
import de.srsoftware.configuration.JsonConfig;
|
||||||
|
import de.srsoftware.tools.ColorLogger;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.net.InetSocketAddress;
|
import java.net.InetSocketAddress;
|
||||||
import java.security.NoSuchAlgorithmException;
|
|
||||||
import java.sql.SQLException;
|
import java.sql.SQLException;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
@@ -16,10 +18,11 @@ import java.util.Optional;
|
|||||||
* Test application
|
* Test application
|
||||||
*/
|
*/
|
||||||
public class Application {
|
public class Application {
|
||||||
private static final String JDBC = "opencloudcal.db.jdbc";
|
private static final System.Logger LOG = System.getLogger(Application.class.getSimpleName());
|
||||||
private static final String USER = "opencloudcal.db.user";
|
private static final String JDBC = "opencloudcal.db.jdbc";
|
||||||
private static final String PASS = "opencloudcal.db.pass";
|
private static final String USER = "opencloudcal.db.user";
|
||||||
private static final String MISSING = "missing required configuration property \"%s\"";
|
private static final String PASS = "opencloudcal.db.pass";
|
||||||
|
private static final String MISSING = "missing required configuration property \"%s\"";
|
||||||
|
|
||||||
private Application() {
|
private Application() {
|
||||||
}
|
}
|
||||||
@@ -40,12 +43,17 @@ public class Application {
|
|||||||
* sandbox
|
* sandbox
|
||||||
* @param args default
|
* @param args default
|
||||||
*/
|
*/
|
||||||
public static void main(String[] args) throws NoSuchAlgorithmException, IOException, SQLException {
|
public static void main(String[] args) throws IOException, SQLException {
|
||||||
JsonConfig jsonConfig = new JsonConfig("OpenCloudCal");
|
LOG.log(INFO, "Starting application…");
|
||||||
var db = connect(jsonConfig);
|
if (LOG instanceof ColorLogger colorLogger) colorLogger.setLogLevel(TRACE);
|
||||||
|
JsonConfig jsonConfig = new JsonConfig("OpenCloudCal");
|
||||||
HttpServer server = HttpServer.create(new InetSocketAddress(8080), 0);
|
Optional<String> staticPath = jsonConfig.get("opencloudcal.static");
|
||||||
new EventList(db).bindPath("/").on(server);
|
var db = connect(jsonConfig);
|
||||||
|
var port = jsonConfig.get("opencloudcal.http.port", 8080);
|
||||||
|
HttpServer server = HttpServer.create(new InetSocketAddress(port), 0);
|
||||||
|
var staticPages = new StaticHandler(staticPath).bindPath("/static").on(server);
|
||||||
|
new IndexHandler(staticPages).bindPath("/").on(server);
|
||||||
|
new ApiHandler(db).bindPath(("/api")).on(server);
|
||||||
server.start();
|
server.start();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,62 +0,0 @@
|
|||||||
/* © SRSoftware 2024 */
|
|
||||||
package de.srsoftware.cal.app;
|
|
||||||
|
|
||||||
import static de.srsoftware.tools.TagFilter.ofType;
|
|
||||||
import static java.lang.System.Logger.Level.DEBUG;
|
|
||||||
import static java.lang.System.Logger.Level.INFO;
|
|
||||||
|
|
||||||
import com.sun.net.httpserver.HttpExchange;
|
|
||||||
import de.srsoftware.cal.api.Appointment;
|
|
||||||
import de.srsoftware.cal.db.Database;
|
|
||||||
import de.srsoftware.tools.PathHandler;
|
|
||||||
import de.srsoftware.tools.Tag;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.sql.SQLException;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
public class EventList extends PathHandler {
|
|
||||||
private static final System.Logger LOG = System.getLogger(EventList.class.getSimpleName());
|
|
||||||
|
|
||||||
private final Database db;
|
|
||||||
|
|
||||||
public EventList(Database db) {
|
|
||||||
this.db = db;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean doGet(String path, HttpExchange ex) throws IOException {
|
|
||||||
try {
|
|
||||||
var events = db.list(null, null);
|
|
||||||
LOG.log(DEBUG, () -> "Found %s events in database".formatted(events.size()));
|
|
||||||
var scaffold = scaffold();
|
|
||||||
var body = scaffold.find(ofType("body")).getFirst();
|
|
||||||
body.add(createTable(events));
|
|
||||||
return sendContent(ex, scaffold.toString(2));
|
|
||||||
} catch (SQLException e) {
|
|
||||||
return serverError(ex, "Failed to fetch list of events!");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private Tag createTable(List<Appointment> events) {
|
|
||||||
var table = Tag.of("table");
|
|
||||||
var head = Tag.of("tr");
|
|
||||||
head.add(Tag.of("th").content("Start")).add(Tag.of("th").content("Ort")).add(Tag.of("th").content("Event"));
|
|
||||||
table.add(head);
|
|
||||||
for (var event : events) {
|
|
||||||
LOG.log(INFO, event.title());
|
|
||||||
var row = Tag.of("tr");
|
|
||||||
row.add(Tag.of("td").content(event.start().toString())).add(Tag.of("td").content(event.location())).add(Tag.of("td").content(event.title())).addTo(table);
|
|
||||||
}
|
|
||||||
return table;
|
|
||||||
}
|
|
||||||
|
|
||||||
private Tag scaffold() {
|
|
||||||
var html = new Tag("html");
|
|
||||||
var head = new Tag("head") //
|
|
||||||
.add(new Tag("meta").attr("charset", "UTF-8"))
|
|
||||||
.add(new Tag("title").content("OpenCloudCal"));
|
|
||||||
var body = new Tag("body");
|
|
||||||
html.add(head, body);
|
|
||||||
return html;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
/* © SRSoftware 2024 */
|
||||||
|
package de.srsoftware.cal.app;
|
||||||
|
|
||||||
|
import com.sun.net.httpserver.HttpExchange;
|
||||||
|
import de.srsoftware.tools.PathHandler;
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
public class IndexHandler extends PathHandler {
|
||||||
|
PathHandler staticPages;
|
||||||
|
public IndexHandler(PathHandler staticPages) {
|
||||||
|
this.staticPages = staticPages;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean doGet(String path, HttpExchange ex) throws IOException {
|
||||||
|
switch (path) {
|
||||||
|
case "/":
|
||||||
|
return staticPages.doGet("/index", ex);
|
||||||
|
}
|
||||||
|
return super.doGet(path, ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
/* © SRSoftware 2024 */
|
||||||
|
package de.srsoftware.cal.app;
|
||||||
|
|
||||||
|
import com.sun.net.httpserver.HttpExchange;
|
||||||
|
import de.srsoftware.tools.PathHandler;
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
|
||||||
|
public class StaticHandler extends PathHandler {
|
||||||
|
private final Optional<String> staticPath;
|
||||||
|
|
||||||
|
public StaticHandler(Optional<String> staticPath) {
|
||||||
|
this.staticPath = staticPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean doGet(String path, HttpExchange ex) throws IOException {
|
||||||
|
if (!path.contains(".")) path += ".html";
|
||||||
|
if (path.startsWith("/")) path = path.substring(1);
|
||||||
|
var url = getClass().getClassLoader().getResource(path);
|
||||||
|
if (staticPath.isPresent()) {
|
||||||
|
var file = Path.of(staticPath.get()).resolve(path).toFile();
|
||||||
|
if (file.exists() && file.isFile()) url = file.toURI().toURL();
|
||||||
|
}
|
||||||
|
if (url == null) return notFound(ex);
|
||||||
|
var conn = url.openConnection();
|
||||||
|
var mime = conn.getContentType();
|
||||||
|
try (var input = conn.getInputStream()) {
|
||||||
|
var bos = new ByteArrayOutputStream();
|
||||||
|
input.transferTo(bos);
|
||||||
|
ex.getResponseHeaders().add(CONTENT_TYPE, mime);
|
||||||
|
return sendContent(ex, bos.toByteArray());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
22
de.srsoftware.cal.app/src/main/resources/event.html
Normal file
22
de.srsoftware.cal.app/src/main/resources/event.html
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<title>SRSoftware OpenCloudCal</title>
|
||||||
|
<script src="/static/script/occ.js"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<nav />
|
||||||
|
<h1>Loading…</h1>
|
||||||
|
<div id="time">Loading…</div>
|
||||||
|
<div id="description">Loading…</div>
|
||||||
|
<div id="tags">Loading…</div>
|
||||||
|
<div id="links">Loading…</div>
|
||||||
|
<div id="attachments">Loading…</div>
|
||||||
|
<script>
|
||||||
|
document.addEventListener("DOMContentLoaded", function(event){
|
||||||
|
console.log("page loaded…");
|
||||||
|
loadEventData();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
29
de.srsoftware.cal.app/src/main/resources/index.html
Normal file
29
de.srsoftware.cal.app/src/main/resources/index.html
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<title>SRSoftware OpenCloudCal</title>
|
||||||
|
<script src="/static/script/occ.js"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<nav />
|
||||||
|
<div>
|
||||||
|
<h1>Event List</h1>
|
||||||
|
<table id="eventlist">
|
||||||
|
<tr>
|
||||||
|
<th>ID</th>
|
||||||
|
<th>Start</th>
|
||||||
|
<th>End</th>
|
||||||
|
<th>Location</th>
|
||||||
|
<th>Title</th>
|
||||||
|
<th>Tags</th>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
document.addEventListener("DOMContentLoaded", function(event){
|
||||||
|
console.log("page loaded…");
|
||||||
|
loadCurrentEvents();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
82
de.srsoftware.cal.app/src/main/resources/script/occ.js
Normal file
82
de.srsoftware.cal.app/src/main/resources/script/occ.js
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
var start = null;
|
||||||
|
var end = null;
|
||||||
|
|
||||||
|
function fetchEvents(start, end){
|
||||||
|
var path = '/api/events/list';
|
||||||
|
if (start) {
|
||||||
|
path += '?start='+start;
|
||||||
|
if (end) path+= '&end='+end;
|
||||||
|
}
|
||||||
|
fetch(path).then(handleEvents);
|
||||||
|
}
|
||||||
|
|
||||||
|
function addCell(row,content,id){
|
||||||
|
var a = document.createElement('a');
|
||||||
|
if (content){
|
||||||
|
a.href = '/static/event?id='+id;
|
||||||
|
a.innerHTML = content;
|
||||||
|
}
|
||||||
|
row.insertCell().appendChild(a);
|
||||||
|
}
|
||||||
|
|
||||||
|
function addRow(json){
|
||||||
|
var table = document.getElementById('eventlist');
|
||||||
|
var row = table.insertRow();
|
||||||
|
addCell(row,json.id,json.id);
|
||||||
|
addCell(row,json.start,json.id);
|
||||||
|
addCell(row,json.end,json.id);
|
||||||
|
addCell(row,json.location,json.id);
|
||||||
|
addCell(row,json.title,json.id);
|
||||||
|
row.appendChild(createTags(json.tags));
|
||||||
|
}
|
||||||
|
|
||||||
|
function createTags(tagList){
|
||||||
|
var td = document.createElement('td');
|
||||||
|
tagList.forEach(val => {
|
||||||
|
var btn = document.createElement('button');
|
||||||
|
btn.onclick = e => toggleTag(val);
|
||||||
|
btn.appendChild(document.createTextNode(val));
|
||||||
|
td.appendChild(btn);
|
||||||
|
td.appendChild(document.createTextNode(' '));
|
||||||
|
});
|
||||||
|
return td;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleEventData(response){
|
||||||
|
if (response.ok){
|
||||||
|
var json = await response.json();
|
||||||
|
document.getElementsByTagName('h1')[0].innerHTML = json.title;
|
||||||
|
document.getElementById('time').innerHTML = json.start + (json.end ? '…'+json.end : '');
|
||||||
|
document.getElementById('description').innerHTML = json.description;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleEvents(response){
|
||||||
|
if (response.ok){
|
||||||
|
var json = await response.json();
|
||||||
|
json.forEach(addRow)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadCurrentEvents(){
|
||||||
|
if (start == null){
|
||||||
|
var now = new Date();
|
||||||
|
var year = now.getFullYear();
|
||||||
|
var month = now.getMonth() + 1;
|
||||||
|
start = year + '-' + (month < 10 ? '0' : '') + month;
|
||||||
|
fetchEvents(start,end);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadEventData(){
|
||||||
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
|
var id = urlParams.get('id');
|
||||||
|
if (id){
|
||||||
|
fetch('/api/event?id='+id).then(handleEventData)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleTag(tag){
|
||||||
|
alert(`toggleTag(${tag})`);
|
||||||
|
}
|
||||||
@@ -5,5 +5,6 @@ dependencies {
|
|||||||
|
|
||||||
implementation("de.srsoftware:tools.optionals:1.0.0")
|
implementation("de.srsoftware:tools.optionals:1.0.0")
|
||||||
implementation("de.srsoftware:tools.util:1.2.1")
|
implementation("de.srsoftware:tools.util:1.2.1")
|
||||||
implementation("de.srsoftware:tools.web:1.3.4")
|
implementation("de.srsoftware:tools.web:1.3.8")
|
||||||
|
implementation("org.json:json:20240303")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,20 +8,23 @@ import de.srsoftware.cal.api.Attachment;
|
|||||||
import de.srsoftware.cal.api.Coords;
|
import de.srsoftware.cal.api.Coords;
|
||||||
import de.srsoftware.cal.api.Link;
|
import de.srsoftware.cal.api.Link;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
import org.json.JSONObject;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* basic class for Appointments
|
* basic class for Appointments
|
||||||
*/
|
*/
|
||||||
public class BaseAppointment implements Appointment {
|
public class BaseAppointment implements Appointment {
|
||||||
private final long id;
|
private static final DateTimeFormatter DATE_TIME = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm");
|
||||||
private final String title, description;
|
private final long id;
|
||||||
private final LocalDateTime end, start;
|
private final String title, description;
|
||||||
private final String hash;
|
private final LocalDateTime end, start;
|
||||||
private Coords coords = null;
|
private final String hash;
|
||||||
private final Set<Attachment> attachments = new HashSet<>();
|
private Coords coords = null;
|
||||||
private final Set<String> tags = new HashSet<>();
|
private final Set<Attachment> attachments = new HashSet<>();
|
||||||
private final Set<Link> links = new HashSet<>();
|
private final Set<String> tags = new HashSet<>();
|
||||||
|
private final Set<Link> links = new HashSet<>();
|
||||||
private final String location;
|
private final String location;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -83,26 +86,6 @@ public class BaseAppointment implements Appointment {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* adds tag
|
|
||||||
* @param newTags the tag to add to the appointment
|
|
||||||
* @return the appointment
|
|
||||||
*/
|
|
||||||
public BaseAppointment tags(String... newTags) {
|
|
||||||
Collections.addAll(tags, newTags);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* adds tag
|
|
||||||
* @param newTags the tag to add to the appointment
|
|
||||||
* @return the appointment
|
|
||||||
*/
|
|
||||||
public BaseAppointment tags(Collection<String> newTags) {
|
|
||||||
tags.addAll(newTags);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* set the coordinates of the attachments
|
* set the coordinates of the attachments
|
||||||
* @param newCoords the coordinates to apply
|
* @param newCoords the coordinates to apply
|
||||||
@@ -133,21 +116,42 @@ public class BaseAppointment implements Appointment {
|
|||||||
return nullable(end);
|
return nullable(end);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public String slug() {
|
|
||||||
return hash;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public long id() {
|
public long id() {
|
||||||
return id;
|
return id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public JSONObject json() {
|
||||||
|
var json = new JSONObject();
|
||||||
|
json.put("attachments", attachments.stream().map(Attachment::json).toList());
|
||||||
|
json.put("coords", nullable(coords).map(Coords::json).orElse(null));
|
||||||
|
json.put("description", description());
|
||||||
|
json.put("end", end().map(end -> end.format(DATE_TIME)).orElse(null));
|
||||||
|
json.put("id", id());
|
||||||
|
json.put("location", location());
|
||||||
|
json.put("slug", slug());
|
||||||
|
json.put("start", start().format(DATE_TIME));
|
||||||
|
json.put("tags", tags());
|
||||||
|
json.put("title", title());
|
||||||
|
json.put("links", urls().stream().map(Link::json).toList());
|
||||||
|
|
||||||
|
return json;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String location() {
|
public String location() {
|
||||||
return location;
|
return location;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String slug() {
|
||||||
|
return hash;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public LocalDateTime start() {
|
public LocalDateTime start() {
|
||||||
return start;
|
return start;
|
||||||
@@ -155,7 +159,27 @@ public class BaseAppointment implements Appointment {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Set<String> tags() {
|
public Set<String> tags() {
|
||||||
return Set.of();
|
return tags;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* adds tag
|
||||||
|
* @param newTags the tag to add to the appointment
|
||||||
|
* @return the appointment
|
||||||
|
*/
|
||||||
|
public BaseAppointment tags(String... newTags) {
|
||||||
|
Collections.addAll(tags, newTags);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* adds tag
|
||||||
|
* @param newTags the tag to add to the appointment
|
||||||
|
* @return the appointment
|
||||||
|
*/
|
||||||
|
public BaseAppointment tags(Collection<String> newTags) {
|
||||||
|
tags.addAll(newTags);
|
||||||
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -170,6 +194,6 @@ public class BaseAppointment implements Appointment {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Set<Link> urls() {
|
public Set<Link> urls() {
|
||||||
return Set.of();
|
return links;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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.0.0")
|
implementation("de.srsoftware:tools.jdbc:1.1.0")
|
||||||
implementation("de.srsoftware:tools.optionals:1.0.0")
|
implementation("de.srsoftware:tools.optionals:1.0.0")
|
||||||
implementation("de.srsoftware:tools.util:1.2.1")
|
implementation("de.srsoftware:tools.util:1.2.1")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.Result;
|
||||||
import java.sql.SQLException;
|
import java.sql.SQLException;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
@@ -22,9 +24,19 @@ public interface Database {
|
|||||||
* @param count the maximum number of appointments to return
|
* @param count the maximum number of appointments to return
|
||||||
* @param offset the number of appointments to skip
|
* @param offset the number of appointments to skip
|
||||||
* @return the list of appointments fetched from the db
|
* @return the list of appointments fetched from the db
|
||||||
|
* @throws SQLException if the appointments cannot be fetched from the DB
|
||||||
*/
|
*/
|
||||||
public List<Appointment> list(Integer count, Integer offset) throws SQLException;
|
public List<Appointment> list(Integer count, Integer offset) throws SQLException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* list appointments unfiltered
|
||||||
|
* @param from restrict appointments to times after this date time
|
||||||
|
* @param till restrict appointments to times before this date time
|
||||||
|
* @return list of appointments in this time span
|
||||||
|
* @throws SQLException if the appointments cannot be fetched from the DB
|
||||||
|
*/
|
||||||
|
public List<Appointment> list(LocalDateTime from, LocalDateTime till) throws SQLException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* list appointments
|
* list appointments
|
||||||
* @param tags only list appointments which have matching tags
|
* @param tags only list appointments which have matching tags
|
||||||
@@ -33,4 +45,6 @@ public interface Database {
|
|||||||
* @return the list of appointments fetched from the db
|
* @return the list of appointments fetched from the db
|
||||||
*/
|
*/
|
||||||
public List<Appointment> listByTags(Set<String> tags, Integer count, Integer offset);
|
public List<Appointment> listByTags(Set<String> tags, Integer count, Integer offset);
|
||||||
|
|
||||||
|
Result<Appointment> loadEvent(long id);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,31 +1,30 @@
|
|||||||
/* © SRSoftware 2024 */
|
/* © SRSoftware 2024 */
|
||||||
package de.srsoftware.cal.db;
|
package de.srsoftware.cal.db;
|
||||||
|
|
||||||
import static de.srsoftware.tools.Optionals.allEmpty;
|
import static de.srsoftware.tools.Optionals.*;
|
||||||
import static de.srsoftware.tools.Optionals.nullable;
|
|
||||||
import static de.srsoftware.tools.Strings.camelCase;
|
import static de.srsoftware.tools.Strings.camelCase;
|
||||||
|
import static de.srsoftware.tools.jdbc.Condition.equal;
|
||||||
|
import static de.srsoftware.tools.jdbc.Condition.moreThan;
|
||||||
|
import static de.srsoftware.tools.jdbc.Query.MARK;
|
||||||
|
import static java.lang.System.Logger.Level.*;
|
||||||
|
|
||||||
import de.srsoftware.cal.BaseAppointment;
|
import de.srsoftware.cal.BaseAppointment;
|
||||||
import de.srsoftware.cal.api.Appointment;
|
import de.srsoftware.cal.api.Appointment;
|
||||||
import de.srsoftware.tools.Calc;
|
import de.srsoftware.tools.Calc;
|
||||||
|
import de.srsoftware.tools.Error;
|
||||||
|
import de.srsoftware.tools.Payload;
|
||||||
|
import de.srsoftware.tools.Result;
|
||||||
import de.srsoftware.tools.jdbc.Query;
|
import de.srsoftware.tools.jdbc.Query;
|
||||||
import java.sql.Connection;
|
import java.sql.*;
|
||||||
import java.sql.DriverManager;
|
import java.time.LocalDateTime;
|
||||||
import java.sql.SQLException;
|
import java.util.*;
|
||||||
import java.sql.Timestamp;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Set;
|
|
||||||
|
|
||||||
public class MariaDB implements Database {
|
public class MariaDB implements Database {
|
||||||
private static final String SELECT_APPOINTMENTS = "SELECT * FROM appointments";
|
private static final System.Logger LOG = System.getLogger(MariaDB.class.getSimpleName());
|
||||||
private static final String SELECT_VERSION = "SELECT value FROM config WHERE keyname = 'dbversion'";
|
private static final String ADD_SLUG = "ALTER TABLE appointments ADD slug VARCHAR(255) UNIQUE";
|
||||||
private static final String ADD_SLUG = "ALTER TABLE appointments ADD slug VARCHAR(255) UNIQUE";
|
private static final String APPOINTMENTS = "appointments";
|
||||||
private static final String INSERT_HASH = "INSERT INTO appointment_hashes (aid, hash) values (?, ?) ON DUPLICATE KEY UPDATE hash=hash;";
|
private static final String AID = "aid";
|
||||||
private static final String UPDATE_DB_VERSION = "UPDATE config SET value = ? WHERE keyname = 'dbversion'";
|
private static Connection connection;
|
||||||
private static final String INSERT_SLUG = "UPDATE IGNORE appointments SET slug = ? WHERE aid = ?";
|
|
||||||
private static Connection connection;
|
|
||||||
|
|
||||||
private MariaDB(Connection conn) throws SQLException {
|
private MariaDB(Connection conn) throws SQLException {
|
||||||
connection = conn;
|
connection = conn;
|
||||||
@@ -33,7 +32,8 @@ public class MariaDB implements Database {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void applyUpdates() throws SQLException {
|
private void applyUpdates() throws SQLException {
|
||||||
var rs = Query.of(SELECT_VERSION).execute(connection);
|
LOG.log(INFO, "Checking for updates…");
|
||||||
|
var rs = Query.select("value").from("config").where("keyname", equal("dbversion")).exec(connection);
|
||||||
var version = 0;
|
var version = 0;
|
||||||
if (rs.next()) {
|
if (rs.next()) {
|
||||||
version = rs.getInt("value");
|
version = rs.getInt("value");
|
||||||
@@ -48,35 +48,39 @@ public class MariaDB implements Database {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void update1() throws SQLException {
|
private void update1() throws SQLException {
|
||||||
|
LOG.log(INFO, "Updating db scheme from version 1 to 2…");
|
||||||
var list = new ArrayList<Appointment>();
|
var list = new ArrayList<Appointment>();
|
||||||
|
|
||||||
connection.setAutoCommit(false);
|
connection.setAutoCommit(false);
|
||||||
|
|
||||||
Query.of(ADD_SLUG).statement(connection).execute();
|
LOG.log(DEBUG, "Adding slug column…");
|
||||||
|
connection.prepareStatement(ADD_SLUG).execute();
|
||||||
var slugMap = new HashMap<Long, String>();
|
var slugMap = new HashMap<Long, String>();
|
||||||
var rs = Query.of(SELECT_APPOINTMENTS).execute(connection);
|
LOG.log(DEBUG, "Reading existing appointments…");
|
||||||
|
var rs = Query.select("*").from("appointments").exec(connection);
|
||||||
while (rs.next()) {
|
while (rs.next()) {
|
||||||
var id = rs.getLong("aid");
|
var id = rs.getLong(AID);
|
||||||
var location = nullable(rs.getString("location"));
|
var location = nullable(nullIfEmpty(rs.getString("location")));
|
||||||
if (location.isEmpty()) continue;
|
if (location.isEmpty()) continue;
|
||||||
var title = rs.getString("title");
|
var title = rs.getString("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"));
|
||||||
if (start.isEmpty()) continue;
|
if (start.isEmpty()) continue;
|
||||||
var slug = "%s@%s".formatted(start.get().toLocalDateTime(), camelCase(location.get()));
|
var slug = "%s@%s".formatted(start.get().toLocalDateTime(), camelCase(location.get().replace(",", "")));
|
||||||
if (slug.length() > 250) slug = slug.substring(0, 250);
|
if (slug.length() > 250) slug = slug.substring(0, 250);
|
||||||
slugMap.put(id, slug);
|
slugMap.put(id, slug);
|
||||||
}
|
}
|
||||||
rs.close();
|
rs.close();
|
||||||
var stmt = Query.of(INSERT_SLUG).statement(connection);
|
LOG.log(DEBUG, "Creating slugs…");
|
||||||
|
var query = Query.updateIgnore("appointments").set("slug").where(AID, equal(MARK)).prepare(connection);
|
||||||
for (var entry : slugMap.entrySet()) {
|
for (var entry : slugMap.entrySet()) {
|
||||||
stmt.setString(1, entry.getValue());
|
query.apply(entry.getValue(), entry.getKey());
|
||||||
stmt.setLong(2, entry.getKey());
|
|
||||||
stmt.execute();
|
|
||||||
}
|
}
|
||||||
stmt = Query.of(UPDATE_DB_VERSION).statement(connection);
|
|
||||||
stmt.setLong(1, 2);
|
LOG.log(DEBUG, "Writing new db version marker…");
|
||||||
stmt.execute();
|
Query.update("config").set("value").where("keyname", equal("dbversion")).prepare(connection).apply(2);
|
||||||
connection.setAutoCommit(true);
|
connection.setAutoCommit(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,23 +98,61 @@ public class MariaDB implements Database {
|
|||||||
return new MariaDB(DriverManager.getConnection(jdbc, user, pass));
|
return new MariaDB(DriverManager.getConnection(jdbc, user, pass));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<Appointment> list(Integer count, Integer offset) throws SQLException {
|
public List<Appointment> list(Integer count, Integer offset) throws SQLException {
|
||||||
var list = new ArrayList<Appointment>();
|
var list = new ArrayList<Appointment>();
|
||||||
var results = Query.of(SELECT_APPOINTMENTS).orderBy("start").execute(connection);
|
var results = Query.select("*").from(APPOINTMENTS).sort("start").exec(connection);
|
||||||
while (results.next()) {
|
while (results.next()) createAppointmentOf(results).optional().ifPresent(list::add);
|
||||||
var id = results.getInt("aid");
|
results.close();
|
||||||
var title = results.getString("title");
|
return list;
|
||||||
var description = results.getString("description");
|
}
|
||||||
if (allEmpty(title, description)) continue;
|
|
||||||
var start = results.getTimestamp("start").toLocalDateTime();
|
@Override
|
||||||
var end = nullable(results.getTimestamp("end")).map(Timestamp::toLocalDateTime).orElse(null);
|
public Result<Appointment> loadEvent(long id) {
|
||||||
var location = results.getString("location");
|
try {
|
||||||
var slug = results.getString("slug");
|
var rs = Query //
|
||||||
if (slug == null) slug = Calc.hash(start + "@" + location).orElse(null);
|
.select("%s.*".formatted(APPOINTMENTS), "GROUP_CONCAT(keyword) AS tags")
|
||||||
list.add(new BaseAppointment(id, title, description, start, end, location, slug));
|
.from(APPOINTMENTS)
|
||||||
|
.leftJoin(AID, "appointment_tags", AID)
|
||||||
|
.leftJoin("tid", "tags", "tid")
|
||||||
|
.groupBy(AID)
|
||||||
|
.where("%s.%s".formatted(APPOINTMENTS, AID), equal(id))
|
||||||
|
.exec(connection);
|
||||||
|
return rs.next() ? createAppointmentOf(rs) : Error.format("Failed to find appointment with id %s", id);
|
||||||
|
} catch (SQLException e) {
|
||||||
|
return Error.of("Failed to load appointment with id = %s".formatted(id), e);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Result<Appointment> createAppointmentOf(ResultSet results) throws SQLException {
|
||||||
|
var id = results.getInt(AID);
|
||||||
|
var title = results.getString("title");
|
||||||
|
var description = results.getString("description");
|
||||||
|
if (allEmpty(title, description)) return Error.format("Title and Description of appointment %s are empty", id);
|
||||||
|
var start = results.getTimestamp("start").toLocalDateTime();
|
||||||
|
var end = nullable(results.getTimestamp("end")).map(Timestamp::toLocalDateTime).orElse(null);
|
||||||
|
var location = results.getString("location");
|
||||||
|
var slug = results.getString("slug");
|
||||||
|
var tags = nullIfEmpty(results.getString("tags"));
|
||||||
|
if (slug == null) slug = Calc.hash(start + "@" + location).orElse(null);
|
||||||
|
var appointment = new BaseAppointment(id, title, description, start, end, location, slug);
|
||||||
|
if (tags != null) appointment.tags(tags.split(","));
|
||||||
|
return Payload.of(appointment);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Appointment> list(LocalDateTime from, LocalDateTime till) throws SQLException {
|
||||||
|
var list = new ArrayList<Appointment>();
|
||||||
|
var results = Query //
|
||||||
|
.select("appointments.*", "GROUP_CONCAT(keyword) AS tags")
|
||||||
|
.from(APPOINTMENTS)
|
||||||
|
.leftJoin(AID, "appointment_tags", AID)
|
||||||
|
.leftJoin("tid", "tags", "tid")
|
||||||
|
.groupBy(AID)
|
||||||
|
.sort("start")
|
||||||
|
.where("start", moreThan(from))
|
||||||
|
.exec(connection);
|
||||||
|
while (results.next()) createAppointmentOf(results).optional().ifPresent(list::add);
|
||||||
results.close();
|
results.close();
|
||||||
return list;
|
return list;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,5 +5,5 @@ dependencies {
|
|||||||
implementation(project(":de.srsoftware.cal.base"))
|
implementation(project(":de.srsoftware.cal.base"))
|
||||||
implementation("de.srsoftware:tools.optionals:1.0.0")
|
implementation("de.srsoftware:tools.optionals:1.0.0")
|
||||||
implementation("de.srsoftware:tools.util:1.2.1")
|
implementation("de.srsoftware:tools.util:1.2.1")
|
||||||
implementation("de.srsoftware:tools.web:1.3.4")
|
implementation("de.srsoftware:tools.web:1.3.8")
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user