started to implemente SQLite backend
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
This commit is contained in:
@@ -2,11 +2,14 @@
|
|||||||
package de.srsoftware.cal.app;
|
package de.srsoftware.cal.app;
|
||||||
|
|
||||||
import static java.lang.System.Logger.Level.*;
|
import static java.lang.System.Logger.Level.*;
|
||||||
|
import static java.text.MessageFormat.format;
|
||||||
|
|
||||||
import com.sun.net.httpserver.HttpServer;
|
import com.sun.net.httpserver.HttpServer;
|
||||||
import de.srsoftware.cal.*;
|
import de.srsoftware.cal.*;
|
||||||
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.cal.db.SqlDb;
|
||||||
|
import de.srsoftware.cal.db.SQlite;
|
||||||
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 de.srsoftware.tools.ColorLogger;
|
||||||
@@ -36,13 +39,20 @@ public class Application {
|
|||||||
private static Database connect(Configuration config) throws SQLException {
|
private static Database connect(Configuration config) throws SQLException {
|
||||||
Optional<String> jdbc = config.get(JDBC);
|
Optional<String> jdbc = config.get(JDBC);
|
||||||
if (jdbc.isEmpty()) throw new RuntimeException(MISSING.formatted(JDBC));
|
if (jdbc.isEmpty()) throw new RuntimeException(MISSING.formatted(JDBC));
|
||||||
|
if (jdbc.get().contains(":mysql:") || jdbc.get().contains(":mariadb:")) return connectMariaDB(config, jdbc.get());
|
||||||
|
if (jdbc.get().contains(":sqlite:")) return SQlite.connect(jdbc.get());
|
||||||
|
throw new RuntimeException(format("Unknown database: {0}",jdbc.get()));
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private static Database connectMariaDB(Configuration config, String jdbc) throws SQLException {
|
||||||
String user = config.get(USER, "opencloudcal");
|
String user = config.get(USER, "opencloudcal");
|
||||||
|
|
||||||
Optional<String> pass = config.get(PASS);
|
Optional<String> pass = config.get(PASS);
|
||||||
if (pass.isEmpty()) throw new RuntimeException(MISSING.formatted(PASS));
|
if (pass.isEmpty()) throw new RuntimeException(MISSING.formatted(PASS));
|
||||||
|
|
||||||
return MariaDB.connect(jdbc.get(), user, pass.get());
|
return MariaDB.connect(jdbc, user, pass.get());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -7,4 +7,5 @@ dependencies {
|
|||||||
implementation("de.srsoftware:tools.jdbc:1.3.0")
|
implementation("de.srsoftware:tools.jdbc:1.3.0")
|
||||||
implementation("de.srsoftware:tools.optionals:1.0.0")
|
implementation("de.srsoftware:tools.optionals:1.0.0")
|
||||||
implementation("de.srsoftware:tools.util:1.3.0")
|
implementation("de.srsoftware:tools.util:1.3.0")
|
||||||
|
implementation("org.xerial:sqlite-jdbc:3.49.0.0")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,419 +1,32 @@
|
|||||||
/* © SRSoftware 2026 */
|
|
||||||
package de.srsoftware.cal.db;
|
package de.srsoftware.cal.db;
|
||||||
|
|
||||||
import static de.srsoftware.cal.Util.extractCoords;
|
import java.sql.Connection;
|
||||||
import static de.srsoftware.cal.db.Fields.*;
|
import java.sql.DriverManager;
|
||||||
import static de.srsoftware.cal.db.Fields.ALL;
|
import java.sql.SQLException;
|
||||||
import static de.srsoftware.tools.Error.error;
|
|
||||||
import static de.srsoftware.tools.Optionals.*;
|
|
||||||
import static de.srsoftware.tools.Result.transform;
|
|
||||||
import static de.srsoftware.tools.jdbc.Condition.*;
|
|
||||||
import static de.srsoftware.tools.jdbc.Query.*;
|
|
||||||
import static java.lang.System.Logger.Level.*;
|
|
||||||
|
|
||||||
import de.srsoftware.cal.BaseAppointment;
|
public class MariaDB extends SqlDb{
|
||||||
import de.srsoftware.cal.api.Appointment;
|
|
||||||
import de.srsoftware.cal.api.Attachment;
|
|
||||||
import de.srsoftware.cal.api.Link;
|
|
||||||
import de.srsoftware.tools.Payload;
|
|
||||||
import de.srsoftware.tools.Result;
|
|
||||||
import de.srsoftware.tools.jdbc.Query;
|
|
||||||
import java.net.MalformedURLException;
|
|
||||||
import java.net.URI;
|
|
||||||
import java.net.URL;
|
|
||||||
import java.sql.*;
|
|
||||||
import java.time.LocalDateTime;
|
|
||||||
import java.util.*;
|
|
||||||
|
|
||||||
public class MariaDB implements Database {
|
protected MariaDB(Connection conn) throws SQLException {
|
||||||
private static final System.Logger LOG = System.getLogger(MariaDB.class.getSimpleName());
|
super(conn);
|
||||||
private static final String APPOINTMENTS = "appointments";
|
|
||||||
private static final String APPOINTMENT_ATTACHMENTS = "appointment_attachments";
|
|
||||||
private static final String APPOINTMENT_TAGS = "appointment_tags";
|
|
||||||
private static final String APPOINTMENT_URLS = "appointment_urls";
|
|
||||||
private static final String CONFIG = "config";
|
|
||||||
private static final Object DB_VERSION = "dbversion";
|
|
||||||
private static final String KEYNAME = "keyname";
|
|
||||||
private static final String TAGS = "tags";
|
|
||||||
private static final String TAG_SEARCH = "tag_search";
|
|
||||||
private static final String URLS = "urls";
|
|
||||||
private static final String VALUE = "value";
|
|
||||||
private Connection connection;
|
|
||||||
|
|
||||||
private MariaDB(Connection conn) throws SQLException {
|
|
||||||
connection = conn;
|
|
||||||
applyUpdates();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void applyUpdates() throws SQLException {
|
|
||||||
LOG.log(INFO, "Checking for updates…");
|
|
||||||
var rs = select(VALUE).from(CONFIG).where(KEYNAME, equal(DB_VERSION)).exec(connection);
|
|
||||||
var version = 0;
|
|
||||||
if (rs.next()) {
|
|
||||||
version = rs.getInt(VALUE);
|
|
||||||
}
|
|
||||||
rs.getStatement().close();
|
|
||||||
rs.close();
|
|
||||||
switch (version) {
|
|
||||||
case 0:
|
|
||||||
createTables();
|
|
||||||
case 1:
|
|
||||||
createTagSearch();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void createTables() {
|
|
||||||
throw new RuntimeException("%s.createTables() not implemented!");
|
|
||||||
}
|
|
||||||
|
|
||||||
private void createTagSearch() throws SQLException {
|
|
||||||
var stmt = connection.prepareStatement("CREATE VIEW tag_search AS SELECT aid, GROUP_CONCAT(keyword) AS tags FROM appointment_tags LEFT JOIN tags ON appointment_tags.tid = tags.tid GROUP BY aid");
|
|
||||||
stmt.execute();
|
|
||||||
stmt.close();
|
|
||||||
Query.update(CONFIG).set(VALUE).where(KEYNAME,equal(DB_VERSION)).prepare(connection).apply(2).close();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Result<Appointment> add(Appointment appointment) {
|
|
||||||
try {
|
|
||||||
var start = Timestamp.valueOf(appointment.start());
|
|
||||||
var end = appointment.end().map(Timestamp::valueOf).orElse(null);
|
|
||||||
var coords = appointment.coords().map(Object::toString).orElse(null);
|
|
||||||
var location = appointment.location().orElse(null);
|
|
||||||
ResultSet keys = insertInto(APPOINTMENTS, TITLE, DESCRIPTION, START, END, LOCATION, COORDS) //
|
|
||||||
.values(appointment.title(), appointment.description(), start, end, location, coords)
|
|
||||||
.execute(connection)
|
|
||||||
.getGeneratedKeys();
|
|
||||||
Appointment saved = null;
|
|
||||||
if (keys.next()) saved = appointment.clone(keys.getLong(1));
|
|
||||||
keys.getStatement().close();
|
|
||||||
keys.close();
|
|
||||||
if (saved == null) return error("Insert query did not return appointment id!");
|
|
||||||
|
|
||||||
writeAttachments(saved);
|
|
||||||
writeLinks(saved);
|
|
||||||
writeTags(saved);
|
|
||||||
|
|
||||||
return Payload.of(saved);
|
|
||||||
} catch (SQLException e) {
|
|
||||||
LOG.log(ERROR, "Failed to store appointment", e);
|
|
||||||
return error(e, "Failed to store appointment");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void writeAttachments(Appointment saved) throws SQLException { // link to attachments
|
|
||||||
var attachments = saved.attachments();
|
|
||||||
InsertQuery assignQuery = null;
|
|
||||||
for (var attachment : attachments) {
|
|
||||||
var urlId = getOrCreateUrl(attachment.url());
|
|
||||||
if (assignQuery == null) assignQuery = insertInto(APPOINTMENT_ATTACHMENTS, AID, UID, MIME);
|
|
||||||
if (urlId.isPresent()) assignQuery.values(saved.id(), urlId.get(), attachment.mime());
|
|
||||||
}
|
|
||||||
if (assignQuery != null) assignQuery.ignoreDuplicates().execute(connection).close();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void writeLinks(Appointment saved) throws SQLException { // link to links
|
|
||||||
var links = saved.links();
|
|
||||||
InsertQuery assignQuery = null;
|
|
||||||
for (var link : links) {
|
|
||||||
var urlId = getOrCreateUrl(link.url());
|
|
||||||
if (assignQuery == null) assignQuery = insertInto(APPOINTMENT_URLS, AID, UID, DESCRIPTION);
|
|
||||||
if (urlId.isPresent()) assignQuery.values(saved.id(), urlId.get(), link.desciption());
|
|
||||||
}
|
|
||||||
if (assignQuery != null) assignQuery.ignoreDuplicates().execute(connection).close();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void writeTags(Appointment saved) throws SQLException {
|
|
||||||
var tags = saved.tags();
|
|
||||||
InsertQuery assignQuery = null;
|
|
||||||
for (var tag : tags) {
|
|
||||||
var tagId = getOrCreateTag(tag);
|
|
||||||
if (assignQuery == null) assignQuery = insertInto(APPOINTMENT_TAGS, AID, TID);
|
|
||||||
if (tagId.isPresent()) assignQuery.values(saved.id(), tagId.get());
|
|
||||||
}
|
|
||||||
if (assignQuery != null) assignQuery.ignoreDuplicates().execute(connection).close();
|
|
||||||
}
|
|
||||||
|
|
||||||
private Optional<Long> getOrCreateUrl(URL url) throws SQLException {
|
|
||||||
var rs = select(UID).from(URLS).where(URL, equal(url.toString())).exec(connection);
|
|
||||||
Long uid = null;
|
|
||||||
if (rs.next()) uid = rs.getLong(1);
|
|
||||||
rs.getStatement().close();
|
|
||||||
rs.close();
|
|
||||||
if (uid == null) {
|
|
||||||
rs = insertInto(URLS, URL).values(url.toString()).execute(connection).getGeneratedKeys();
|
|
||||||
if (rs.next()) uid = rs.getLong(1);
|
|
||||||
rs.getStatement().close();
|
|
||||||
rs.close();
|
|
||||||
}
|
|
||||||
return nullable(uid);
|
|
||||||
}
|
|
||||||
|
|
||||||
private Optional<Long> getOrCreateTag(String tag) throws SQLException {
|
|
||||||
var rs = select(TID).from(TAGS).where(KEYWORD, equal(tag)).exec(connection);
|
|
||||||
Long tid = null;
|
|
||||||
if (rs.next()) tid = rs.getLong(1);
|
|
||||||
rs.getStatement().close();
|
|
||||||
rs.close();
|
|
||||||
if (tid == null) {
|
|
||||||
rs = insertInto(TAGS, KEYWORD).values(tag).execute(connection).getGeneratedKeys();
|
|
||||||
if (rs.next()) tid = rs.getLong(1);
|
|
||||||
rs.getStatement().close();
|
|
||||||
rs.close();
|
|
||||||
}
|
|
||||||
return nullable(tid);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Database connect(String jdbc, String user, String pass) throws SQLException {
|
public static Database connect(String jdbc, String user, String pass) throws SQLException {
|
||||||
return new MariaDB(DriverManager.getConnection(jdbc, user, pass));
|
return new MariaDB(DriverManager.getConnection(jdbc, user, pass));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
protected void createAppointmentsTable() throws SQLException {
|
||||||
public Result<List<String>> findTags(String infix) {
|
connection.prepareStatement("CREATE TABLE IF NOT EXISTS appointments (aid INT PRIMARY KEY AUTO_INCREMENT, title TEXT NOT NULL, description TEXT, start DATETIME NOT NULL, end DATETIME, location TEXT, coords TEXT)").execute();
|
||||||
try {
|
|
||||||
List<String> results = new ArrayList<>();
|
|
||||||
var rs = select(KEYWORD).from(TAGS).where(KEYWORD, like("%%%s%%".formatted(infix))).sort(KEYWORD).exec(connection);
|
|
||||||
while (rs.next()) results.add(rs.getString(KEYWORD));
|
|
||||||
rs.getStatement().close();
|
|
||||||
rs.close();
|
|
||||||
return Payload.of(results);
|
|
||||||
} catch (SQLException e) {
|
|
||||||
return error(e, "failed to gather tags from DB.");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public Result<List<Appointment>> list(LocalDateTime from, LocalDateTime till, Integer count, Integer offset, Collection<String> tags) {
|
|
||||||
List<Long> aids = null;
|
|
||||||
if (tags != null && !tags.isEmpty()){
|
|
||||||
var query = select(AID).from(TAG_SEARCH);
|
|
||||||
for (var tag : tags) query.where(TAGS,like("%"+tag+"%"));
|
|
||||||
|
|
||||||
try {
|
protected void createUrlsTable() throws SQLException {
|
||||||
var rs = query.exec(connection);
|
connection.prepareStatement("CREATE TABLE IF NOT EXISTS urls (uid INT PRIMARY KEY AUTO_INCREMENT, url TEXT NOT NULL)").execute();
|
||||||
aids = new ArrayList<>();
|
connection.prepareStatement("CREATE TABLE IF NOT EXISTS appointment_urls (aid INT NOT NULL, uid INT NOT NULL, description TEXT, PRIMARY KEY(aid,uid))").execute();
|
||||||
while (rs.next()) aids.add(rs.getLong(AID));
|
|
||||||
rs.getStatement().close();
|
|
||||||
rs.close();
|
|
||||||
} catch (SQLException e) {
|
|
||||||
return SqlError.of(e,"Failed to read appointment ids for tags %s",tags);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
var query = select("appointments.*", "GROUP_CONCAT(keyword) AS tags")
|
|
||||||
.from(APPOINTMENTS)
|
|
||||||
.leftJoin(AID, "appointment_tags", AID)
|
|
||||||
.leftJoin("tid", "tags", "tid")
|
|
||||||
.groupBy(AID)
|
|
||||||
.sort("start ASC");
|
|
||||||
if (aids != null && !aids.isEmpty()) query.where(APPOINTMENTS+"."+AID,in(aids.toArray()));
|
|
||||||
if (from != null) query.where(START, moreThan(Timestamp.valueOf(from)));
|
|
||||||
if (till != null) query.where(END, lessThan(Timestamp.valueOf(till)));
|
|
||||||
if (count != null) query.limit(count);
|
|
||||||
if (offset != null) query.skip(offset);
|
|
||||||
try {
|
|
||||||
var results = query.exec(connection);
|
|
||||||
var list = new ArrayList<Appointment>();
|
|
||||||
while (results.next()) createAppointmentOf(results).optional().ifPresent(list::add);
|
|
||||||
results.getStatement().close();
|
|
||||||
results.close();
|
|
||||||
addAttachments(list);
|
|
||||||
return Payload.of(list);
|
|
||||||
} catch (SQLException e) {
|
|
||||||
return SqlError.of(e, "Failed to fetch appointments from database!");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void addAttachments(ArrayList<Appointment> list) {
|
protected void createTagsTable() throws SQLException {
|
||||||
if (list.isEmpty()) return;
|
connection.prepareStatement("CREATE TABLE IF NOT EXISTS tags (tid INT PRIMARY KEY AUTO_INCREMENT, keyword VARCHAR(100) NOT NULL)").execute();
|
||||||
var map = new HashMap<Long,BaseAppointment>();
|
connection.prepareStatement("CREATE TABLE IF NOT EXISTS appointment_tags (aid INT NOT NULL, tid INT NOT NULL, PRIMARY KEY(aid, tid))").execute();
|
||||||
|
|
||||||
list.stream().filter(app -> app instanceof BaseAppointment)
|
|
||||||
.map(BaseAppointment.class::cast)
|
|
||||||
.forEach(app -> map.put(app.id(),app));
|
|
||||||
var keys = map.keySet().toArray();
|
|
||||||
try {
|
|
||||||
var rs = select(ALL).from(APPOINTMENT_ATTACHMENTS).leftJoin(UID,URLS,UID).where(AID,in(keys)).exec(connection);
|
|
||||||
while (rs.next()){
|
|
||||||
var aid = rs.getLong(AID);
|
|
||||||
var app = map.get(aid);
|
|
||||||
if (app == null) continue;
|
|
||||||
var mime = rs.getString(MIME);
|
|
||||||
var uri = rs.getString(URL);
|
|
||||||
try {
|
|
||||||
var url = URI.create(uri).toURL();
|
|
||||||
app.add(new Attachment(url,mime));
|
|
||||||
} catch (Exception e) {
|
|
||||||
LOG.log(WARNING,"Failed to create URL object from %s",uri);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
rs.getStatement().close();
|
|
||||||
rs.close();
|
|
||||||
} catch (Exception e) {
|
|
||||||
LOG.log(WARNING,"Failed to load attachments.",e);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public Result<Appointment> loadEvent(long id) {
|
|
||||||
try {
|
|
||||||
var rs = select(ALL).from(APPOINTMENTS).where(AID, equal(id)).exec(connection);
|
|
||||||
Result<Appointment> result = rs.next() ? createAppointmentOf(rs).map(this::loadExtra) : NotFound.of("Failed to find appointment with id %s", id);
|
|
||||||
rs.getStatement().close();
|
|
||||||
rs.close();
|
|
||||||
return result;
|
|
||||||
} catch (SQLException e) {
|
|
||||||
return SqlError.of(e, "Failed to load appointment with id = %s", id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Result<Appointment> loadEvent(String location, LocalDateTime start) {
|
|
||||||
try {
|
|
||||||
var rs = select(ALL).from(APPOINTMENTS).where(LOCATION, equal(location)).where(START, equal(Timestamp.valueOf(start))).exec(connection);
|
|
||||||
Result<Appointment> result = rs.next() ? createAppointmentOf(rs).map(this::loadExtra) : error("Failed to find appointment starting %s @ %s", start, location);
|
|
||||||
rs.getStatement().close();
|
|
||||||
rs.close();
|
|
||||||
return result;
|
|
||||||
} catch (SQLException e) {
|
|
||||||
return error(e, "Failed to load appointment starting %s @ %s", start, location);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private Result<Appointment> loadExtra(Result<BaseAppointment> res) {
|
|
||||||
return loadTags(res).map(this::loadLinks).map(this::loadAttachments);
|
|
||||||
}
|
|
||||||
|
|
||||||
private 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 = select(KEYWORD).from(APPOINTMENT_TAGS).leftJoin(TID, "tags", TID).where(AID, equal(id)).exec(connection);
|
|
||||||
while (rs.next()) event.tags(rs.getString(1));
|
|
||||||
rs.getStatement().close();
|
|
||||||
rs.close();
|
|
||||||
return Payload.of(event);
|
|
||||||
} catch (SQLException e) {
|
|
||||||
return error(e, "Failed to load tags for appointment %s", id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private 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 = 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 (Exception e) {
|
|
||||||
LOG.log(WARNING, () -> "Failed to convert %s to URI!".formatted(u));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
rs.getStatement().close();
|
|
||||||
rs.close();
|
|
||||||
return Payload.of(event);
|
|
||||||
} catch (SQLException e) {
|
|
||||||
return error(e, "Failed to load tags for appointment %s", id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private 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 = 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.getStatement().close();
|
|
||||||
rs.close();
|
|
||||||
return Payload.of(event);
|
|
||||||
} catch (SQLException e) {
|
|
||||||
return error(e, "Failed to load tags for appointment %s".formatted(id));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private Result<BaseAppointment> 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("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 appointment = new BaseAppointment(id, title, description, start, end, location);
|
|
||||||
|
|
||||||
try {
|
|
||||||
extractCoords(results.getString(COORDS)).optional().ifPresent(appointment::coords);
|
|
||||||
} catch (SQLException e) {
|
|
||||||
LOG.log(TRACE, "Result set did not cointain coords!");
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
var tags = nullIfEmpty(results.getString("tags"));
|
|
||||||
if (tags != null) appointment.tags(tags.split(","));
|
|
||||||
} catch (SQLException e) {
|
|
||||||
LOG.log(TRACE, "Result set did not cointain tags!");
|
|
||||||
}
|
|
||||||
return Payload.of(appointment);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public List<Appointment> listByTags(Set<String> tags, Integer count, Integer offset) {
|
|
||||||
return List.of();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Result<Long> removeAppointment(long id) {
|
|
||||||
try {
|
|
||||||
delete().from(APPOINTMENTS).where(AID, equal(id)).execute(connection);
|
|
||||||
delete().from(APPOINTMENT_TAGS).where(AID, equal(id)).execute(connection);
|
|
||||||
delete().from(APPOINTMENT_ATTACHMENTS).where(AID, equal(id)).execute(connection);
|
|
||||||
delete().from(APPOINTMENT_URLS).where(AID, equal(id)).execute(connection);
|
|
||||||
return Payload.of(id);
|
|
||||||
} catch (SQLException e) {
|
|
||||||
return SqlError.of(e, "Failed to delete event %s", id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Result<Appointment> update(Appointment event) {
|
|
||||||
var start = Timestamp.valueOf(event.start());
|
|
||||||
var end = event.end().map(Timestamp::valueOf).orElse(null);
|
|
||||||
var coords = event.coords().map(Object::toString).orElse(null);
|
|
||||||
var location = event.location().orElse(null);
|
|
||||||
try {
|
|
||||||
long id = event.id();
|
|
||||||
Query.update(APPOINTMENTS)
|
|
||||||
.set(TITLE, DESCRIPTION, START, END, LOCATION, COORDS)
|
|
||||||
.where(AID, equal(id))
|
|
||||||
.prepare(connection)
|
|
||||||
.apply(event.title(), event.description(), start, end, location, coords)
|
|
||||||
.close();
|
|
||||||
|
|
||||||
delete().from(APPOINTMENT_TAGS).where(AID, equal(id)).execute(connection);
|
|
||||||
writeTags(event);
|
|
||||||
delete().from(APPOINTMENT_ATTACHMENTS).where(AID, equal(id)).execute(connection);
|
|
||||||
writeAttachments(event);
|
|
||||||
delete().from(APPOINTMENT_URLS).where(AID, equal(id)).execute(connection);
|
|
||||||
writeLinks(event);
|
|
||||||
|
|
||||||
return Payload.of(event);
|
|
||||||
} catch (SQLException sqle) {
|
|
||||||
return error(sqle, "Failed to update database entry");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
package de.srsoftware.cal.db;
|
||||||
|
|
||||||
|
import java.sql.Connection;
|
||||||
|
import java.sql.DriverManager;
|
||||||
|
import java.sql.SQLException;
|
||||||
|
|
||||||
|
|
||||||
|
public class SQlite extends SqlDb {
|
||||||
|
|
||||||
|
protected SQlite(Connection conn) throws SQLException {
|
||||||
|
super(conn);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void createTagsTable() throws SQLException {
|
||||||
|
connection.prepareStatement("CREATE TABLE IF NOT EXISTS tags (tid INTEGER PRIMARY KEY AUTOINCREMENT, keyword VARCHAR(100) NOT NULL)").execute();
|
||||||
|
connection.prepareStatement("CREATE TABLE IF NOT EXISTS appointment_tags (aid INT NOT NULL, tid INT NOT NULL, PRIMARY KEY(aid, tid))").execute();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void createUrlsTable() throws SQLException {
|
||||||
|
connection.prepareStatement("CREATE TABLE IF NOT EXISTS urls (uid INTEGER PRIMARY KEY AUTOINCREMENT, url TEXT NOT NULL)").execute();
|
||||||
|
connection.prepareStatement("CREATE TABLE IF NOT EXISTS appointment_urls (aid INT NOT NULL, uid INT NOT NULL, description TEXT, PRIMARY KEY(aid,uid))").execute();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void createAppointmentsTable() throws SQLException {
|
||||||
|
connection.prepareStatement("CREATE TABLE IF NOT EXISTS appointments (aid INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL, description TEXT, start DATETIME NOT NULL, end DATETIME, location TEXT, coords TEXT)").execute();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static SQlite connect(String jdbc) throws SQLException {
|
||||||
|
return new SQlite(DriverManager.getConnection(jdbc));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,449 @@
|
|||||||
|
/* © SRSoftware 2026 */
|
||||||
|
package de.srsoftware.cal.db;
|
||||||
|
|
||||||
|
import static de.srsoftware.cal.Util.extractCoords;
|
||||||
|
import static de.srsoftware.cal.db.Fields.*;
|
||||||
|
import static de.srsoftware.cal.db.Fields.ALL;
|
||||||
|
import static de.srsoftware.tools.Error.error;
|
||||||
|
import static de.srsoftware.tools.Optionals.*;
|
||||||
|
import static de.srsoftware.tools.Result.transform;
|
||||||
|
import static de.srsoftware.tools.jdbc.Condition.*;
|
||||||
|
import static de.srsoftware.tools.jdbc.Query.*;
|
||||||
|
import static java.lang.System.Logger.Level.*;
|
||||||
|
|
||||||
|
import de.srsoftware.cal.BaseAppointment;
|
||||||
|
import de.srsoftware.cal.api.Appointment;
|
||||||
|
import de.srsoftware.cal.api.Attachment;
|
||||||
|
import de.srsoftware.cal.api.Link;
|
||||||
|
import de.srsoftware.tools.Payload;
|
||||||
|
import de.srsoftware.tools.Result;
|
||||||
|
import de.srsoftware.tools.jdbc.Query;
|
||||||
|
import java.net.MalformedURLException;
|
||||||
|
import java.net.URI;
|
||||||
|
import java.net.URL;
|
||||||
|
import java.sql.*;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
public abstract class SqlDb implements Database {
|
||||||
|
private static final System.Logger LOG = System.getLogger(SqlDb.class.getSimpleName());
|
||||||
|
private static final String APPOINTMENTS = "appointments";
|
||||||
|
private static final String APPOINTMENT_ATTACHMENTS = "appointment_attachments";
|
||||||
|
private static final String APPOINTMENT_TAGS = "appointment_tags";
|
||||||
|
private static final String APPOINTMENT_URLS = "appointment_urls";
|
||||||
|
private static final String CONFIG = "config";
|
||||||
|
private static final Object DB_VERSION = "dbversion";
|
||||||
|
private static final String KEYNAME = "keyname";
|
||||||
|
private static final String TAGS = "tags";
|
||||||
|
private static final String TAG_SEARCH = "tag_search";
|
||||||
|
private static final String URLS = "urls";
|
||||||
|
private static final String VALUE = "value";
|
||||||
|
protected Connection connection;
|
||||||
|
|
||||||
|
protected SqlDb(Connection conn) throws SQLException {
|
||||||
|
connection = conn;
|
||||||
|
applyUpdates();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void applyUpdates() throws SQLException {
|
||||||
|
LOG.log(INFO, "Checking for updates…");
|
||||||
|
var version = 0;
|
||||||
|
try {
|
||||||
|
var rs = select(VALUE).from(CONFIG).where(KEYNAME, equal(DB_VERSION)).exec(connection);
|
||||||
|
if (rs.next()) {
|
||||||
|
version = rs.getInt(VALUE);
|
||||||
|
}
|
||||||
|
rs.getStatement().close();
|
||||||
|
rs.close();
|
||||||
|
} catch (SQLException sqle){
|
||||||
|
if (!sqle.getMessage().contains("no such table: config")){
|
||||||
|
throw sqle;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
switch (version) {
|
||||||
|
case 0:
|
||||||
|
createTables();
|
||||||
|
case 1:
|
||||||
|
createTagSearch();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract void createAppointmentsTable() throws SQLException;
|
||||||
|
|
||||||
|
private void createAttachmentsTable() throws SQLException {
|
||||||
|
connection.prepareStatement("CREATE TABLE IF NOT EXISTS appointment_attachments (aid INT NOT NULL, uid INT NOT NULL, mime TEXT, PRIMARY KEY(aid,uid))").execute();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private void createConfigTable() throws SQLException {
|
||||||
|
connection.prepareStatement("CREATE TABLE IF NOT EXISTS config (keyname VARCHAR(100) PRIMARY KEY, value TEXT NOT NULL)").execute();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
protected abstract void createTagsTable() throws SQLException;
|
||||||
|
|
||||||
|
|
||||||
|
private void createImportedTable() throws SQLException {
|
||||||
|
connection.prepareStatement("CREATE TABLE IF NOT EXISTS imported_appointments (md5hash BINARY(32) PRIMARY KEY, aid INT NOT NULL)").execute();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void createTables() throws SQLException {
|
||||||
|
createConfigTable();
|
||||||
|
createAttachmentsTable();
|
||||||
|
createTagsTable();
|
||||||
|
createUrlsTable();
|
||||||
|
createAppointmentsTable();
|
||||||
|
createImportedTable();
|
||||||
|
Query.insertInto(CONFIG,KEYNAME,VALUE).values(DB_VERSION,1).execute(connection).close();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract void createUrlsTable() throws SQLException;
|
||||||
|
|
||||||
|
private void createTagSearch() throws SQLException {
|
||||||
|
var stmt = connection.prepareStatement("CREATE VIEW tag_search AS SELECT aid, GROUP_CONCAT(keyword) AS tags FROM appointment_tags LEFT JOIN tags ON appointment_tags.tid = tags.tid GROUP BY aid");
|
||||||
|
stmt.execute();
|
||||||
|
stmt.close();
|
||||||
|
Query.update(CONFIG).set(VALUE).where(KEYNAME,equal(DB_VERSION)).prepare(connection).apply(2).close();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Result<Appointment> add(Appointment appointment) {
|
||||||
|
try {
|
||||||
|
var start = Timestamp.valueOf(appointment.start());
|
||||||
|
var end = appointment.end().map(Timestamp::valueOf).orElse(null);
|
||||||
|
var coords = appointment.coords().map(Object::toString).orElse(null);
|
||||||
|
var location = appointment.location().orElse(null);
|
||||||
|
ResultSet keys = insertInto(APPOINTMENTS, TITLE, DESCRIPTION, START, END, LOCATION, COORDS) //
|
||||||
|
.values(appointment.title(), appointment.description(), start, end, location, coords)
|
||||||
|
.execute(connection)
|
||||||
|
.getGeneratedKeys();
|
||||||
|
Appointment saved = null;
|
||||||
|
if (keys.next()) saved = appointment.clone(keys.getLong(1));
|
||||||
|
keys.getStatement().close();
|
||||||
|
keys.close();
|
||||||
|
if (saved == null) return error("Insert query did not return appointment id!");
|
||||||
|
|
||||||
|
writeAttachments(saved);
|
||||||
|
writeLinks(saved);
|
||||||
|
writeTags(saved);
|
||||||
|
|
||||||
|
return Payload.of(saved);
|
||||||
|
} catch (SQLException e) {
|
||||||
|
LOG.log(ERROR, "Failed to store appointment", e);
|
||||||
|
return error(e, "Failed to store appointment");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void writeAttachments(Appointment saved) throws SQLException { // link to attachments
|
||||||
|
var attachments = saved.attachments();
|
||||||
|
InsertQuery assignQuery = null;
|
||||||
|
for (var attachment : attachments) {
|
||||||
|
var urlId = getOrCreateUrl(attachment.url());
|
||||||
|
if (assignQuery == null) assignQuery = insertInto(APPOINTMENT_ATTACHMENTS, AID, UID, MIME);
|
||||||
|
if (urlId.isPresent()) assignQuery.values(saved.id(), urlId.get(), attachment.mime());
|
||||||
|
}
|
||||||
|
if (assignQuery != null) assignQuery.ignoreDuplicates().execute(connection).close();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void writeLinks(Appointment saved) throws SQLException { // link to links
|
||||||
|
var links = saved.links();
|
||||||
|
InsertQuery assignQuery = null;
|
||||||
|
for (var link : links) {
|
||||||
|
var urlId = getOrCreateUrl(link.url());
|
||||||
|
if (assignQuery == null) assignQuery = insertInto(APPOINTMENT_URLS, AID, UID, DESCRIPTION);
|
||||||
|
if (urlId.isPresent()) assignQuery.values(saved.id(), urlId.get(), link.desciption());
|
||||||
|
}
|
||||||
|
if (assignQuery != null) assignQuery.ignoreDuplicates().execute(connection).close();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void writeTags(Appointment saved) throws SQLException {
|
||||||
|
var tags = saved.tags();
|
||||||
|
InsertQuery assignQuery = null;
|
||||||
|
for (var tag : tags) {
|
||||||
|
var tagId = getOrCreateTag(tag);
|
||||||
|
if (assignQuery == null) assignQuery = insertInto(APPOINTMENT_TAGS, AID, TID);
|
||||||
|
if (tagId.isPresent()) assignQuery.values(saved.id(), tagId.get());
|
||||||
|
}
|
||||||
|
if (assignQuery != null) assignQuery.ignoreDuplicates().execute(connection).close();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Optional<Long> getOrCreateUrl(URL url) throws SQLException {
|
||||||
|
var rs = select(UID).from(URLS).where(URL, equal(url.toString())).exec(connection);
|
||||||
|
Long uid = null;
|
||||||
|
if (rs.next()) uid = rs.getLong(1);
|
||||||
|
rs.getStatement().close();
|
||||||
|
rs.close();
|
||||||
|
if (uid == null) {
|
||||||
|
rs = insertInto(URLS, URL).values(url.toString()).execute(connection).getGeneratedKeys();
|
||||||
|
if (rs.next()) uid = rs.getLong(1);
|
||||||
|
rs.getStatement().close();
|
||||||
|
rs.close();
|
||||||
|
}
|
||||||
|
return nullable(uid);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Optional<Long> getOrCreateTag(String tag) throws SQLException {
|
||||||
|
var rs = select(TID).from(TAGS).where(KEYWORD, equal(tag)).exec(connection);
|
||||||
|
Long tid = null;
|
||||||
|
if (rs.next()) tid = rs.getLong(1);
|
||||||
|
rs.getStatement().close();
|
||||||
|
rs.close();
|
||||||
|
if (tid == null) {
|
||||||
|
rs = insertInto(TAGS, KEYWORD).values(tag).execute(connection).getGeneratedKeys();
|
||||||
|
if (rs.next()) tid = rs.getLong(1);
|
||||||
|
rs.getStatement().close();
|
||||||
|
rs.close();
|
||||||
|
}
|
||||||
|
return nullable(tid);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Result<List<String>> findTags(String infix) {
|
||||||
|
try {
|
||||||
|
List<String> results = new ArrayList<>();
|
||||||
|
var rs = select(KEYWORD).from(TAGS).where(KEYWORD, like("%%%s%%".formatted(infix))).sort(KEYWORD).exec(connection);
|
||||||
|
while (rs.next()) results.add(rs.getString(KEYWORD));
|
||||||
|
rs.getStatement().close();
|
||||||
|
rs.close();
|
||||||
|
return Payload.of(results);
|
||||||
|
} catch (SQLException e) {
|
||||||
|
return error(e, "failed to gather tags from DB.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Result<List<Appointment>> list(LocalDateTime from, LocalDateTime till, Integer count, Integer offset, Collection<String> tags) {
|
||||||
|
List<Long> aids = null;
|
||||||
|
if (tags != null && !tags.isEmpty()){
|
||||||
|
var query = select(AID).from(TAG_SEARCH);
|
||||||
|
for (var tag : tags) query.where(TAGS,like("%"+tag+"%"));
|
||||||
|
|
||||||
|
try {
|
||||||
|
var rs = query.exec(connection);
|
||||||
|
aids = new ArrayList<>();
|
||||||
|
while (rs.next()) aids.add(rs.getLong(AID));
|
||||||
|
rs.getStatement().close();
|
||||||
|
rs.close();
|
||||||
|
} catch (SQLException e) {
|
||||||
|
return SqlError.of(e,"Failed to read appointment ids for tags %s",tags);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var query = select("appointments.*", "GROUP_CONCAT(keyword) AS tags")
|
||||||
|
.from(APPOINTMENTS)
|
||||||
|
.leftJoin(AID, "appointment_tags", AID)
|
||||||
|
.leftJoin("tid", "tags", "tid")
|
||||||
|
.groupBy(AID)
|
||||||
|
.sort("start ASC");
|
||||||
|
if (aids != null && !aids.isEmpty()) query.where(APPOINTMENTS+"."+AID,in(aids.toArray()));
|
||||||
|
if (from != null) query.where(START, moreThan(Timestamp.valueOf(from)));
|
||||||
|
if (till != null) query.where(END, lessThan(Timestamp.valueOf(till)));
|
||||||
|
if (count != null) query.limit(count);
|
||||||
|
if (offset != null) query.skip(offset);
|
||||||
|
try {
|
||||||
|
var results = query.exec(connection);
|
||||||
|
var list = new ArrayList<Appointment>();
|
||||||
|
while (results.next()) createAppointmentOf(results).optional().ifPresent(list::add);
|
||||||
|
results.getStatement().close();
|
||||||
|
results.close();
|
||||||
|
addAttachments(list);
|
||||||
|
return Payload.of(list);
|
||||||
|
} catch (SQLException e) {
|
||||||
|
return SqlError.of(e, "Failed to fetch appointments from database!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addAttachments(ArrayList<Appointment> list) {
|
||||||
|
if (list.isEmpty()) return;
|
||||||
|
var map = new HashMap<Long,BaseAppointment>();
|
||||||
|
|
||||||
|
list.stream().filter(app -> app instanceof BaseAppointment)
|
||||||
|
.map(BaseAppointment.class::cast)
|
||||||
|
.forEach(app -> map.put(app.id(),app));
|
||||||
|
var keys = map.keySet().toArray();
|
||||||
|
try {
|
||||||
|
var rs = select(ALL).from(APPOINTMENT_ATTACHMENTS).leftJoin(UID,URLS,UID).where(AID,in(keys)).exec(connection);
|
||||||
|
while (rs.next()){
|
||||||
|
var aid = rs.getLong(AID);
|
||||||
|
var app = map.get(aid);
|
||||||
|
if (app == null) continue;
|
||||||
|
var mime = rs.getString(MIME);
|
||||||
|
var uri = rs.getString(URL);
|
||||||
|
try {
|
||||||
|
var url = URI.create(uri).toURL();
|
||||||
|
app.add(new Attachment(url,mime));
|
||||||
|
} catch (Exception e) {
|
||||||
|
LOG.log(WARNING,"Failed to create URL object from %s",uri);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rs.getStatement().close();
|
||||||
|
rs.close();
|
||||||
|
} catch (Exception e) {
|
||||||
|
LOG.log(WARNING,"Failed to load attachments.",e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Result<Appointment> loadEvent(long id) {
|
||||||
|
try {
|
||||||
|
var rs = select(ALL).from(APPOINTMENTS).where(AID, equal(id)).exec(connection);
|
||||||
|
Result<Appointment> result = rs.next() ? createAppointmentOf(rs).map(this::loadExtra) : NotFound.of("Failed to find appointment with id %s", id);
|
||||||
|
rs.getStatement().close();
|
||||||
|
rs.close();
|
||||||
|
return result;
|
||||||
|
} catch (SQLException e) {
|
||||||
|
return SqlError.of(e, "Failed to load appointment with id = %s", id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Result<Appointment> loadEvent(String location, LocalDateTime start) {
|
||||||
|
try {
|
||||||
|
var rs = select(ALL).from(APPOINTMENTS).where(LOCATION, equal(location)).where(START, equal(Timestamp.valueOf(start))).exec(connection);
|
||||||
|
Result<Appointment> result = rs.next() ? createAppointmentOf(rs).map(this::loadExtra) : error("Failed to find appointment starting %s @ %s", start, location);
|
||||||
|
rs.getStatement().close();
|
||||||
|
rs.close();
|
||||||
|
return result;
|
||||||
|
} catch (SQLException e) {
|
||||||
|
return error(e, "Failed to load appointment starting %s @ %s", start, location);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Result<Appointment> loadExtra(Result<BaseAppointment> res) {
|
||||||
|
return loadTags(res).map(this::loadLinks).map(this::loadAttachments);
|
||||||
|
}
|
||||||
|
|
||||||
|
private 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 = select(KEYWORD).from(APPOINTMENT_TAGS).leftJoin(TID, "tags", TID).where(AID, equal(id)).exec(connection);
|
||||||
|
while (rs.next()) event.tags(rs.getString(1));
|
||||||
|
rs.getStatement().close();
|
||||||
|
rs.close();
|
||||||
|
return Payload.of(event);
|
||||||
|
} catch (SQLException e) {
|
||||||
|
return error(e, "Failed to load tags for appointment %s", id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private 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 = 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 (Exception e) {
|
||||||
|
LOG.log(WARNING, () -> "Failed to convert %s to URI!".formatted(u));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rs.getStatement().close();
|
||||||
|
rs.close();
|
||||||
|
return Payload.of(event);
|
||||||
|
} catch (SQLException e) {
|
||||||
|
return error(e, "Failed to load tags for appointment %s", id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private 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 = 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.getStatement().close();
|
||||||
|
rs.close();
|
||||||
|
return Payload.of(event);
|
||||||
|
} catch (SQLException e) {
|
||||||
|
return error(e, "Failed to load tags for appointment %s".formatted(id));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Result<BaseAppointment> 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("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 appointment = new BaseAppointment(id, title, description, start, end, location);
|
||||||
|
|
||||||
|
try {
|
||||||
|
extractCoords(results.getString(COORDS)).optional().ifPresent(appointment::coords);
|
||||||
|
} catch (SQLException e) {
|
||||||
|
LOG.log(TRACE, "Result set did not cointain coords!");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
var tags = nullIfEmpty(results.getString("tags"));
|
||||||
|
if (tags != null) appointment.tags(tags.split(","));
|
||||||
|
} catch (SQLException e) {
|
||||||
|
LOG.log(TRACE, "Result set did not cointain tags!");
|
||||||
|
}
|
||||||
|
return Payload.of(appointment);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Appointment> listByTags(Set<String> tags, Integer count, Integer offset) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Result<Long> removeAppointment(long id) {
|
||||||
|
try {
|
||||||
|
delete().from(APPOINTMENTS).where(AID, equal(id)).execute(connection);
|
||||||
|
delete().from(APPOINTMENT_TAGS).where(AID, equal(id)).execute(connection);
|
||||||
|
delete().from(APPOINTMENT_ATTACHMENTS).where(AID, equal(id)).execute(connection);
|
||||||
|
delete().from(APPOINTMENT_URLS).where(AID, equal(id)).execute(connection);
|
||||||
|
return Payload.of(id);
|
||||||
|
} catch (SQLException e) {
|
||||||
|
return SqlError.of(e, "Failed to delete event %s", id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Result<Appointment> update(Appointment event) {
|
||||||
|
var start = Timestamp.valueOf(event.start());
|
||||||
|
var end = event.end().map(Timestamp::valueOf).orElse(null);
|
||||||
|
var coords = event.coords().map(Object::toString).orElse(null);
|
||||||
|
var location = event.location().orElse(null);
|
||||||
|
try {
|
||||||
|
long id = event.id();
|
||||||
|
Query.update(APPOINTMENTS)
|
||||||
|
.set(TITLE, DESCRIPTION, START, END, LOCATION, COORDS)
|
||||||
|
.where(AID, equal(id))
|
||||||
|
.prepare(connection)
|
||||||
|
.apply(event.title(), event.description(), start, end, location, coords)
|
||||||
|
.close();
|
||||||
|
|
||||||
|
delete().from(APPOINTMENT_TAGS).where(AID, equal(id)).execute(connection);
|
||||||
|
writeTags(event);
|
||||||
|
delete().from(APPOINTMENT_ATTACHMENTS).where(AID, equal(id)).execute(connection);
|
||||||
|
writeAttachments(event);
|
||||||
|
delete().from(APPOINTMENT_URLS).where(AID, equal(id)).execute(connection);
|
||||||
|
writeLinks(event);
|
||||||
|
|
||||||
|
return Payload.of(event);
|
||||||
|
} catch (SQLException sqle) {
|
||||||
|
return error(sqle, "Failed to update database entry");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user