288 lines
12 KiB
Java
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));
|
|
}
|
|
} |