From 444e2e86a94f65704c438788dd5b739598d0d783 Mon Sep 17 00:00:00 2001 From: Stephan Richter Date: Tue, 23 Sep 2025 15:37:28 +0200 Subject: [PATCH] improved tag inheritance Signed-off-by: Stephan Richter --- .../srsoftware/umbrella/task/TaskModule.java | 213 +++++++++--------- 1 file changed, 108 insertions(+), 105 deletions(-) diff --git a/task/src/main/java/de/srsoftware/umbrella/task/TaskModule.java b/task/src/main/java/de/srsoftware/umbrella/task/TaskModule.java index db2f460..479fab6 100644 --- a/task/src/main/java/de/srsoftware/umbrella/task/TaskModule.java +++ b/task/src/main/java/de/srsoftware/umbrella/task/TaskModule.java @@ -38,28 +38,28 @@ public class TaskModule extends BaseHandler implements TaskService { private final TaskDb taskDb; - public TaskModule(Configuration config) throws UmbrellaException { + public TaskModule(Configuration config) throws UmbrellaException { super(); var dbFile = config.get(CONFIG_DATABASE).orElseThrow(() -> missingFieldException(CONFIG_DATABASE)); - taskDb = new SqliteDb(connect(dbFile)); + taskDb = new SqliteDb(connect(dbFile)); ModuleRegistry.add(this); } private void addMember(Task task, long userId) { var user = userService().loadUser(userId); - var member = new Member(user,READ_ONLY); - task.members().put(userId,member); + var member = new Member(user, READ_ONLY); + task.members().put(userId, member); task.dirty(MEMBERS); } private boolean deleteTask(HttpExchange ex, long taskId, UmbrellaUser user) throws IOException { var task = loadMembers(taskDb.load(taskId)); var member = task.members().get(user.id()); - if (member == null || !member.mayWrite()) throw forbidden("You are not allowed to delete {0}",task.name()); + if (member == null || !member.mayWrite()) throw forbidden("You are not allowed to delete {0}", task.name()); taskDb.delete(task); - noteService().deleteEntity(TASK,""+taskId); - tagService().deleteEntity(TASK,taskId); - return sendContent(ex,Map.of(DELETED,taskId)); + noteService().deleteEntity(TASK, "" + taskId); + tagService().deleteEntity(TASK, taskId); + return sendContent(ex, Map.of(DELETED, taskId)); } @Override @@ -74,11 +74,11 @@ public class TaskModule extends BaseHandler implements TaskService { default -> { var taskId = Long.parseLong(head); head = path.pop(); - yield head == null ? deleteTask(ex,taskId,user.get()) : super.doDelete(path,ex); + yield head == null ? deleteTask(ex, taskId, user.get()) : super.doDelete(path, ex); } }; - } catch (UmbrellaException e){ - return send(ex,e); + } catch (UmbrellaException e) { + return send(ex, e); } } @@ -92,15 +92,15 @@ public class TaskModule extends BaseHandler implements TaskService { var head = path.pop(); return switch (head) { case PERMISSIONS -> getPermissionList(ex); - case null -> getUserTasks(user.get(),ex); + case null -> getUserTasks(user.get(), ex); default -> { var taskId = Long.parseLong(head); head = path.pop(); - yield head == null ? getTask(ex,taskId,user.get()) : super.doGet(path,ex); + yield head == null ? getTask(ex, taskId, user.get()) : super.doGet(path, ex); } }; - } catch (UmbrellaException e){ - return send(ex,e); + } catch (UmbrellaException e) { + return send(ex, e); } } @@ -117,11 +117,11 @@ public class TaskModule extends BaseHandler implements TaskService { default -> { var taskId = Long.parseLong(head); head = path.pop(); - yield head == null ? patchTask(ex,taskId,user.get()) : super.doPatch(path,ex); + yield head == null ? patchTask(ex, taskId, user.get()) : super.doPatch(path, ex); } }; } catch (UmbrellaException e){ - return send(ex,e); + return send(ex, e); } } @@ -134,20 +134,20 @@ public class TaskModule extends BaseHandler implements TaskService { if (user.isEmpty()) return unauthorized(ex); var head = path.pop(); return switch (head) { - case ADD -> postNewTask(user.get(),ex); - case ESTIMATED_TIMES -> estimatedTimes(user.get(),ex); - case LIST -> postTaskList(user.get(),ex); - case SEARCH -> postSearch(user.get(),ex); - default -> super.doPost(path,ex); + case ADD -> postNewTask(user.get(), ex); + case ESTIMATED_TIMES -> estimatedTimes(user.get(), ex); + case LIST -> postTaskList(user.get(), ex); + case SEARCH -> postSearch(user.get(), ex); + default -> super.doPost(path, ex); }; - } catch (UmbrellaException e){ - return send(ex,e); + } catch (UmbrellaException e) { + return send(ex, e); } } private void dropMember(Task task, long userId) { if (task.members().get(userId).permission() == OWNER) throw forbidden("You may not remove the owner of the task"); - taskDb.dropMember(task.id(),userId); + taskDb.dropMember(task.id(), userId); task.members().remove(userId); } @@ -157,32 +157,32 @@ public class TaskModule extends BaseHandler implements TaskService { var companyId = cid.longValue(); var company = companyService().get(companyId); if (!companyService().membership(companyId,user.id())) throw forbidden("You are mot a member of company {0}",company.name()); - var projectMap = projectService().listCompanyProjects(companyId,false); + var projectMap = projectService().listCompanyProjects(companyId, false); var taskMap = taskDb.listTasks(projectMap.keySet()); - var taskTree = new HashMap>(); - taskMap.values().stream().filter(task -> !is0(task.estimatedTime())).forEach(task -> placeInTree(task,taskTree,taskMap)); - var result = new ArrayList>(); + var taskTree = new HashMap>(); + taskMap.values().stream().filter(task -> !is0(task.estimatedTime())).forEach(task -> placeInTree(task, taskTree, taskMap)); + var result = new ArrayList>(); projectMap.values().forEach(project -> { var mappedProject = new HashMap<>(project.toMap()); - var children = taskTree.values().stream().filter(root -> project.id() == (Long)root.get(PROJECT_ID)).toList(); + var children = taskTree.values().stream().filter(root -> project.id() == (Long) root.get(PROJECT_ID)).toList(); if (!children.isEmpty()) { mappedProject.put(TASKS, children); result.add(mappedProject); } }); - return sendContent(ex,result); + return sendContent(ex, result); } private boolean getPermissionList(HttpExchange ex) throws IOException { - var map = new HashMap(); + var map = new HashMap(); for (var permission : Permission.values()) map.put(permission.code(),permission.name()); - return sendContent(ex,map); + return sendContent(ex, map); } private boolean getTask(HttpExchange ex, long taskId, UmbrellaUser user) throws IOException { var task = loadMembers(taskDb.load(taskId)); if (!task.hasMember(user)) throw forbidden("You are not a member of {0}",task.name()); - return sendContent(ex,task); + return sendContent(ex, task); } private boolean getUserTasks(UmbrellaUser user, HttpExchange ex) throws IOException { @@ -192,24 +192,24 @@ public class TaskModule extends BaseHandler implements TaskService { if (params.get(OFFSET) instanceof String o) try { offset = Long.parseLong(o); } catch (NumberFormatException e) { - throw invalidFieldException(OFFSET,"number"); + throw invalidFieldException(OFFSET, "number"); } if (params.get(LIMIT) instanceof String l) try { limit = Long.parseLong(l); } catch (NumberFormatException e) { - throw invalidFieldException(LIMIT,"number"); + throw invalidFieldException(LIMIT, "number"); } Set projectIds = projectService().listUserProjects(user.id(), true).keySet(); var list = taskDb.listUserTasks(user.id(), limit, offset, false).stream() .filter(task -> projectIds.contains(task.projectId())) // drop tasks assigned to project we are not member of .map(Task::toMap) .toList(); - return sendContent(ex,list); + return sendContent(ex, list); } @Override public Map listCompanyTasks(long companyId) throws UmbrellaException { - var projectList = projectService().listCompanyProjects(companyId,false); + var projectList = projectService().listCompanyProjects(companyId, false); return taskDb.listTasks(projectList.keySet()); } @@ -218,11 +218,11 @@ public class TaskModule extends BaseHandler implements TaskService { return taskDb.listTasks(List.of(projectId)); } - private Task loadTaskOrNull(long taskId){ + private Task loadTaskOrNull(long taskId) { try { return taskDb.load(taskId); - } catch (UmbrellaException e){ - LOG.log(WARNING,e.getMessage()); + } catch (UmbrellaException e) { + LOG.log(WARNING, e.getMessage()); return null; } } @@ -234,57 +234,57 @@ public class TaskModule extends BaseHandler implements TaskService { @Override public Collection loadMembers(Collection taskList) { - var userMap = new HashMap(); - for (var task : taskList){ - for (var entry : taskDb.getMembers(task).entrySet()){ + var userMap = new HashMap(); + for (var task : taskList) { + for (var entry : taskDb.getMembers(task).entrySet()) { var userId = entry.getKey(); var permission = entry.getValue(); - var user = userMap.computeIfAbsent(userId,k -> userService().loadUser(userId)); - task.members().put(userId,new Member(user,permission)); + var user = userMap.computeIfAbsent(userId, k -> userService().loadUser(userId)); + task.members().put(userId, new Member(user, permission)); } } return taskList; } - private Map placeInTree(Task task, HashMap> taskTree, Map taskMap) { + private Map placeInTree(Task task, HashMap> taskTree, Map taskMap) { var mappedTask = task.toMap(); - if (task.parentTaskId() != null){ + if (task.parentTaskId() != null) { Task parent = taskMap.get(task.parentTaskId()); - var trunk = placeInTree(parent,taskTree,taskMap); + var trunk = placeInTree(parent, taskTree, taskMap); @SuppressWarnings("unchecked") ArrayList children = (ArrayList) trunk.computeIfAbsent(CHILDREN, k -> new ArrayList<>()); children.add(mappedTask); return mappedTask; } - taskTree.put(task.id(),mappedTask); + taskTree.put(task.id(), mappedTask); return mappedTask; } private void patchMembers(Task task, JSONObject json) { var members = task.members(); - for (var key : json.keySet()){ + for (var key : json.keySet()) { long userId; try { userId = Long.parseLong(key); } catch (NumberFormatException e) { - throw invalidFieldException(USER_ID,"long"); + throw invalidFieldException(USER_ID, "long"); } - var permission = switch (json.get(key)){ + var permission = switch (json.get(key)) { case Number code -> Permission.of(code.intValue()); case String name -> Permission.valueOf(name); - default -> throw invalidFieldException(PERMISSION,"int / String"); + default -> throw invalidFieldException(PERMISSION, "int / String"); }; if (permission == OWNER) { // if a new person is about to become the task owner - for (var member : members.values()){ // alter the previous owners to editors - if (member.permission() == OWNER) members.put(member.user().id(),new Member(member.user(),EDIT)); + for (var member : members.values()) { // alter the previous owners to editors + if (member.permission() == OWNER) members.put(member.user().id(), new Member(member.user(), EDIT)); } } if (permission == ASSIGNEE) { // if a new person is about to become the task owner - for (var member : members.values()){ // alter the previous owners to editors - if (member.permission() == ASSIGNEE) members.put(member.user().id(),new Member(member.user(),EDIT)); + for (var member : members.values()) { // alter the previous owners to editors + if (member.permission() == ASSIGNEE) members.put(member.user().id(), new Member(member.user(), EDIT)); } } - members.put(userId,new Member(userService().loadUser(userId),permission)); + members.put(userId, new Member(userService().loadUser(userId), permission)); task.dirty(MEMBERS); } } @@ -292,20 +292,20 @@ public class TaskModule extends BaseHandler implements TaskService { private boolean patchTask(HttpExchange ex, long taskId, UmbrellaUser user) throws IOException { var task = loadMembers(taskDb.load(taskId)); var member = task.members().get(user.id()); - if (member == null || member.permission() == READ_ONLY ) throw forbidden("You are not a allowed to edit {0}!",task.name()); + if (member == null || member.permission() == READ_ONLY) throw forbidden("You are not a allowed to edit {0}!", task.name()); var json = json(ex); - if (json.has(DROP_MEMBER) && json.get(DROP_MEMBER) instanceof Number id) dropMember(task,id.longValue()); - if (json.has(MEMBERS) && json.get(MEMBERS) instanceof JSONObject memberJson) patchMembers(task,memberJson); - if (json.has(NEW_MEMBER) && json.get(NEW_MEMBER) instanceof Number num) addMember(task,num.longValue()); - if (json.has(PARENT_TASK_ID) && json.get(PARENT_TASK_ID) instanceof Number ptid && newParentIsSubtask(task,ptid.longValue())) throw forbidden("Task must not be sub-task of itself."); + if (json.has(DROP_MEMBER) && json.get(DROP_MEMBER) instanceof Number id) dropMember(task, id.longValue()); + if (json.has(MEMBERS) && json.get(MEMBERS) instanceof JSONObject memberJson) patchMembers(task, memberJson); + if (json.has(NEW_MEMBER) && json.get(NEW_MEMBER) instanceof Number num) addMember(task, num.longValue()); + if (json.has(PARENT_TASK_ID) && json.get(PARENT_TASK_ID) instanceof Number ptid && newParentIsSubtask(task, ptid.longValue())) throw forbidden("Task must not be sub-task of itself."); taskDb.save(task.patch(json)); - return sendContent(ex,task); + return sendContent(ex, task); } private boolean newParentIsSubtask(Task task, long newParent) { var parent = taskDb.load(newParent); - while (parent != null){ + while (parent != null) { if (task.id() == parent.id()) return true; if (parent.parentTaskId() == null) break; parent = taskDb.load(parent.parentTaskId()); @@ -320,52 +320,55 @@ public class TaskModule extends BaseHandler implements TaskService { long projectId = pid.longValue(); var project = projectService().load(projectId); projectService().loadMembers(List.of(project)); - var members = project.members(); + var members = project.members(); var member = members.get(user.id()); if (member == null || member.permission() == READ_ONLY) throw forbidden("You are not allowed to create new tasks in this project"); var parentTask = json.has(PARENT_TASK_ID) && json.get(PARENT_TASK_ID) instanceof Number par ? taskService().load(Set.of(par.longValue())).get(par.longValue()) : null; - if (parentTask != null){ - taskService().loadMembers(parentTask); - members = parentTask.members(); - member = members.get(user.id()); - if (member == null || member.permission() == READ_ONLY) throw forbidden("You are not allowed to add sub-stasks to {0}",parentTask.name()); + if (parentTask != null) { + taskService().loadMembers(parentTask); + members = parentTask.members(); + member = members.get(user.id()); + if (member == null || member.permission() == READ_ONLY) throw forbidden("You are not allowed to add sub-stasks to {0}", parentTask.name()); } - var newMembers = new HashMap(); - for (var mem : members.values()){ // Assign members from project or parent task - var permission = mem.permission() == OWNER ? EDIT : mem.permission(); - newMembers.put(mem.user().id(),permission); + var newMembers = new HashMap(); + for (var mem : members.values()) { // Assign members from project or parent task + var permission = mem.permission() == OWNER ? EDIT : mem.permission(); + newMembers.put(mem.user().id(), permission); } - if (json.has(MEMBERS) &&json.get(MEMBERS) instanceof JSONObject mems){ - // check of assignee has been set by client - for (var k : mems.keySet()){ - try { - var userId = Long.parseLong(k); - var permName = mems.getJSONObject(k).getJSONObject(PERMISSION).getString(NAME); - if (Permission.valueOf(permName) == ASSIGNEE) newMembers.put(userId, ASSIGNEE); - } catch (Exception ignored){ - LOG.log(WARNING,"Failed to parse {0}",mems.get(k)); - } - } + if (json.has(MEMBERS) && json.get(MEMBERS) instanceof JSONObject mems) { + // check of assignee has been set by client + for (var k : mems.keySet()) { + try { + var userId = Long.parseLong(k); + var permName = mems.getJSONObject(k).getJSONObject(PERMISSION).getString(NAME); + if (Permission.valueOf(permName) == ASSIGNEE) newMembers.put(userId, ASSIGNEE); + } catch (Exception ignored) { + LOG.log(WARNING, "Failed to parse {0}", mems.get(k)); + } + } } - // set ownership to current user - newMembers.put(user.id(),OWNER); + // set ownership to current user + newMembers.put(user.id(), OWNER); - json.put(MEMBERS,Map.of()); // reset member map for task-to-be-created + json.put(MEMBERS, Map.of()); // reset member map for task-to-be-created Task task = Task.of(json); task = taskDb.save(task); - // do actual member assignment - for (var entry : newMembers.entrySet()) taskDb.setMember(task.id(),entry.getKey(),entry.getValue()); + // do actual member assignment + for (var entry : newMembers.entrySet()) taskDb.setMember(task.id(), entry.getKey(), entry.getValue()); - if (json.has(TAGS) && json.get(TAGS) instanceof JSONArray arr){ - var tagList = arr.toList().stream().filter(e -> e instanceof String).map(String.class::cast).toList(); - tagService().save(TASK,task.id(),null,tagList); + Collection tagList = null; + if (json.has(TAGS) && json.get(TAGS) instanceof JSONArray arr) { + tagList = arr.toList().stream().filter(e -> e instanceof String).map(String.class::cast).toList(); } - return sendContent(ex,loadMembers(task)); + if ((tagList == null || tagList.isEmpty()) && parentTask != null) tagList = tagService().getTags(TASK, parentTask.id(), user); + if ((tagList == null || tagList.isEmpty())) tagList = tagService().getTags(PROJECT, projectId, user); + if (tagList != null && !tagList.isEmpty()) tagService().save(TASK, task.id(), null, tagList); + return sendContent(ex, loadMembers(task)); } private boolean postSearch(UmbrellaUser user, HttpExchange ex) throws IOException { @@ -374,27 +377,27 @@ public class TaskModule extends BaseHandler implements TaskService { var projectId = json.has(PROJECT_ID) && json.get(PROJECT_ID) instanceof Number pid ? pid.longValue() : null; var keys = Arrays.asList(key.split(" ")); var fulltext = json.has(FULLTEXT) && json.get(FULLTEXT) instanceof Boolean val && val; - var tasks = taskDb.find(user.id(),keys,fulltext); + var tasks = taskDb.find(user.id(), keys, fulltext); if (projectId != null) tasks = tasks.values().stream().filter(task -> task.projectId() == projectId).collect(Collectors.toMap(Task::id, t -> t)); - return sendContent(ex,mapValues(tasks)); + return sendContent(ex, mapValues(tasks)); } private boolean postTaskList(UmbrellaUser user, HttpExchange ex) throws IOException { var json = json(ex); - LOG.log(WARNING,"Missing permission check in {0}.postTaskList!",getClass().getSimpleName()); + LOG.log(WARNING, "Missing permission check in {0}.postTaskList!", getClass().getSimpleName()); var showClosed = json.has(SHOW_CLOSED) && json.get(SHOW_CLOSED) instanceof Boolean bool ? bool : false; var noIndex = json.has(NO_INDEX) && json.get(NO_INDEX) instanceof Boolean bool ? bool : false; var projectId = json.has(PROJECT_ID) && json.get(PROJECT_ID) instanceof Number number ? number.longValue() : null; var parentTaskId = json.has(PARENT_TASK_ID) && json.get(PARENT_TASK_ID) instanceof Number number ? number.longValue() : null; if (isSet(projectId)) { - if (parentTaskId == null) return sendContent(ex,mapValues(taskDb.listRootTasks(projectId, user,showClosed))); - var projectTasks = taskDb.listProjectTasks(projectId,parentTaskId,noIndex); + if (parentTaskId == null) return sendContent(ex, mapValues(taskDb.listRootTasks(projectId, user, showClosed))); + var projectTasks = taskDb.listProjectTasks(projectId, parentTaskId, noIndex); loadMembers(projectTasks.values()); - return sendContent(ex,mapValues(projectTasks)); + return sendContent(ex, mapValues(projectTasks)); } - if (isSet(parentTaskId)) return sendContent(ex,mapValues(taskDb.listChildrenOf(parentTaskId,user,showClosed))); + if (isSet(parentTaskId)) return sendContent(ex, mapValues(taskDb.listChildrenOf(parentTaskId, user, showClosed))); var taskIds = json.has(IDS) && json.get(IDS) instanceof JSONArray ids ? ids.toList().stream().map(Object::toString).map(Long::parseLong).toList() : null; - if (isSet(taskIds)) return sendContent(ex,mapValues(taskDb.load(taskIds))); - return sendEmptyResponse(HTTP_NOT_IMPLEMENTED,ex); + if (isSet(taskIds)) return sendContent(ex, mapValues(taskDb.load(taskIds))); + return sendEmptyResponse(HTTP_NOT_IMPLEMENTED, ex); } }