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.
 
 
 
 
 

560 lines
19 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.tools.jdbc.Condition.equal;
import static de.srsoftware.tools.jdbc.Condition.isNull;
import static de.srsoftware.tools.jdbc.Query.*;
import static de.srsoftware.tools.jdbc.Query.SelectQuery.ALL;
import static de.srsoftware.umbrella.core.Constants.*;
import static de.srsoftware.umbrella.core.ModuleRegistry.noteService;
import static de.srsoftware.umbrella.core.exceptions.UmbrellaException.databaseException;
import static de.srsoftware.umbrella.stock.Constants.*;
import static java.lang.System.Logger.Level.ERROR;
import static java.lang.System.Logger.Level.WARNING;
import static java.text.MessageFormat.format;
import de.srsoftware.tools.jdbc.Query;
import de.srsoftware.umbrella.core.BaseDb;
import de.srsoftware.umbrella.core.api.Owner;
import de.srsoftware.umbrella.core.model.*;
import de.srsoftware.umbrella.core.model.Location;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.*;
public class SqliteDb extends BaseDb implements StockDb {
private record LegacyLocation(String id, String parent, String name, String description){
public static LegacyLocation of(ResultSet rs) throws SQLException {
return new LegacyLocation(rs.getString(ID), rs.getString(LOCATION_ID), rs.getString(NAME), rs.getString(DESCRIPTION));
}
public String owner() {
var parts = id.split(":");
if (parts.length != 3) throw databaseException("Expected legacy location id to be of the form ss:dd:ss, encountered {0}!",id);
return String.join(":", parts[0], parts[1]);
}
};
public SqliteDb(Connection connection) {
super(connection);
}
@Override
public Property addNewProperty(long itemId, String name, Object value, String unit) {
try {
db.setAutoCommit(false);
var rs = insertInto(TABLE_PROPERTIES,NAME,TYPE,UNIT).values(name,0,unit).execute(db).getGeneratedKeys();
Long propertyId = null;
if (rs.next()) propertyId = rs.getLong(1);
rs.close();
if (propertyId == null || propertyId == 0) throw databaseException("Failed to create new property {0} to DB",name);
insertInto(TABLE_ITEM_PROPERTIES,ITEM_ID,PROPERTY_ID,VALUE).values(itemId,propertyId,value).execute(db);
return new Property(propertyId,name,value,unit);
} catch (SQLException e) {
throw databaseException("Failed to create new property {0} to DB",name);
}
}
@Override
public Location delete(DbLocation location) {
try {
Query.delete().from(TABLE_LOCATIONS).where(ID,equal(location.id())).execute(db);
return location;
} catch (SQLException e){
throw databaseException("Failed to delete \"{0}\"",location.name());
}
}
/**
* id, owner, owner_id, code, name, location_id
* @throws SQLException
*/
private void createIntermediateItemsTable() throws SQLException { // create intermediate table
var sql = """
CREATE TABLE IF NOT EXISTS items_temp (
{0} INTEGER PRIMARY KEY,
{1} VARCHAR(50) NOT NULL,
{2} LONG NOT NULL,
{3} VARCHAR(255),
{4} VARCHAR(255) NOT NULL,
{5} LONG NOT NULL)""";
sql = format(sql, ID, OWNER, OWNER_NUMBER, CODE, NAME, LOCATION_ID);
db.prepareStatement(sql).execute();
}
/**
* id, owner, parent_location_id, name, description
* @throws SQLException
*/
private void createIntermediateLocationTable() throws SQLException { // create intermediate table
var sql = """
CREATE TABLE IF NOT EXISTS locations_temp (
{0} INTEGER PRIMARY KEY,
{1} VARCHAR(50) NOT NULL,
{2} LONG DEFAULT NULL,
{3} VARCHAR(255) NOT NULL,
{4} TEXT)""";
sql = format(sql, ID, OWNER, PARENT_LOCATION_ID, NAME, DESCRIPTION);
db.prepareStatement(sql).execute();
}
/**
* item_id, prop_id , value
* @throws SQLException
*/
private void createIntermediatePropsTable() throws SQLException { // create intermediate table
var sql = "CREATE TABLE IF NOT EXISTS item_props_temp ( {0} LONG NOT NULL, {1} LONG NOT NULL, {2} LONG NOT NULL, PRIMARY KEY({0}, {1}))";
sql = format(sql, ITEM_ID, PROPERTY_ID, VALUE);
db.prepareStatement(sql).execute();
}
private void createItemsTable() {
try {
var sql = "CREATE TABLE IF NOT EXISTS {0} ( {1} VARCHAR(255) PRIMARY KEY, {2} VARCHAR(255) NOT NULL, {3} TEXT, {4} VARCHAR(255))";
sql = format(sql, TABLE_ITEMS, ID, CODE, NAME, LOCATION_ID);
db.prepareStatement(sql).execute();
} catch (SQLException e) {
throw databaseException(ERROR_FAILED_CREATE_TABLE,TABLE_ITEMS);
}
}
private void createItemPropsTable() {
try {
var sql = "CREATE TABLE IF NOT EXISTS {0} ( {1} INT NOT NULL, {2} INT NOT NULL, {3} VARCHAR(255) NOT NULL, PRIMARY KEY({1}, {2}))";
sql = format(sql, TABLE_ITEM_PROPERTIES, ITEM_ID, PROPERTY_ID,VALUE);
db.prepareStatement(sql).execute();
} catch (SQLException e) {
throw databaseException(ERROR_FAILED_CREATE_TABLE,TABLE_ITEM_PROPERTIES);
}
}
private void createLocationsTable() {
try {
var sql = "CREATE TABLE IF NOT EXISTS {0} ( {1} VARCHAR(255) PRIMARY KEY, {2} VARCHAR(255) DEFAULT NULL, {3} VARCHAR(255) NOT NULL, {4} TEXT)";
sql = format(sql, TABLE_LOCATIONS, ID, LOCATION_ID, NAME, DESCRIPTION);
db.prepareStatement(sql).execute();
} catch (SQLException e) {
throw databaseException(ERROR_FAILED_CREATE_TABLE,TABLE_LOCATIONS);
}
}
private void createPropertiesTable() {
try {
var sql = "CREATE TABLE IF NOT EXISTS {0} ( {1} LONG PRIMARY KEY, {2} VARCHAR(255) NOT NULL, {3} INT NOT NULL, {4} VARCHAR(255))";
sql = format(sql, TABLE_PROPERTIES, ID, NAME, TYPE, UNIT);
db.prepareStatement(sql).execute();
} catch (SQLException e) {
throw databaseException(ERROR_FAILED_CREATE_TABLE,TABLE_PROPERTIES);
}
}
@Override
protected int createTables() {
int currentVersion = createSettingsTable();
switch (currentVersion){
case 0:
createLocationsTable();
createItemsTable();
createPropertiesTable();
createItemPropsTable();
case 1:
dropTokenTable();
case 2:
transformTables();
}
return setCurrentVersion(3);
}
private void dropTokenTable() {
try {
db.prepareStatement("DROP TABLE IF EXISTS tokens").execute();
} catch (SQLException e) {
throw databaseException("Failed to drop table tokens!");
}
}
@Override
public Collection<DbLocation> listChildLocations(long parentId) {
try {
var rs = select(ALL).from(TABLE_LOCATIONS).where(PARENT_LOCATION_ID,equal(parentId)).exec(db);
var list = new ArrayList<DbLocation>();
while (rs.next()) list.add(DbLocation.of(rs));
rs.close();
return list;
} catch (SQLException e){
throw databaseException("Failed to load child locations for {0}",parentId);
}
}
@Override
public Collection<DbLocation> listCompanyLocations(Company company) {
try {
var rs = select(ALL).from(TABLE_LOCATIONS).where(OWNER,equal(company.dbCode())).where(PARENT_LOCATION_ID,isNull()).exec(db);
var list = new ArrayList<DbLocation>();
while (rs.next()) list.add(DbLocation.of(rs));
rs.close();
return list;
} catch (SQLException e){
throw databaseException("Failed to load locations for user {0}",company.name());
}
}
@Override
public Collection<Item> listItemsAt(Location location) {
try {
var rs = select(ALL).from(TABLE_ITEMS).where(LOCATION_ID,equal(location.id())).exec(db);
var list = new ArrayList<Item>();
while (rs.next()) list.add(Item.of(rs));
rs.close();
for (var item : list) loadProperties(item);
return list;
} catch (SQLException e){
throw databaseException("Failed to load items at {0}",location);
}
}
@Override
public Collection<Item> listItemsOf(Company company) {
try {
var owner = company.dbCode();
var rs = select(ALL).from(TABLE_ITEMS).where(OWNER,equal(owner)).exec(db);
var list = new ArrayList<Item>();
while (rs.next()) list.add(Item.of(rs));
rs.close();
for (var item : list) loadProperties(item);
return list;
} catch (SQLException e){
throw databaseException("Failed to load items of {0}",company);
}
}
@Override
public Item loadProperties(Item item){
try {
var rs = select(ALL).from(TABLE_ITEM_PROPERTIES).leftJoin(PROPERTY_ID, TABLE_PROPERTIES, ID).where(ITEM_ID, equal(item.id())).exec(db);
while (rs.next()) item.properties().add(Property.of(rs));
rs.close();
return item;
} catch (SQLException e){
throw databaseException("Failed to load properties of {0}",item.name());
}
}
@Override
public Item loadItem(long id) {
var query = select(ALL).from(TABLE_ITEMS).where(ID,equal(id));
return loadItem(query);
}
@Override
public Item loadItem(String owner, long itemNumber) {
var query = select(ALL).from(TABLE_ITEMS).where(OWNER,equal(owner)).where(OWNER_NUMBER,equal(itemNumber));
return loadItem(query);
}
private Item loadItem(SelectQuery query){
try {
var rs = query.exec(db);
Item result = null;
if (rs.next()) result = Item.of(rs);
rs.close();
if (result != null) return result;
} catch (SQLException ignored) {
}
throw databaseException("Failed to load item");
}
public DbLocation loadLocation(long locationId) {
try {
var rs = select(ALL).from(TABLE_LOCATIONS).where(ID,equal(locationId)).exec(db);
DbLocation loc = null;
if (rs.next()) loc = DbLocation.of(rs);
rs.close();
if (loc != null) return loc;
throw databaseException("Failed to load location with id = {0}",locationId);
} catch (SQLException e){
throw databaseException("Failed to load location with id = {0}",locationId);
}
}
@Override
public Collection<Property> listProperties() {
try {
var rs = select(ALL).from(TABLE_PROPERTIES).exec(db);
var list = new ArrayList<Property>();
while (rs.next()) list.add(Property.of(rs));
rs.close();
return list;
} catch (SQLException e){
throw databaseException("Failed to load properties!");
}
}
@Override
public Collection<DbLocation> listUserLocations(UmbrellaUser user) {
try {
var rs = select(ALL).from(TABLE_LOCATIONS).where(OWNER,equal(user.dbCode())).where(PARENT_LOCATION_ID,isNull()).exec(db);
var list = new ArrayList<DbLocation>();
while (rs.next()) list.add(DbLocation.of(rs));
rs.close();
return list;
} catch (SQLException e){
throw databaseException("Failed to load locations for user {0}",user.name());
}
}
@Override
public long nextItemNumberFor(Owner owner) {
try {
var rs = select("max(owner_number)").from(TABLE_ITEMS).where(OWNER,equal(owner.dbCode())).exec(db);
long number = rs.next() ? rs.getLong(1) : 0;
rs.close();
return number +1L;
} catch (SQLException e) {
throw databaseException("Failed to read last item number for {0}",owner);
}
}
@Override
public Map<String, Object> pathToLocation(Location target) {
var root = new HashMap<String,Object>();
var location = loadLocation(target.id());
root.put(NAME,location.name());
root.put(ID,location.id());
try {
while (!is0(location.parent())) {
var rs = select(ALL).from(TABLE_LOCATIONS).where(ID, equal(location.parent())).exec(db);
var parent = DbLocation.of(rs);
rs.close();
var current = new HashMap<String,Object>();
current.put(NAME,parent.name());
current.put(ID,parent.id());
current.put(LOCATIONS,List.of(root));
root = current;
location = parent;
}
} catch (SQLException e){
throw databaseException("Failed to load path to location {0}",target);
}
return root;
}
private void replaceItemsTable() throws SQLException {
db.prepareStatement(format("DROP TABLE {0}",TABLE_ITEMS)).execute();
db.prepareStatement(format("ALTER TABLE {0} RENAME TO {1}","items_temp",TABLE_ITEMS)).execute();
}
private void replaceItemPropsTable() throws SQLException {
db.prepareStatement(format("DROP TABLE {0}",TABLE_ITEM_PROPERTIES)).execute();
db.prepareStatement(format("ALTER TABLE {0} RENAME TO {1}","item_props_temp",TABLE_ITEM_PROPERTIES)).execute();
}
private void replaceLocationsTable() throws SQLException {
db.prepareStatement(format("DROP TABLE {0}",TABLE_LOCATIONS)).execute();
db.prepareStatement(format("ALTER TABLE {0} RENAME TO {1}","locations_temp",TABLE_LOCATIONS)).execute();
}
@Override
public DbLocation save(DbLocation location) {
var parentId = is0(location.parent()) ? null : location.parent();
if (location.id() == 0) { // new location
try {
var rs = insertInto(TABLE_LOCATIONS,OWNER,PARENT_LOCATION_ID,NAME,DESCRIPTION)
.values(location.owner().dbCode(),parentId,location.name(),location.description())
.execute(db).getGeneratedKeys();
long id = 0;
if (rs.next()) id = rs.getLong(1);
rs.close();
if (id == 0) throw databaseException("Failed to save new location ({0})",location.name());
return location.id(id);
} catch (SQLException e){
throw databaseException("Failed to save new location ({0})",location.name());
}
} else {
try {
update(TABLE_LOCATIONS)
.set(OWNER, PARENT_LOCATION_ID, NAME, DESCRIPTION)
.where(ID,equal(location.id()))
.prepare(db)
.apply(location.owner().dbCode(), parentId, location.name(), location.description())
.close();
return location.clear();
} catch (SQLException e){
throw databaseException("Updating location \"{0}\" not implemented",location.name());
}
}
}
@Override
public Item save(Item item) {
if (item.id() == 0){
try {
var rs = insertInto(TABLE_ITEMS, OWNER, OWNER_NUMBER, CODE, NAME, LOCATION_ID)
.values(item.owner().dbCode(), item.ownerNumber(), item.code(), item.name(), item.location().id())
.execute(db).getGeneratedKeys();
if (rs.next()) item.id(rs.getLong(1));
rs.close();
return item;
} catch (SQLException e) {
throw databaseException("Failed to save new item to database!");
}
} else if (item.isDirty()) {
try {
var location = item.location();
var query = update(TABLE_ITEMS).where(ID, equal(item.id()));
if (location == null) {
query.set(CODE,NAME);
} else {
query.set(CODE,NAME,LOCATION_ID);
}
var pq = query.prepare(db);
if (location == null) {
pq.apply(item.code(),item.name());
} else {
pq.apply(item.code(),item.name(),item.location().id());
}
return item.clear();
} catch (SQLException e){
throw databaseException("Failed to update item {0}",item.name());
}
}
return item;
}
@Override
public Property setProperty(long itemId, long existingPropId, Object value) {
try {
Property prop = null;
var rs = select(ALL).from(TABLE_PROPERTIES).where(ID,equal(existingPropId)).exec(db);
if (rs.next()) prop = Property.of(rs);
rs.close();
if (prop == null) throw databaseException("Failed to add new property to item {0}",itemId);
if ("".equals(value)){
Query.delete().from(TABLE_ITEM_PROPERTIES).where(ITEM_ID,equal(itemId)).where(PROPERTY_ID,equal(existingPropId)).execute(db);
} else {
replaceInto(TABLE_ITEM_PROPERTIES,ITEM_ID,PROPERTY_ID,VALUE).values(itemId,existingPropId,value).execute(db);
}
return prop.value(value);
} catch (SQLException e) {
throw databaseException("Failed to add new property to item {0}",itemId);
}
}
private HashMap<String, Long> transformItems(Map<String, Long> oldLocationIdsToNew) throws SQLException {
var rs = select(ALL).from(TABLE_ITEMS).exec(db);
var insert = insertInto("items_temp",OWNER, OWNER_NUMBER, LOCATION_ID, CODE, NAME);
var oldToNew = new HashMap<String,Long>(); // maps from old item ids to new ones
while (rs.next()){
var oldId = rs.getString(ID);
var parts = oldId.split(":");
if (parts.length != 3) throw databaseException("Expected old item id to be of the form ss:dd:dd, encountered {0}!",oldId);
var owner = String.join(":",parts[0], parts[1]);
long ownerNumber;
try {
ownerNumber = Long.parseLong(parts[2]);
} catch (NumberFormatException e) {
throw databaseException("Expected old item id to be of the form ss:dd:dd, encountered {0}!",oldId);
}
var oldLocationId = rs.getString(LOCATION_ID);
var locationId = oldLocationIdsToNew.get(oldLocationId);
if (locationId == null) throw databaseException("Item {0} of {1} {2} refers to location {3}, which is unknown!",oldId,parts[0],parts[1],oldLocationId);
var rs2 = insert.values(owner, ownerNumber, locationId, rs.getString(CODE), rs.getString(NAME)).execute(db).getGeneratedKeys();
oldToNew.put(oldId,rs2.getLong(1));
rs2.close();
}
rs.close();
insert.execute(db);
return oldToNew;
}
private Map<String, Long> transformLocations() throws SQLException {
var locations = new ArrayList<LegacyLocation>();
var oldToNew = new HashMap<String,Long>();
var rs = select(ALL).from(TABLE_LOCATIONS).exec(db);
while (rs.next()) locations.add(LegacyLocation.of(rs));
rs.close();
var query = insertInto("locations_temp", OWNER, PARENT_LOCATION_ID, NAME, DESCRIPTION);
while (!locations.isEmpty()){
var legacyLocation = locations.removeFirst();
var parentRef = nullIfEmpty(legacyLocation.parent());
Long parentId = null;
if (parentRef != null) {
parentId = oldToNew.get(parentRef);
if (parentId == null) { // parent not processed, re-add to end of queue
LOG.log(WARNING,"Postponing {0}, as {1} is not present…",legacyLocation.id,legacyLocation.parent);
locations.add(legacyLocation);
continue;
}
}
var owner = legacyLocation.owner();
rs = query.values(owner, parentId, legacyLocation.name, legacyLocation.description).execute(db).getGeneratedKeys();
oldToNew.put(legacyLocation.id(),rs.getLong(1));
rs.close();
}
return oldToNew;
}
private void transformProperties(HashMap<String, Long> oldItemIdsToNew) throws SQLException {
var rs = select(ALL).from(TABLE_ITEM_PROPERTIES).exec(db);
var insert = insertInto("item_props_temp",ITEM_ID, PROPERTY_ID, VALUE);
while (rs.next()){
var oldItemId = rs.getString(ITEM_ID);
var itemId = oldItemIdsToNew.get(oldItemId);
if (itemId == null) {
throw databaseException("Old item id ({0}) has no new counterpart!",oldItemId);
}
insert.values(itemId, rs.getLong(PROPERTY_ID), rs.getString(VALUE));
}
rs.close();
insert.execute(db).close();
}
private void transformTables(){
try {
db.setAutoCommit(false);
createIntermediateLocationTable();
createIntermediateItemsTable();
createIntermediatePropsTable();
var oldLocationIdsToNew = transformLocations();
var oldItemIdsToNew = transformItems(oldLocationIdsToNew);
updateNotes(oldItemIdsToNew);
transformProperties(oldItemIdsToNew);
replaceLocationsTable();
replaceItemsTable();
replaceItemPropsTable();
db.setAutoCommit(true);
} catch (Exception e) {
try {
db.rollback();
} catch (SQLException ignored) {
}
LOG.log(ERROR,"Failed to transform {0} table!",TABLE_LOCATIONS,e);
throw databaseException("Failed to transform {0} table!",TABLE_LOCATIONS);
}
}
private void updateNotes(HashMap<String, Long> oldItemIdsToNew) {
var noteService = noteService();
Map<Long, Note> notes = noteService.getNotes("stock", null);
for (var entry : notes.entrySet()){
var note = entry.getValue();
var oldEntityId = note.entityId();
var newEntityId = oldItemIdsToNew.get(oldEntityId);
if (newEntityId != null){
noteService.save(note.entityId(""+newEntityId));
}
}
}
}