package de.srsoftware.widerhall.data; import de.srsoftware.widerhall.Configuration; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.sql.Connection; import java.sql.DriverManager; import java.sql.ResultSet; import java.sql.SQLException; import java.util.*; import static de.srsoftware.widerhall.Util.t; public class Database { public static final String HASHED_PASS = "hashedPassword"; public static final String SALT = "salt"; private static final Logger LOG = LoggerFactory.getLogger(Database.class); private static Database singleton = null; private final Connection conn; public class Request{ private final StringBuilder sql; private final HashMap> where = new HashMap<>(); private final HashMap values = new HashMap<>(); private final HashMap setValues = new HashMap<>(); public Request(String sql) { this(new StringBuilder(sql)); } public Request(StringBuilder sql) { this.sql = sql; } public ResultSet exec() throws SQLException { LOG.debug("Executing {}",this); var args = new ArrayList<>(); if (!where.isEmpty()){ var clauses = new ArrayList(); sql.append(" WHERE "); for (var entry : where.entrySet()){ var arr = new String[entry.getValue().size()]; Arrays.fill(arr,"?"); var marks = String.join(", ",arr); clauses.add("("+entry.getKey()+" IN ("+marks+"))"); args.addAll(entry.getValue()); } sql.append(String.join(" AND ",clauses)); } try { var stmt = Database.this.conn.prepareStatement(sql()); if (!args.isEmpty()) { for (int i = 0; i < args.size(); i++) stmt.setObject(i+1, args.get(i)); } return stmt.executeQuery(); } catch (SQLException sqle) { throw new SQLException(t("Query '{}' failed:",sql),sqle); } } public void run() throws SQLException { LOG.debug("Running {}",this); var args = new ArrayList<>(); if (!setValues.isEmpty()){ var keys = new ArrayList(); var expressions = new ArrayList(); for (var entry : setValues.entrySet()) { expressions.add(entry.getKey()+" = ?"); args.add(entry.getValue()); } sql.append(" SET ").append(String.join(", ",expressions)); } if (!values.isEmpty()){ var keys = new ArrayList(); for (var entry : values.entrySet()) { keys.add(entry.getKey()); args.add(entry.getValue()); } sql.append("(") .append(String.join(", ",keys)) .append(")") .append(" VALUES "); var arr = new String[args.size()]; Arrays.fill(arr,"?"); var marks = String.join(", ",arr); sql.append("(").append(marks).append(")"); } if (!where.isEmpty()){ var clauses = new ArrayList(); sql.append(" WHERE "); for (var entry : where.entrySet()){ var arr = new String[entry.getValue().size()]; Arrays.fill(arr,"?"); var marks = String.join(", ",arr); clauses.add("("+entry.getKey()+" IN ("+marks+"))"); args.addAll(entry.getValue()); } sql.append(String.join(" AND ",clauses)); } try { var stmt = conn.prepareStatement(sql()); if (!args.isEmpty()) { for (int i = 0; i < args.size(); i++) stmt.setObject(i+1, args.get(i)); } stmt.execute(); } catch (SQLException sqle) { throw new SQLException(t("Query '{}' failed:",sql),sqle); } } public Request set(String key, Object value) { setValues.put(key,value); return this; } public String sql() { return sql.toString(); } @Override public String toString() { StringBuffer sql = new StringBuffer(this.sql); if (!setValues.isEmpty()){ var keys = new ArrayList(); var expressions = new ArrayList(); for (var entry : setValues.entrySet()) expressions.add(entry.getKey()+" = "+entry.getValue()); sql.append(" SET ").append(String.join(", ",expressions)); } if (!values.isEmpty()){ var keys = new ArrayList(); var vals = new ArrayList(); for (var entry : values.entrySet()) { keys.add(entry.getKey()); vals.add(entry.getValue().toString()); } sql.append("(") .append(String.join(", ",keys)) .append(")") .append(" VALUES ") .append("(") .append(String.join(",",vals)) .append(")"); } if (!where.isEmpty()){ var clauses = new ArrayList(); sql.append(" WHERE "); for (var entry : where.entrySet()) clauses.add("("+entry.getKey()+" IN ("+String.join(", ",entry.getValue().stream().map(Object::toString).toList())+"))"); sql.append(String.join(" AND ",clauses)); } return "Request{" + "sql=" + sql + '}'; } public Request values(Map newValues) { values.putAll(newValues); return this; } public Request values(String key, Object value) { values.put(key,value); return this; } public Request where(String key, Object ... values) { for (var val : values) where(key,val); return this; } public Request where(String key, Collection values) { for (var val : values) where(key,val); return this; } private Request where(String key, Object value) { var list = where.get(key); if (list == null) where.put(key,list = new ArrayList<>()); list.add(value); return this; } } public Database(Connection connection) { this.conn = connection; } private Database assertTables() throws SQLException { if (!tableExists(User.TABLE_NAME)) User.createTable(); if (!tableExists(MailingList.TABLE_NAME)) MailingList.createTable(); if (!tableExists(ListMember.TABLE_NAME)) ListMember.createTable(); return this; } public Request deleteFrom(String tableName){ return new Request(new StringBuilder("DELETE FROM ").append(tableName).append(" ")); } public Request insertInto(String tableName){ return new Request(new StringBuilder("INSERT INTO ").append(tableName).append(" ")); } public boolean isOpen() { return conn instanceof Connection; } public static Database open() { if (singleton == null){ Configuration config = Configuration.instance(); var dbFile = config.dbFile(); String url = "jdbc:sqlite:"+dbFile; LOG.debug("Opening {}",url); dbFile.getParentFile().mkdirs(); try { singleton = new Database(DriverManager.getConnection(url)); singleton.assertTables(); // must not be concatenated to exception above (assertTables accesses singleton)! } catch (SQLException sqle) { sqle.printStackTrace(); } } return singleton; } public Request query(StringBuilder sql) { return new Request(sql); } public Request select(String tableName,String ... fields) { StringBuilder sql = new StringBuilder("SELECT "); if (fields == null || fields.length == 0){ sql.append("*"); } else { sql.append(String.join(", ",fields)); } return new Request(sql.append(" FROM ").append(tableName)); } public boolean tableExists(String tbName) throws SQLException { try { var sql = new StringBuilder("SELECT EXISTS (SELECT name FROM sqlite_schema WHERE type='table' AND name='") .append(tbName) .append("')"); ResultSet rs = query( sql).exec(); int val = 0; if (rs.next()) val = rs.getInt(1); rs.close(); return val > 0; } catch (SQLException e) { throw new SQLException(t("Was not able to check existence of table {}!",tbName),e); } } public Request update(String tableName) { return new Request(new StringBuilder("UPDATE ").append(tableName)); } public static String xor(Object a, Object b){ // https://stackoverflow.com/a/16443025/1285585 return "(~("+a+"&"+b+"))&("+a+"|"+b+")"; } }