diff --git a/core/src/main/java/de/srsoftware/umbrella/core/Constants.java b/core/src/main/java/de/srsoftware/umbrella/core/Constants.java index 59892d2..8218b81 100644 --- a/core/src/main/java/de/srsoftware/umbrella/core/Constants.java +++ b/core/src/main/java/de/srsoftware/umbrella/core/Constants.java @@ -11,7 +11,9 @@ public class Constants { public static final String ADDRESS = "address"; public static final String ATTACHMENTS = "attachments"; public static final String AUTHORIZATION = "Authorization"; + public static final String BODY = "body"; + public static final String CODE = "code"; public static final String COMPANY = "company"; public static final String COMPANY_ID = "company_id"; diff --git a/core/src/main/java/de/srsoftware/umbrella/core/api/ProjectService.java b/core/src/main/java/de/srsoftware/umbrella/core/api/ProjectService.java index 822dc91..231613a 100644 --- a/core/src/main/java/de/srsoftware/umbrella/core/api/ProjectService.java +++ b/core/src/main/java/de/srsoftware/umbrella/core/api/ProjectService.java @@ -4,10 +4,17 @@ package de.srsoftware.umbrella.core.api; import de.srsoftware.umbrella.core.exceptions.UmbrellaException; import de.srsoftware.umbrella.core.model.Project; import java.util.Collection; +import java.util.List; import java.util.Map; public interface ProjectService { - public Collection listCompanyProjects(long companyId, boolean includeClosed) throws UmbrellaException; - public Map listUserProjects(long userId, boolean includeClosed) throws UmbrellaException; CompanyService companyService(); + public Map listCompanyProjects(long companyId, boolean includeClosed) throws UmbrellaException; + public Map listUserProjects(long userId, boolean includeClosed) throws UmbrellaException; + public Collection loadMembers(Collection projects); + public default Project loadMembers(Project project){ + loadMembers(List.of(project)); + return project; + } + Map> mapProjects(Map projects); } diff --git a/core/src/main/java/de/srsoftware/umbrella/core/model/Member.java b/core/src/main/java/de/srsoftware/umbrella/core/model/Member.java index f41b5a0..569d95f 100644 --- a/core/src/main/java/de/srsoftware/umbrella/core/model/Member.java +++ b/core/src/main/java/de/srsoftware/umbrella/core/model/Member.java @@ -7,9 +7,9 @@ import static de.srsoftware.umbrella.core.Constants.*; import de.srsoftware.tools.Mappable; import java.util.Map; -public record Member(long userId, Permission permission) implements Mappable { +public record Member(UmbrellaUser user, Permission permission) implements Mappable { @Override public Map toMap() { - return Map.of(USER_ID,userId,PERMISSION,permission.toMap()); + return Map.of(USER,user.toMap(),PERMISSION,permission.toMap()); } } diff --git a/core/src/main/java/de/srsoftware/umbrella/core/model/Permission.java b/core/src/main/java/de/srsoftware/umbrella/core/model/Permission.java index 21d26d0..4ef3c92 100644 --- a/core/src/main/java/de/srsoftware/umbrella/core/model/Permission.java +++ b/core/src/main/java/de/srsoftware/umbrella/core/model/Permission.java @@ -1,12 +1,11 @@ /* © SRSoftware 2025 */ package de.srsoftware.umbrella.core.model; -import de.srsoftware.tools.Mappable; - import static de.srsoftware.umbrella.core.Constants.CODE; import static de.srsoftware.umbrella.core.Constants.NAME; import static java.text.MessageFormat.format; +import de.srsoftware.tools.Mappable; import java.security.InvalidParameterException; import java.util.Map; diff --git a/core/src/main/java/de/srsoftware/umbrella/core/model/Project.java b/core/src/main/java/de/srsoftware/umbrella/core/model/Project.java index 82c662d..9c326a2 100644 --- a/core/src/main/java/de/srsoftware/umbrella/core/model/Project.java +++ b/core/src/main/java/de/srsoftware/umbrella/core/model/Project.java @@ -1,19 +1,14 @@ /* © SRSoftware 2025 */ package de.srsoftware.umbrella.core.model; -import static de.srsoftware.tools.Optionals.isSet; import static de.srsoftware.tools.Optionals.nullable; import static de.srsoftware.umbrella.core.Constants.*; import static de.srsoftware.umbrella.core.Util.markdown; -import static de.srsoftware.umbrella.core.exceptions.UmbrellaException.invalidFieldException; -import static de.srsoftware.umbrella.core.exceptions.UmbrellaException.missingFieldException; import de.srsoftware.tools.Mappable; import java.sql.ResultSet; import java.sql.SQLException; import java.util.*; - -import de.srsoftware.umbrella.core.exceptions.UmbrellaException; import org.json.JSONObject; public class Project implements Mappable { @@ -49,8 +44,7 @@ public class Project implements Mappable { } public boolean hasMember(UmbrellaUser user) { - var member = members.get(user.id()); - return isSet(member) && member.userId() == user.id(); + return members.containsKey(user.id()); } public long id(){ @@ -74,7 +68,6 @@ public class Project implements Mappable { for (var key : json.keySet()){ switch (key){ case DESCRIPTION: description = json.getString(key); break; - case MEMBERS: patchMembers(json.getJSONObject(MEMBERS)); break; case NAME: name = json.getString(key); break; case STATUS: status = json.get(key) instanceof Number number ? Status.of(number.intValue()) : Status.valueOf(json.getString(key)); break; default: key = null; @@ -84,20 +77,6 @@ public class Project implements Mappable { return this; } - private void patchMembers(JSONObject json) throws UmbrellaException { - for (var key : json.keySet()){ - long userId; - try { - userId = Long.parseLong(key); - } catch (NumberFormatException e) { - throw invalidFieldException(USER_ID,"long"); - } - if (!(json.get(key) instanceof Number number)) throw invalidFieldException(PERMISSION,"int"); - var permission = Permission.of(number.intValue()); - members.put(userId,new Member(userId,permission)); - } - } - public boolean showClosed(){ return showClosed; } diff --git a/project/src/main/java/de/srsoftware/umbrella/project/ProjectDb.java b/project/src/main/java/de/srsoftware/umbrella/project/ProjectDb.java index 041976e..63dfb1d 100644 --- a/project/src/main/java/de/srsoftware/umbrella/project/ProjectDb.java +++ b/project/src/main/java/de/srsoftware/umbrella/project/ProjectDb.java @@ -2,14 +2,14 @@ package de.srsoftware.umbrella.project; import de.srsoftware.umbrella.core.exceptions.UmbrellaException; +import de.srsoftware.umbrella.core.model.Permission; import de.srsoftware.umbrella.core.model.Project; import java.util.Map; public interface ProjectDb { + Map getMembers(Project project); + Project load(long projectId) throws UmbrellaException; Map ofCompany(long companyId, boolean includeClosed) throws UmbrellaException; Map ofUser(long userId, boolean includeClosed) throws UmbrellaException; - Project save(Project prj) throws UmbrellaException; - - Project load(long projectId) throws UmbrellaException; } diff --git a/project/src/main/java/de/srsoftware/umbrella/project/ProjectModule.java b/project/src/main/java/de/srsoftware/umbrella/project/ProjectModule.java index 089554b..a07b955 100644 --- a/project/src/main/java/de/srsoftware/umbrella/project/ProjectModule.java +++ b/project/src/main/java/de/srsoftware/umbrella/project/ProjectModule.java @@ -10,7 +10,6 @@ import static de.srsoftware.umbrella.core.model.Status.OPEN; import static de.srsoftware.umbrella.project.Constants.CONFIG_DATABASE; import static java.lang.Boolean.TRUE; import static java.net.HttpURLConnection.HTTP_OK; -import static java.util.Comparator.comparing; import com.sun.net.httpserver.HttpExchange; import de.srsoftware.configuration.Configuration; @@ -39,19 +38,6 @@ public class ProjectModule extends BaseHandler implements ProjectService { users = companies.userService(); } - private boolean addMembers(Project project, HttpExchange ex) throws IOException { - var map = project.toMap(); - var members = new HashMap>(); - for (var entry : project.members().entrySet()){ - var userId = entry.getKey(); - var perm = entry.getValue().permission().toMap(); - members.put(userId,Map.of(USER,users.loadUser(userId).toMap(),PERMISSION,perm)); - } - if (!members.isEmpty()) map.put(MEMBERS,members); - project.companyId().map(companies::get).map(Company::toMap).ifPresent(data -> map.put(COMPANY,data)); - return sendContent(ex,map); - } - @Override public CompanyService companyService() { return companies; @@ -70,10 +56,7 @@ public class ProjectModule extends BaseHandler implements ProjectService { default -> { var projectId = Long.parseLong(head); head = path.pop(); - yield switch (head){ - case null -> getProject(ex,projectId,user.get()); - default -> super.doGet(path,ex); - }; + yield head == null ? getProject(ex,projectId,user.get()) : super.doGet(path,ex); } }; } catch (UmbrellaException e){ @@ -129,56 +112,81 @@ public class ProjectModule extends BaseHandler implements ProjectService { } private boolean getProject(HttpExchange ex, long projectId, UmbrellaUser user) throws IOException, UmbrellaException { - var project = projects.load(projectId); + var project = loadMembers(projects.load(projectId)); if (!project.hasMember(user)) throw forbidden("You are not a member of {0}",project.name()); - return addMembers(project,ex); + var map = project.toMap(); + project.companyId().map(companies::get).map(Company::toMap).ifPresent(data -> map.put(COMPANY,data)); + return sendContent(ex,map); } - public Collection listCompanyProjects(long companyId, boolean includeClosed) throws UmbrellaException { - return projects.ofCompany(companyId, includeClosed).values().stream().sorted(comparing(Project::name)).toList(); + public Map listCompanyProjects(long companyId, boolean includeClosed) throws UmbrellaException { + var projectList = projects.ofCompany(companyId, includeClosed); + loadMembers(projectList.values()); + return projectList; } private boolean listCompanyProjects(HttpExchange ex, UmbrellaUser user, long companyId) throws IOException, UmbrellaException { var company = companies.get(companyId); if (!companies.membership(companyId,user.id())) throw forbidden("You are mot a member of company {0}",company.name()); - var projects = listCompanyProjects(companyId,false) - .stream() - .map(Project::toMap) - .map(HashMap::new); - return sendContent(ex,projects); + var projects = listCompanyProjects(companyId,false); + return sendContent(ex,mapProjects(projects)); } @Override public Map listUserProjects(long userId, boolean includeClosed) throws UmbrellaException { - return projects.ofUser(userId, includeClosed); + var projectMap = projects.ofUser(userId, includeClosed); + loadMembers(projectMap.values()); + return projectMap; } private boolean listUserProjects(HttpExchange ex, UmbrellaUser user, boolean showClosed) throws IOException, UmbrellaException { - var projects = new HashMap>(); - for (var entry : listUserProjects(user.id(),showClosed).entrySet()) { - var project = entry.getValue(); - var map = project.toMap(); - var members = new HashMap>(); - var userMap = new HashMap(); - for (var memberEntry : project.members().entrySet()){ - var perm = memberEntry.getValue().permission().name(); - var userId = memberEntry.getKey(); - var u = userMap.get(userId); - if (u == null) userMap.put(userId,u = users.loadUser(userId)); - members.put(userId,Map.of(USER,u.toMap(),PERMISSION,perm)); + var projects = listUserProjects(user.id(),showClosed); + return sendContent(ex,mapProjects(projects)); + } + + @Override + public Collection loadMembers(Collection projectList) { + var userMap = new HashMap(); + for (var project : projectList){ + for (var entry : projects.getMembers(project).entrySet()){ + var userId = entry.getKey(); + var permission = entry.getValue(); + var user = userMap.computeIfAbsent(userId,k -> users.loadUser(userId)); + project.members().put(userId,new Member(user,permission)); } - if (!members.isEmpty()) map.put(MEMBERS,members); - projects.put(entry.getKey(),map); } - return sendContent(ex,projects); + return projectList; + } + + @Override + public Map> mapProjects(Map projects) { + var mapped = new HashMap>(); + for (var entry : projects.entrySet()) mapped.put(entry.getKey(),entry.getValue().toMap()); + return mapped; + } + + private void patchMembers(Project project, JSONObject json) { + var members = project.members(); + for (var key : json.keySet()){ + long userId; + try { + userId = Long.parseLong(key); + } catch (NumberFormatException e) { + throw invalidFieldException(USER_ID,"long"); + } + if (!(json.get(key) instanceof Number number)) throw invalidFieldException(PERMISSION,"int"); + var permission = Permission.of(number.intValue()); + members.put(userId,new Member(users.loadUser(userId),permission)); + } } private boolean patchProject(HttpExchange ex, long projectId, UmbrellaUser user) throws IOException, UmbrellaException { - var project = projects.load(projectId); + var project = loadMembers(projects.load(projectId)); if (!project.hasMember(user)) throw forbidden("You are not a member of {0}",project.name()); var json = json(ex); + if (json.has(MEMBERS) && json.get(MEMBERS) instanceof JSONObject memberJson) patchMembers(project,memberJson); projects.save(project.patch(json)); - return addMembers(project,ex); + return sendContent(ex,project.toMap()); } @@ -195,7 +203,8 @@ public class ProjectModule extends BaseHandler implements ProjectService { if (json.has(SETTINGS) && json.get(SETTINGS) instanceof JSONObject settingsJson){ showClosed = settingsJson.has(SHOW_CLOSED) && settingsJson.get(SHOW_CLOSED) == TRUE; } - var prj = new Project(0,name,description, OPEN,companyId,showClosed, Map.of(user.id(),new Member(user.id(), OWNER))); + var owner = Map.of(user.id(),new Member(user,OWNER)); + var prj = new Project(0,name,description, OPEN,companyId,showClosed, owner); prj = projects.save(prj); return sendContent(ex,prj); } @@ -205,6 +214,5 @@ public class ProjectModule extends BaseHandler implements ProjectService { var showClosed = json.has(SHOW_CLOSED) && json.get(SHOW_CLOSED) instanceof Boolean bool ? bool : false; if (json.has(COMPANY_ID) && json.get(COMPANY_ID) instanceof Number companyId) return listCompanyProjects(ex, user, companyId.longValue()); return listUserProjects(ex,user,showClosed); - } } \ No newline at end of file diff --git a/project/src/main/java/de/srsoftware/umbrella/project/SqliteDb.java b/project/src/main/java/de/srsoftware/umbrella/project/SqliteDb.java index 9f8f31e..6c962e0 100644 --- a/project/src/main/java/de/srsoftware/umbrella/project/SqliteDb.java +++ b/project/src/main/java/de/srsoftware/umbrella/project/SqliteDb.java @@ -16,7 +16,6 @@ import static java.text.MessageFormat.format; import de.srsoftware.tools.jdbc.Query; import de.srsoftware.umbrella.core.exceptions.UmbrellaException; -import de.srsoftware.umbrella.core.model.Member; import de.srsoftware.umbrella.core.model.Permission; import de.srsoftware.umbrella.core.model.Project; import java.sql.Connection; @@ -36,19 +35,6 @@ public class SqliteDb implements ProjectDb { init(); } - private Map addMembers(Map projects) throws SQLException, UmbrellaException { - Object[] ids = projects.keySet().toArray(); - var rs = select(ALL).from(TABLE_PROJECT_USERS).where(PROJECT_ID,in(ids)).exec(db); - while (rs.next()){ - var userId = rs.getLong(USER_ID); - var projectId = rs.getLong(PROJECT_ID); - var permission = Permission.of(rs.getInt(PERMISSIONS)); - projects.get(projectId).members().put(userId, new Member(userId,permission)); - } - rs.close(); - return projects; - } - private int createTables() { createProjectTables(); return createSettingsTable(); @@ -129,6 +115,19 @@ CREATE TABLE IF NOT EXISTS {0} ( {1} VARCHAR(255) PRIMARY KEY, {2} VARCHAR(255) } } + @Override + public Map getMembers(Project project) { + try { + var result = new HashMap(); + var rs = select(ALL).from(TABLE_PROJECT_USERS).where(PROJECT_ID,equal(project.id())).exec(db); + while (rs.next()) result.put(rs.getLong(USER_ID),Permission.of(rs.getInt(PERMISSIONS))); + rs.close(); + return result; + } catch (SQLException e){ + throw new UmbrellaException(HTTP_SERVER_ERROR,"Faailed to load project members"); + } + } + private void init(){ var version = createTables(); LOG.log(INFO,"Updated project db to version {0}",version); @@ -142,7 +141,6 @@ CREATE TABLE IF NOT EXISTS {0} ( {1} VARCHAR(255) PRIMARY KEY, {2} VARCHAR(255) if (rs.next()) result = Project.of(rs); rs.close(); if (result == null) throw UmbrellaException.notFound("No project found for id {0}",projectId); - addMembers(Map.of(projectId,result)); return result; } catch (SQLException e) { throw new UmbrellaException(HTTP_SERVER_ERROR,"Failed to load items from database"); @@ -161,7 +159,7 @@ CREATE TABLE IF NOT EXISTS {0} ( {1} VARCHAR(255) PRIMARY KEY, {2} VARCHAR(255) projects.put(project.id(),project); } rs.close(); - return addMembers(projects); + return projects; } catch (SQLException e) { throw new UmbrellaException(HTTP_SERVER_ERROR,"Failed to load items from database"); } @@ -181,7 +179,7 @@ CREATE TABLE IF NOT EXISTS {0} ( {1} VARCHAR(255) PRIMARY KEY, {2} VARCHAR(255) projects.put(project.id(),project); } rs.close(); - return addMembers(projects); + return projects; } catch (SQLException e) { throw new UmbrellaException(HTTP_SERVER_ERROR,"Failed to load items from database"); } @@ -198,7 +196,7 @@ CREATE TABLE IF NOT EXISTS {0} ( {1} VARCHAR(255) PRIMARY KEY, {2} VARCHAR(255) if (id != null){ if (!prj.members().isEmpty()) { var query = insertInto(TABLE_PROJECT_USERS, PROJECT_ID, USER_ID, PERMISSIONS); - for (var entry : prj.members().entrySet()) query.values(id, entry.getKey(), entry.getValue().permission().code()); + for (var member : prj.members().entrySet()) query.values(id, member.getKey(), member.getValue().permission().code()); query.execute(db).close(); } return new Project(id, prj.name(), prj.description(),prj.status(),prj.companyId().orElse(null),prj.showClosed(),prj.members()); diff --git a/task/src/main/java/de/srsoftware/umbrella/task/SqliteDb.java b/task/src/main/java/de/srsoftware/umbrella/task/SqliteDb.java index 634f01d..1ab486c 100644 --- a/task/src/main/java/de/srsoftware/umbrella/task/SqliteDb.java +++ b/task/src/main/java/de/srsoftware/umbrella/task/SqliteDb.java @@ -16,8 +16,8 @@ import de.srsoftware.umbrella.core.model.Task; import de.srsoftware.umbrella.core.model.UmbrellaUser; import java.sql.Connection; import java.sql.SQLException; +import java.util.Collection; import java.util.HashMap; -import java.util.List; public class SqliteDb implements TaskDb { @@ -28,7 +28,7 @@ public class SqliteDb implements TaskDb { db = connection; } - public HashMap listTasks(List projectIds) throws UmbrellaException { + public HashMap listTasks(Collection projectIds) throws UmbrellaException { try { var tasks = new HashMap(); var rs = select(ALL).from(TABLE_TASKS).where(PROJECT_ID, in(projectIds.toArray())).exec(db); 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 bde2b96..62cc582 100644 --- a/task/src/main/java/de/srsoftware/umbrella/task/TaskModule.java +++ b/task/src/main/java/de/srsoftware/umbrella/task/TaskModule.java @@ -25,6 +25,9 @@ import de.srsoftware.umbrella.core.api.TaskService; import de.srsoftware.umbrella.core.api.UserService; import de.srsoftware.umbrella.core.exceptions.UmbrellaException; import de.srsoftware.umbrella.core.model.*; +import de.srsoftware.umbrella.core.model.Task; +import de.srsoftware.umbrella.core.model.Token; +import de.srsoftware.umbrella.core.model.UmbrellaUser; import java.io.IOException; import java.util.*; @@ -90,17 +93,17 @@ public class TaskModule extends BaseHandler implements TaskService { var companyId = cid.longValue(); var company = companies.get(companyId); if (!companies.membership(companyId,user.id())) throw forbidden("You are mot a member of company {0}",company.name()); - var projects = this.projects.listCompanyProjects(companyId,false); - var map = taskDb.listTasks(projects.stream().map(Project::id).toList()); - var tree = new HashMap>(); - map.values().stream().filter(task -> !is0(task.estimatedTime())).forEach(task -> placeInTree(task,tree,map)); + var projectMap = this.projects.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>(); - projects.forEach(project -> { - var projectMap = new HashMap<>(project.toMap()); - var children = tree.values().stream().filter(root -> project.id() == (Long)root.get(PROJECT_ID)).toList(); + 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(); if (!children.isEmpty()) { - projectMap.put(TASKS, children); - result.add(projectMap); + mappedProject.put(TASKS, children); + result.add(mappedProject); } }); return sendContent(ex,result); @@ -121,7 +124,7 @@ public class TaskModule extends BaseHandler implements TaskService { @Override public HashMap listCompanyTasks(long companyId) throws UmbrellaException { var projectList = projects.listCompanyProjects(companyId,false); - return taskDb.listTasks(projectList.stream().map(Project::id).toList()); + return taskDb.listTasks(projectList.keySet()); } @Override @@ -129,18 +132,18 @@ public class TaskModule extends BaseHandler implements TaskService { return taskDb.listTasks(List.of(projectId)); } - private Map placeInTree(Task task, HashMap> tree, Map map) { - var taskMap = task.toMap(); + private Map placeInTree(Task task, HashMap> taskTree, Map taskMap) { + var mappedTask = task.toMap(); if (task.parentTaskId() != null){ - Task parent = map.get(task.parentTaskId()); - var trunk = placeInTree(parent,tree,map); + Task parent = taskMap.get(task.parentTaskId()); + var trunk = placeInTree(parent,taskTree,taskMap); @SuppressWarnings("unchecked") - ArrayList children = (ArrayList) trunk.computeIfAbsent(CHILDREN, k -> new ArrayList()); - children.add(taskMap); - return taskMap; + ArrayList children = (ArrayList) trunk.computeIfAbsent(CHILDREN, k -> new ArrayList<>()); + children.add(mappedTask); + return mappedTask; } - tree.put(task.id(),taskMap); - return taskMap; + taskTree.put(task.id(),mappedTask); + return mappedTask; } private boolean postTaskList(UmbrellaUser user, HttpExchange ex) throws IOException {