refactor: rename packages and files to use 'flash' prefix for consistency

This commit is contained in:
Relism
2026-04-26 12:34:49 +02:00
parent 9e19f439be
commit 8a52c4f143
279 changed files with 2774 additions and 752 deletions
@@ -0,0 +1,71 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>dev.relism</groupId>
<artifactId>flash-extensions</artifactId>
<version>1.1-indev6</version>
</parent>
<artifactId>flash-ext-data-jdbc</artifactId>
<properties>
<jacoco.version>0.8.12</jacoco.version>
</properties>
<dependencies>
<dependency>
<groupId>dev.relism</groupId>
<artifactId>flash-ext-data-core</artifactId>
</dependency>
<dependency>
<groupId>dev.relism</groupId>
<artifactId>flash</artifactId>
</dependency>
<dependency>
<groupId>com.zaxxer</groupId>
<artifactId>HikariCP</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>${jacoco.version}</version>
<configuration>
<rules>
<rule>
<element>BUNDLE</element>
<limits>
<limit>
<counter>LINE</counter>
<value>COVEREDRATIO</value>
<minimum>0.80</minimum>
</limit>
</limits>
</rule>
</rules>
</configuration>
</plugin>
</plugins>
</build>
</project>
@@ -0,0 +1,169 @@
package dev.relism.flash.ext.data.jdbc;
import dev.relism.flash.ext.data.core.*;
import java.sql.*;
import java.util.*;
import java.util.stream.Collectors;
public abstract class JdbcRepository<T, ID> extends Repository<T, ID> {
private final String table;
private final String idColumn;
protected JdbcRepository(String table, String idColumn) {
this.table = table;
this.idColumn = idColumn;
}
protected Connection connection() {
return Tx.resource(Connection.class);
}
// ── Subclass contract ─────────────────────────────────────────────────────
protected abstract T mapRow(ResultSet rs) throws SQLException;
protected abstract void bindInsert(PreparedStatement ps, T entity) throws SQLException;
protected abstract void bindUpdate(PreparedStatement ps, T entity) throws SQLException;
protected abstract String insertSql();
protected abstract String updateSql();
// ── Repository abstract impl ──────────────────────────────────────────────
@Override
protected Optional<T> doFindById(ID id) {
return queryOne("select * from " + table + " where " + idColumn + " = ?",
ps -> ps.setObject(1, id));
}
@Override
protected List<T> doFindAll() {
return queryMany("select * from " + table, ps -> {});
}
@Override
protected List<T> doFindAll(int page, int size) {
return queryMany("select * from " + table + " limit ? offset ?", ps -> {
ps.setInt(1, size);
ps.setInt(2, page * size);
});
}
@Override
protected List<T> doFindAll(Sort sort) {
return queryMany("select * from " + table + orderClause(sort), ps -> {});
}
@Override
protected List<T> doFindAll(int page, int size, Sort sort) {
return queryMany("select * from " + table + orderClause(sort) + " limit ? offset ?",
ps -> {
ps.setInt(1, size);
ps.setInt(2, page * size);
});
}
@Override
protected Page<T> doFindPage(int page, int size) {
long total = doCount();
return new Page<>(doFindAll(page, size), page, size, total);
}
@Override
protected Page<T> doFindPage(int page, int size, Sort sort) {
long total = doCount();
return new Page<>(doFindAll(page, size, sort), page, size, total);
}
@Override
protected T doSave(T entity) {
try (PreparedStatement ps = connection().prepareStatement(
insertSql(), Statement.RETURN_GENERATED_KEYS)) {
bindInsert(ps, entity);
ps.executeUpdate();
applyGeneratedKey(ps, entity);
return entity;
} catch (SQLException e) { throw new TxException(e); }
}
@Override
protected T doUpdate(T entity) {
try (PreparedStatement ps = connection().prepareStatement(updateSql())) {
bindUpdate(ps, entity);
ps.executeUpdate();
return entity;
} catch (SQLException e) { throw new TxException(e); }
}
@Override
protected void doDelete(T entity) {
throw new UnsupportedOperationException("Override doDelete() or use deleteById()");
}
@Override
protected void doDeleteById(ID id) {
mutate("delete from " + table + " where " + idColumn + " = ?",
ps -> ps.setObject(1, id));
}
@Override
protected boolean doExistsById(ID id) {
return queryOne("select 1 from " + table + " where " + idColumn + " = ?",
ps -> ps.setObject(1, id),
rs -> rs.getInt(1)).isPresent();
}
@Override
protected long doCount() {
return queryOne("select count(*) from " + table, ps -> {},
rs -> rs.getLong(1)).orElse(0L);
}
// ── Query helpers ─────────────────────────────────────────────────────────
protected Optional<T> queryOne(String sql, SqlBinder params) {
List<T> r = queryMany(sql, params);
return r.isEmpty() ? Optional.empty() : Optional.of(r.get(0));
}
protected <R> Optional<R> queryOne(String sql, SqlBinder params,
SqlMapper<R> mapper) {
try (PreparedStatement ps = connection().prepareStatement(sql)) {
params.bind(ps);
try (ResultSet rs = ps.executeQuery()) {
return rs.next() ? Optional.of(mapper.map(rs)) : Optional.empty();
}
} catch (SQLException e) { throw new TxException(e); }
}
protected List<T> queryMany(String sql, SqlBinder params) {
try (PreparedStatement ps = connection().prepareStatement(sql)) {
params.bind(ps);
try (ResultSet rs = ps.executeQuery()) {
List<T> results = new ArrayList<>();
while (rs.next()) results.add(mapRow(rs));
return results;
}
} catch (SQLException e) { throw new TxException(e); }
}
protected int mutate(String sql, SqlBinder params) {
try (PreparedStatement ps = connection().prepareStatement(sql)) {
params.bind(ps);
return ps.executeUpdate();
} catch (SQLException e) { throw new TxException(e); }
}
protected void applyGeneratedKey(PreparedStatement ps, T entity) throws SQLException {
// override when entity has a generated PK
}
private String orderClause(Sort sort) {
return " order by " + sort.columns().stream()
.map(c -> c.column() + (c.asc() ? " ASC" : " DESC"))
.collect(Collectors.joining(", "));
}
@FunctionalInterface public interface SqlBinder { void bind(PreparedStatement ps) throws SQLException; }
@FunctionalInterface public interface SqlMapper<R> { R map(ResultSet rs) throws SQLException; }
}
@@ -0,0 +1,124 @@
package dev.relism.flash.ext.data.jdbc;
import dev.relism.flash.ext.data.core.*;
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.Objects;
public class JdbcTxManager implements TxManager {
private static final TxResourceKey JDBC_STATUS_KEY = TxResourceKey.of("jdbc.tx.status");
private final DataSource ds;
public JdbcTxManager(DataSource ds) {
this.ds = Objects.requireNonNull(ds);
}
@Override
public TxStatus begin(TxDefinition definition) {
return switch (definition.propagation()) {
case REQUIRED -> ResourceRegistry.isBound(JDBC_STATUS_KEY)
? joinExisting(definition)
: beginNew(definition);
case REQUIRES_NEW -> beginNew(definition);
case MANDATORY -> {
if (!ResourceRegistry.isBound(JDBC_STATUS_KEY))
throw new IllegalStateException("MANDATORY: no active transaction");
yield joinExisting(definition);
}
case NOT_SUPPORTED -> throw new UnsupportedOperationException("NOT_SUPPORTED is not implemented");
};
}
private TxStatus beginNew(TxDefinition definition) {
try {
JdbcTxStatus suspended = ResourceRegistry.getOrNull(JDBC_STATUS_KEY, JdbcTxStatus.class);
if (suspended != null) {
ResourceRegistry.unbind(JDBC_STATUS_KEY);
}
Connection conn = ds.getConnection();
conn.setAutoCommit(false);
if (definition.readOnly()) conn.setReadOnly(true);
if (definition.isolation() != TransactionIsolation.DEFAULT) {
conn.setTransactionIsolation(definition.isolation().level());
}
JdbcTxStatus status = new JdbcTxStatus(
conn,
true,
definition.readOnly(),
suspended,
new JdbcTxStatus.RollbackMarker()
);
ResourceRegistry.bind(JDBC_STATUS_KEY, status);
return status;
} catch (SQLException e) {
throw new TxException(e);
}
}
private TxStatus joinExisting(TxDefinition definition) {
JdbcTxStatus existing = ResourceRegistry.get(JDBC_STATUS_KEY, JdbcTxStatus.class);
if (definition.readOnly() && !existing.isReadOnly()) {
throw new TxException("Cannot join read-write tx as read-only");
}
return new JdbcTxStatus(
existing.connection(),
false,
definition.readOnly(),
null,
existing.rollbackMarker()
);
}
@Override
public void commit(TxStatus status) {
JdbcTxStatus s = (JdbcTxStatus) status;
if (!s.isNewTransaction()) {
return;
}
try {
if (s.isRollbackOnly()) {
s.connection().rollback();
ResourceRegistry.fireSynchronizations(TxOutcome.ROLLED_BACK);
return;
}
s.connection().commit();
ResourceRegistry.fireSynchronizations(TxOutcome.COMMITTED);
} catch (SQLException e) {
throw new TxException(e);
} finally {
cleanupAndResume(s);
}
}
@Override
public void rollback(TxStatus status) {
JdbcTxStatus s = (JdbcTxStatus) status;
if (!s.isNewTransaction()) {
s.markRollbackOnly();
return;
}
try {
s.connection().rollback();
ResourceRegistry.fireSynchronizations(TxOutcome.ROLLED_BACK);
} catch (SQLException e) {
throw new TxException(e);
} finally {
cleanupAndResume(s);
}
}
private void cleanupAndResume(JdbcTxStatus status) {
ResourceRegistry.unbind(JDBC_STATUS_KEY);
try {
status.connection().close();
} catch (SQLException ignored) {
}
JdbcTxStatus suspended = status.suspended();
if (suspended != null) {
ResourceRegistry.bind(JDBC_STATUS_KEY, suspended);
}
}
}
@@ -0,0 +1,46 @@
package dev.relism.flash.ext.data.jdbc;
import dev.relism.flash.ext.data.core.TxStatus;
import java.sql.Connection;
import java.util.Objects;
class JdbcTxStatus implements TxStatus {
static final class RollbackMarker {
boolean rollbackOnly;
}
private final Connection connection;
private final boolean newTransaction;
private final boolean readOnly;
private final JdbcTxStatus suspended;
private final RollbackMarker rollbackMarker;
JdbcTxStatus(
Connection connection,
boolean newTransaction,
boolean readOnly,
JdbcTxStatus suspended,
RollbackMarker rollbackMarker
) {
this.connection = Objects.requireNonNull(connection);
this.newTransaction = newTransaction;
this.readOnly = readOnly;
this.suspended = suspended;
this.rollbackMarker = rollbackMarker;
}
@Override public boolean isNewTransaction() { return newTransaction; }
@Override public boolean isReadOnly() { return readOnly; }
@Override public boolean isRollbackOnly() { return rollbackMarker.rollbackOnly; }
@Override public void markRollbackOnly() { rollbackMarker.rollbackOnly = true; }
@Override
public <R> R resource(Class<R> type) {
return type.cast(connection);
}
Connection connection() { return connection; }
JdbcTxStatus suspended() { return suspended; }
RollbackMarker rollbackMarker() { return rollbackMarker; }
}
@@ -0,0 +1,104 @@
package dev.relism.flash.ext.data.jdbc;
import dev.relism.flash.ext.data.core.*;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import static org.junit.jupiter.api.Assertions.*;
class JdbcTxManagerTest {
private final JdbcTxManager manager = new JdbcTxManager(dataSource());
@AfterEach
void cleanup() {
ResourceRegistry.clear();
}
@Test
void required_starts_new_when_absent() {
TxStatus s = manager.begin(TxDefinition.DEFAULTS);
assertTrue(s.isNewTransaction());
assertDoesNotThrow(() -> manager.commit(s));
}
@Test
void required_joins_existing_when_present() {
TxStatus outer = manager.begin(TxDefinition.DEFAULTS);
TxStatus inner = manager.begin(TxDefinition.DEFAULTS.withPropagation(TransactionPropagation.REQUIRED));
assertSame(outer.resource(Connection.class), inner.resource(Connection.class));
assertFalse(inner.isNewTransaction());
manager.rollback(outer);
}
@Test
void requires_new_creates_distinct_connection() {
TxStatus outer = manager.begin(TxDefinition.DEFAULTS);
TxStatus inner = manager.begin(TxDefinition.DEFAULTS.withPropagation(TransactionPropagation.REQUIRES_NEW));
assertNotSame(outer.resource(Connection.class), inner.resource(Connection.class));
manager.commit(inner);
manager.rollback(outer);
}
@Test
void rollback_on_joined_marks_outer_rollback_only() {
TxStatus outer = manager.begin(TxDefinition.DEFAULTS);
TxStatus inner = manager.begin(TxDefinition.DEFAULTS.withPropagation(TransactionPropagation.REQUIRED));
manager.rollback(inner);
assertTrue(outer.isRollbackOnly());
manager.rollback(outer);
}
private static DataSource dataSource() {
return new DataSource() {
@Override
public Connection getConnection() throws SQLException {
return DriverManager.getConnection("jdbc:h2:mem:tx-jdbc;DB_CLOSE_DELAY=-1");
}
@Override
public Connection getConnection(String username, String password) throws SQLException {
return DriverManager.getConnection("jdbc:h2:mem:tx-jdbc;DB_CLOSE_DELAY=-1", username, password);
}
@Override
public <T> T unwrap(Class<T> iface) {
throw new UnsupportedOperationException();
}
@Override
public boolean isWrapperFor(Class<?> iface) {
return false;
}
@Override
public java.io.PrintWriter getLogWriter() {
throw new UnsupportedOperationException();
}
@Override
public void setLogWriter(java.io.PrintWriter out) {
throw new UnsupportedOperationException();
}
@Override
public void setLoginTimeout(int seconds) {
throw new UnsupportedOperationException();
}
@Override
public int getLoginTimeout() {
return 0;
}
@Override
public java.util.logging.Logger getParentLogger() {
throw new UnsupportedOperationException();
}
};
}
}