working on bookmark editing

Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
This commit is contained in:
2026-03-13 14:50:12 +01:00
parent b71fd4492c
commit 2d6b017352
13 changed files with 283 additions and 26 deletions

View File

@@ -1,6 +1,7 @@
description = "Umbrella : Bookmarks" description = "Umbrella : Bookmarks"
dependencies{ dependencies{
implementation(project(":bus"))
implementation(project(":core")) implementation(project(":core"))
} }

View File

@@ -1,8 +1,10 @@
/* © SRSoftware 2025 */ /* © SRSoftware 2025 */
package de.srsoftware.umbrella.bookmarks; package de.srsoftware.umbrella.bookmarks;
import static de.srsoftware.umbrella.messagebus.events.Event.EventType;
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.ModuleRegistry.tagService;
import static de.srsoftware.umbrella.core.Util.mapValues; import static de.srsoftware.umbrella.core.Util.mapValues;
import static de.srsoftware.umbrella.core.constants.Field.*; import static de.srsoftware.umbrella.core.constants.Field.*;
import static de.srsoftware.umbrella.core.constants.Field.TAGS; import static de.srsoftware.umbrella.core.constants.Field.TAGS;
@@ -10,7 +12,11 @@ import static de.srsoftware.umbrella.core.constants.Module.BOOKMARK;
import static de.srsoftware.umbrella.core.constants.Path.LIST; import static de.srsoftware.umbrella.core.constants.Path.LIST;
import static de.srsoftware.umbrella.core.constants.Path.SEARCH; import static de.srsoftware.umbrella.core.constants.Path.SEARCH;
import static de.srsoftware.umbrella.core.exceptions.UmbrellaException.*; import static de.srsoftware.umbrella.core.exceptions.UmbrellaException.*;
import static de.srsoftware.umbrella.messagebus.MessageBus.messageBus;
import static de.srsoftware.umbrella.messagebus.events.Event.EventType.CREATE;
import static de.srsoftware.umbrella.messagebus.events.Event.EventType.UPDATE;
import static java.net.HttpURLConnection.HTTP_BAD_REQUEST; import static java.net.HttpURLConnection.HTTP_BAD_REQUEST;
import static java.net.HttpURLConnection.HTTP_OK;
import com.sun.net.httpserver.HttpExchange; import com.sun.net.httpserver.HttpExchange;
import de.srsoftware.configuration.Configuration; import de.srsoftware.configuration.Configuration;
@@ -19,14 +25,18 @@ import de.srsoftware.tools.SessionToken;
import de.srsoftware.umbrella.core.BaseHandler; import de.srsoftware.umbrella.core.BaseHandler;
import de.srsoftware.umbrella.core.ModuleRegistry; import de.srsoftware.umbrella.core.ModuleRegistry;
import de.srsoftware.umbrella.core.api.BookmarkService; import de.srsoftware.umbrella.core.api.BookmarkService;
import de.srsoftware.umbrella.core.constants.Text;
import de.srsoftware.umbrella.core.exceptions.UmbrellaException; import de.srsoftware.umbrella.core.exceptions.UmbrellaException;
import de.srsoftware.umbrella.core.model.Token; import de.srsoftware.umbrella.core.model.Token;
import de.srsoftware.umbrella.core.model.UmbrellaUser; import de.srsoftware.umbrella.core.model.UmbrellaUser;
import java.awt.print.Book;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList; import java.util.*;
import java.util.Arrays;
import java.util.Map; import de.srsoftware.umbrella.messagebus.events.BookmarkEvent;
import java.util.Optional; import de.srsoftware.umbrella.messagebus.events.Event;
import de.srsoftware.umbrella.messagebus.events.TaskEvent;
import org.json.JSONArray; import org.json.JSONArray;
public class BookmarkApi extends BaseHandler implements BookmarkService { public class BookmarkApi extends BaseHandler implements BookmarkService {
@@ -39,6 +49,30 @@ public class BookmarkApi extends BaseHandler implements BookmarkService {
ModuleRegistry.add(this); ModuleRegistry.add(this);
} }
private boolean deleteBookmark(UmbrellaUser user, HttpExchange ex, long urlId) throws IOException {
var bookmark = db.load(urlId,user.id());
db.remove(user, bookmark);
return sendEmptyResponse(HTTP_OK,ex);
}
@Override
public boolean doDelete(Path path, HttpExchange ex) throws IOException {
addCors(ex);
try {
Optional<Token> token = SessionToken.from(ex).map(Token::of);
var user = ModuleRegistry.userService().loadUser(token);
if (user.isEmpty()) return unauthorized(ex);
var head = path.pop();
if (head == null) throw missingField(ID);
var id = Long.parseLong(head);
return deleteBookmark(user.get(),ex,id);
} catch (NumberFormatException e){
throw invalidField(ID, Text.NUMBER);
} catch (UmbrellaException e){
return send(ex,e);
}
}
@Override @Override
public boolean doGet(Path path, HttpExchange ex) throws IOException { public boolean doGet(Path path, HttpExchange ex) throws IOException {
addCors(ex); addCors(ex);
@@ -51,21 +85,33 @@ public class BookmarkApi extends BaseHandler implements BookmarkService {
case LIST -> getUserBookmarks(user.get(),ex); case LIST -> getUserBookmarks(user.get(),ex);
case null -> super.doPost(path,ex); case null -> super.doPost(path,ex);
default -> { default -> {
var id = Long.parseLong(head); var urlId = Long.parseLong(head);
yield getBookmark(user.get(),id,ex); yield getBookmark(user.get(),urlId,ex);
} }
}; };
} catch (NumberFormatException e){ } catch (NumberFormatException e){
return sendContent(ex,HTTP_BAD_REQUEST,"Invalid project id"); throw invalidField(ID, Text.NUMBER);
} catch (UmbrellaException e){ } catch (UmbrellaException e){
return send(ex,e); return send(ex,e);
} }
} }
private boolean getBookmark(UmbrellaUser user, long id, HttpExchange ex) throws IOException { @Override
var bookmark = db.load(id,user.id()); public boolean doPatch(Path path, HttpExchange ex) throws IOException {
ModuleRegistry.tagService().getTags(BOOKMARK, id, user).forEach(bookmark.tags()::add); addCors(ex);
return sendContent(ex,bookmark); try {
Optional<Token> token = SessionToken.from(ex).map(Token::of);
var user = ModuleRegistry.userService().loadUser(token);
if (user.isEmpty()) return unauthorized(ex);
var head = path.pop();
if (head == null) throw missingField(ID);
var id = Long.parseLong(head);
return patchBookmark(user.get(),ex,id);
} catch (NumberFormatException e){
throw invalidField(ID, Text.NUMBER);
} catch (UmbrellaException e){
return send(ex,e);
}
} }
@Override @Override
@@ -93,6 +139,12 @@ public class BookmarkApi extends BaseHandler implements BookmarkService {
return db.findUrls(key); return db.findUrls(key);
} }
private boolean getBookmark(UmbrellaUser user, long urlId, HttpExchange ex) throws IOException {
var bookmark = db.load(urlId,user.id());
tagService().getTags(BOOKMARK, urlId, user).forEach(bookmark.tags()::add);
return sendContent(ex,bookmark);
}
private boolean getUserBookmarks(UmbrellaUser user, HttpExchange ex) throws IOException { private boolean getUserBookmarks(UmbrellaUser user, HttpExchange ex) throws IOException {
var param = queryParam(ex); var param = queryParam(ex);
long offset = switch (param.get(OFFSET)){ long offset = switch (param.get(OFFSET)){
@@ -110,6 +162,23 @@ public class BookmarkApi extends BaseHandler implements BookmarkService {
return sendContent(ex,mapValues(bookmarks)); return sendContent(ex,mapValues(bookmarks));
} }
private boolean patchBookmark(UmbrellaUser user, HttpExchange ex, long urlId) throws IOException {
var bookmark = db.load(urlId,user.id());
var tags = tagService().getTags(BOOKMARK,urlId,user);
var json = json(ex);
var comment = bookmark.comment();
var url = bookmark.url();
if (json.has(COMMENT) && json.get(COMMENT) instanceof String c) comment = c;
if (json.has(URL) && json.get(URL) instanceof String u) url = u;
var newBookmark = db.save(url,comment, List.of(user.id()),bookmark.timestamp());
if (newBookmark.urlId() != urlId) {
tagService().save(BOOKMARK,newBookmark.urlId(),List.of(user.id()),tags);
db.remove(user, bookmark);
//messageBus().dispatch(new BookmarkEvent(user,newBookmark,CREATE));
} else messageBus().dispatch(new BookmarkEvent(user,newBookmark,UPDATE));
return sendContent(ex,newBookmark);
}
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 missingField(URL); if (!(json.has(URL) && json.get(URL) instanceof String url)) throw missingField(URL);
@@ -123,10 +192,11 @@ public class BookmarkApi extends BaseHandler implements BookmarkService {
} }
} }
var bookmark = db.save(url,comment, userList); var bookmark = db.save(url,comment, userList);
messageBus().dispatch(new BookmarkEvent(user,bookmark,CREATE));
if (json.has(TAGS) && json.get(TAGS) instanceof JSONArray tagList){ if (json.has(TAGS) && json.get(TAGS) instanceof JSONArray tagList){
var list = tagList.toList().stream().map(Object::toString).toList(); var list = tagList.toList().stream().map(Object::toString).toList();
ModuleRegistry.tagService().save(BOOKMARK,bookmark.urlId(), userList, list); tagService().save(BOOKMARK,bookmark.urlId(), userList, list);
} }
return sendContent(ex,bookmark); return sendContent(ex,bookmark);
} }

