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; }