Compare commits
3 Commits
b397a8e812
...
dc1b85a774
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dc1b85a774 | ||
|
|
503b2d35b1 | ||
|
|
90c2e55edc |
1
.gitignore
vendored
1
.gitignore
vendored
@ -2,3 +2,4 @@ build/
|
|||||||
instance/
|
instance/
|
||||||
*.class
|
*.class
|
||||||
*.db
|
*.db
|
||||||
|
.vscode/
|
||||||
|
|||||||
28
Main.java
28
Main.java
@ -1,11 +1,37 @@
|
|||||||
import java.sql.Connection;
|
import java.sql.Connection;
|
||||||
import java.sql.SQLException;
|
import java.sql.SQLException;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
import src.db.Database;
|
import src.models.UserModel;
|
||||||
|
import src.models.squirrel.Database;
|
||||||
|
import src.models.squirrel.ModelManager;
|
||||||
|
|
||||||
public class Main {
|
public class Main {
|
||||||
public static void main(String[] args) throws SQLException {
|
public static void main(String[] args) throws SQLException {
|
||||||
|
ModelManager.initializeModels();
|
||||||
|
Database.migrate = conn -> src.Migration.run(conn);
|
||||||
|
Database.getConnection();
|
||||||
|
|
||||||
|
UserModel userModel = ModelManager.get(UserModel.class);
|
||||||
|
List<UserModel> users = userModel.where(java.util.Collections.emptyMap());
|
||||||
|
for (UserModel user : users) {
|
||||||
|
user.set("name", user.get("name") + " Updated");
|
||||||
|
user.save();
|
||||||
|
System.out.println(user);
|
||||||
|
}
|
||||||
|
|
||||||
Connection conn = Database.getConnection();
|
Connection conn = Database.getConnection();
|
||||||
|
// Example: Run a simple SQL query
|
||||||
|
try (var stmt = conn.createStatement();
|
||||||
|
var rs = stmt.executeQuery("SELECT COUNT(*) AS user_count FROM users")) {
|
||||||
|
if (rs.next()) {
|
||||||
|
int userCount = rs.getInt("user_count");
|
||||||
|
System.out.println("Total users: " + userCount);
|
||||||
|
}
|
||||||
|
} catch (SQLException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
|
||||||
Database.close();
|
Database.close();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
1
run.sh
1
run.sh
@ -9,6 +9,7 @@ if ! command -v java >/dev/null 2>&1; then
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
find src -name "*.java" > sources.txt
|
find src -name "*.java" > sources.txt
|
||||||
|
echo "Main.java" >> sources.txt
|
||||||
if ! javac -d build -cp "./extern/sqlite-jdbc-3.50.3.0.jar" @sources.txt Main.java; then
|
if ! javac -d build -cp "./extern/sqlite-jdbc-3.50.3.0.jar" @sources.txt Main.java; then
|
||||||
echo "Error: javac failed to compile sources."
|
echo "Error: javac failed to compile sources."
|
||||||
exit 1
|
exit 1
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
package src.db;
|
package src;
|
||||||
|
|
||||||
import java.sql.Connection;
|
import java.sql.Connection;
|
||||||
import java.sql.Statement;
|
import java.sql.Statement;
|
||||||
@ -1,16 +0,0 @@
|
|||||||
package src.models;
|
|
||||||
|
|
||||||
import java.sql.Connection;
|
|
||||||
import src.db.Database;
|
|
||||||
|
|
||||||
public abstract class Model {
|
|
||||||
protected Connection conn;
|
|
||||||
|
|
||||||
public Model() {
|
|
||||||
try {
|
|
||||||
this.conn = Database.getConnection();
|
|
||||||
} catch (java.sql.SQLException e) {
|
|
||||||
throw new RuntimeException("Failed to get DB connection", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
75
src/models/UserModel.java
Normal file
75
src/models/UserModel.java
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
package src.models;
|
||||||
|
|
||||||
|
import javax.crypto.SecretKeyFactory;
|
||||||
|
import javax.crypto.spec.PBEKeySpec;
|
||||||
|
|
||||||
|
import src.models.squirrel.Model;
|
||||||
|
import src.models.squirrel.ModelManager;
|
||||||
|
|
||||||
|
import java.security.SecureRandom;
|
||||||
|
import java.util.Base64;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class UserModel extends Model {
|
||||||
|
|
||||||
|
{
|
||||||
|
tableName = "users";
|
||||||
|
columns.add("id");
|
||||||
|
columns.add("name");
|
||||||
|
columns.add("email");
|
||||||
|
columns.add("password_hash");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final int ITERATIONS = 65536;
|
||||||
|
private static final int KEY_LENGTH = 256;
|
||||||
|
|
||||||
|
public static UserModel register(String name, String email, String password) {
|
||||||
|
if (password == null || password.trim().isEmpty()) {
|
||||||
|
throw new IllegalArgumentException("Password cannot be empty");
|
||||||
|
}
|
||||||
|
|
||||||
|
UserModel userModel = ModelManager.get(UserModel.class);
|
||||||
|
List<UserModel> existingUser = userModel.where(Map.of("email", email));
|
||||||
|
if (existingUser != null && !existingUser.isEmpty()) {
|
||||||
|
throw new IllegalArgumentException("Email already exists");
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] salt = getSalt();
|
||||||
|
UserModel user = new UserModel();
|
||||||
|
|
||||||
|
user.set("name", name);
|
||||||
|
user.set("email", email);
|
||||||
|
user.set("password_hash", hashPassword(password, salt));
|
||||||
|
user.create();
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String hashPassword(String password, byte[] salt) {
|
||||||
|
try {
|
||||||
|
PBEKeySpec spec = new PBEKeySpec(password.toCharArray(), salt, ITERATIONS, KEY_LENGTH);
|
||||||
|
SecretKeyFactory skf = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
|
||||||
|
byte[] hash = skf.generateSecret(spec).getEncoded();
|
||||||
|
return Base64.getEncoder().encodeToString(hash);
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RuntimeException("Failed to hash password", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static byte[] getSalt() {
|
||||||
|
SecureRandom sr = new SecureRandom();
|
||||||
|
byte[] salt = new byte[16];
|
||||||
|
sr.nextBytes(salt);
|
||||||
|
return salt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean validatePassword(String password, String storedHash, byte[] salt) {
|
||||||
|
try {
|
||||||
|
String newHash = hashPassword(password, salt);
|
||||||
|
return Arrays.equals(Base64.getDecoder().decode(storedHash), Base64.getDecoder().decode(newHash));
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RuntimeException("Failed to validate password", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,19 +1,22 @@
|
|||||||
package src.db;
|
package src.models.squirrel;
|
||||||
|
|
||||||
import java.sql.Connection;
|
import java.sql.Connection;
|
||||||
import java.sql.DriverManager;
|
import java.sql.DriverManager;
|
||||||
import java.sql.SQLException;
|
import java.sql.SQLException;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
public class Database {
|
public class Database {
|
||||||
private static Connection conn;
|
private static Connection conn;
|
||||||
|
public static Consumer<Connection> migrate;
|
||||||
|
|
||||||
public static Connection getConnection() throws SQLException {
|
public static Connection getConnection() throws SQLException {
|
||||||
if (conn == null || conn.isClosed()) {
|
if (conn == null || conn.isClosed()) {
|
||||||
String url = "jdbc:sqlite:instance/test.db";
|
String url = "jdbc:sqlite:instance/test.db";
|
||||||
conn = DriverManager.getConnection(url);
|
conn = DriverManager.getConnection(url);
|
||||||
Migration.run(conn);
|
if (migrate != null) {
|
||||||
|
migrate.accept(conn);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return conn;
|
return conn;
|
||||||
}
|
}
|
||||||
|
|
||||||
259
src/models/squirrel/Model.java
Normal file
259
src/models/squirrel/Model.java
Normal file
@ -0,0 +1,259 @@
|
|||||||
|
package src.models.squirrel;
|
||||||
|
|
||||||
|
import java.sql.*;
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
public abstract class Model {
|
||||||
|
protected static Connection conn;
|
||||||
|
|
||||||
|
protected String tableName;
|
||||||
|
protected Set<String> columns = new HashSet<>();
|
||||||
|
protected Map<String, Object> attributes = new LinkedHashMap<>();
|
||||||
|
|
||||||
|
protected boolean rowMode = false;
|
||||||
|
|
||||||
|
public Model() {
|
||||||
|
if (conn == null) {
|
||||||
|
try {
|
||||||
|
conn = Database.getConnection();
|
||||||
|
} catch (SQLException e) {
|
||||||
|
throw new RuntimeException("Failed to get DB connection for model '" + getTableName() + "'", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------------
|
||||||
|
// Query (class/global mode only)
|
||||||
|
// -------------------------------
|
||||||
|
public <T extends Model> List<T> all() {
|
||||||
|
ensureClassMode("all()");
|
||||||
|
String sql = "SELECT * FROM " + getTableName();
|
||||||
|
try (PreparedStatement stmt = conn.prepareStatement(sql)) {
|
||||||
|
ResultSet rs = stmt.executeQuery();
|
||||||
|
List<T> results = new ArrayList<>();
|
||||||
|
while (rs.next()) {
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
T instance = (T) this.getClass().getDeclaredConstructor().newInstance();
|
||||||
|
instance.seedFromRow(rs);
|
||||||
|
instance.rowMode = true;
|
||||||
|
results.add(instance);
|
||||||
|
}
|
||||||
|
rs.close();
|
||||||
|
return results;
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RuntimeException("Failed to execute all() for model '" + getTableName() + "'", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public <T extends Model> T find(int id) {
|
||||||
|
ensureClassMode("find()");
|
||||||
|
String sql = "SELECT * FROM " + getTableName() + " WHERE id = ?";
|
||||||
|
try (PreparedStatement stmt = conn.prepareStatement(sql)) {
|
||||||
|
stmt.setInt(1, id);
|
||||||
|
ResultSet rs = stmt.executeQuery();
|
||||||
|
if (rs.next()) {
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
T instance = (T) this.getClass().getDeclaredConstructor().newInstance();
|
||||||
|
instance.seedFromRow(rs);
|
||||||
|
instance.rowMode = true;
|
||||||
|
rs.close();
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RuntimeException("Failed to execute find() for model '" + getTableName() + "'", e);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public <T extends Model> List<T> where(Map<String, Object> filters) {
|
||||||
|
ensureClassMode("where()");
|
||||||
|
StringBuilder where = new StringBuilder("1=1");
|
||||||
|
List<Object> values = new ArrayList<>();
|
||||||
|
for (var entry : filters.entrySet()) {
|
||||||
|
where.append(" AND ").append(entry.getKey()).append(" = ?");
|
||||||
|
values.add(entry.getValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
String sql = "SELECT * FROM " + getTableName() + " WHERE " + where;
|
||||||
|
try (PreparedStatement stmt = conn.prepareStatement(sql)) {
|
||||||
|
for (int i = 0; i < values.size(); i++) {
|
||||||
|
stmt.setObject(i + 1, values.get(i));
|
||||||
|
}
|
||||||
|
ResultSet rs = stmt.executeQuery();
|
||||||
|
List<T> results = new ArrayList<>();
|
||||||
|
while (rs.next()) {
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
T instance = (T) this.getClass().getDeclaredConstructor().newInstance();
|
||||||
|
instance.seedFromRow(rs);
|
||||||
|
instance.rowMode = true;
|
||||||
|
results.add(instance);
|
||||||
|
}
|
||||||
|
rs.close();
|
||||||
|
return results;
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RuntimeException("Failed to execute where() for model '" + getTableName() + "'", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------------
|
||||||
|
// Row-only actions
|
||||||
|
// -------------------------------
|
||||||
|
public boolean create() {
|
||||||
|
ensureRowMode("create()");
|
||||||
|
validateColumns(attributes.keySet());
|
||||||
|
StringBuilder cols = new StringBuilder();
|
||||||
|
StringBuilder vals = new StringBuilder();
|
||||||
|
List<Object> params = new ArrayList<>();
|
||||||
|
for (String col : attributes.keySet()) {
|
||||||
|
if (col.equals("id")) continue;
|
||||||
|
if (cols.length() > 0) {
|
||||||
|
cols.append(", ");
|
||||||
|
vals.append(", ");
|
||||||
|
}
|
||||||
|
cols.append(col);
|
||||||
|
vals.append("?");
|
||||||
|
params.add(attributes.get(col));
|
||||||
|
}
|
||||||
|
String sql = "INSERT INTO " + getTableName() + " (" + cols + ") VALUES (" + vals + ")";
|
||||||
|
try (PreparedStatement stmt = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) {
|
||||||
|
for (int i = 0; i < params.size(); i++) {
|
||||||
|
stmt.setObject(i + 1, params.get(i));
|
||||||
|
}
|
||||||
|
int affected = stmt.executeUpdate();
|
||||||
|
if (affected == 0) {
|
||||||
|
throw new RuntimeException("Failed to insert row for model '" + getTableName() + "'");
|
||||||
|
}
|
||||||
|
ResultSet keys = stmt.getGeneratedKeys();
|
||||||
|
if (keys.next()) {
|
||||||
|
set("id", keys.getInt(1));
|
||||||
|
}
|
||||||
|
keys.close();
|
||||||
|
return true;
|
||||||
|
} catch (SQLException e) {
|
||||||
|
throw new RuntimeException("Failed to execute create() for model '" + getTableName() + "'", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean save() {
|
||||||
|
ensureRowMode("save()");
|
||||||
|
if (!attributes.containsKey("id")) {
|
||||||
|
throw new IllegalStateException("No id set for row instance");
|
||||||
|
}
|
||||||
|
int id = (int) attributes.get("id");
|
||||||
|
|
||||||
|
StringBuilder set = new StringBuilder();
|
||||||
|
List<Object> values = new ArrayList<>();
|
||||||
|
|
||||||
|
for (var entry : attributes.entrySet()) {
|
||||||
|
if (entry.getKey().equals("id")) continue;
|
||||||
|
if (set.length() > 0) set.append(", ");
|
||||||
|
set.append(entry.getKey()).append("=?");
|
||||||
|
values.add(entry.getValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
String sql = "UPDATE " + getTableName() + " SET " + set + " WHERE id=?";
|
||||||
|
try (PreparedStatement stmt = conn.prepareStatement(sql)) {
|
||||||
|
for (int i = 0; i < values.size(); i++) {
|
||||||
|
stmt.setObject(i + 1, values.get(i));
|
||||||
|
}
|
||||||
|
stmt.setInt(values.size() + 1, id);
|
||||||
|
return stmt.executeUpdate() > 0;
|
||||||
|
} catch (SQLException e) {
|
||||||
|
throw new RuntimeException("Failed to execute save() for model '" + getTableName() + "'", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean delete() {
|
||||||
|
ensureRowMode("delete()");
|
||||||
|
if (!attributes.containsKey("id")) {
|
||||||
|
throw new IllegalStateException("No id set for row instance");
|
||||||
|
}
|
||||||
|
int id = (int) attributes.get("id");
|
||||||
|
String sql = "DELETE FROM " + getTableName() + " WHERE id = ?";
|
||||||
|
try (PreparedStatement stmt = conn.prepareStatement(sql)) {
|
||||||
|
stmt.setInt(1, id);
|
||||||
|
return stmt.executeUpdate() > 0;
|
||||||
|
} catch (SQLException e) {
|
||||||
|
throw new RuntimeException("Failed to execute delete() for model '" + getTableName() + "'", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Object get(String column) {
|
||||||
|
ensureRowMode("get()");
|
||||||
|
return attributes.get(column);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void set(String column, Object value) {
|
||||||
|
ensureRowMode("set()");
|
||||||
|
if (!columns.contains(column)) {
|
||||||
|
throw new IllegalArgumentException("Unknown column '" + column + "' for model '" + getTableName() + "'");
|
||||||
|
}
|
||||||
|
attributes.put(column, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------------
|
||||||
|
// Helpers
|
||||||
|
// -------------------------------
|
||||||
|
protected void seedFromRow(ResultSet rs) {
|
||||||
|
try {
|
||||||
|
attributes = new LinkedHashMap<>();
|
||||||
|
ResultSetMetaData meta = rs.getMetaData();
|
||||||
|
for (int i = 1; i <= meta.getColumnCount(); i++) {
|
||||||
|
attributes.put(meta.getColumnName(i), rs.getObject(i));
|
||||||
|
}
|
||||||
|
} catch (SQLException e) {
|
||||||
|
throw new RuntimeException("Failed to seed from row for model '" + getTableName() + "'", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void validateColumns(Collection<String> keys) {
|
||||||
|
for (String key : keys) {
|
||||||
|
if (!columns.contains(key)) {
|
||||||
|
throw new IllegalArgumentException(
|
||||||
|
"Unknown column '" + key + "' for model '" + getTableName() + "'"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected String getTableName() {
|
||||||
|
if (this.tableName != null && !this.tableName.isEmpty()) {
|
||||||
|
return this.tableName;
|
||||||
|
}
|
||||||
|
String className = this.getClass().getSimpleName();
|
||||||
|
return className.toLowerCase() + "s";
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ensureRowMode(String action) {
|
||||||
|
if (!rowMode) {
|
||||||
|
throw new IllegalStateException(action + " requires a row-bound instance");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ensureClassMode(String action) {
|
||||||
|
if (rowMode) {
|
||||||
|
throw new IllegalStateException(action + " requires a class/global instance");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------------
|
||||||
|
// toString
|
||||||
|
// -------------------------------
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
if (!rowMode) {
|
||||||
|
return "[Model: " + getClass().getSimpleName() + " | table=" + getTableName() + "]";
|
||||||
|
}
|
||||||
|
StringBuilder sb = new StringBuilder(getClass().getSimpleName() + " { ");
|
||||||
|
if (attributes.containsKey("id")) {
|
||||||
|
sb.append("id: ").append(attributes.get("id")).append(", ");
|
||||||
|
}
|
||||||
|
for (var entry : attributes.entrySet()) {
|
||||||
|
if (entry.getKey().equals("id")) continue;
|
||||||
|
sb.append(entry.getKey()).append(": ").append(entry.getValue()).append(", ");
|
||||||
|
}
|
||||||
|
if (!attributes.isEmpty()) sb.setLength(sb.length() - 2);
|
||||||
|
sb.append(" }");
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
31
src/models/squirrel/ModelManager.java
Normal file
31
src/models/squirrel/ModelManager.java
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
package src.models.squirrel;
|
||||||
|
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
import src.models.UserModel;
|
||||||
|
|
||||||
|
public class ModelManager {
|
||||||
|
private static final Map<Class<? extends Model>, Model> models = new HashMap<>();
|
||||||
|
|
||||||
|
public static void initializeModels() {
|
||||||
|
List<Class<? extends Model>> modelClasses = Arrays.asList(
|
||||||
|
UserModel.class
|
||||||
|
);
|
||||||
|
for (Class<? extends Model> clazz : modelClasses) {
|
||||||
|
try {
|
||||||
|
Model instance = clazz.getDeclaredConstructor().newInstance();
|
||||||
|
models.put(clazz, instance);
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static <T extends Model> T get(Class<T> clazz) {
|
||||||
|
Model instance = models.get(clazz);
|
||||||
|
if (instance == null) {
|
||||||
|
throw new IllegalStateException("Model not initialized: " + clazz.getSimpleName());
|
||||||
|
}
|
||||||
|
return clazz.cast(instance);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user