View File

@@ -2,6 +2,8 @@
package de.srsoftware.umbrella.bookmarks; package de.srsoftware.umbrella.bookmarks;
import de.srsoftware.umbrella.core.model.Bookmark; import de.srsoftware.umbrella.core.model.Bookmark;
import de.srsoftware.umbrella.core.model.UmbrellaUser;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.Collection; import java.util.Collection;
import java.util.Map; import java.util.Map;
@@ -15,6 +17,8 @@ public interface BookmarkDb {
Bookmark load(long id, long userId); Bookmark load(long id, long userId);
void remove(UmbrellaUser user, Bookmark bookmark);
Bookmark save(String url, String comment, Collection<Long> userIds, LocalDateTime datetime); Bookmark save(String url, String comment, Collection<Long> userIds, LocalDateTime datetime);
default Bookmark save(String url, String comment, Collection<Long> userIds){ default Bookmark save(String url, String comment, Collection<Long> userIds){

View File

@@ -7,15 +7,24 @@ import static de.srsoftware.tools.jdbc.Query.*;
import static de.srsoftware.tools.jdbc.Query.SelectQuery.ALL; 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.Errors.*; import static de.srsoftware.umbrella.core.Errors.*;
import static de.srsoftware.umbrella.core.ModuleRegistry.tagService;
import static de.srsoftware.umbrella.core.constants.Field.*; import static de.srsoftware.umbrella.core.constants.Field.*;
import static de.srsoftware.umbrella.core.constants.Text.BOOKMARK; import static de.srsoftware.umbrella.core.constants.Text.BOOKMARK;
import static de.srsoftware.umbrella.core.exceptions.UmbrellaException.*; import static de.srsoftware.umbrella.core.exceptions.UmbrellaException.*;
import static de.srsoftware.umbrella.messagebus.MessageBus.messageBus;
import static de.srsoftware.umbrella.messagebus.events.Event.EventType.CREATE;
import static de.srsoftware.umbrella.messagebus.events.Event.EventType.UPDATE;
import static java.text.MessageFormat.format; import static java.text.MessageFormat.format;
import static java.time.ZoneOffset.UTC; import static java.time.ZoneOffset.UTC;
import de.srsoftware.umbrella.core.BaseDb; import de.srsoftware.umbrella.core.BaseDb;
import de.srsoftware.umbrella.core.constants.Module;
import de.srsoftware.umbrella.core.model.Bookmark; import de.srsoftware.umbrella.core.model.Bookmark;
import de.srsoftware.umbrella.core.model.Translatable; import de.srsoftware.umbrella.core.model.Translatable;
import de.srsoftware.umbrella.core.model.UmbrellaUser;
import de.srsoftware.umbrella.messagebus.events.BookmarkEvent;
import de.srsoftware.umbrella.messagebus.events.Event;
import java.sql.Connection; import java.sql.Connection;
import java.sql.SQLException; import java.sql.SQLException;
import java.time.LocalDateTime; import java.time.LocalDateTime;
@@ -117,16 +126,29 @@ CREATE TABLE IF NOT EXISTS {0} (
} }
@Override @Override
public Bookmark load(long id, long userId) { public Bookmark load(long urlId, long userId) {
try { try {
Bookmark result = null; Bookmark result = null;
var rs = select(ALL).from(TABLE_URLS).leftJoin(ID,TABLE_URL_COMMENTS,URL_ID).where(ID,equal(id)).where(USER_ID,equal(userId)).exec(db); var rs = select(ALL).from(TABLE_URLS).leftJoin(ID,TABLE_URL_COMMENTS,URL_ID).where(ID,equal(urlId)).where(USER_ID,equal(userId)).exec(db);
if (rs.next()) result = Bookmark.of(rs); if (rs.next()) result = Bookmark.of(rs);
rs.close(); rs.close();
if (result != null) return result; if (result != null) return result;
throw failedToLoadObject(Translatable.t(BOOKMARK),id); throw failedToLoadObject(Translatable.t(BOOKMARK),urlId);
} catch (SQLException e) { } catch (SQLException e) {
throw failedToLoadObject(Translatable.t(BOOKMARK),id).causedBy(e); throw failedToLoadObject(Translatable.t(BOOKMARK),urlId).causedBy(e);
}
}
@Override
public void remove(UmbrellaUser user, Bookmark bookmark) {
try {
var urlId = bookmark.urlId();
var userId = user.id();
delete().from(TABLE_URL_COMMENTS).where(USER_ID, equal(userId)).where(URL_ID, equal(urlId)).execute(db);
messageBus().dispatch(new BookmarkEvent(user,bookmark, Event.EventType.DELETE));
tagService().deleteEntity(Module.BOOKMARK,urlId,userId);
} catch (SQLException e){
throw failedToDropObject(BOOKMARK);
} }
} }

View File

@@ -0,0 +1,41 @@
package de.srsoftware.umbrella.messagebus.events;
import de.srsoftware.umbrella.core.constants.Field;
import de.srsoftware.umbrella.core.model.Bookmark;
import de.srsoftware.umbrella.core.model.Task;
import de.srsoftware.umbrella.core.model.Translatable;
import de.srsoftware.umbrella.core.model.UmbrellaUser;
import java.util.Collection;
import java.util.List;
import static de.srsoftware.umbrella.core.constants.Module.BOOKMARK;
import static de.srsoftware.umbrella.core.model.Translatable.t;
public class BookmarkEvent extends Event<Bookmark> {
public BookmarkEvent(UmbrellaUser initiator, Bookmark bookmark, EventType type){
super(initiator,BOOKMARK,bookmark,type);
}
@Override
public Collection<UmbrellaUser> audience() {
return List.of(initiator());
}
@Override
public Translatable describe() {
return switch (eventType()){
case CREATE -> t("New bookmark created");
case DELETE -> t("The bookmark '{url}' has been deleted", Field.URL, payload().url());
case UPDATE -> t("Bookmark updated");
default -> null;
};
}
@Override
public Translatable subject() {
return describe();
}
}

View File

@@ -8,7 +8,9 @@ import java.util.List;
import java.util.Map; import java.util.Map;
public interface TagService { public interface TagService {
void deleteEntity(String task, long taskId); void deleteEntity(String module, long taskId);
void deleteEntity(String module, long urlId, long userId);
Map<String, List<Long>> getTagUses(UmbrellaUser user, String tag); Map<String, List<Long>> getTagUses(UmbrellaUser user, String tag);

View File

@@ -1,7 +1,7 @@
<script> <script>
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import { api } from '../../urls.svelte'; import { api, eventStream } from '../../urls.svelte';
import { error, yikes } from '../../warn.svelte'; import { error, yikes } from '../../warn.svelte';
import { t } from '../../translations.svelte'; import { t } from '../../translations.svelte';
@@ -11,6 +11,7 @@
import Template from './Template.svelte'; import Template from './Template.svelte';
let bookmarks = $state(null); let bookmarks = $state(null);
let eventSource = null;
let loader = { let loader = {
offset : 0, offset : 0,
limit : 16, limit : 16,
@@ -45,6 +46,36 @@
} }
} }
function handleCreateEvent(evt){
let data = JSON.parse(evt.data);
if (data.record) {
console.log({created:data.record});
bookmarks = bookmarks.push(data.record);
}
}
function handleDeleteEvent(evt){
let data = JSON.parse(evt.data);
if (data.record && data.record.id) {
console.log({deleted:data.record});
bookmarks = bookmarks.filter(b => b.id != data.record.id);
}
}
function handleUpdateEvent(evt){
let data = JSON.parse(evt.data);
if (data.record && data.record.id) {
console.log({updated:data.record});
bookmarks = bookmarks.map(b => data.record.id == b.id ? data.record : b);
}
}
function load(){
loadBookmarks();
eventSource = eventStream(handleCreateEvent,handleUpdateEvent,handleDeleteEvent);
}
async function loadBookmarks(){ async function loadBookmarks(){
const url = api(`bookmark/list?offset=${loader.offset}&limit=${loader.limit}`); const url = api(`bookmark/list?offset=${loader.offset}&limit=${loader.limit}`);
const resp = await fetch(url,{credentials:'include'}); const resp = await fetch(url,{credentials:'include'});
@@ -92,7 +123,7 @@
} }
} }
onMount(loadBookmarks); onMount(load);
</script> </script>
<svelte:head> <svelte:head>

