Browse Source

implemented storing of bookmarks

Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
featue/module_registry
Stephan Richter 3 months ago
parent
commit
61b5a6ffbb
  1. 40
      bookmark/src/main/java/de/srsoftware/umbrella/bookmarks/BookmarkApi.java
  2. 1
      bookmark/src/main/java/de/srsoftware/umbrella/bookmarks/BookmarkDb.java
  3. 3
      bookmark/src/main/java/de/srsoftware/umbrella/bookmarks/Constants.java
  4. 26
      bookmark/src/main/java/de/srsoftware/umbrella/bookmarks/SqliteDb.java
  5. 1
      core/build.gradle.kts
  6. 17
      core/src/main/java/de/srsoftware/umbrella/core/Util.java
  7. 2
      frontend/src/App.svelte
  8. 1
      frontend/src/Components/Menu.svelte
  9. 43
      frontend/src/routes/bookmark/Index.svelte
  10. 1
      translations/src/main/resources/de.json

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

@ -1,13 +1,24 @@
/* © SRSoftware 2025 */ /* © SRSoftware 2025 */
package de.srsoftware.umbrella.bookmarks; package de.srsoftware.umbrella.bookmarks;
import static de.srsoftware.umbrella.bookmarks.Constants.CONFIG_DATABASE; 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.URL;
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_NOT_IMPLEMENTED;
import com.sun.net.httpserver.HttpExchange;
import de.srsoftware.configuration.Configuration; import de.srsoftware.configuration.Configuration;
import de.srsoftware.tools.Path;
import de.srsoftware.tools.SessionToken;
import de.srsoftware.umbrella.core.BaseHandler; import de.srsoftware.umbrella.core.BaseHandler;
import de.srsoftware.umbrella.core.api.UserService; import de.srsoftware.umbrella.core.api.UserService;
import de.srsoftware.umbrella.core.exceptions.UmbrellaException;
import de.srsoftware.umbrella.core.model.Token;
import de.srsoftware.umbrella.core.model.UmbrellaUser;
import java.io.IOException;
import java.util.Optional;
public class BookmarkApi extends BaseHandler { public class BookmarkApi extends BaseHandler {
private final BookmarkDb db; private final BookmarkDb db;
@ -18,4 +29,31 @@ public class BookmarkApi extends BaseHandler {
db = new SqliteDb(connect(dbFile)); db = new SqliteDb(connect(dbFile));
users = userService; users = userService;
} }
@Override
public boolean doPost(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 SAVE -> postBookmark(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);
}
}
private boolean postBookmark(UmbrellaUser user, HttpExchange ex) throws IOException {
var json = json(ex);
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);
var urlHash = db.save(url,comment, user.id());
return sendContent(ex,urlHash);
}
} }

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

@ -2,4 +2,5 @@
package de.srsoftware.umbrella.bookmarks; package de.srsoftware.umbrella.bookmarks;
public interface BookmarkDb { public interface BookmarkDb {
String save(String url, String comment, long userId);
} }

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

@ -2,7 +2,10 @@
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 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";

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

@ -1,13 +1,16 @@
/* © SRSoftware 2025 */ /* © SRSoftware 2025 */
package de.srsoftware.umbrella.bookmarks; package de.srsoftware.umbrella.bookmarks;
import static de.srsoftware.umbrella.bookmarks.Constants.TABLE_URLS; import static de.srsoftware.tools.jdbc.Query.insertInto;
import static de.srsoftware.umbrella.bookmarks.Constants.TABLE_URL_COMMENTS; import static de.srsoftware.tools.jdbc.Query.replaceInto;
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 java.lang.System.Logger.Level.ERROR; import static java.lang.System.Logger.Level.ERROR;
import de.srsoftware.umbrella.core.BaseDb; import de.srsoftware.umbrella.core.BaseDb;
import de.srsoftware.umbrella.core.exceptions.UmbrellaException;
import java.sql.Connection; import java.sql.Connection;
import java.sql.SQLException; import java.sql.SQLException;
@ -30,10 +33,10 @@ 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 "url_comments" (
`url_hash` VARCHAR ( 255 ) NOT NULL, `hash` VARCHAR ( 255 ) NOT NULL,
`user_id` LONG NOT NULL, `user_id` LONG NOT NULL,
`comment` TEXT NOT NULL, `comment` TEXT NOT NULL,
PRIMARY KEY (`url_hash`,`user_id`) PRIMARY KEY (`hash`,`user_id`)
)"""; )""";
try { try {
var stmt = db.prepareStatement(sql); var stmt = db.prepareStatement(sql);
@ -56,4 +59,19 @@ CREATE TABLE IF NOT EXISTS "url_comments" (
throw new RuntimeException(e); throw new RuntimeException(e);
} }
} }
@Override
public String save(String url, String comment, long userId) {
var hash = sha1(url);
try {
replaceInto(TABLE_URLS,HASH,URL)
.values(hash,url).execute(db).close();
replaceInto(TABLE_URL_COMMENTS,HASH,USER_ID,COMMENT)
.values(hash,userId,comment)
.execute(db).close();
return hash;
} catch (SQLException e) {
throw new UmbrellaException("Failed to store url");
}
}
} }

1
core/build.gradle.kts

