Browse Source

working on calendar application

Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
main
Stephan Richter 6 months ago
parent
commit
9f806637c2
  1. 1
      de.srsoftware.cal.api/build.gradle.kts
  2. 7
      de.srsoftware.cal.api/src/main/java/de/srsoftware/cal/api/Appointment.java
  3. 5
      de.srsoftware.cal.api/src/main/java/de/srsoftware/cal/api/Attachment.java
  4. 6
      de.srsoftware.cal.api/src/main/java/de/srsoftware/cal/api/Coords.java
  5. 5
      de.srsoftware.cal.api/src/main/java/de/srsoftware/cal/api/Link.java
  6. 9
      de.srsoftware.cal.app/build.gradle.kts
  7. 81
      de.srsoftware.cal.app/src/main/java/de/srsoftware/cal/app/ApiHandler.java
  8. 30
      de.srsoftware.cal.app/src/main/java/de/srsoftware/cal/app/Application.java
  9. 62
      de.srsoftware.cal.app/src/main/java/de/srsoftware/cal/app/EventList.java
  10. 22
      de.srsoftware.cal.app/src/main/java/de/srsoftware/cal/app/IndexHandler.java
  11. 38
      de.srsoftware.cal.app/src/main/java/de/srsoftware/cal/app/StaticHandler.java
  12. 22
      de.srsoftware.cal.app/src/main/resources/event.html
  13. 29
      de.srsoftware.cal.app/src/main/resources/index.html
  14. 82
      de.srsoftware.cal.app/src/main/resources/script/occ.js
  15. 3
      de.srsoftware.cal.base/build.gradle.kts
  16. 92
      de.srsoftware.cal.base/src/main/java/de/srsoftware/cal/BaseAppointment.java
  17. 2
      de.srsoftware.cal.db/build.gradle.kts
  18. 14
      de.srsoftware.cal.db/src/main/java/de/srsoftware/cal/db/Database.java
  19. 128
      de.srsoftware.cal.db/src/main/java/de/srsoftware/cal/db/MariaDB.java
  20. 2
      de.srsoftware.cal.importer/build.gradle.kts

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

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

7
de.srsoftware.cal.api/src/main/java/de/srsoftware/cal/api/Appointment.java

@ -4,6 +4,7 @@ package de.srsoftware.cal.api; @@ -4,6 +4,7 @@ package de.srsoftware.cal.api;
import java.time.LocalDateTime;
import java.util.Optional;
import java.util.Set;
import org.json.JSONObject;
/**
* This is the central object of the calendar: An appointment
@ -41,6 +42,12 @@ public interface Appointment { @@ -41,6 +42,12 @@ public interface Appointment {
long id();
/**
* get a json representation of this Appointment
* @return a JSON Object
*/
JSONObject json();
/**
* descriptive text of the location, e.g. address
* @return location text

5
de.srsoftware.cal.api/src/main/java/de/srsoftware/cal/api/Attachment.java

@ -2,6 +2,8 @@ @@ -2,6 +2,8 @@
package de.srsoftware.cal.api;
import java.net.URL;
import java.util.Map;
import org.json.JSONObject;
/**
* an attachment for appointments
@ -9,4 +11,7 @@ import java.net.URL; @@ -9,4 +11,7 @@ import java.net.URL;
* @param mime the mime type of the attached document
*/
public record Attachment(URL url, String mime) {
public JSONObject json() {
return new JSONObject(Map.of("url", url.toString(), "mime", mime));
}
}

6
de.srsoftware.cal.api/src/main/java/de/srsoftware/cal/api/Coords.java

@ -1,10 +1,16 @@ @@ -1,10 +1,16 @@
/* © SRSoftware 2024 */
package de.srsoftware.cal.api;
import java.util.Map;
import org.json.JSONObject;
/**
* cartesian coords
* @param longitude the longitude
* @param latitude the latitude
*/
public record Coords(double longitude, double latitude) {
public JSONObject json() {
return new JSONObject(Map.of("lon", longitude, "lat", latitude));
}
}

5
de.srsoftware.cal.api/src/main/java/de/srsoftware/cal/api/Link.java

@ -2,6 +2,8 @@ @@ -2,6 +2,8 @@
package de.srsoftware.cal.api;
import java.net.URL;
import java.util.Map;
import org.json.JSONObject;
/**
* Links are additional content that may be added to appointments
@ -9,4 +11,7 @@ import java.net.URL; @@ -9,4 +11,7 @@ import java.net.URL;
* @param desciption some information about the target of the link
*/
public record Link(URL url, String desciption) {
public JSONObject json() {
return new JSONObject(Map.of("description", desciption(), "url", url().toString()));
}
}

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