View File

@@ -1,19 +1,41 @@
<script> <script>
import { target } from '../../urls.svelte'; import { useTinyRouter } from 'svelte-tiny-router';
import { api, drop, target } from '../../urls.svelte';
import { error, yikes } from '../../warn.svelte';
import { t } from '../../translations.svelte';
import Tags from '../tags/TagList.svelte'; import Tags from '../tags/TagList.svelte';
const router = useTinyRouter();
let { bookmark } = $props(); let { bookmark } = $props();
async function del(bookmark){
if (confirm(t('confirm_delete',{element:bookmark.url}))){
var url = api(`bookmark/${bookmark.id}`)
var res = await drop(url);
if (res.ok){
yikes();
router.navigate('/bookmark')
} else error(res);
}
}
function edit(bookmark){
router.navigate(`/bookmark/${bookmark.id}/view`);
}
</script> </script>
{#if bookmark} {#if bookmark}
<fieldset class="bookmark"> <fieldset class="bookmark">
<legend> <legend>
<a href={bookmark.url} target="_blank" class="url">{bookmark.url}</a> <a href={bookmark.url} target="_blank" class="url">{bookmark.url}</a>
<a class="symbol" onclick={e => edit(bookmark)} title={t('edit_object',{object:t('bookmark')})} ></a>
<a class="symbol" onclick={e => del(bookmark)} title={t('delete_object',{object:t('bookmark')})} ></a>
</legend> </legend>
<legend class="date"> <legend class="date">
{bookmark.timestamp.replace('T',' ')} {bookmark.timestamp.replace('T',' ')}
</legend> </legend>
{@html target(bookmark.comment.rendered)} {@html target(bookmark.comment.rendered)}
<Tags module="bookmark" id={bookmark.id} /> <Tags module="bookmark" id={bookmark.id} />
<button onclick={e => edit(bookmark.id)} >{t('edit')}</button>
</fieldset> </fieldset>
{/if} {/if}

View File

@@ -2,8 +2,11 @@
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import Bookmark from './Template.svelte'; import Bookmark from './Template.svelte';
import LineEditor from '../../Components/LineEditor.svelte';
import MarkdownEditor from '../../Components/MarkdownEditor.svelte';
import Tags from '../tags/TagList.svelte';
import { api } from '../../urls.svelte'; import { api, patch } from '../../urls.svelte';
import { error, yikes } from '../../warn.svelte'; import { error, yikes } from '../../warn.svelte';
import { t } from '../../translations.svelte'; import { t } from '../../translations.svelte';
@@ -24,7 +27,55 @@
} }
} }
function visit(ev){
window.open(bookmark.url, '_blank').focus();
}
async function update(field,value){
var url = api(`bookmark/${id}`);
var res = await patch(url,{[field]:value});
if (res.ok){
yikes();
bookmark = await res.json();
if (id != bookmark.id){
id = bookmark.id;
history.pushState({}, null, `/bookmark/${id}/view`);
}
return true;
}
error(res);
return false;
}
onMount(load); onMount(load);
</script> </script>
<Template {bookmark} /> {#if bookmark}
<fieldset>
<legend>{t('bookmark')} {id}</legend>
<table class="edit bookmark">
<tbody>
<tr>
<th>{t('URL')}</th>
<td>
<LineEditor value={bookmark.url} editable={true} onSet={url => update('url',url)} />
<button onclick={visit}>{t('open')}</button>
</td>
</tr>
<tr>
<th>{t('description')}</th>
<td>
<MarkdownEditor editable={true} value={bookmark.comment} onSet={desc => update('comment',desc)}/>
</td>
</tr>
<tr>
<th>{t('tags')}</th>
<td>
<Tags module="bookmark" id={bookmark.id} />
</td>
</tr>
</tbody>
</table>
</fieldset>
{/if}

View File

@@ -2,7 +2,7 @@
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import { useTinyRouter } from 'svelte-tiny-router'; import { useTinyRouter } from 'svelte-tiny-router';
import { api, get, patch } from '../../urls.svelte.js'; import { api, eventStream, get, patch } from '../../urls.svelte.js';
import { error, yikes } from '../../warn.svelte'; import { error, yikes } from '../../warn.svelte';
import { t } from '../../translations.svelte.js'; import { t } from '../../translations.svelte.js';

View File

@@ -218,10 +218,16 @@ CREATE TABLE IF NOT EXISTS {0} (
@Override @Override
public void deleteEntity(String module, long entityId) { public void deleteEntity(String module, long entityId) {
deleteEntity(module,entityId,-1);
}
@Override
public void deleteEntity(String module, long entityId, long userId) {
try { try {
Query.delete().from(TABLE_TAGS) var query = Query.delete().from(TABLE_TAGS)
.where(MODULE,iEqual(module)).where(ENTITY_ID,equal(entityId)) .where(MODULE,iEqual(module)).where(ENTITY_ID,equal(entityId));
.execute(db); if (userId>0) query.where(USER_ID,equal(userId));
query.execute(db);
} catch (SQLException e){ } catch (SQLException e){
throw failedToDropObject(Translatable.t("{module}.{id}", MODULE,module, ID,entityId)).causedBy(e); throw failedToDropObject(Translatable.t("{module}.{id}", MODULE,module, ID,entityId)).causedBy(e);
} }

View File

@@ -13,6 +13,8 @@ public interface TagDB {
void deleteEntity(String module, long entityId); void deleteEntity(String module, long entityId);
void deleteEntity(String module, long entityId, long userId);
Map<String, List<Long>> getUses(String tag, long id); Map<String, List<Long>> getUses(String tag, long id);
Collection<Tuple<String, Long>> list(long userId); Collection<Tuple<String, Long>> list(long userId);

View File

@@ -134,6 +134,11 @@ public class TagModule extends BaseHandler implements TagService {
return sendContent(ex,tuples.map(t -> Map.of(TAG,t.a,COUNT,t.b))); return sendContent(ex,tuples.map(t -> Map.of(TAG,t.a,COUNT,t.b)));
} }
@Override
public void deleteEntity(String module, long entityId, long userId) {
tagDb.deleteEntity(module,entityId,userId);
}
@Override @Override
public void save(String module, long entityId, Collection<Long> userIds, Collection<String> tags) { public void save(String module, long entityId, Collection<Long> userIds, Collection<String> tags) {
tagDb.save(userIds,module,entityId,tags); tagDb.save(userIds,module,entityId,tags);