Browse Source

working on event detail page

Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
main
Stephan Richter 9 months ago
parent
commit
a90627d976
  1. 2
      de.srsoftware.cal.api/build.gradle.kts
  2. 5
      de.srsoftware.cal.app/build.gradle.kts
  3. 3
      de.srsoftware.cal.app/src/main/java/de/srsoftware/cal/app/Application.java
  4. 2
      de.srsoftware.cal.base/build.gradle.kts
  5. 2
      de.srsoftware.cal.db/build.gradle.kts
  6. 104
      de.srsoftware.cal.db/src/main/java/de/srsoftware/cal/db/MariaDB.java
  7. 2
      de.srsoftware.cal.importer/build.gradle.kts
  8. 7
      de.srsoftware.cal.web/build.gradle.kts
  9. 27
      de.srsoftware.cal.web/src/main/java/de/srsoftware/cal/ApiHandler.java
  10. 2
      de.srsoftware.cal.web/src/main/java/de/srsoftware/cal/IndexHandler.java
  11. 8
      de.srsoftware.cal.web/src/main/java/de/srsoftware/cal/StaticHandler.java
  12. 10
      de.srsoftware.cal.web/src/main/resources/event.html
  13. 0
      de.srsoftware.cal.web/src/main/resources/index.html
  14. 41
      de.srsoftware.cal.web/src/main/resources/script/occ.js

2
de.srsoftware.cal.api/build.gradle.kts

@ -1,6 +1,6 @@
description = "OpenCloudCal : API" description = "OpenCloudCal : API"
dependencies { dependencies {
implementation("de.srsoftware:tools.util:1.2.1") implementation("de.srsoftware:tools.util:1.2.2")
implementation("org.json:json:20240303") implementation("org.json:json:20240303")
} }

5
de.srsoftware.cal.app/build.gradle.kts

@ -1,17 +1,14 @@
description = "OpenCloudCal : Application" description = "OpenCloudCal : Application"
dependencies { dependencies {
implementation(project(":de.srsoftware.cal.api"))
implementation(project(":de.srsoftware.cal.db")) implementation(project(":de.srsoftware.cal.db"))
implementation(project(":de.srsoftware.cal.importer")) implementation(project(":de.srsoftware.cal.importer"))
implementation(project(":de.srsoftware.cal.web"))
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.3") 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.web:1.3.8") 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")
} }

3
de.srsoftware.cal.app/src/main/java/de/srsoftware/cal/app/Application.java

@ -4,6 +4,9 @@ package de.srsoftware.cal.app;
import static java.lang.System.Logger.Level.*; import static java.lang.System.Logger.Level.*;
import com.sun.net.httpserver.HttpServer; import com.sun.net.httpserver.HttpServer;
import de.srsoftware.cal.ApiHandler;
import de.srsoftware.cal.IndexHandler;
import de.srsoftware.cal.StaticHandler;
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;

2
de.srsoftware.cal.base/build.gradle.kts

@ -4,7 +4,7 @@ dependencies {
implementation(project(":de.srsoftware.cal.api")) implementation(project(":de.srsoftware.cal.api"))
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.2")
implementation("de.srsoftware:tools.web:1.3.8") implementation("de.srsoftware:tools.web:1.3.8")
implementation("org.json:json:20240303") implementation("org.json:json:20240303")
} }

2
de.srsoftware.cal.db/build.gradle.kts

@ -6,5 +6,5 @@ dependencies {
implementation("de.srsoftware:tools.jdbc:1.1.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.2")
} }

104
de.srsoftware.cal.db/src/main/java/de/srsoftware/cal/db/MariaDB.java