@ -11,6 +11,7 @@ repositories {
dependencies { dependencies {
implementation("de.srsoftware:tools.mime:1.1.2") implementation("de.srsoftware:tools.mime:1.1.2")
implementation("de.srsoftware:tools.util:2.0.4")
implementation("org.xerial:sqlite-jdbc:3.49.0.0") implementation("org.xerial:sqlite-jdbc:3.49.0.0")
testImplementation(platform("org.junit:junit-bom:5.10.0")) testImplementation(platform("org.junit:junit-bom:5.10.0"))
testImplementation("org.junit.jupiter:junit-jupiter") testImplementation("org.junit.jupiter:junit-jupiter")

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

@ -3,6 +3,7 @@ package de.srsoftware.umbrella.core;
import static de.srsoftware.tools.MimeType.MIME_FORM_URL; import static de.srsoftware.tools.MimeType.MIME_FORM_URL;
import static de.srsoftware.tools.MimeType.MIME_JSON; import static de.srsoftware.tools.MimeType.MIME_JSON;
import static de.srsoftware.tools.Strings.hex;
import static de.srsoftware.umbrella.core.Constants.*; import static de.srsoftware.umbrella.core.Constants.*;
import static java.lang.System.Logger.Level.*; import static java.lang.System.Logger.Level.*;
import static java.lang.System.Logger.Level.WARNING; import static java.lang.System.Logger.Level.WARNING;
@ -16,6 +17,8 @@ import java.io.*;
import java.net.HttpURLConnection; import java.net.HttpURLConnection;
import java.net.URI; import java.net.URI;
import java.net.URL; import java.net.URL;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.time.Instant; import java.time.Instant;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.time.ZoneId; import java.time.ZoneId;
@ -29,6 +32,15 @@ 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;
static {
try {
SHA1 = MessageDigest.getInstance("SHA-1");
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
}
private Util(){} private Util(){}
@ -155,6 +167,11 @@ public class Util {
} }
} }
public static String sha1(String plain){
var bytes = SHA1.digest(plain.getBytes(UTF_8));
return hex(bytes);
}
public static void setPlantUmlJar(File file){ public static void setPlantUmlJar(File file){
LOG.log(INFO,"Using plantuml @ {0}",file.getAbsolutePath()); LOG.log(INFO,"Using plantuml @ {0}",file.getAbsolutePath());
plantumlJar = file; plantumlJar = file;

2
frontend/src/App.svelte

@ -7,6 +7,7 @@
import AddDoc from "./routes/document/Add.svelte"; import AddDoc from "./routes/document/Add.svelte";
import AddTask from "./routes/task/Add.svelte"; import AddTask from "./routes/task/Add.svelte";
import Bookmarks from "./routes/bookmark/Index.svelte";
import Callback from "./routes/user/OidcCallback.svelte"; import Callback from "./routes/user/OidcCallback.svelte";
import DocList from "./routes/document/List.svelte"; import DocList from "./routes/document/List.svelte";
import EditService from "./routes/user/EditService.svelte"; import EditService from "./routes/user/EditService.svelte";
@ -50,6 +51,7 @@
<!-- https://github.com/notnotsamuel/svelte-tiny-router --> <!-- https://github.com/notnotsamuel/svelte-tiny-router -->
<Menu /> <Menu />
<Route path="/" component={User} /> <Route path="/" component={User} />
<Route path="/bookmark" component={Bookmarks} />
<Route path="/document" component={DocList} /> <Route path="/document" component={DocList} />
<Route path="/document/add" component={AddDoc} /> <Route path="/document/add" component={AddDoc} />
<Route path="/document/:id/send" component={SendDoc} /> <Route path="/document/:id/send" component={SendDoc} />

1
frontend/src/Components/Menu.svelte

@ -41,6 +41,7 @@ onMount(fetchModules);
<a href="#" onclick={() => go('/project')}>{t('projects')}</a> <a href="#" onclick={() => go('/project')}>{t('projects')}</a>
<a href="#" onclick={() => go('/task')}>{t('tasks')}</a> <a href="#" onclick={() => go('/task')}>{t('tasks')}</a>
<a href="#" onclick={() => go('/document')}>{t('documents')}</a> <a href="#" onclick={() => go('/document')}>{t('documents')}</a>
<a href="#" onclick={() => go('/bookmark')}>{t('bookmarks')}</a>
<a href="#" onclick={() => go('/notes')}>{t('notes')}</a> <a href="#" onclick={() => go('/notes')}>{t('notes')}</a>
<a href="https://svelte.dev/tutorial/svelte/state" target="_blank">{t('tutorial')}</a> <a href="https://svelte.dev/tutorial/svelte/state" target="_blank">{t('tutorial')}</a>
{#each modules as module,i}<a href={module.url}>{module.name}</a>{/each} {#each modules as module,i}<a href={module.url}>{module.name}</a>{/each}

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

@ -0,0 +1,43 @@
<script>
import { api } from '../../urls.svelte.js';
import { t } from '../../translations.svelte.js';
import Editor from '../../Components/MarkdownEditor.svelte';
let comment = $state({source:null,rendered:null});
let error = $state(null);
let link = $state(null);
async function onclick(ev){
let data = {
url : link,
comment : comment.source
};
const url = api('bookmark/save');
const resp = await fetch(url,{
credentials : 'include',
method : 'POST',
body : JSON.stringify(data)
});
if (resp.ok) {
} else {
error = await resp.text();
}
}
</script>
<fieldset>
<legend>{t('Bookmarks')}</legend>
{#if error}
<span class="error">{error}</span>
{/if}
<label>
{t('URL')}
<input bind:value={link} />
</label>
<label>
{t('Comment')}
<Editor simple={true} bind:value={comment} />
</label>
<button {onclick}>{t('save')}</button>
</fieldset>

1
translations/src/main/resources/de.json

@ -15,6 +15,7 @@
"base_url": "Basis-URL", "base_url": "Basis-URL",
"basic_data": "Basis-Daten", "basic_data": "Basis-Daten",
"bookmark": "Lesezeichen", "bookmark": "Lesezeichen",
"bookmarks": "Lesezeichen",
"by": "von", "by": "von",
"client_id": "Client-ID", "client_id": "Client-ID",

Loading…
Cancel
Save