Java-basierte Mailinglisten-Anwendung, die auf IMAP+SMTP aufsetzt, und damit (fast) jede Mailbox in eine Mailingliste verwandeln kann.
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.
 
 
 
 

413 lines
14 KiB

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;
import static de.srsoftware.widerhall.data.MailingList.HOLD_TIME;
/**
* @author Stephan Richter, 2022
* This class abstracts away all needed database operations
*/
public class Database {
private static final Logger LOG = LoggerFactory.getLogger(Database.class);
private static Database singleton = null; // we only need one db handle ever. This will be it.
private final Connection conn; // the actual db connection handle within the singleton
/**
* This class encapsulates a compiled request.
* A compiled request is a ready-to execute sql string paired with a list of parameters
*/
public class CompiledRequest{
private final String sql;
private final List<Object> args;
/**
* create new instance
* @param sql final sql, ready to be executed
* @param args arguments for the execution of the query
*/
public CompiledRequest(String sql, List<Object> args){
this.sql = sql;
this.args = args;
}
/**
* build the final SQL and execute the actual request (for SELECT statements)
* @return the result set produced by the SELECT statement
* @throws SQLException
*/
public ResultSet exec() throws SQLException {
LOG.debug("Führe {} aus",this);
try {
var stmt = conn.prepareStatement(sql);
for (int i = 0; i < args.size(); i++) stmt.setObject(i+1, args.get(i));
return stmt.executeQuery();
} catch (SQLException sqle) { // add sql to the exception
throw new SQLException(t("Query '{}' fehlgeschlagen:",this),sqle);
}
}
/**
* build the final SQL and execure the actual request (for statements that generate no result)
* @throws SQLException
*/
public void run() throws SQLException {
LOG.debug("Führe {} aus",this);
try {
var stmt = conn.prepareStatement(sql);
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 '{}' fehlgeschlagen:",this),sqle);
}
}
@Override
public String toString() {
var filled = sql;
int pos = filled.indexOf('?');
int idx = 0;
while (pos>0){
var fill = args.get(idx++);
var s = fill == null ? "NULL" : (fill instanceof Number ? ""+fill : "'"+fill+"'");
filled = filled.substring(0,pos)+s+filled.substring(pos+1);
pos = filled.indexOf('?',pos+s.length());
}
return filled;
}
}
/**
* helper class to allow daisy-chaining request modifiers
*/
public class Request{
private final StringBuilder sql; // buffer the sql to be built
private final HashMap<String, List<Object>> where = new HashMap<>(); // buffer condition statements for select
private final HashMap<String,Object> values = new HashMap<>(); // buffer values for insert/update statements
private final List<String> sortFields = new ArrayList<>();
/**
* Start to create a new request with the initial SQL
* @param sql initial sql for the request
*/
public Request(StringBuilder sql) {
this.sql = sql;
}
/**
* conditions are assembled in the following form:
* SELECT … FROM … WHERE [key] in ([values])
* <values> are build as list of question marks, whose arguments are applied later
* @param args
*/
private void applyConditions(ArrayList<Object> args){
if (!where.isEmpty()){
var clauses = new ArrayList<String>();
sql.append(" WHERE ");
for (var entry : where.entrySet()){
{ // build the list of question marks
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()); // will be applied to the prepared statement later
}
sql.append(String.join(" AND ",clauses));
}
}
private void applySorting(){
if (!sortFields.isEmpty()) sql.append(" ORDER BY ").append(String.join(", ",sortFields));
}
@Override
protected Request clone() {
Request clone = new Request(new StringBuilder(sql));
clone.where.putAll(where);
clone.values.putAll(values);
return clone;
}
/**
* finalize sql, save sql and arguments as compiled request
* @return
*/
public CompiledRequest compile(Object ...additionalArgs){
var args = new ArrayList<>();
applyValues(args);
applyConditions(args);
applySorting();
if (additionalArgs != null) {
for (Object arg : additionalArgs) args.add(arg);
}
return new CompiledRequest(sql.toString(),args);
}
/**
* apply values (for insert or update statements)
* @param args
*/
private void applyValues(ArrayList<Object> args){
if (values.isEmpty()) return;
var currentSql = sql();
if (currentSql.startsWith("INSERT")){
var keys = new ArrayList<String>();
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 (currentSql.startsWith("UPDATE")){
var expressions = new ArrayList<String>();
for (var entry : values.entrySet()) {
expressions.add(entry.getKey()+" = ?");
args.add(entry.getValue());
}
sql.append(" SET ").append(String.join(", ",expressions));
}
}
public void run() throws SQLException {
compile().run();
}
/**
* set single value for a certain key
* @param key
* @param value
* @return
*/
public Request set(String key, Object value) {
values.put(key,value);
return this;
}
public Request sort(String field) {
sortFields.add(field);
return this;
}
/**
* get the current (i.e. non-final) sql
* @return
*/
public String sql() {
return sql.toString();
}
@Override
public String toString() {
return "Request("+clone().compile()+')';
}
/**
* apply a map of values (for insert/update statements)
* @param newValues
* @return
*/
public Request values(Map<String,Object> newValues) {
values.putAll(newValues);
return this;
}
/**
* add a where condition in the form of … WHERE [key] in ([values])
* @param key
* @param values
* @return
*/
public Request where(String key, Object ... values) {
for (var val : values) where(key,val);
return this;
}
/**
* add a where condition in the form of … WHERE [key] in ([values])
* @param key
* @param values
* @return
*/
public Request where(String key, Collection<? extends Object> values) {
for (var val : values) where(key,val);
return this;
}
/**
* add a where condition in the form of … WHERE [key] in ([value])
* @param key
* @param value
* @return
*/
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;
}
/**
* take care of table creation, if required tables do not exist
* @return
* @throws SQLException
*/
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();
if (!tableExists(Post.TABLE_NAME)) Post.createTable();
if (!columnExists(MailingList.TABLE_NAME,HOLD_TIME)) MailingList.createHoldTimeColumn();
return this;
}
private boolean columnExists(String tableName, String columnName) throws SQLException {
var rs = Database.open().select("pragma_table_info('"+tableName+"')","COUNT(*) AS num").where("name",columnName).compile().exec();
try {
if (rs.next()) return rs.getInt("num") > 0;
} finally {
rs.close();
}
return false;
}
public void createColumn(String tableName, String colName, String...typeArgs) throws SQLException {
var sql = new StringBuilder("ALTER TABLE ")
.append(tableName)
.append(" ADD COLUMN ")
.append(colName)
.append(" ")
.append(String.join(" ",typeArgs));
new Request(sql).compile().run();
}
/**
* prepare a deletion statement
* @param tableName
* @return
*/
public Request deleteFrom(String tableName){
return new Request(new StringBuilder("DELETE FROM ").append(tableName).append(" "));
}
/**
* prepare an insertion statement
* @param tableName
* @return
*/
public Request insertInto(String tableName){
return new Request(new StringBuilder("INSERT INTO ").append(tableName).append(" "));
}
/**
* check if we have an active db connection
* @return
*/
public boolean isOpen() {
return conn instanceof Connection;
}
/**
* Open the database, if it is not open.
* Will create the singleton on first call.
* Consecutive calls will just return the singleton.
* @return
*/
public static Database open() {
if (singleton == null){
Configuration config = Configuration.instance();
var dbFile = config.dbFile();
String url = "jdbc:sqlite:"+dbFile;
LOG.debug("Öffne {}",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;
}
/**
* Create a query from a pre-set StringBuilder
* @param sql
* @return
*/
public Request query(StringBuilder sql) {
return new Request(sql);
}
/**
* create a SELECT [flields] FROM [table] request.
* If no fields are supplied, a request in the form SELECT * FROM [table] will be generated.
* @param tableName
* @param fields
* @return
*/
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));
}
/**
* check, whether a table with the provided name exists
* @param tbName
* @return
* @throws SQLException
*/
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).compile().exec();
int val = 0;
if (rs.next()) val = rs.getInt(1);
rs.close();
return val > 0;
} catch (SQLException e) {
throw new SQLException(t("Konnte Existenz der Tabelle {} nicht prüfen!",tbName),e);
}
}
/**
* create an UPDATE [tableName] query
* @param tableName
* @return
*/
public Request update(String tableName) {
return new Request(new StringBuilder("UPDATE ").append(tableName));
}
}