OpenSource Projekt-Management-Software
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

395 lines
16 KiB

/* © SRSoftware 2025 */
package de.srsoftware.umbrella.stock;
import static de.srsoftware.tools.Optionals.is0;
import static de.srsoftware.tools.Optionals.nullIfEmpty;
import static de.srsoftware.umbrella.core.ConnectionProvider.connect;
import static de.srsoftware.umbrella.core.Constants.*;
import static de.srsoftware.umbrella.core.Field.ITEM;
import static de.srsoftware.umbrella.core.ModuleRegistry.companyService;
import static de.srsoftware.umbrella.core.ModuleRegistry.userService;
import static de.srsoftware.umbrella.core.exceptions.UmbrellaException.*;
import static de.srsoftware.umbrella.stock.Constants.*;
import static java.lang.System.Logger.Level.WARNING;
import static java.text.MessageFormat.format;
import static java.util.Comparator.comparing;
import com.sun.net.httpserver.HttpExchange;
import de.srsoftware.configuration.Configuration;
import de.srsoftware.tools.Mappable;
import de.srsoftware.tools.Path;
import de.srsoftware.tools.SessionToken;
import de.srsoftware.umbrella.core.BaseHandler;
import de.srsoftware.umbrella.core.ModuleRegistry;
import de.srsoftware.umbrella.core.api.Owner;
import de.srsoftware.umbrella.core.api.StockService;
import de.srsoftware.umbrella.core.exceptions.UmbrellaException;
import de.srsoftware.umbrella.core.model.*;
import de.srsoftware.umbrella.core.model.Location;
import java.io.IOException;
import java.util.*;
import org.json.JSONObject;
public class StockModule extends BaseHandler implements StockService {
private final StockDb stockDb;
public StockModule(Configuration config) throws UmbrellaException {
super();
var dbFile = config.get(CONFIG_DATABASE).orElseThrow(() -> missingFieldException(CONFIG_DATABASE));
stockDb = new SqliteDb(connect(dbFile));
ModuleRegistry.add(this);
}
private boolean assigned(Owner owner, UmbrellaUser user){
owner = owner.resolve();
if (owner instanceof UmbrellaUser u && user.id() == u.id()) return true;
if (owner instanceof Company comp) return companyService().membership(comp.id(),user.id());
return false;
}
private boolean deleteLocation(UmbrellaUser user, Location locationRef, HttpExchange ex) throws IOException {
var location = locationRef.resolve();
var owner = location.owner().resolve();
if (!assigned(owner,user)) throw forbidden("You are not allowed to modify \"{0}\"",location);
if (!stockDb.listItemsAt(location).isEmpty()) throw forbidden("\"{0}\" cannot be deleted, as it contains items!",location);
if (!stockDb.listChildLocations(location.id()).isEmpty()) throw forbidden("\"{0}\" cannot be deleted, as it contains other locations!",location);
return sendContent(ex,stockDb.delete(location));
}
@Override
public boolean doDelete(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 LOCATION -> {
try {
var location = Location.of(Long.parseLong(path.pop()));
yield deleteLocation(user.get(), location, ex);
} catch (NumberFormatException e){
yield super.doGet(path,ex);
}
}
case null, default -> super.doDelete(path,ex);
};
} catch (UmbrellaException e){
return send(ex,e);
}
}
@Override
public boolean doGet(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 COMPANY -> {
try {
var company = companyService().get(Long.parseLong(path.pop()));
yield getItem(user.get(),company,path,ex);
} catch (NumberFormatException e){
yield super.doGet(path,ex);
}
}
case LOCATION -> {
try {
var location = Location.of(Long.parseLong(path.pop()));
yield getLocationEntities(location, ex);
} catch (Exception e){
yield super.doGet(path,ex);
}
}
case LOCATIONS -> getLocations(path,user.get(),ex);
case PROPERTIES -> getProperties(ex);
case USER -> {
try {
var userId = Long.parseLong(path.pop());
if (userId != user.get().id()) throw forbidden("You are not allowed to access items of another user!");
yield getItem(user.get(),user.get(),path,ex);
} catch (NumberFormatException e){
yield super.doGet(path,ex);
}
}
case null, default -> super.doGet(path,ex);
};
} catch (UmbrellaException e){
return send(ex,e);
}
}
@Override
public boolean doPatch(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);
return switch (path.pop()){
case MOVE_ITEM -> patchMoveItem(user.get(), path,ex);
case LOCATION -> {
try {
var id = Long.parseLong(path.pop());
yield patchLocation(id, user.get(), ex);
} catch (NumberFormatException nfe){
yield super.doPatch(path,ex);
}
}
case null -> patchItem(user.get(),ex);
default -> super.doPatch(path,ex);
};
} catch (UmbrellaException e){
return send(ex,e);
}
}
@Override
public boolean doPost(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 ITEM -> postItem(user.get(), ex);
case LOCATION -> postLocation(user.get(),ex);
case PROPERTY -> postProperty(user.get(),ex);
case null, default -> super.doPost(path,ex);
};
} catch (UmbrellaException e){
return send(ex,e);
}
}
private boolean getChildLocations(UmbrellaUser user, long parentId, HttpExchange ex) throws IOException {
LOG.log(WARNING,"No security check implemented for {0}.getChildLocations(user, parentId, ex)!",getClass().getSimpleName()); // TODO check, that user is allowed to request that location
return sendContent(ex, stockDb.listChildLocations(parentId).stream().sorted(comparing(l -> l.name().toLowerCase())).map(DbLocation::toMap));
}
private boolean getItem(UmbrellaUser user, Owner owner, Path path, HttpExchange ex) throws IOException {
if (!assigned(owner,user)) throw forbidden("You are not allowed to access items of {0}",owner);
return switch (path.pop()){
case ITEM -> {
try {
var itemId = Long.parseLong(path.pop());
var item = stockDb.loadItem(owner.dbCode(),itemId);
yield getLocationEntities(item.location(),ex);
} catch (NumberFormatException e) {
yield super.doGet(path,ex);
}
}
case null, default -> super.doGet(path,ex);
};
}
private boolean getLocationEntities(Location location, HttpExchange ex) throws IOException {
var items = stockDb.listItemsAt(location).stream().map(Item::toMap).toList();
var owner = location.resolve().owner();
List<Long> userIds = switch (owner.type()){
case COMPANY -> companyService().getMembers(owner.id()).stream().map(UmbrellaUser::id).toList();
case USER -> List.of(owner.id());
case null, default -> throw unprocessable("Unprocessable owner type: {0}",owner.type());
};
var pathToLocation = stockDb.pathToLocation(location);
return sendContent(ex,Map.of(ITEMS,items,USERS,userIds,PATH,pathToLocation,LOCATION,location.resolve().toMap()));
}
private boolean getLocations(Path path, UmbrellaUser user, HttpExchange ex) throws IOException {
var head = path.pop();
return switch (head){
case BELOW -> {
try {
var id = Long.parseLong(path.pop());
yield getChildLocations(user, id, ex);
} catch (Exception e){
yield super.doGet(path,ex);
}
}
case OF_USER -> getUserLocations(user,ex);
case null, default -> super.doGet(path,ex);
};
}
private boolean getProperties(HttpExchange ex) throws IOException {
return sendContent(ex,stockDb.listProperties().stream().map(Property::toMap).toList());
}
private boolean getUserLocations(UmbrellaUser user, HttpExchange ex) throws IOException {
var result = new ArrayList<Object>();
var userLocations = stockDb.listUserLocations(user);
result.add(Map.of(
PARENT, Map.of(USER, user.id()),
NAME,user.name(),
LOCATIONS,userLocations.stream().map(DbLocation::toMap).toList()));
var companies = companyService().listCompaniesOf(user);
companies.values().stream().sorted(comparing(a -> a.name().toLowerCase())).forEach(company -> {
var locations = stockDb.listCompanyLocations(company);
result.add(Map.of(
PARENT, Map.of(COMPANY, company.id()),
NAME,company.name(),
LOCATIONS,locations.stream().sorted(comparing(a -> a.name().toLowerCase())).map(DbLocation::toMap).toList()));
});
return sendContent(ex, result);
}
@Override
public DbLocation loadLocation(long locationId) {
return stockDb.loadLocation(locationId);
}
private boolean patchItem(UmbrellaUser user, HttpExchange ex) throws IOException {
var json = json(ex);
if (!(json.get(ID) instanceof Number id)) throw missingFieldException(ID);
json.remove(ID);
var item = stockDb.loadItem(id.longValue());
item.patch(json);
return sendContent(ex,stockDb.save(item));
}
private boolean patchMoveItem(UmbrellaUser user, Path path, HttpExchange ex) throws IOException {
var json = json(ex);
if (!(json.get(ITEM) instanceof Number itemId)) throw missingFieldException(ITEM);
if (!(json.get(TARGET) instanceof Number locationId)) throw missingFieldException(TARGET);
var item = stockDb.loadItem(itemId.longValue());
var itemOwner = item.owner().resolve();
if (!assigned(itemOwner,user)) throw forbidden("You are not allowed to alter the location of \"{0}\"!",item.name());
var target = stockDb.loadLocation(locationId.longValue());
var locOwner = target.resolve().owner().resolve();
if (!assigned(locOwner,user)) throw forbidden("You are not allowed to modify \"{0}\"!",target.name());
if (!locOwner.equals(itemOwner)) throw unprocessable("You may not move items from one owner ({0}) to another ({1})",itemOwner,locOwner);
stockDb.save(item.location(target));
return sendContent(ex,item);
}
private boolean patchLocation(long locationId, UmbrellaUser user, HttpExchange ex) throws IOException {
var json = json(ex);
var location = stockDb.loadLocation(locationId);
var owner = location.owner().resolve();
if (!assigned(owner,user)) throw forbidden("You are not allowed to edit \"{0}\"!",location.name());
if (json.has(PARENT_LOCATION_ID) && json.get(PARENT_LOCATION_ID) instanceof Number parentId){
if (parentId.longValue() != 0L) {
var target = stockDb.loadLocation(parentId.longValue());
if (target.id() == location.id()) throw forbidden("Location must not be it`s own parent!");
var current = target;
while (current != null){
if (current.id() == locationId) throw forbidden("Location cannot be contained in itself!");
current = is0(current.parent()) ? null : stockDb.loadLocation(current.parent());
}
var targetOwner = target.owner().resolve();
if (!assigned(targetOwner, user)) throw forbidden("You are not allowed to edit \"{0}\"!", target.name());
if (!targetOwner.equals(owner)) throw unprocessable("You may not move locations from one owner ({0}) to another ({1})", owner, targetOwner);
LOG.log(WARNING,"Not checking, if location is moved to on of its own children!");
}
}
location = stockDb.save(location.patch(json));
return sendContent(ex,location);
}
private boolean postItem(UmbrellaUser user, HttpExchange ex) throws IOException {
var json = json(ex);
if (!json.has(NAME) || !(json.get(NAME) instanceof String name)) throw missingFieldException(NAME);
if (!json.has(CODE) || !(json.get(CODE) instanceof String code)) throw missingFieldException(CODE);
if (!json.has(LOCATION) || !(json.get(LOCATION) instanceof JSONObject locationData)) throw missingFieldException(LOCATION);
var location = stockDb.loadLocation(locationData.getLong(ID));
var owner = location.owner().resolve();
if (!assigned(owner,user)) throw forbidden("You are not allowed to add items to {0}!",location);
var number = stockDb.nextItemNumberFor(owner);
var newItem = new Item(0,owner,number,location,code,name);
return sendContent(ex,stockDb.save(newItem));
}
private boolean postLocation(UmbrellaUser user, HttpExchange ex) throws IOException {
var json = json(ex);
if (!(json.get(NAME) instanceof String name)) throw missingFieldException(NAME);
if (!(json.get(PARENT) instanceof JSONObject parentData)) throw missingFieldException(PARENT);
var key = parentData.keySet().stream().findFirst().orElseThrow(() -> missingFieldException(PARENT));
if (!(parentData.get(key) instanceof Number id)) throw missingFieldException(key);
Location parent;
Owner owner;
switch (key){
case COMPANY:
owner = companyService().get(id.longValue());
parent = null;
break;
case USER:
owner = userService().loadUser(id.longValue());
parent = null;
break;
case LOCATION:
parent = stockDb.loadLocation(id.longValue());
owner = parent.resolve().owner().resolve();
break;
default: throw unprocessable("Unknown parent object: {0} → {1}",key,id);
};
var loc = new DbLocation(0,owner,parent == null?null:parent.id(),name,null);
return sendContent(ex,stockDb.save(loc));
}
private boolean postProperty(UmbrellaUser user, HttpExchange ex) throws IOException {
var json = json(ex);
if (!(json.get(ITEM) instanceof JSONObject itemData)) throw missingFieldException(ITEM);
if (!(itemData.get(ID) instanceof Number itemId)) throw missingFieldException(ID);
if (!(json.get("add_prop") instanceof JSONObject propData)) throw missingFieldException("add_prop");
if (!propData.has(VALUE)) throw missingFieldException(VALUE);
var value = propData.get(VALUE);
if (value == null) throw missingFieldException(VALUE);
Property property = null;
if (propData.get("existing_prop_id") instanceof Number existingPropId && existingPropId.longValue() != 0L){
property = stockDb.setProperty(itemId.longValue(),existingPropId.longValue(),value);
} else {
if (!(propData.get("new_prop") instanceof JSONObject newProp)) throw unprocessable("data must contain either add_prop.existing_prop_id or add_prop.new_prop!");
if (!(newProp.get(NAME) instanceof String name) || name.isBlank()) throw unprocessable("data.add_prop.new_prop does not contain name!");
var unit = newProp.get(UNIT) instanceof String u ? nullIfEmpty(u) : null;
property = stockDb.addNewProperty(itemId.longValue(),name,value,unit);
}
return sendContent(ex,property);
}
private Mappable toOwner(JSONObject owner) {
var keys = owner.keySet();
if (keys.size() != 1) throw unprocessable("{0} expected to have only one child!",OWNER);
String key = new ArrayList<>(keys).getFirst();
return switch (key) {
case COMPANY -> companyService().get(owner.getLong(key));
case USER -> userService().loadUser(owner.getLong(key));
default -> throw invalidFieldException(format("Single child of {0}", OWNER), format("either {0} or {1}", COMPANY, USER));
};
}
private long toOwnerId(JSONObject owner) {
var keys = owner.keySet();
if (keys.size() != 1) throw unprocessable("{0} expected to have only one child!",OWNER);
String key = new ArrayList<>(keys).getFirst();
return switch (key) {
case COMPANY -> -owner.getLong(key);
case USER -> owner.getLong(key);
default -> throw invalidFieldException(format("Single child of {0}", OWNER), format("either {0} or {1}", COMPANY, USER));
};
}
@Override
public Collection<Object> redefineMe(long company_id) {
return List.of();
}
}