Files
Umbrella/project/src/main/java/de/srsoftware/umbrella/project/ProjectModule.java
Stephan Richter db95cdbb58
Some checks failed
Build Docker Image / Docker-Build (push) Failing after 3m32s
Build Docker Image / Clean-Registry (push) Successful in -5s
Merge branch 'module/poll' into dev
2026-03-09 11:19:22 +01:00

288 lines
12 KiB
Java

/* © SRSoftware 2025 */
package de.srsoftware.umbrella.project;
import static de.srsoftware.umbrella.core.ConnectionProvider.connect;
import static de.srsoftware.umbrella.core.ModuleRegistry.*;
import static de.srsoftware.umbrella.core.Util.mapValues;
import static de.srsoftware.umbrella.core.constants.Field.*;
import static de.srsoftware.umbrella.core.constants.Field.PERMISSION;
import static de.srsoftware.umbrella.core.constants.Field.SETTINGS;
import static de.srsoftware.umbrella.core.constants.Field.TAGS;
import static de.srsoftware.umbrella.core.constants.Module.PROJECT;
import static de.srsoftware.umbrella.core.constants.Path.*;
import static de.srsoftware.umbrella.core.constants.Text.*;
import static de.srsoftware.umbrella.core.exceptions.UmbrellaException.*;
import static de.srsoftware.umbrella.core.model.Permission.*;
import static de.srsoftware.umbrella.core.model.Permission.OWNER;
import static de.srsoftware.umbrella.core.model.Status.OPEN;
import static de.srsoftware.umbrella.core.model.Status.PREDEFINED;
import static de.srsoftware.umbrella.core.model.Translatable.t;
import static de.srsoftware.umbrella.messagebus.MessageBus.messageBus;
import static de.srsoftware.umbrella.messagebus.events.Event.EventType.CREATE;
import static de.srsoftware.umbrella.project.Constants.CONFIG_DATABASE;
import static java.lang.Boolean.TRUE;
import static java.net.HttpURLConnection.HTTP_BAD_REQUEST;
import com.sun.net.httpserver.HttpExchange;
import de.srsoftware.configuration.Configuration;
import de.srsoftware.tools.SessionToken;
import de.srsoftware.umbrella.core.*;
import de.srsoftware.umbrella.core.api.ProjectService;
import de.srsoftware.umbrella.core.constants.Field;
import de.srsoftware.umbrella.core.constants.Path;
import de.srsoftware.umbrella.core.constants.Text;
import de.srsoftware.umbrella.core.exceptions.UmbrellaException;
import de.srsoftware.umbrella.core.model.*;
import de.srsoftware.umbrella.messagebus.events.ProjectEvent;
import java.io.IOException;
import java.util.*;
import org.json.JSONArray;
import org.json.JSONObject;
public class ProjectModule extends BaseHandler implements ProjectService {
private final ProjectDb projectDb;
public ProjectModule(Configuration config) throws UmbrellaException {
super();
var dbFile = config.get(CONFIG_DATABASE).orElseThrow(() -> missingField(CONFIG_DATABASE));
projectDb = new SqliteDb(connect(dbFile));
ModuleRegistry.add(this);
}
private UmbrellaUser addMember(Project project, long userId) {
var user = userService().loadUser(userId);
var member = new Member(user,READ_ONLY);
project.members().put(userId,member);
project.dirty(MEMBERS);
return user;
}
@Override
public boolean doGet(de.srsoftware.tools.Path path, HttpExchange ex) throws IOException {
addCors(ex);
try {
Optional<Token> token = SessionToken.from(ex).map(Token::of);
var user = userService().loadUser(token);
if (user.isEmpty()) return unauthorized(ex);
var head = path.pop();
return switch (head) {
case null -> super.doGet(path, ex);
default -> {
var projectId = Long.parseLong(head);
head = path.pop();
yield switch (head) {
case null -> getProject(ex, projectId, user.get());
default -> super.doGet(path, ex);
};
}
};
} catch (UmbrellaException e){
return send(ex,e);
}
}
@Override
public boolean doPatch(de.srsoftware.tools.Path path, HttpExchange ex) throws IOException {
addCors(ex);
try {
Optional<Token> token = SessionToken.from(ex).map(Token::of);
var user = userService().loadUser(token);
if (user.isEmpty()) return unauthorized(ex);
var head = path.pop();
return switch (head) {
default -> {
var projectId = Long.parseLong(head);
head = path.pop();
yield switch (head){
case null -> patchProject(ex,projectId,user.get());
default -> super.doPatch(path,ex);
};
}
};
} catch (UmbrellaException e){
return send(ex,e);
}
}
@Override
public boolean doPost(de.srsoftware.tools.Path path, HttpExchange ex) throws IOException {
addCors(ex);
try {
Optional<Token> token = SessionToken.from(ex).map(Token::of);
var user = userService().loadUser(token);
if (user.isEmpty()) return unauthorized(ex);
var head = path.pop();
return switch (head) {
case LIST -> postProjectList(ex, user.get());
case SEARCH -> postSearch(user.get(),ex);
case null -> postProject(ex, user.get());
default -> {
var projectId = Long.parseLong(head);
head = path.pop();
yield switch (head){
case Path.STATE -> postNewState(ex,projectId,user.get());
case null, default -> super.doGet(path, ex);
};
}
};
} catch (NumberFormatException e){
return sendContent(ex,HTTP_BAD_REQUEST,"Invalid project id");
} catch (UmbrellaException e){
return send(ex,e);
}
}
private void dropMember(Project project, long userId) {
if (project.members().get(userId).permission() == OWNER) throw forbidden("You may not remove the owner of the project");
projectDb.dropMember(project.id(),userId);
project.members().remove(userId);
}
private boolean getProject(HttpExchange ex, long projectId, UmbrellaUser user) throws IOException, UmbrellaException {
var project = loadMembers(projectDb.load(projectId));
if (!project.hasMember(user)) throw notAmember(t(PROJECT_WITH_ID,ID,project.name()));
var map = project.toMap();
project.companyId().map(companyService()::get).map(Company::toMap).ifPresent(data -> map.put(Field.COMPANY,data));
return sendContent(ex,map);
}
public Map<Long, Project> listCompanyProjects(long companyId, boolean includeClosed) throws UmbrellaException {
var projectList = projectDb.ofCompany(companyId, includeClosed);
loadMembers(projectList.values());
return projectList;
}
private boolean listCompanyProjects(HttpExchange ex, UmbrellaUser user, long companyId) throws IOException, UmbrellaException {
var company = companyService().get(companyId);
if (!companyService().membership(companyId,user.id())) throw notAmember(t(COMPANY_WITH_ID,ID,company.name()));
var projects = listCompanyProjects(companyId,false);
return sendContent(ex,mapValues(projects));
}
@Override
public Map<Long, Project> listUserProjects(long userId, boolean includeClosed) throws UmbrellaException {
var projectMap = projectDb.ofUser(userId, includeClosed);
loadMembers(projectMap.values());
return projectMap;
}
private boolean listUserProjects(HttpExchange ex, UmbrellaUser user, boolean showClosed) throws IOException, UmbrellaException {
var projects = listUserProjects(user.id(),showClosed);
return sendContent(ex,mapValues(projects));
}
@Override
public Project load(long projectId) {
return projectDb.load(projectId);
}
@Override
public Collection<Project> loadMembers(Collection<Project> projectList) {
var userMap = new HashMap<Long,UmbrellaUser>();
for (var project : projectList){
for (var entry : projectDb.getMembers(project).entrySet()){
var userId = entry.getKey();
var permission = entry.getValue();
var user = userMap.computeIfAbsent(userId,k -> userService().loadUser(userId));
project.members().put(userId,new Member(user,permission));
}
}
return projectList;
}
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 invalidField(USER_ID,t(LONG));
}
if (!(json.get(key) instanceof Number number)) throw invalidField(PERMISSION,t(Text.NUMBER));
var permission = Permission.of(number.intValue());
if (permission == OWNER) { // if a new person is about to become the project 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));
}
}
members.put(userId,new Member(userService().loadUser(userId),permission));
project.dirty(MEMBERS);
}
}
private boolean patchProject(HttpExchange ex, long projectId, UmbrellaUser user) throws IOException, UmbrellaException {
var project = loadMembers(projectDb.load(projectId));
if (!project.hasMember(user)) throw notAmember(t(PROJECT_WITH_ID,ID,project.name()));
var old = project.toMap();
var json = json(ex);
UmbrellaUser newMember = null;
if (json.has(DROP_MEMBER) && json.get(DROP_MEMBER) instanceof Number id) dropMember(project,id.longValue());
if (json.has(MEMBERS) && json.get(MEMBERS) instanceof JSONObject memberJson) patchMembers(project,memberJson);
if (json.has(NEW_MEMBER) && json.get(NEW_MEMBER) instanceof Number num) newMember = addMember(project,num.longValue());
project = projectDb.save(project.patch(json), user);
messageBus().dispatch(newMember != null ? new ProjectEvent(user,project,newMember) : new ProjectEvent(user,project, old));
return sendContent(ex,project.toMap());
}
private boolean postNewState(HttpExchange ex, long projectId, UmbrellaUser user) throws IOException {
var project = loadMembers(load(projectId));
if (!project.hasMember(user)) throw notAmember(t(PROJECT_WITH_ID,ID,project.name()));
var json = json(ex);
if (!(json.has(Field.CODE) && json.get(Field.CODE) instanceof Number code)) throw missingField(Field.CODE);
if (!(json.has(NAME) && json.get(NAME) instanceof String name)) throw missingField(NAME);
var newState = new Status(name,code.intValue());
return sendContent(ex, projectDb.save(projectId,newState));
}
private boolean postProject(HttpExchange ex, UmbrellaUser user) throws IOException, UmbrellaException {
var json = json(ex);
if (!(json.has(NAME) && json.get(NAME) instanceof String name)) throw missingField(NAME);
String description = null;
if (json.has(DESCRIPTION)){
var desc = json.get(DESCRIPTION);
if (desc instanceof String d) description = d;
if (desc instanceof JSONObject nested && nested.has(SOURCE) && nested.get(SOURCE) instanceof String d) description = d;
}
Long companyId = null;
if (json.has(COMPANY_ID) && json.get(COMPANY_ID) instanceof Number number){
if (!companyService().membership(number.longValue(), user.id())) throw notAmember(t(COMPANY_WITH_ID, ID,number));
companyId = number.longValue();
}
var showClosed = false;
if (json.has(SETTINGS) && json.get(SETTINGS) instanceof JSONObject settingsJson){
showClosed = settingsJson.has(SHOW_CLOSED) && settingsJson.get(SHOW_CLOSED) == TRUE;
}
var owner = Map.of(user.id(),new Member(user,OWNER));
var prj = new Project(0,name,description, OPEN.code(),companyId,showClosed, owner, PREDEFINED);
prj = projectDb.save(prj,user);
if (json.has(TAGS) && json.get(TAGS) instanceof JSONArray arr){
var tagList = arr.toList().stream().filter(elem -> elem instanceof String).map(String.class::cast).toList();
tagService().save(PROJECT,prj.id(),null,tagList);
}
messageBus().dispatch(new ProjectEvent(user,prj, CREATE));
return sendContent(ex,prj);
}
private boolean postProjectList(HttpExchange ex, UmbrellaUser user) throws IOException {
var json = json(ex);
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);
}
private boolean postSearch(UmbrellaUser user, HttpExchange ex) throws IOException {
var json = json(ex);
if (!(json.has(KEY) && json.get(KEY) instanceof String key)) throw missingField(KEY);
var keys = Arrays.asList(key.split(" "));
var fulltext = json.has(FULLTEXT) && json.get(FULLTEXT) instanceof Boolean val && val;
var projects = projectDb.find(user.id(),keys,fulltext);
return sendContent(ex,mapValues(projects));
}
}