@ -7,8 +7,11 @@ dependencies { @@ -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")
}

81
de.srsoftware.cal.app/src/main/java/de/srsoftware/cal/app/ApiHandler.java

@ -0,0 +1,81 @@ @@ -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;
}
}
}

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

@ -1,14 +1,16 @@ @@ -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; @@ -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 { @@ -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();
}
}

62
de.srsoftware.cal.app/src/main/java/de/srsoftware/cal/app/EventList.java

@ -1,62 +0,0 @@ @@ -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;
}
}

22
de.srsoftware.cal.app/src/main/java/de/srsoftware/cal/app/IndexHandler.java

@ -0,0 +1,22 @@ @@ -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);
}
}

38
de.srsoftware.cal.app/src/main/java/de/srsoftware/cal/app/StaticHandler.java

@ -0,0 +1,38 @@ @@ -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

@ -0,0 +1,22 @@ @@ -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

@ -0,0 +1,29 @@ @@ -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

@ -0,0 +1,82 @@ @@ -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})`);
}

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

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

92
de.srsoftware.cal.base/src/main/java/de/srsoftware/cal/BaseAppointment.java

@ -8,20 +8,23 @@ import de.srsoftware.cal.api.Attachment; @@ -8,20 +8,23 @@ import de.srsoftware.cal.api.Attachment;
import de.srsoftware.cal.api.Coords;
import de.srsoftware.cal.api.Link;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.*;
import org.json.JSONObject;
/**
* basic class for Appointments
*/
public class BaseAppointment implements Appointment {
private final long id;
private final String title, description;
private final LocalDateTime end, start;
private final String hash;
private Coords coords = null;
private final Set<Attachment> attachments = new HashSet<>();
private final Set<String> tags = new HashSet<>();
private final Set<Link> links = new HashSet<>();
private static final DateTimeFormatter DATE_TIME = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm");
private final long id;
private final String title, description;
private final LocalDateTime end, start;
private final String hash;
private Coords coords = null;
private final Set<Attachment> attachments = new HashSet<>();
private final Set<String> tags = new HashSet<>();
private final Set<Link> links = new HashSet<>();
private final String location;
/**
@ -83,26 +86,6 @@ public class BaseAppointment implements Appointment { @@ -83,26 +86,6 @@ public class BaseAppointment implements Appointment {
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
* @param newCoords the coordinates to apply
@ -133,21 +116,42 @@ public class BaseAppointment implements Appointment { @@ -133,21 +116,42 @@ public class BaseAppointment implements Appointment {
return nullable(end);
}
@Override
public String slug() {
return hash;
}
@Override
public long 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
public String location() {
return location;
}
@Override
public String slug() {
return hash;
}
@Override
public LocalDateTime start() {
return start;
@ -155,7 +159,27 @@ public class BaseAppointment implements Appointment { @@ -155,7 +159,27 @@ public class BaseAppointment implements Appointment {
@Override
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
@ -170,6 +194,6 @@ public class BaseAppointment implements Appointment { @@ -170,6 +194,6 @@ public class BaseAppointment implements Appointment {
@Override
public Set<Link> urls() {
return Set.of();
return links;
}
}

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

@ -4,7 +4,7 @@ dependencies { @@ -4,7 +4,7 @@ dependencies {
implementation(project(":de.srsoftware.cal.api"))
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.util:1.2.1")
}

14
de.srsoftware.cal.db/src/main/java/de/srsoftware/cal/db/Database.java

@ -2,7 +2,9 @@ @@ -2,7 +2,9 @@
package de.srsoftware.cal.db;
import de.srsoftware.cal.api.Appointment;
import de.srsoftware.tools.Result;
import java.sql.SQLException;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Set;
@ -22,9 +24,19 @@ public interface Database { @@ -22,9 +24,19 @@ public interface Database {
* @param count the maximum number of appointments to return
* @param offset the number of appointments to skip
* @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;
/**
* 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
* @param tags only list appointments which have matching tags
@ -33,4 +45,6 @@ public interface Database { @@ -33,4 +45,6 @@ public interface Database {
* @return the list of appointments fetched from the db
*/
public List<Appointment> listByTags(Set<String> tags, Integer count, Integer offset);
Result<Appointment> loadEvent(long id);
}

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

@ -1,31 +1,30 @@ @@ -1,31 +1,30 @@
/* © SRSoftware 2024 */
package de.srsoftware.cal.db;
import static de.srsoftware.tools.Optionals.allEmpty;
import static de.srsoftware.tools.Optionals.nullable;
import static de.srsoftware.tools.Optionals.*;
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.api.Appointment;
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 java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Set;
import java.sql.*;
import java.time.LocalDateTime;
import java.util.*;
public class MariaDB implements Database {
private static final String SELECT_APPOINTMENTS = "SELECT * FROM appointments";
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 INSERT_HASH = "INSERT INTO appointment_hashes (aid, hash) values (?, ?) ON DUPLICATE KEY UPDATE hash=hash;";
private static final String UPDATE_DB_VERSION = "UPDATE config SET value = ? WHERE keyname = 'dbversion'";
private static final String INSERT_SLUG = "UPDATE IGNORE appointments SET slug = ? WHERE aid = ?";
private static Connection connection;
private static final System.Logger LOG = System.getLogger(MariaDB.class.getSimpleName());
private static final String ADD_SLUG = "ALTER TABLE appointments ADD slug VARCHAR(255) UNIQUE";
private static final String APPOINTMENTS = "appointments";
private static final String AID = "aid";
private static Connection connection;
private MariaDB(Connection conn) throws SQLException {
connection = conn;
@ -33,7 +32,8 @@ public class MariaDB implements Database { @@ -33,7 +32,8 @@ public class MariaDB implements Database {
}
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;
if (rs.next()) {
version = rs.getInt("value");
@ -48,35 +48,39 @@ public class MariaDB implements Database { @@ -48,35 +48,39 @@ public class MariaDB implements Database {
}
private void update1() throws SQLException {
LOG.log(INFO, "Updating db scheme from version 1 to 2…");
var list = new ArrayList<Appointment>();
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 rs = Query.of(SELECT_APPOINTMENTS).execute(connection);
LOG.log(DEBUG, "Reading existing appointments…");
var rs = Query.select("*").from("appointments").exec(connection);
while (rs.next()) {
var id = rs.getLong("aid");
var location = nullable(rs.getString("location"));
var id = rs.getLong(AID);
var location = nullable(nullIfEmpty(rs.getString("location")));
if (location.isEmpty()) continue;
var title = rs.getString("title");
LOG.log(TRACE, () -> "%s: %s".formatted(id, title));
var descr = rs.getString("description");
if (allEmpty(title, descr)) continue;
var start = nullable(rs.getTimestamp("start"));
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);
slugMap.put(id, slug);
}
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()) {
stmt.setString(1, entry.getValue());
stmt.setLong(2, entry.getKey());
stmt.execute();
query.apply(entry.getValue(), entry.getKey());
}
stmt = Query.of(UPDATE_DB_VERSION).statement(connection);
stmt.setLong(1, 2);
stmt.execute();
LOG.log(DEBUG, "Writing new db version marker…");
Query.update("config").set("value").where("keyname", equal("dbversion")).prepare(connection).apply(2);
connection.setAutoCommit(true);
}
@ -94,23 +98,61 @@ public class MariaDB implements Database { @@ -94,23 +98,61 @@ public class MariaDB implements Database {
return new MariaDB(DriverManager.getConnection(jdbc, user, pass));
}
@Override
public List<Appointment> list(Integer count, Integer offset) throws SQLException {
var list = new ArrayList<Appointment>();
var results = Query.of(SELECT_APPOINTMENTS).orderBy("start").execute(connection);
while (results.next()) {
var id = results.getInt("aid");
var title = results.getString("title");
var description = results.getString("description");
if (allEmpty(title, description)) continue;
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");
if (slug == null) slug = Calc.hash(start + "@" + location).orElse(null);
list.add(new BaseAppointment(id, title, description, start, end, location, slug));
var results = Query.select("*").from(APPOINTMENTS).sort("start").exec(connection);
while (results.next()) createAppointmentOf(results).optional().ifPresent(list::add);
results.close();
return list;
}
@Override
public Result<Appointment> loadEvent(long id) {
try {
var rs = Query //
.select("%s.*".formatted(APPOINTMENTS), "GROUP_CONCAT(keyword) AS tags")
.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();
return list;
}

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

@ -5,5 +5,5 @@ dependencies { @@ -5,5 +5,5 @@ dependencies {
implementation(project(":de.srsoftware.cal.base"))
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("de.srsoftware:tools.web:1.3.8")
}

Loading…
Cancel
Save