Browse Source

got rid of slug idea, as its functionality can be implemented without an additional db field

Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
main
Stephan Richter 4 months ago
parent
commit
d797de60c1
  1. 6
      de.srsoftware.cal.api/src/main/java/de/srsoftware/cal/api/Appointment.java
  2. 14
      de.srsoftware.cal.base/src/main/java/de/srsoftware/cal/BaseAppointment.java
  3. 14
      de.srsoftware.cal.base/src/main/java/de/srsoftware/cal/BaseImporter.java
  4. 8
      de.srsoftware.cal.db/src/main/java/de/srsoftware/cal/db/Database.java
  5. 1
      de.srsoftware.cal.db/src/main/java/de/srsoftware/cal/db/Fields.java
  6. 57
      de.srsoftware.cal.db/src/main/java/de/srsoftware/cal/db/MariaDB.java
  7. 30
      de.srsoftware.cal.web/src/main/java/de/srsoftware/cal/ApiHandler.java
  8. 75
      de.srsoftware.cal.web/src/main/resources/script/edit.js

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

@ -55,12 +55,6 @@ public interface Appointment {
*/ */
String location(); String location();
/**
* create a unique identifier based on the event content
* @return the slug
*/
String slug();
/** /**
* The date and time, when the appointment starts * The date and time, when the appointment starts
* @return the start time as LocalDateTime * @return the start time as LocalDateTime

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

@ -20,7 +20,6 @@ public class BaseAppointment implements Appointment {
private long id; private long id;
private final String title, description; private final String title, description;
private final LocalDateTime end, start; private final LocalDateTime end, start;
private final String slug;
private Coords coords = null; private Coords coords = null;
private final Set<Attachment> attachments = new HashSet<>(); private final Set<Attachment> attachments = new HashSet<>();
private final Set<String> tags = new HashSet<>(); private final Set<String> tags = new HashSet<>();
@ -36,10 +35,9 @@ public class BaseAppointment implements Appointment {
* @param end set the end date * @param end set the end date
* @param location set the location * @param location set the location
*/ */
public BaseAppointment(long id, String title, String description, LocalDateTime start, LocalDateTime end, String location, String slug) { public BaseAppointment(long id, String title, String description, LocalDateTime start, LocalDateTime end, String location) {
this.description = description; this.description = description;
this.end = end; this.end = end;
this.slug = slug;
this.id = id; this.id = id;
this.location = location; this.location = location;
this.start = start; this.start = start;
@ -103,7 +101,7 @@ public class BaseAppointment implements Appointment {
@Override @Override
public Appointment clone(long newId) { public Appointment clone(long newId) {
return new BaseAppointment(newId, title, description, start, end, location, slug) // return new BaseAppointment(newId, title, description, start, end, location) //
.coords(coords) .coords(coords)
.addLinks(links) .addLinks(links)
.add(attachments) .add(attachments)
@ -139,7 +137,6 @@ public class BaseAppointment implements Appointment {
json.put("end", end().map(end -> end.format(DATE_TIME)).orElse(null)); json.put("end", end().map(end -> end.format(DATE_TIME)).orElse(null));
json.put("id", id()); json.put("id", id());
json.put("location", location()); json.put("location", location());
json.put("slug", slug());
json.put("start", start().format(DATE_TIME)); json.put("start", start().format(DATE_TIME));
json.put("tags", tags()); json.put("tags", tags());
json.put("title", title()); json.put("title", title());
@ -153,13 +150,6 @@ public class BaseAppointment implements Appointment {
return location; return location;
} }
@Override
public String slug() {
return slug;
}
@Override @Override
public LocalDateTime start() { public LocalDateTime start() {
return start; return start;

14
de.srsoftware.cal.base/src/main/java/de/srsoftware/cal/BaseImporter.java

@ -2,7 +2,6 @@
package de.srsoftware.cal; package de.srsoftware.cal;
import static de.srsoftware.tools.Result.transform; import static de.srsoftware.tools.Result.transform;
import static java.nio.charset.StandardCharsets.UTF_8;
import de.srsoftware.cal.api.*; import de.srsoftware.cal.api.*;
import de.srsoftware.tools.*; import de.srsoftware.tools.*;
@ -101,9 +100,7 @@ public abstract class BaseImporter implements Importer {
if (locationResult.optional().isEmpty()) return transform(locationResult); if (locationResult.optional().isEmpty()) return transform(locationResult);
var location = locationResult.optional().get(); var location = locationResult.optional().get();
var hash = hash("%s@%s".formatted(start, location)); var event = new BaseAppointment(id, title, description, start, end, location) //
var event = new BaseAppointment(id, title, description, start, end, location, hash) //
.add(extractAttachments(eventTag)) .add(extractAttachments(eventTag))
.addLinks(extractLinks(eventTag)) .addLinks(extractLinks(eventTag))
.tags(extractTags(eventTag)); .tags(extractTags(eventTag));
@ -204,15 +201,6 @@ public abstract class BaseImporter implements Importer {
.flatMap(result -> result.optional().stream()); .flatMap(result -> result.optional().stream());
} }
/**
* create a hash from a text
* @param plain the plain text
* @return the hash of the plain text
*/
protected String hash(String plain){
return Strings.hex(digest.digest(plain.getBytes(UTF_8)));
}
protected static <T> Result<T> invalidParameter(Result<?> result) { protected static <T> Result<T> invalidParameter(Result<?> result) {
return Error.format("Invalid parameter: %s", result.getClass().getSimpleName()); return Error.format("Invalid parameter: %s", result.getClass().getSimpleName());
} }

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

@ -2,9 +2,7 @@
package de.srsoftware.cal.db; package de.srsoftware.cal.db;
import de.srsoftware.cal.api.Appointment; import de.srsoftware.cal.api.Appointment;
import de.srsoftware.tools.Calc;
import de.srsoftware.tools.Result; import de.srsoftware.tools.Result;
import de.srsoftware.tools.Strings;
import java.sql.SQLException; import java.sql.SQLException;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.List; import java.util.List;
@ -50,11 +48,7 @@ public interface Database {
Result<Appointment> loadEvent(long id); Result<Appointment> loadEvent(long id);
Result<Appointment> loadEvent(String slug); Result<Appointment> loadEvent(String location, LocalDateTime start);
Result<List<String>> findTags(String infix); Result<List<String>> findTags(String infix);
public static String slug(String location, LocalDateTime start) {
return Calc.sha256(start + "@" + location).map(Strings::base64).orElse(null);
}
} }

1
de.srsoftware.cal.db/src/main/java/de/srsoftware/cal/db/Fields.java

@ -10,7 +10,6 @@ public class Fields {
public static final String KEYWORD = "keyword"; public static final String KEYWORD = "keyword";
public static final String LOCATION = "location"; public static final String LOCATION = "location";
public static final String MIME = "mime"; public static final String MIME = "mime";
public static final String SLUG = "slug";
public static final String START = "start"; public static final String START = "start";
public static final String TID = "tid"; public static final String TID = "tid";
public static final String TITLE = "title"; public static final String TITLE = "title";

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

@ -1,7 +1,6 @@
/* © SRSoftware 2024 */ /* © SRSoftware 2024 */
package de.srsoftware.cal.db; package de.srsoftware.cal.db;
import static de.srsoftware.cal.db.Database.slug;
import static de.srsoftware.cal.db.Fields.*; import static de.srsoftware.cal.db.Fields.*;
import static de.srsoftware.cal.db.Fields.ALL; import static de.srsoftware.cal.db.Fields.ALL;
import static de.srsoftware.tools.Optionals.*; import static de.srsoftware.tools.Optionals.*;
@ -27,14 +26,12 @@ import java.util.*;
public class MariaDB implements Database { public class MariaDB implements Database {
private static final System.Logger LOG = System.getLogger(MariaDB.class.getSimpleName()); 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 APPOINTMENTS = "appointments";
private static final String APPOINTMENT_TAGS = "appointment_tags"; private static final String APPOINTMENT_TAGS = "appointment_tags";
private static final String APPOINTMENT_URLS = "appointment_urls"; private static final String APPOINTMENT_URLS = "appointment_urls";
private static final String URLS = "urls"; private static final String URLS = "urls";
private static final String APPOINTMENT_ATTACHMENTS = "appointment_attachments"; private static final String APPOINTMENT_ATTACHMENTS = "appointment_attachments";
private static final String TAGS = "tags"; private static final String TAGS = "tags";
private static final String SLUG = "slug";
private static Connection connection; private static Connection connection;
private MariaDB(Connection conn) throws SQLException { private MariaDB(Connection conn) throws SQLException {
@ -53,47 +50,9 @@ public class MariaDB implements Database {
switch (version) { switch (version) {
case 0: case 0:
createTables(); createTables();
case 1:
update1();
} }
} }
private void update1() throws SQLException {
LOG.log(INFO, "Updating db scheme from version 1 to 2…");
var list = new ArrayList<Appointment>();
connection.setAutoCommit(false);
LOG.log(DEBUG, "Adding slug column…");
connection.prepareStatement(ADD_SLUG).execute();
var slugMap = new HashMap<Long, String>();
LOG.log(DEBUG, "Reading existing appointments…");
var rs = select(ALL).from("appointments").exec(connection);
while (rs.next()) {
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")).map(Timestamp::toLocalDateTime);
if (start.isEmpty()) continue;
var slug = slug(location.get(), start.get());
slugMap.put(id, slug);
}
rs.close();
LOG.log(DEBUG, "Creating slugs…");
var query = updateIgnore("appointments").set("slug").where(AID, equal(MARK)).prepare(connection);
for (var entry : slugMap.entrySet()) {
query.apply(entry.getValue(), entry.getKey());
}
LOG.log(DEBUG, "Writing new db version marker…");
update("config").set("value").where("keyname", equal("dbversion")).prepare(connection).apply(2);
connection.setAutoCommit(true);
}
private void createTables() { private void createTables() {
throw new RuntimeException("%s.createTables() not implemented!"); throw new RuntimeException("%s.createTables() not implemented!");
} }
@ -102,8 +61,8 @@ public class MariaDB implements Database {
@Override @Override
public Result<Appointment> add(Appointment appointment) { public Result<Appointment> add(Appointment appointment) {
try { try {
ResultSet keys = insertInto(APPOINTMENTS, TITLE, DESCRIPTION, START, END, LOCATION, COORDS, SLUG) // ResultSet keys = insertInto(APPOINTMENTS, TITLE, DESCRIPTION, START, END, LOCATION, COORDS) //
.values(appointment.title(), appointment.description(), appointment.start(), appointment.end().orElse(null), appointment.location(), appointment.coords().orElse(null), appointment.slug()) .values(appointment.title(), appointment.description(), appointment.start(), appointment.end().orElse(null), appointment.location(), appointment.coords().orElse(null))
.execute(connection) .execute(connection)
.getGeneratedKeys(); .getGeneratedKeys();
Appointment saved = null; Appointment saved = null;
@ -215,14 +174,14 @@ public class MariaDB implements Database {
} }
@Override @Override
public Result<Appointment> loadEvent(String slug) { public Result<Appointment> loadEvent(String location, LocalDateTime start) {
try { try {
var rs = select(ALL).from(APPOINTMENTS).where(SLUG, equal(slug)).exec(connection); 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(MariaDB::loadExtra) : Error.format("Failed to find appointment with slug %s", slug); Result<Appointment> result = rs.next() ? createAppointmentOf(rs).map(MariaDB::loadExtra) : Error.format("Failed to find appointment starting %s @ %s".formatted(start, location));
rs.close(); rs.close();
return result; return result;
} catch (SQLException e) { } catch (SQLException e) {
return Error.of("Failed to load appointment with slug = %s".formatted(slug), e); return Error.of("Failed to load appointment starting %s @ %s".formatted(start, location), e);
} }
} }
@ -298,9 +257,7 @@ public class MariaDB implements Database {
var start = results.getTimestamp("start").toLocalDateTime(); var start = results.getTimestamp("start").toLocalDateTime();
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 appointment = new BaseAppointment(id, title, description, start, end, location);
if (slug == null) slug = slug(location, start);
var appointment = new BaseAppointment(id, title, description, start, end, location, slug);
try { try {
var tags = nullIfEmpty(results.getString("tags")); var tags = nullIfEmpty(results.getString("tags"));
if (tags != null) appointment.tags(tags.split(",")); if (tags != null) appointment.tags(tags.split(","));

30
de.srsoftware.cal.web/src/main/java/de/srsoftware/cal/ApiHandler.java

@ -2,8 +2,7 @@
package de.srsoftware.cal; package de.srsoftware.cal;
import static de.srsoftware.cal.db.Fields.*; import static de.srsoftware.cal.db.Fields.*;
import static de.srsoftware.tools.Optionals.nullIfEmpty; import static de.srsoftware.tools.Optionals.*;
import static de.srsoftware.tools.Optionals.nullable;
import static java.lang.System.*; import static java.lang.System.*;
import static java.lang.System.Logger.Level.WARNING; import static java.lang.System.Logger.Level.WARNING;
import static java.time.format.DateTimeFormatter.ISO_DATE_TIME; import static java.time.format.DateTimeFormatter.ISO_DATE_TIME;
@ -56,22 +55,18 @@ public class ApiHandler extends PathHandler {
}; };
} }
// spotless:off
private boolean editEvent(HttpExchange ex) throws IOException { private boolean editEvent(HttpExchange ex) throws IOException {
var json = json(ex); var json = json(ex);
// spotless:off var location = json.has(LOCATION) ? json.getString(LOCATION) : null;
var slug = json.has(SLUG) ? nullIfEmpty(json.getString(SLUG)) : null; var start = json.has(START) ? LocalDateTime.parse(json.getString(START)) : null;
// spotless:on if (allSet(location, start)) {
if (slug == null) sendContent(ex, Error.of("No slug value in appointment")); var existingAppointment = db.loadEvent(location, start).optional();
var existingAppointment = db.loadEvent(slug); if (existingAppointment.isPresent()) return update(ex, existingAppointment.get(), json);
return switch (existingAppointment) { }
case Payload<Appointment> payload // return createEvent(ex, json);
-> update(ex, payload.get(), json);
case Error<Appointment> err //
-> err.toString().startsWith("Failed to find appointment with slug") ? createEvent(ex, json):
sendContent(ex, err);
default -> serverError(ex, existingAppointment);
};
} }
// spotless:on
private boolean createEvent(HttpExchange ex, JSONObject json) throws IOException { private boolean createEvent(HttpExchange ex, JSONObject json) throws IOException {
var description = json.has(DESCRIPTION) ? nullIfEmpty(json.getString(DESCRIPTION)) : null; var description = json.has(DESCRIPTION) ? nullIfEmpty(json.getString(DESCRIPTION)) : null;
@ -84,10 +79,7 @@ public class ApiHandler extends PathHandler {
var endDate = nullable(end).map(dt -> LocalDateTime.parse(dt, ISO_DATE_TIME)).orElse(null); var endDate = nullable(end).map(dt -> LocalDateTime.parse(dt, ISO_DATE_TIME)).orElse(null);
var location = json.has(LOCATION) ? json.getString(LOCATION) : null; var location = json.has(LOCATION) ? json.getString(LOCATION) : null;
if (location == null) return sendContent(ex, Error.of("location missing")); if (location == null) return sendContent(ex, Error.of("location missing"));
var clientSlug = json.has(SLUG) ? json.getString(SLUG) : null; var event = new BaseAppointment(0, title, description, startDate, endDate, location);
var serverSlug = Database.slug(location, startDate);
if (!serverSlug.equals(clientSlug)) return sendContent(ex, Error.of("Slug mismatch!"));
var event = new BaseAppointment(0, title, description, startDate, endDate, location, serverSlug);
if (json.has(ATTACHMENTS)) { if (json.has(ATTACHMENTS)) {
json.getJSONArray(ATTACHMENTS).forEach(att -> { json.getJSONArray(ATTACHMENTS).forEach(att -> {
Payload // Payload //

75
de.srsoftware.cal.web/src/main/resources/script/edit.js

@ -50,6 +50,8 @@ async function handleTags(response){
input.focus(); input.focus();
}; };
select.appendChild(option); select.appendChild(option);
select.onkeyup = selectKeyPress;
}); });
select.style.display = 'block'; select.style.display = 'block';
select.style.height = (22*tags.length)+'px'; select.style.height = (22*tags.length)+'px';
@ -61,20 +63,41 @@ function onMapClick(e) {
if (marker) marker.setLatLng(e.latlng); if (marker) marker.setLatLng(e.latlng);
} }
function addTag(tag){
function tagKeyPress(e){
var input = e.target;
if (e.keyCode == 13){
var tag = input.value;
if (!tag) return;
var list = element('taglist'); var list = element('taglist');
if (selectedTags.size<1) list.innerHTML = ''; if (selectedTags.size<1) list.innerHTML = '';
list.innerHTML += `<span><button onclick="this.parentNode.parentNode.removeChild(this.parentNode);" title="click to remove">${input.value}</button> </span>`; list.innerHTML += `<span><button onclick="this.parentNode.parentNode.removeChild(this.parentNode);" title="click to remove">${tag}</button> </span>`;
selectedTags.add(tag); selectedTags.add(tag);
input.value = '';
var btn = element('save'); var btn = element('save');
btn.removeAttribute("disabled"); btn.removeAttribute("disabled");
btn.onclick= function(){ saveEvent(); }; btn.onclick= function(){ saveEvent(); };
var select = element('proposals');
select.innerHTML = '';
select.style.display = 'none';
var input = element('tags-input')
input.value = '';
input.focus();
}
function selectKeyPress(e){
if (e.keyCode == 13){
addTag(e.target.value);
}
}
function tagKeyPress(e){
var input = e.target;
if (e.keyCode == 13){
var tag = input.value;
if (!tag) return;
addTag(tag);
} else if (e.keyCode == 40){
var select = element('proposals');
var val = select.firstChild.value;
select.value = val;
select.focus();
} else { } else {
if (keyTimer != null) clearTimeout(keyTimer); if (keyTimer != null) clearTimeout(keyTimer);
setTimeout(() => fetchTags(input.value),500); setTimeout(() => fetchTags(input.value),500);
@ -113,39 +136,21 @@ function getAttachments(){
return urls; return urls;
} }
function camelCase(text) {
if (text == null) {
return null;
} else {
var slug = '';
var upper = false;
for(var i = 0; i < text.length; i++) {
var c = text.charAt(i);
if (c == ' ') {
upper = true;
} else {
slug += upper ? c.toUpperCase() : c;
upper = false;
}
}
return slug;
}
}
async function slug(start,location){
const hash = await crypto.subtle.digest("SHA-256", (new TextEncoder()).encode(`${start}@${location}`));
return btoa(String.fromCharCode(...new Uint8Array(hash)));
}
function showError(message){ function showError(message){
var span = document.createElement('div'); var span = document.createElement('div');
span.setAttribute('class','error'); span.setAttribute('class','error');
span.innerHTML = "<span>Error: "+message+"<span>"; span.innerHTML = "<span>Error: "+message+"<span>";
span.style.opacity = '1';
span.style.transition = 'opacity 1000ms linear';
element('taglist').appendChild(span); element('taglist').appendChild(span);
setTimeout(() => {
span.style.opacity = '0';
setTimeout(() => span.parentNode.removeChild(span),1000);
},4000);
} }
async function handleSave(response){ async function handleSave(response){
if (response.ok){ if (response.ok){
var json = await response.json(); var json = await response.json();
@ -163,7 +168,6 @@ async function saveEvent(){
var location = element('location').value; var location = element('location').value;
var start = element('start').value; var start = element('start').value;
var slugVal = await slug(start,location);
var event = { var event = {
title : element('title').value, title : element('title').value,
description : element('description').value, description : element('description').value,
@ -173,8 +177,7 @@ async function saveEvent(){
tags: getTags(), tags: getTags(),
links: getLinks(), links: getLinks(),
coords: element('coords').value, coords: element('coords').value,
attachments: getAttachments(), attachments: getAttachments()
slug: slugVal
}; };
fetch('/api/event/edit',{ fetch('/api/event/edit',{
method: 'POST', method: 'POST',

Loading…
Cancel
Save