working on calendar application
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
This commit is contained in:
@@ -7,8 +7,11 @@ dependencies {
|
||||
|
||||
implementation("de.srsoftware:configuration.api: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.optionals:1.0.0")
|
||||
implementation("de.srsoftware:tools.util:1.2.1")
|
||||
implementation("de.srsoftware:tools.web:1.3.4")
|
||||
implementation("com.mysql:mysql-connector-j:9.1.0")}
|
||||
implementation("de.srsoftware:tools.web:1.3.8")
|
||||
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 */
|
||||
package de.srsoftware.cal.app;
|
||||
|
||||
import static java.lang.System.Logger.Level.*;
|
||||
|
||||
import com.sun.net.httpserver.HttpServer;
|
||||
import de.srsoftware.cal.db.Database;
|
||||
import de.srsoftware.cal.db.MariaDB;
|
||||
import de.srsoftware.configuration.Configuration;
|
||||
import de.srsoftware.configuration.JsonConfig;
|
||||
import de.srsoftware.tools.ColorLogger;
|
||||
import java.io.IOException;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.sql.SQLException;
|
||||
import java.util.Optional;
|
||||
|
||||
@@ -16,10 +18,11 @@ import java.util.Optional;
|
||||
* Test application
|
||||
*/
|
||||
public class Application {
|
||||
private static final String JDBC = "opencloudcal.db.jdbc";
|
||||
private static final String USER = "opencloudcal.db.user";
|
||||
private static final String PASS = "opencloudcal.db.pass";
|
||||
private static final String MISSING = "missing required configuration property \"%s\"";
|
||||
private static final System.Logger LOG = System.getLogger(Application.class.getSimpleName());
|
||||
private static final String JDBC = "opencloudcal.db.jdbc";
|
||||
private static final String USER = "opencloudcal.db.user";
|
||||
private static final String PASS = "opencloudcal.db.pass";
|
||||
private static final String MISSING = "missing required configuration property \"%s\"";
|
||||
|
||||
private Application() {
|
||||
}
|
||||
@@ -40,12 +43,17 @@ public class Application {
|
||||
* sandbox
|
||||
* @param args default
|
||||
*/
|
||||
public static void main(String[] args) throws NoSuchAlgorithmException, IOException, SQLException {
|
||||
JsonConfig jsonConfig = new JsonConfig("OpenCloudCal");
|
||||
var db = connect(jsonConfig);
|
||||
|
||||
HttpServer server = HttpServer.create(new InetSocketAddress(8080), 0);
|
||||
new EventList(db).bindPath("/").on(server);
|
||||
public static void main(String[] args) throws IOException, SQLException {
|
||||
LOG.log(INFO, "Starting application…");
|
||||
if (LOG instanceof ColorLogger colorLogger) colorLogger.setLogLevel(TRACE);
|
||||
JsonConfig jsonConfig = new JsonConfig("OpenCloudCal");
|
||||
Optional<String> staticPath = jsonConfig.get("opencloudcal.static");
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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})`);
|
||||
}
|
||||
Reference in New Issue
Block a user