@ -2,6 +2,7 @@
package de.srsoftware.cal.db; package de.srsoftware.cal.db;
import static de.srsoftware.tools.Optionals.*; import static de.srsoftware.tools.Optionals.*;
import static de.srsoftware.tools.Result.transform;
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.equal;
import static de.srsoftware.tools.jdbc.Condition.moreThan; import static de.srsoftware.tools.jdbc.Condition.moreThan;
@ -10,11 +11,15 @@ 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.cal.api.Attachment;
import de.srsoftware.cal.api.Link;
import de.srsoftware.tools.Calc; import de.srsoftware.tools.Calc;
import de.srsoftware.tools.Error; import de.srsoftware.tools.Error;
import de.srsoftware.tools.Payload; import de.srsoftware.tools.Payload;
import de.srsoftware.tools.Result; import de.srsoftware.tools.Result;
import de.srsoftware.tools.jdbc.Query; import de.srsoftware.tools.jdbc.Query;
import java.net.MalformedURLException;
import java.net.URI;
import java.sql.*; import java.sql.*;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.*; import java.util.*;
@ -24,6 +29,17 @@ public class MariaDB implements Database {
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 APPOINTMENTS = "appointments";
private static final String AID = "aid"; private static final String AID = "aid";
private static final String ALL = "*";
private static final String KEYWORD = "keyword";
private static final String APPOINTMENT_TAGS = "appointment_tags";
private static final String URL = "url";
private static final String APPOINTMENT_URLS = "appointment_urls";
private static final String DESCRIPTION = "description";
private static final String UID = "uid";
private static final String URLS = "urls";
private static final String TID = "tid";
private static final String MIME = "mime";
private static final String APPOINTMENT_ATTACHMENTS = "appointment_attachments";
private static Connection connection; private static Connection connection;
private MariaDB(Connection conn) throws SQLException { private MariaDB(Connection conn) throws SQLException {
@ -57,7 +73,7 @@ public class MariaDB implements Database {
connection.prepareStatement(ADD_SLUG).execute(); connection.prepareStatement(ADD_SLUG).execute();
var slugMap = new HashMap<Long, String>(); var slugMap = new HashMap<Long, String>();
LOG.log(DEBUG, "Reading existing appointments…"); LOG.log(DEBUG, "Reading existing appointments…");
var rs = Query.select("*").from("appointments").exec(connection); var rs = Query.select(ALL).from("appointments").exec(connection);
while (rs.next()) { while (rs.next()) {
var id = rs.getLong(AID); var id = rs.getLong(AID);
var location = nullable(nullIfEmpty(rs.getString("location"))); var location = nullable(nullIfEmpty(rs.getString("location")));
@ -101,7 +117,7 @@ public class MariaDB implements Database {
@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.select("*").from(APPOINTMENTS).sort("start").exec(connection); var results = Query.select(ALL).from(APPOINTMENTS).sort("start").exec(connection);
while (results.next()) createAppointmentOf(results).optional().ifPresent(list::add); while (results.next()) createAppointmentOf(results).optional().ifPresent(list::add);
results.close(); results.close();
return list; return list;
@ -110,21 +126,80 @@ public class MariaDB implements Database {
@Override @Override
public Result<Appointment> loadEvent(long id) { public Result<Appointment> loadEvent(long id) {
try { try {
var rs = Query // var rs = Query.select(ALL).from(APPOINTMENTS).where(AID, equal(id)).exec(connection);
.select("%s.*".formatted(APPOINTMENTS), "GROUP_CONCAT(keyword) AS tags") Result<Appointment> result = rs.next() ? createAppointmentOf(rs).map(MariaDB::loadExtra) : Error.format("Failed to find appointment with id %s", id);
.from(APPOINTMENTS) rs.close();
.leftJoin(AID, "appointment_tags", AID) return result;
.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) { } catch (SQLException e) {
return Error.of("Failed to load appointment with id = %s".formatted(id), e); return Error.of("Failed to load appointment with id = %s".formatted(id), e);
} }
} }
private Result<Appointment> createAppointmentOf(ResultSet results) throws SQLException { private static Result<Appointment> loadExtra(Result<BaseAppointment> res) {
return loadTags(res).map(MariaDB::loadLinks).map(MariaDB::loadAttachments);
}
private static Result<BaseAppointment> loadTags(Result<BaseAppointment> res) {
if (res.optional().isEmpty()) return transform(res);
BaseAppointment event = res.optional().get();
var id = event.id();
try {
var rs = Query.select(KEYWORD).from(APPOINTMENT_TAGS).leftJoin(TID, "tags", TID).where(AID, equal(id)).exec(connection);
while (rs.next()) event.tags(rs.getString(1));
rs.close();
return Payload.of(event);
} catch (SQLException e) {
return Error.of("Failed to load tags for appointment %s".formatted(id), e);
}
}
private static Result<BaseAppointment> loadLinks(Result<BaseAppointment> res) {
if (res.optional().isEmpty()) return transform(res);
BaseAppointment event = res.optional().get();
var id = event.id();
try {
var rs = Query.select(URL, DESCRIPTION).from(APPOINTMENT_URLS).leftJoin(UID, URLS, UID).where(AID, equal(id)).exec(connection);
while (rs.next()) {
var u = rs.getString(URL);
try {
var url = URI.create(u).toURL();
var description = rs.getString(DESCRIPTION);
event.addLinks(new Link(url, description));
} catch (MalformedURLException e) {
LOG.log(WARNING, () -> "Failed to convert %s to URI!".formatted(u));
}
}
rs.close();
return Payload.of(event);
} catch (SQLException e) {
return Error.of("Failed to load tags for appointment %s".formatted(id), e);
}
}
private static Result<Appointment> loadAttachments(Result<BaseAppointment> res) {
if (res.optional().isEmpty()) return transform(res);
BaseAppointment event = res.optional().get();
var id = event.id();
try {
var rs = Query.select(URL, MIME).from(APPOINTMENT_ATTACHMENTS).leftJoin(UID, URLS, UID).where(AID, equal(id)).exec(connection);
while (rs.next()) {
var u = rs.getString(URL);
try {
var url = URI.create(u).toURL();
var mime = rs.getString(MIME);
event.add(new Attachment(url, mime));
} catch (MalformedURLException e) {
LOG.log(WARNING, () -> "Failed to convert %s to URI!".formatted(u));
}
}
rs.close();
return Payload.of(event);
} catch (SQLException e) {
return Error.of("Failed to load tags for appointment %s".formatted(id), e);
}
}
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");
@ -133,10 +208,13 @@ public class MariaDB implements Database {
var end = nullable(results.getTimestamp("end")).map(Timestamp::toLocalDateTime).orElse(null); var end = nullable(results.getTimestamp("end")).map(Timestamp::toLocalDateTime).orElse(null);
var location = results.getString("location"); var location = results.getString("location");
var slug = results.getString("slug"); var slug = results.getString("slug");
var tags = nullIfEmpty(results.getString("tags"));
if (slug == null) slug = Calc.hash(start + "@" + location).orElse(null); if (slug == null) slug = Calc.hash(start + "@" + location).orElse(null);
var appointment = new BaseAppointment(id, title, description, start, end, location, slug); var appointment = new BaseAppointment(id, title, description, start, end, location, slug);
try {
var tags = nullIfEmpty(results.getString("tags"));
if (tags != null) appointment.tags(tags.split(",")); if (tags != null) appointment.tags(tags.split(","));
} catch (SQLException e) {
}
return Payload.of(appointment); return Payload.of(appointment);
} }

2
de.srsoftware.cal.importer/build.gradle.kts

@ -4,6 +4,6 @@ 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.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.2")
implementation("de.srsoftware:tools.web:1.3.8") implementation("de.srsoftware:tools.web:1.3.8")
} }

7
de.srsoftware.cal.web/build.gradle.kts

@ -1,4 +1,11 @@
description = "OpenCloudCal : Web" description = "OpenCloudCal : Web"
dependencies { dependencies {
implementation(project(":de.srsoftware.cal.api"))
implementation(project(":de.srsoftware.cal.db"))
implementation("de.srsoftware:tools.http:1.0.3")
implementation("de.srsoftware:tools.optionals:1.0.0")
implementation("de.srsoftware:tools.util:1.2.2")
implementation("org.json:json:20240303")
} }

27
de.srsoftware.cal.app/src/main/java/de/srsoftware/cal/app/ApiHandler.java → de.srsoftware.cal.web/src/main/java/de/srsoftware/cal/ApiHandler.java

@ -1,5 +1,5 @@
/* © SRSoftware 2024 */ /* © SRSoftware 2024 */
package de.srsoftware.cal.app; package de.srsoftware.cal;
import static de.srsoftware.tools.Optionals.nullable; import static de.srsoftware.tools.Optionals.nullable;
import static java.lang.System.Logger; import static java.lang.System.Logger;
@ -35,7 +35,7 @@ public class ApiHandler extends PathHandler {
return switch (path) { return switch (path) {
case "/event" -> loadEvent(ex,params); case "/event" -> loadEvent(ex,params);
case "/events/list" -> listEvents(ex,params); case "/events/list" -> listEvents(ex,params);
default -> notFound(ex); default -> PathHandler.notFound(ex);
}; };
} }
@ -44,32 +44,29 @@ public class ApiHandler extends PathHandler {
var start = nullable(params.get("start")).map(ApiHandler::toLocalDateTime).orElse(null); var start = nullable(params.get("start")).map(ApiHandler::toLocalDateTime).orElse(null);
var end = nullable(params.get("end")).map(ApiHandler::toLocalDateTime).orElse(null); var end = nullable(params.get("end")).map(ApiHandler::toLocalDateTime).orElse(null);
try { try {
return sendContent(ex,db.list(start, end).stream().map(Appointment::json).toList()); return PathHandler.sendContent(ex,db.list(start, end).stream().map(Appointment::json).toList());
} catch (SQLException e) { } catch (SQLException e) {
LOG.log(WARNING,"Failed to fetch events (start = {0}, end = {1}!",start,end,e); LOG.log(WARNING,"Failed to fetch events (start = {0}, end = {1}!",start,end,e);
} }
return notFound(ex); return PathHandler.notFound(ex);
} }
private boolean loadEvent(HttpExchange ex, Map<String, String> params) throws IOException { private boolean loadEvent(HttpExchange ex, Map<String, String> params) throws IOException {
var id = params.get("id"); var id = params.get("id");
if (id != null) try { if (id != null) try {
return sendContent(ex,db.loadEvent(Long.parseLong(id)).map(ApiHandler::toJson)); return PathHandler.sendContent(ex,db.loadEvent(Long.parseLong(id)).map(ApiHandler::toJson));
} catch (NumberFormatException | IOException nfe){ } catch (NumberFormatException | IOException nfe){
return sendContent(ex, Error.format("%s is not a numeric event id!",id)); return PathHandler.sendContent(ex, Error.format("%s is not a numeric event id!",id));
} }
return sendContent(ex,Error.of("ID missing")); return PathHandler.sendContent(ex,Error.of("ID missing"));
} }
private static Result<JSONObject> toJson(Result<Appointment> appointmentResult) { private static Result<JSONObject> toJson(Result<Appointment> res) {
var opt = appointmentResult.optional(); var opt = res.optional();
return opt.isEmpty() ? transform(appointmentResult) : if (opt.isEmpty()) return Result.transform(res);
Payload.of(opt.get().json()); return 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) { private static LocalDateTime toLocalDateTime(String dateString) {
try { try {
@ -78,4 +75,4 @@ public class ApiHandler extends PathHandler {
return null; return null;
} }
} }
} }

2
de.srsoftware.cal.app/src/main/java/de/srsoftware/cal/app/IndexHandler.java → de.srsoftware.cal.web/src/main/java/de/srsoftware/cal/IndexHandler.java

@ -1,5 +1,5 @@
/* © SRSoftware 2024 */ /* © SRSoftware 2024 */
package de.srsoftware.cal.app; package de.srsoftware.cal;
import com.sun.net.httpserver.HttpExchange; import com.sun.net.httpserver.HttpExchange;
import de.srsoftware.tools.PathHandler; import de.srsoftware.tools.PathHandler;

8
de.srsoftware.cal.app/src/main/java/de/srsoftware/cal/app/StaticHandler.java → de.srsoftware.cal.web/src/main/java/de/srsoftware/cal/StaticHandler.java

@ -1,5 +1,5 @@
/* © SRSoftware 2024 */ /* © SRSoftware 2024 */
package de.srsoftware.cal.app; package de.srsoftware.cal;
import com.sun.net.httpserver.HttpExchange; import com.sun.net.httpserver.HttpExchange;
import de.srsoftware.tools.PathHandler; import de.srsoftware.tools.PathHandler;
@ -25,14 +25,14 @@ public class StaticHandler extends PathHandler {
var file = Path.of(staticPath.get()).resolve(path).toFile(); var file = Path.of(staticPath.get()).resolve(path).toFile();
if (file.exists() && file.isFile()) url = file.toURI().toURL(); if (file.exists() && file.isFile()) url = file.toURI().toURL();
} }
if (url == null) return notFound(ex); if (url == null) return PathHandler.notFound(ex);
var conn = url.openConnection(); var conn = url.openConnection();
var mime = conn.getContentType(); var mime = conn.getContentType();
try (var input = conn.getInputStream()) { try (var input = conn.getInputStream()) {
var bos = new ByteArrayOutputStream(); var bos = new ByteArrayOutputStream();
input.transferTo(bos); input.transferTo(bos);
ex.getResponseHeaders().add(CONTENT_TYPE, mime); ex.getResponseHeaders().add(PathHandler.CONTENT_TYPE, mime);
return sendContent(ex, bos.toByteArray()); return PathHandler.sendContent(ex, bos.toByteArray());
} }
} }
} }

