Browse Source

implemented loading of bookmarks and bookmark list

featue/module_registry
Stephan Richter 3 months ago
parent
commit
eac9eaeb9f
  1. 32
      bookmark/src/main/java/de/srsoftware/umbrella/bookmarks/BookmarkApi.java
  2. 8
      bookmark/src/main/java/de/srsoftware/umbrella/bookmarks/BookmarkDb.java
  3. 2
      bookmark/src/main/java/de/srsoftware/umbrella/bookmarks/Constants.java
  4. 51
      bookmark/src/main/java/de/srsoftware/umbrella/bookmarks/SqliteDb.java
  5. 4
      core/src/main/java/de/srsoftware/umbrella/core/Constants.java
  6. 16
      core/src/main/java/de/srsoftware/umbrella/core/Util.java
  7. 37
      core/src/main/java/de/srsoftware/umbrella/core/model/Bookmark.java
  8. 22
      core/src/main/java/de/srsoftware/umbrella/core/model/Hash.java
  9. 50
      frontend/src/routes/bookmark/Index.svelte
  10. 11
      web/src/main/resources/web/css/default.css

32
bookmark/src/main/java/de/srsoftware/umbrella/bookmarks/BookmarkApi.java

@ -3,10 +3,12 @@ package de.srsoftware.umbrella.bookmarks;
import static de.srsoftware.umbrella.bookmarks.Constants.*; import static de.srsoftware.umbrella.bookmarks.Constants.*;
import static de.srsoftware.umbrella.core.ConnectionProvider.connect; import static de.srsoftware.umbrella.core.ConnectionProvider.connect;
import static de.srsoftware.umbrella.core.Constants.COMMENT;
import static de.srsoftware.umbrella.core.Constants.URL; import static de.srsoftware.umbrella.core.Constants.URL;
import static de.srsoftware.umbrella.core.Paths.LIST;
import static de.srsoftware.umbrella.core.Util.mapValues;
import static de.srsoftware.umbrella.core.exceptions.UmbrellaException.missingFieldException; import static de.srsoftware.umbrella.core.exceptions.UmbrellaException.missingFieldException;
import static java.net.HttpURLConnection.HTTP_BAD_REQUEST; import static java.net.HttpURLConnection.HTTP_BAD_REQUEST;
import static java.net.HttpURLConnection.HTTP_NOT_IMPLEMENTED;
import com.sun.net.httpserver.HttpExchange; import com.sun.net.httpserver.HttpExchange;
import de.srsoftware.configuration.Configuration; import de.srsoftware.configuration.Configuration;
@ -30,6 +32,25 @@ public class BookmarkApi extends BaseHandler {
users = userService; users = userService;
} }
@Override
public boolean doGet(Path path, HttpExchange ex) throws IOException {
addCors(ex);
try {
Optional<Token> token = SessionToken.from(ex).map(Token::of);
var user = users.loadUser(token);
if (user.isEmpty()) return unauthorized(ex);
var head = path.pop();
return switch (head) {
case LIST -> getUserBookmarks(user.get(),ex);
case null, default -> super.doPost(path,ex);
};
} catch (NumberFormatException e){
return sendContent(ex,HTTP_BAD_REQUEST,"Invalid project id");
} catch (UmbrellaException e){
return send(ex,e);
}
}
@Override @Override
public boolean doPost(Path path, HttpExchange ex) throws IOException { public boolean doPost(Path path, HttpExchange ex) throws IOException {
addCors(ex); addCors(ex);
@ -49,11 +70,16 @@ public class BookmarkApi extends BaseHandler {
} }
} }
private boolean getUserBookmarks(UmbrellaUser user, HttpExchange ex) throws IOException {
var bookmarks = db.list(user.id());
return sendContent(ex,mapValues(bookmarks));
}
private boolean postBookmark(UmbrellaUser user, HttpExchange ex) throws IOException { private boolean postBookmark(UmbrellaUser user, HttpExchange ex) throws IOException {
var json = json(ex); var json = json(ex);
if (!(json.has(URL) && json.get(URL) instanceof String url)) throw missingFieldException(URL); if (!(json.has(URL) && json.get(URL) instanceof String url)) throw missingFieldException(URL);
if (!(json.has(COMMENT) && json.get(COMMENT) instanceof String comment)) throw missingFieldException(COMMENT); if (!(json.has(COMMENT) && json.get(COMMENT) instanceof String comment)) throw missingFieldException(COMMENT);
var urlHash = db.save(url,comment, user.id()); var bookmark = db.save(url,comment, user.id());
return sendContent(ex,urlHash); return sendContent(ex,bookmark);
} }
} }

