diff --git a/pom.xml b/pom.xml
index 58ac528..892daa3 100644
--- a/pom.xml
+++ b/pom.xml
@@ -6,7 +6,7 @@
org.example
Widerhall
- 0.0.3
+ 0.0.4
diff --git a/src/main/java/de/srsoftware/widerhall/data/Database.java b/src/main/java/de/srsoftware/widerhall/data/Database.java
index b653b4a..b4e1b3a 100644
--- a/src/main/java/de/srsoftware/widerhall/data/Database.java
+++ b/src/main/java/de/srsoftware/widerhall/data/Database.java
@@ -12,72 +12,130 @@ import java.util.*;
import static de.srsoftware.widerhall.Util.t;
+/**
+ * @author Stephan Richter, 2022
+ * This class abstracts away all needed database operations
+ */
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;
+ 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
- public class Request{
+ public class CompiledRequest{
+ private final String sql;
+ private final List args;
+
+ public CompiledRequest(String sql, List 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("Executing {}",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 '{}' failed:",this),sqle);
+ }
+ }
- private final StringBuilder sql;
- private final HashMap> where = new HashMap<>();
- private final HashMap values = new HashMap<>();
- private final HashMap setValues = new HashMap<>();
+ /**
+ * build the final SQL and execure the actual request (for statements that generate no result)
+ * @throws SQLException
+ */
+ public void run() throws SQLException {
+ LOG.debug("Running {}",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 '{}' failed:",this),sqle);
+ }
+ }
- public Request(String sql) {
- this(new StringBuilder(sql));
+ @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> where = new HashMap<>(); // buffer condition statements for select
+ private final HashMap values = new HashMap<>(); // buffer values for insert/update statements
+
+ /**
+ * Start to create a new request with the initial SQL
+ * @param sql initial sql for the request
+ */
public Request(StringBuilder sql) {
this.sql = sql;
}
- public ResultSet exec() throws SQLException {
- LOG.debug("Executing {}",this);
- var args = new ArrayList<>();
+ /**
+ * conditions are assembled in the following form:
+ * SELECT … FROM … WHERE in ()
+ * are build as list of question marks, whose arguments are applied later
+ * @param args
+ */
+ private void applyConditions(ArrayList args){
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());
+ { // 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));
-
- }
- 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<>();
+ @Override
+ protected Request clone() {
+ Request clone = new Request(sql);
+ clone.where.putAll(where);
+ clone.values.putAll(values);
+ return clone;
+ }
- 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));
- }
+ public CompiledRequest compile(){
+ var args = new ArrayList<>();
+ applyValues(args);
+ applyConditions(args);
+ return new CompiledRequest(sql.toString(),args);
+ }
- if (!values.isEmpty()){
+ private void applyValues(ArrayList args){
+ if (values.isEmpty()) return;
+ var currentSql = sql();
+ if (currentSql.startsWith("INSERT")){
var keys = new ArrayList();
for (var entry : values.entrySet()) {
keys.add(entry.getKey());
@@ -92,34 +150,18 @@ public class Database {
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));
+ if (currentSql.startsWith("UPDATE")){
+ var expressions = new ArrayList();
+ for (var entry : values.entrySet()) {
+ expressions.add(entry.getKey()+" = ?");
+ args.add(entry.getValue());
}
- stmt.execute();
- } catch (SQLException sqle) {
- throw new SQLException(t("Query '{}' failed:",sql),sqle);
+ sql.append(" SET ").append(String.join(", ",expressions));
}
}
public Request set(String key, Object value) {
- setValues.put(key,value);
+ values.put(key,value);
return this;
}
@@ -129,40 +171,7 @@ public class Database {
@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 + '}';
+ return "Request("+clone().compile()+')';
}
public Request values(Map newValues) {
@@ -170,11 +179,6 @@ public class Database {
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;
@@ -254,7 +258,7 @@ public class Database {
var sql = new StringBuilder("SELECT EXISTS (SELECT name FROM sqlite_schema WHERE type='table' AND name='")
.append(tbName)
.append("')");
- ResultSet rs = query( sql).exec();
+ ResultSet rs = query(sql).compile().exec();
int val = 0;
if (rs.next()) val = rs.getInt(1);
rs.close();
diff --git a/src/main/java/de/srsoftware/widerhall/data/ListMember.java b/src/main/java/de/srsoftware/widerhall/data/ListMember.java
index 908eb8f..b0206fa 100644
--- a/src/main/java/de/srsoftware/widerhall/data/ListMember.java
+++ b/src/main/java/de/srsoftware/widerhall/data/ListMember.java
@@ -32,7 +32,7 @@ public class ListMember {
}
public static User confirm(String token) throws SQLException {
- var rs = Database.open().select(TABLE_NAME).where(TOKEN,token).exec();
+ var rs = Database.open().select(TABLE_NAME).where(TOKEN,token).compile().exec();
if (rs.next()){
var lm = new ListMember(rs.getString(LIST_EMAIL),rs.getString(USER_EMAIL),rs.getInt(STATE),rs.getString(TOKEN));
rs.close();
@@ -45,6 +45,7 @@ public class ListMember {
.set(STATE, newState) //drop confirmation state, set subscriber state
.where(LIST_EMAIL,lm.listEmail)
.where(USER_EMAIL,lm.userEmail)
+ .compile()
.run();
}
return user;
@@ -70,7 +71,7 @@ public class ListMember {
.append(STATE).append(" ").append(INT).append(", ")
.append(TOKEN).append(" ").append(VARCHAR).append(", ")
.append("PRIMARY KEY (").append(LIST_EMAIL).append(", ").append(USER_EMAIL).append("));");
- Database.open().query(sql).run();
+ Database.open().query(sql).compile().run();
}
public boolean hasState(int testState) {
@@ -83,7 +84,7 @@ public class ListMember {
try {
var request = Database.open().select(TABLE_NAME, LIST_EMAIL, STATE+" & "+STATE_OWNER+" as "+STATE);
if (!user.hashPermission(User.PERMISSION_ADMIN)) request = request.where(USER_EMAIL, user.email()).where(STATE, STATE_OWNER);
- var rs = request.exec();
+ var rs = request.compile().exec();
while (rs.next()) list.add(rs.getString(LIST_EMAIL));
} catch (SQLException e) {
LOG.warn("Listing memberships lists failed: ",e);
@@ -97,6 +98,7 @@ public class ListMember {
.select(TABLE_NAME)
.where(LIST_EMAIL,list.email())
.where(USER_EMAIL,user.email())
+ .compile()
.exec();
try {
if (rs.next()) {
@@ -115,6 +117,7 @@ public class ListMember {
var rs = Database.open()
.select(TABLE_NAME)
.where(LIST_EMAIL,listEmail)
+ .compile()
.exec();
var temp = new HashMap();
while (rs.next()) temp.put(rs.getString(USER_EMAIL),rs.getInt(STATE));
@@ -139,7 +142,7 @@ public class ListMember {
vals.put(USER_EMAIL,userEmail);
vals.put(STATE,state);
if (token != null) vals.put(TOKEN,token);
- Database.open().insertInto(TABLE_NAME).values(vals).run();
+ Database.open().insertInto(TABLE_NAME).values(vals).compile().run();
return this;
}
@@ -152,6 +155,7 @@ public class ListMember {
var rs = db.select(TABLE_NAME)
.where(LIST_EMAIL,list.email())
.where(USER_EMAIL,user.email())
+ .compile()
.exec();
while (rs.next()){
int state = Util.unset(rs.getInt(STATE),STATE_SUBSCRIBER,STATE_AWAITING_CONFIRMATION);
@@ -159,12 +163,14 @@ public class ListMember {
db.deleteFrom(TABLE_NAME)
.where(LIST_EMAIL,list.email())
.where(USER_EMAIL,user.email())
+ .compile()
.run();
} else { // update entry: whitdraw subscription
db.update(TABLE_NAME)
.set(STATE,state)
.where(LIST_EMAIL,list.email())
.where(USER_EMAIL,user.email())
+ .compile()
.run();
}
}
diff --git a/src/main/java/de/srsoftware/widerhall/data/MailingList.java b/src/main/java/de/srsoftware/widerhall/data/MailingList.java
index a470a96..ad16927 100644
--- a/src/main/java/de/srsoftware/widerhall/data/MailingList.java
+++ b/src/main/java/de/srsoftware/widerhall/data/MailingList.java
@@ -69,7 +69,7 @@ public class MailingList {
.append(SMTP_PASS).append(" ").append(VARCHAR).append(", ")
.append(STATE).append(" ").append(INT)
.append(");");
- Database.open().query(sql).run();
+ Database.open().query(sql).compile().run();
}
public String email() {
@@ -79,12 +79,12 @@ public class MailingList {
public void enable(boolean enable) throws SQLException {
state = enable ? state | STATE_ENABLED : state ^ (state & STATE_ENABLED);
- Database.open().update(TABLE_NAME).set(STATE,state).where(EMAIL, email()).run();
+ Database.open().update(TABLE_NAME).set(STATE,state).where(EMAIL, email()).compile().run();
}
public void hide(boolean hide) throws SQLException {
state = hide ? state ^ (state & STATE_PUBLIC) : state | STATE_PUBLIC;
- Database.open().update(TABLE_NAME).set(STATE,state).where(EMAIL, email()).run();
+ Database.open().update(TABLE_NAME).set(STATE,state).where(EMAIL, email()).compile().run();
}
public boolean isOpenFor(User user) {
@@ -129,7 +129,7 @@ public class MailingList {
var rs = Database.open()
.select(TABLE_NAME)
.where(EMAIL,listEmail)
- .exec();
+ .compile().exec();
if (rs.next()) lists.put(listEmail,ml = MailingList.from(rs));
} catch (SQLException e) {
LOG.debug("Failed to load MailingList: ",e);
@@ -156,7 +156,7 @@ public class MailingList {
var rs = Database.open()
.select(TABLE_NAME,EMAIL, "(" + STATE + " & " + STATE_PUBLIC + ") as test")
.where("test", STATE_PUBLIC)
- .exec();
+ .compile().exec();
var emails = new ArrayList();
while (rs.next()) emails.add(rs.getString(EMAIL));
rs.close();
@@ -182,7 +182,8 @@ public class MailingList {
private MailingList save() throws SQLException {
- Database.open().insertInto(TABLE_NAME)
+ Database.open()
+ .insertInto(TABLE_NAME)
.values(Map.ofEntries(
Map.entry(EMAIL, email),
Map.entry(NAME, name),
@@ -195,6 +196,7 @@ public class MailingList {
Map.entry(SMTP_USER, smtp.username()),
Map.entry(SMTP_PASS, smtp.password()),
Map.entry(STATE, state)))
+ .compile()
.run();
return this;
}
@@ -222,14 +224,14 @@ public class MailingList {
try {
if (user == null) return openLists();
if (user.hashPermission(PERMISSION_ADMIN)) {
- var rs = Database.open().select(TABLE_NAME).exec();
+ var rs = Database.open().select(TABLE_NAME).compile().exec();
var result = new HashSet();
while (rs.next()) result.add(MailingList.from(rs));
rs.close();
return result;
}
var listEmails = ListMember.listsOwnedBy(user);
- var rs = Database.open().select(TABLE_NAME).where(EMAIL, listEmails).exec();
+ var rs = Database.open().select(TABLE_NAME).where(EMAIL, listEmails).compile().exec();
var result = openLists();
while (rs.next()) result.add(MailingList.from(rs));
rs.close();
diff --git a/src/main/java/de/srsoftware/widerhall/data/User.java b/src/main/java/de/srsoftware/widerhall/data/User.java
index 7c333ca..0fdb597 100644
--- a/src/main/java/de/srsoftware/widerhall/data/User.java
+++ b/src/main/java/de/srsoftware/widerhall/data/User.java
@@ -19,6 +19,8 @@ public class User {
private static final HashMap users = new HashMap<>();
public static final int PERMISSION_ADMIN = 1;
public static final int PERMISSION_CREATE_LISTS = 2;
+ public static final String SALT = "salt";
+ public static final String HASHED_PASS = "hashedPassword";
private String email, salt, hashedPass, name;
private int permissions;
@@ -56,7 +58,12 @@ public class User {
public void addPermission(int newPermission) throws SQLException {
permissions |= newPermission;
- Database.open().update(TABLE_NAME).set(PERMISSIONS,permissions).where(EMAIL,email()).run();
+ Database.open()
+ .update(TABLE_NAME)
+ .set(PERMISSIONS,permissions)
+ .where(EMAIL,email())
+ .compile()
+ .run();
}
@@ -81,12 +88,12 @@ public class User {
.append(HASHED_PASS).append(" ").append(VARCHAR)
.append(");");
- Database.open().query(sql).run();
+ Database.open().query(sql).compile().run();
}
public void dropPermission(int newPermission) throws SQLException {
permissions ^= (permissions & newPermission);
- Database.open().update(TABLE_NAME).set(PERMISSIONS,permissions).run();
+ Database.open().update(TABLE_NAME).set(PERMISSIONS,permissions).compile().run();
}
public boolean hashPermission(int permission){
@@ -102,7 +109,7 @@ public class User {
var userList = new ArrayList();
var query = Database.open().select(TABLE_NAME);
if (emails != null && !emails.isEmpty()) query.where(EMAIL,emails);
- var rs = query.exec();
+ var rs = query.compile().exec();
while (rs.next()) userList.add(User.from(rs));
return userList;
}
@@ -124,6 +131,7 @@ public class User {
ResultSet rs = Database.open()
.select(TABLE_NAME)
.where(EMAIL,email)
+ .compile()
.exec();
try {
if (rs.next()) {
@@ -145,7 +153,7 @@ public class User {
public static boolean noUsers() throws SQLException {
- var rs = Database.open().select(TABLE_NAME,"count(*)").exec();
+ var rs = Database.open().select(TABLE_NAME,"count(*)").compile().exec();
try {
if (rs.next()) {
return rs.getInt(1) < 1;
@@ -175,6 +183,7 @@ public class User {
if (hashedPass != null) values.put(HASHED_PASS,hashedPass);
Database.open().insertInto(TABLE_NAME)
.values(values)
+ .compile()
.run();
return this;
}