10
de.srsoftware.cal.app/src/main/resources/event.html → de.srsoftware.cal.web/src/main/resources/event.html

@ -7,11 +7,11 @@
<body> <body>
<nav /> <nav />
<h1>Loading…</h1> <h1>Loading…</h1>
<div id="time">Loading…</div> <div id="time">Loading time</div>
<div id="description">Loading…</div> <div id="description">Loading description</div>
<div id="tags">Loading…</div> <div id="links">Loading links</div>
<div id="links">Loading…</div> <div id="attachments">Loading attachments</div>
<div id="attachments">Loading…</div> <div id="tags">Loading tags</div>
<script> <script>
document.addEventListener("DOMContentLoaded", function(event){ document.addEventListener("DOMContentLoaded", function(event){
console.log("page loaded…"); console.log("page loaded…");

0
de.srsoftware.cal.app/src/main/resources/index.html → de.srsoftware.cal.web/src/main/resources/index.html

41
de.srsoftware.cal.app/src/main/resources/script/occ.js → de.srsoftware.cal.web/src/main/resources/script/occ.js

@ -45,12 +45,49 @@ function createTags(tagList){
async function handleEventData(response){ async function handleEventData(response){
if (response.ok){ if (response.ok){
var json = await response.json(); var json = await response.json();
document.getElementsByTagName('h1')[0].innerHTML = json.title; document.getElementsByTagName('h1')[0].innerHTML = json.title ? json.title : '';
document.getElementById('time').innerHTML = json.start + (json.end ? '…'+json.end : ''); document.getElementById('time').innerHTML = json.start + (json.end ? '…'+json.end : '');
document.getElementById('description').innerHTML = json.description; addDescription(json);
document.getElementById('tags').innerHTML = "Tags: "+json.tags.join(" ");
var links = document.getElementById('links');
links.innerHTML = "";
links.appendChild(linkList(json.links));
attachmentList(json.attachments);
} }
} }
function addDescription(json){
var desc = json.description ? json.description : '';
if (desc.indexOf('<')<0) desc = desc.replace(/\r?\n|\r/g,'<br/>');
document.getElementById('description').innerHTML = desc;
}
function attachmentList(json){
var attachments = document.getElementById('attachments');
attachments.innerHTML = '';
for (var attachment of json){
if (attachment.mime.startsWith('image')){
var img = document.createElement('img');
img.src = attachment.url;
attachments.appendChild(img);
}
}
}
function linkList(json){
var ul = document.createElement('ul');
for (var inner of json) {
var a = document.createElement('a');
a.href = inner.url;
a.innerHTML = inner.description;
var li = document.createElement('li')
li.appendChild(a);
ul.appendChild(li);
}
return ul;
}
async function handleEvents(response){ async function handleEvents(response){
if (response.ok){ if (response.ok){
var json = await response.json(); var json = await response.json();
Loading…
Cancel
Save