8
bookmark/src/main/java/de/srsoftware/umbrella/bookmarks/BookmarkDb.java

@ -1,6 +1,12 @@
/* © SRSoftware 2025 */ /* © SRSoftware 2025 */
package de.srsoftware.umbrella.bookmarks; package de.srsoftware.umbrella.bookmarks;
import de.srsoftware.umbrella.core.model.Bookmark;
import de.srsoftware.umbrella.core.model.Hash;
import java.util.Map;
public interface BookmarkDb { public interface BookmarkDb {
String save(String url, String comment, long userId); Bookmark save(String url, String comment, long userId);
Map<Hash, Bookmark> list(long id);
} }

2
bookmark/src/main/java/de/srsoftware/umbrella/bookmarks/Constants.java

@ -2,9 +2,7 @@
package de.srsoftware.umbrella.bookmarks; package de.srsoftware.umbrella.bookmarks;
public class Constants { public class Constants {
public static final String COMMENT = "comment";
public static final String CONFIG_DATABASE = "umbrella.modules.bookmark.database"; public static final String CONFIG_DATABASE = "umbrella.modules.bookmark.database";
public static final String HASH = "hash";
public static final String SAVE = "save"; public static final String SAVE = "save";
public static final String TABLE_URLS = "urls"; public static final String TABLE_URLS = "urls";
public static final String TABLE_URL_COMMENTS = "url_comments"; public static final String TABLE_URL_COMMENTS = "url_comments";

51
bookmark/src/main/java/de/srsoftware/umbrella/bookmarks/SqliteDb.java

@ -1,18 +1,26 @@
/* © SRSoftware 2025 */ /* © SRSoftware 2025 */
package de.srsoftware.umbrella.bookmarks; package de.srsoftware.umbrella.bookmarks;
import static de.srsoftware.tools.jdbc.Query.insertInto; import static de.srsoftware.tools.jdbc.Query.*;
import static de.srsoftware.tools.jdbc.Query.replaceInto; import static de.srsoftware.tools.jdbc.Query.SelectQuery.ALL;
import static de.srsoftware.umbrella.bookmarks.Constants.*; import static de.srsoftware.umbrella.bookmarks.Constants.*;
import static de.srsoftware.umbrella.core.Constants.*; import static de.srsoftware.umbrella.core.Constants.*;
import static de.srsoftware.umbrella.core.Constants.ERROR_FAILED_CREATE_TABLE; import static de.srsoftware.umbrella.core.Constants.ERROR_FAILED_CREATE_TABLE;
import static de.srsoftware.umbrella.core.Util.sha1; import static de.srsoftware.umbrella.core.Util.sha1;
import static java.lang.System.Logger.Level.ERROR; import static java.lang.System.Logger.Level.ERROR;
import static java.text.MessageFormat.format;
import static java.time.ZoneOffset.UTC;
import de.srsoftware.tools.jdbc.Condition;
import de.srsoftware.umbrella.core.BaseDb; import de.srsoftware.umbrella.core.BaseDb;
import de.srsoftware.umbrella.core.exceptions.UmbrellaException; import de.srsoftware.umbrella.core.exceptions.UmbrellaException;
import de.srsoftware.umbrella.core.model.Bookmark;
import de.srsoftware.umbrella.core.model.Hash;
import java.sql.Connection; import java.sql.Connection;
import java.sql.SQLException; import java.sql.SQLException;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.Map;
public class SqliteDb extends BaseDb implements BookmarkDb { public class SqliteDb extends BaseDb implements BookmarkDb {
public SqliteDb(Connection conn) { public SqliteDb(Connection conn) {
@ -32,14 +40,15 @@ public class SqliteDb extends BaseDb implements BookmarkDb {
private void createUrlCommentsTable() { private void createUrlCommentsTable() {
var sql = """ var sql = """
CREATE TABLE IF NOT EXISTS "url_comments" ( CREATE TABLE IF NOT EXISTS {0} (
`hash` VARCHAR ( 255 ) NOT NULL, `{1}` VARCHAR ( 255 ) NOT NULL,
`user_id` LONG NOT NULL, `{2}` LONG NOT NULL,
`comment` TEXT NOT NULL, `{3}` TEXT NOT NULL,
PRIMARY KEY (`hash`,`user_id`) `{4}` DATETIME NOT NULL,
PRIMARY KEY (`{1}`,`{2}`)
)"""; )""";
try { try {
var stmt = db.prepareStatement(sql); var stmt = db.prepareStatement(format(sql,TABLE_URL_COMMENTS,HASH,USER_ID,COMMENT,TIMESTAMP));
stmt.execute(); stmt.execute();
stmt.close(); stmt.close();
} catch (SQLException e) { } catch (SQLException e) {
@ -61,15 +70,33 @@ CREATE TABLE IF NOT EXISTS "url_comments" (
} }
@Override @Override
public String save(String url, String comment, long userId) { public Map<Hash, Bookmark> list(long userId) {
try {
var map = new HashMap<Hash,Bookmark>();
var rs = select(ALL).from(TABLE_URL_COMMENTS).leftJoin(HASH,TABLE_URLS,HASH).where(USER_ID, Condition.equal(userId)).exec(db);
while (rs.next()){
var bookmark = Bookmark.of(rs);
map.put(bookmark.hash(),bookmark);
}
rs.close();;
return map;
} catch (SQLException e) {
throw new UmbrellaException("Failed to load bookmark list");
}
}
@Override
public Bookmark save(String url, String comment, long userId) {
var hash = sha1(url); var hash = sha1(url);
try { try {
var timestamp = LocalDateTime.now();
replaceInto(TABLE_URLS,HASH,URL) replaceInto(TABLE_URLS,HASH,URL)
.values(hash,url).execute(db).close(); .values(hash,url).execute(db).close();
replaceInto(TABLE_URL_COMMENTS,HASH,USER_ID,COMMENT) replaceInto(TABLE_URL_COMMENTS,HASH,USER_ID,COMMENT, TIMESTAMP)
.values(hash,userId,comment) .values(hash,userId,comment,timestamp.toEpochSecond(UTC))
.execute(db).close(); .execute(db).close();
return hash; return Bookmark.of(url,comment,timestamp);
} catch (SQLException e) { } catch (SQLException e) {
throw new UmbrellaException("Failed to store url"); throw new UmbrellaException("Failed to store url");
} }

4
core/src/main/java/de/srsoftware/umbrella/core/Constants.java

@ -17,6 +17,7 @@ public class Constants {
public static final String BODY = "body"; public static final String BODY = "body";
public static final String CODE = "code"; public static final String CODE = "code";
public static final String COMMENT = "comment";
public static final String COMPANY = "company"; public static final String COMPANY = "company";
public static final String COMPANY_ID = "company_id"; public static final String COMPANY_ID = "company_id";
public static final String CONTENT_TYPE = "Content-Type"; public static final String CONTENT_TYPE = "Content-Type";
@ -57,6 +58,8 @@ public class Constants {
public static final String GET = "GET"; public static final String GET = "GET";
public static final String HASH = "hash";
public static final String ID = "id"; public static final String ID = "id";
public static final String JSONARRAY = "json array"; public static final String JSONARRAY = "json array";
@ -112,6 +115,7 @@ public class Constants {
public static final String TITLE = "title"; public static final String TITLE = "title";
public static final String TIMESTAMP = "timestamp"; public static final String TIMESTAMP = "timestamp";
public static final String TOKEN = "token"; public static final String TOKEN = "token";
public static final String TYPE = "type";
public static final String UMBRELLA = "Umbrella"; public static final String UMBRELLA = "Umbrella";
public static final String URL = "url"; public static final String URL = "url";

16
core/src/main/java/de/srsoftware/umbrella/core/Util.java

@ -13,6 +13,7 @@ import com.xrbpowered.jparsedown.JParsedown;
import de.srsoftware.tools.Mappable; import de.srsoftware.tools.Mappable;
import de.srsoftware.tools.Query; import de.srsoftware.tools.Query;
import de.srsoftware.umbrella.core.exceptions.UmbrellaException; import de.srsoftware.umbrella.core.exceptions.UmbrellaException;
import de.srsoftware.umbrella.core.model.Hash;
import java.io.*; import java.io.*;
import java.net.HttpURLConnection; import java.net.HttpURLConnection;
import java.net.URI; import java.net.URI;
@ -32,11 +33,12 @@ public class Util {
private static final Pattern UML_PATTERN = Pattern.compile("@start(\\w+)(.*)@end(\\1)",Pattern.DOTALL); private static final Pattern UML_PATTERN = Pattern.compile("@start(\\w+)(.*)@end(\\1)",Pattern.DOTALL);
private static File plantumlJar = null; private static File plantumlJar = null;
private static final JParsedown MARKDOWN = new JParsedown(); private static final JParsedown MARKDOWN = new JParsedown();
private static final MessageDigest SHA1; public static final String SHA1 = "SHA-1";
private static final MessageDigest SHA1_DIGEST;
static { static {
try { try {
SHA1 = MessageDigest.getInstance("SHA-1"); SHA1_DIGEST = MessageDigest.getInstance(SHA1);
} catch (NoSuchAlgorithmException e) { } catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e); throw new RuntimeException(e);
} }
@ -54,8 +56,8 @@ public class Util {
}; };
} }
public static Map<Long,Map<String,Object>> mapValues(Map<Long, ? extends Mappable> map){ public static <KEY> Map<KEY,Map<String,Object>> mapValues(Map<KEY, ? extends Mappable> map){
var result = new HashMap<Long,Map<String,Object>>(); var result = new HashMap<KEY,Map<String,Object>>();
for (var entry : map.entrySet()) result.put(entry.getKey(),entry.getValue().toMap()); for (var entry : map.entrySet()) result.put(entry.getKey(),entry.getValue().toMap());
return result; return result;
} }
@ -167,9 +169,9 @@ public class Util {
} }
} }
public static String sha1(String plain){ public static Hash sha1(String plain){
var bytes = SHA1.digest(plain.getBytes(UTF_8)); var bytes = SHA1_DIGEST.digest(plain.getBytes(UTF_8));
return hex(bytes); return new Hash(hex(bytes),SHA1);
} }
public static void setPlantUmlJar(File file){ public static void setPlantUmlJar(File file){

37
core/src/main/java/de/srsoftware/umbrella/core/model/Bookmark.java

@ -0,0 +1,37 @@
/* © SRSoftware 2025 */
package de.srsoftware.umbrella.core.model;
import static de.srsoftware.umbrella.core.Constants.*;
import static de.srsoftware.umbrella.core.Util.SHA1;
import static de.srsoftware.umbrella.core.Util.sha1;
import static java.time.ZoneOffset.UTC;
import de.srsoftware.tools.Mappable;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Map;
public record Bookmark(String url, Hash hash, String comment, LocalDateTime timestamp, Collection<String> tags) implements Mappable {
public static Bookmark of(ResultSet rs) throws SQLException {
return new Bookmark(rs.getString(URL),new Hash(rs.getString(HASH),SHA1),rs.getString(COMMENT),LocalDateTime.ofEpochSecond(rs.getLong(TIMESTAMP),0, UTC),new ArrayList<>());
}
public static Bookmark of(String url, String comment, LocalDateTime timestamp){
return new Bookmark(url,sha1(url),comment,timestamp,new ArrayList<>());
}
@Override
public Map<String, Object> toMap() {
return Map.of(
URL, url,
COMMENT, comment,
HASH, Map.of(VALUE,hash.value(),TYPE,hash.type()),
TAGS, tags,
TIMESTAMP, timestamp.withNano(0)
);
}
}

22
core/src/main/java/de/srsoftware/umbrella/core/model/Hash.java

@ -0,0 +1,22 @@
/* © SRSoftware 2025 */
package de.srsoftware.umbrella.core.model;
import java.util.Objects;
public record Hash(String value, String type){
@Override
public boolean equals(Object o) {
if (!(o instanceof Hash(String v, String t))) return false;
return Objects.equals(type, t) && Objects.equals(value, v);
}
@Override
public int hashCode() {
return Objects.hash(value, type);
}
@Override
public String toString() {
return value;
}
}

50
frontend/src/routes/bookmark/Index.svelte

@ -1,12 +1,31 @@
<script> <script>
import { api } from '../../urls.svelte.js'; import { onMount } from 'svelte';
import { t } from '../../translations.svelte.js';
import Editor from '../../Components/MarkdownEditor.svelte'; import { api } from '../../urls.svelte.js';
import { t } from '../../translations.svelte.js';
let comment = $state({source:null,rendered:null}); import Editor from '../../Components/MarkdownEditor.svelte';
let error = $state(null);
let link = $state(null); let bookmarks = $state(null);
let comment = $state({source:null,rendered:null});
let error = $state(null);
let link = $state(null);
async function loadBookmarks(){
const url = api('bookmark/list');
const resp = await fetch(url,{credentials:'include'});
if (resp.ok){
const raw = await resp.json();
bookmarks = Object.values(raw)
.sort(
(a, b) => new Date(b.timestamp) - new Date(a.timestamp)
);
error = false;
} else {
error = await resp.html();
}
}
async function onclick(ev){ async function onclick(ev){
let data = { let data = {
@ -17,13 +36,17 @@
const resp = await fetch(url,{ const resp = await fetch(url,{
credentials : 'include', credentials : 'include',
method : 'POST', method : 'POST',
body : JSON.stringify(data) body : JSON.stringify(data)
}); });
if (resp.ok) { if (resp.ok) {
const bookmark = await resp.json();
bookmarks.unshift(bookmark);
} else { } else {
error = await resp.text(); error = await resp.text();
} }
} }
onMount(loadBookmarks);
</script> </script>
<fieldset> <fieldset>
@ -40,4 +63,17 @@
<Editor simple={true} bind:value={comment} /> <Editor simple={true} bind:value={comment} />
</label> </label>
<button {onclick}>{t('save')}</button> <button {onclick}>{t('save')}</button>
{#if bookmarks}
{#each bookmarks as bookmark}
<fieldset class="bookmark">
<legend>
<a href={bookmark.url} target="_blank" class="url">{bookmark.url}</a>
</legend>
<legend class="date">
{bookmark.timestamp.replace('T',' ')}
</legend>
{bookmark.comment}
</fieldset>
{/each}
{/if}
</fieldset> </fieldset>

11
web/src/main/resources/web/css/default.css

@ -244,3 +244,14 @@ textarea{
color: orange; color: orange;
border: 0 none; border: 0 none;
} }
fieldset.bookmark{
position: relative;
}
fieldset.bookmark legend.date{
position: absolute;
right: 0;
top: -17px;
background: black;
}
Loading…
Cancel
Save