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,84 @@
<?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-core</artifactId>
<dependencies>
<dependency>
<groupId>dev.relism</groupId>
<artifactId>flash</artifactId>
</dependency>
<dependency>
<groupId>jakarta.transaction</groupId>
<artifactId>jakarta.transaction-api</artifactId>
</dependency>
<dependency>
<groupId>jakarta.annotation</groupId>
<artifactId>jakarta.annotation-api</artifactId>
</dependency>
<dependency>
<groupId>jakarta.validation</groupId>
<artifactId>jakarta.validation-api</artifactId>
</dependency>
</dependencies>
<properties>
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<jacoco.version>0.8.12</jacoco.version>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>${jacoco.version}</version>
<executions>
<execution>
<id>jacoco-initialize</id>
<goals>
<goal>prepare-agent</goal>
</goals>
</execution>
<execution>
<id>jacoco-site</id>
<phase>verify</phase>
<goals>
<goal>report</goal>
</goals>
<configuration>
<rules>
<rule>
<element>BUNDLE</element>
<limits>
<limit>
<counter>LINE</counter>
<value>COVEREDRATIO</value>
<minimum>0.80</minimum>
</limit>
</limits>
</rule>
</rules>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.2.5</version>
</plugin>
</plugins>
</build>
</project>
@@ -0,0 +1,55 @@
package dev.relism.flash.ext.data;
import dev.relism.flash.ext.data.core.Tx;
import dev.relism.flash.ext.data.core.TxDefinition;
import dev.relism.flash.ext.data.core.TxManager;
import dev.relism.flash.ext.data.core.TransactionPropagation;
import dev.relism.flash.extension.ExtensionPhase;
import dev.relism.flash.extension.FlashContext;
import dev.relism.flash.extension.FlashExtension;
import dev.relism.flash.extension.FlashRegistrar;
import dev.relism.flash.routing.Middleware;
import jakarta.transaction.Transactional;
import java.util.List;
import java.util.Objects;
public final class DataExtension implements FlashExtension {
private final TxManager txManager;
public DataExtension(TxManager txManager) {
this.txManager = Objects.requireNonNull(txManager);
}
@Override
public void provide(FlashContext ctx) {
Tx.init(txManager);
ctx.provide(TxManager.class, txManager);
ctx.addAnnotationProcessor(handlerClass -> {
Transactional ann = handlerClass.getAnnotation(Transactional.class);
if (ann == null) {
return List.of();
}
TxDefinition definition = TxDefinition.DEFAULTS
.withPropagation(mapTxType(ann.value()));
Middleware middleware = next -> (req, res) -> {
return Tx.call(definition, () -> next.handle(req, res));
};
return List.of(middleware);
});
}
@Override
public int priority() {
return ExtensionPhase.EARLY.value;
}
private TransactionPropagation mapTxType(Transactional.TxType txType) {
return switch (txType) {
case REQUIRED, SUPPORTS -> TransactionPropagation.REQUIRED;
case REQUIRES_NEW -> TransactionPropagation.REQUIRES_NEW;
case MANDATORY -> TransactionPropagation.MANDATORY;
case NOT_SUPPORTED, NEVER -> TransactionPropagation.NOT_SUPPORTED;
};
}
}
@@ -0,0 +1,15 @@
package dev.relism.flash.ext.data.core;
import java.util.List;
public record Page<T>(
List<T> content,
int page,
int size,
long total
) {
public int totalPages() { return size == 0 ? 0 : (int) Math.ceil((double) total / size); }
public boolean hasNext() { return page + 1 < totalPages(); }
public boolean hasPrev() { return page > 0; }
public boolean isEmpty() { return content.isEmpty(); }
}
@@ -0,0 +1,112 @@
package dev.relism.flash.ext.data.core;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
/**
* Base repository. Subclasses only extend this — never HibernateRepository
* or JdbcRepository directly. The concrete backing is transparent.
*
* Every method auto-wraps in REQUIRED transaction — safe to call with or
* without an active transaction on the thread.
*/
public abstract class Repository<T, ID> {
private final TxDefinition required = TxDefinition.DEFAULTS
.withPropagation(TransactionPropagation.REQUIRED);
// ── CRUD ──────────────────────────────────────────────────────────────────
public Optional<T> findById(ID id) {
return tx(() -> doFindById(id));
}
public List<T> findAll() {
return tx(this::doFindAll);
}
public List<T> findAll(int page, int size) {
return tx(() -> doFindAll(page, size));
}
public List<T> findAll(Sort sort) {
return tx(() -> doFindAll(sort));
}
public List<T> findAll(int page, int size, Sort sort) {
return tx(() -> doFindAll(page, size, sort));
}
public Page<T> findPage(int page, int size) {
return tx(() -> doFindPage(page, size));
}
public Page<T> findPage(int page, int size, Sort sort) {
return tx(() -> doFindPage(page, size, sort));
}
public T save(T entity) {
return tx(() -> doSave(entity));
}
public List<T> saveAll(Iterable<T> entities) {
return tx(() -> {
List<T> saved = new ArrayList<>();
for (T e : entities) saved.add(doSave(e));
return saved;
});
}
public T update(T entity) {
return tx(() -> doUpdate(entity));
}
public void delete(T entity) {
tx(() -> { doDelete(entity); return null; });
}
public void deleteById(ID id) {
tx(() -> { doDeleteById(id); return null; });
}
public void deleteAll(Iterable<T> entities) {
tx(() -> { entities.forEach(this::doDelete); return null; });
}
public boolean existsById(ID id) {
return tx(() -> doExistsById(id));
}
public long count() {
return tx(this::doCount);
}
// ── Auto-wrap helper ──────────────────────────────────────────────────────
/**
* Ensures the work runs inside a transaction.
* If one is already active (caller annotated @Transactional or inside Tx.run)
* it joins it — no new connection opened.
* If none is active it opens one, commits, and closes it transparently.
*/
protected final <R> R tx(Tx.TxCallable<R> work) {
return Tx.call(required, work);
}
// ── Abstract — implemented by HibernateRepository / JdbcRepository ────────
protected abstract Optional<T> doFindById(ID id);
protected abstract List<T> doFindAll();
protected abstract List<T> doFindAll(int page, int size);
protected abstract List<T> doFindAll(Sort sort);
protected abstract List<T> doFindAll(int page, int size, Sort sort);
protected abstract Page<T> doFindPage(int page, int size);
protected abstract Page<T> doFindPage(int page, int size, Sort sort);
protected abstract T doSave(T entity);
protected abstract T doUpdate(T entity);
protected abstract void doDelete(T entity);
protected abstract void doDeleteById(ID id);
protected abstract boolean doExistsById(ID id);
protected abstract long doCount();
}
@@ -0,0 +1,63 @@
package dev.relism.flash.ext.data.core;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
public final class ResourceRegistry {
private static final ThreadLocal<Map<TxResourceKey, Object>> RESOURCES =
ThreadLocal.withInitial(HashMap::new);
private static final ThreadLocal<List<TxSynchronization>> SYNCHRONIZATIONS =
ThreadLocal.withInitial(ArrayList::new);
private ResourceRegistry() {}
public static void bind(TxResourceKey key, Object value) {
RESOURCES.get().put(key, Objects.requireNonNull(value));
}
public static boolean isBound(TxResourceKey key) {
return RESOURCES.get().containsKey(key);
}
public static void unbind(TxResourceKey key) {
RESOURCES.get().remove(key);
}
public static void clear() {
RESOURCES.get().clear();
SYNCHRONIZATIONS.get().clear();
}
public static <R> R get(TxResourceKey key, Class<R> type) {
Object value = RESOURCES.get().get(key);
if (value == null) {
throw new IllegalStateException("No resource bound for key: " + key);
}
return type.cast(value);
}
public static <R> R getOrNull(TxResourceKey key, Class<R> type) {
Object value = RESOURCES.get().get(key);
return value == null ? null : type.cast(value);
}
public static void addSynchronization(TxSynchronization sync) {
SYNCHRONIZATIONS.get().add(Objects.requireNonNull(sync));
}
public static void fireSynchronizations(TxOutcome outcome) {
List<TxSynchronization> syncs = List.copyOf(SYNCHRONIZATIONS.get());
SYNCHRONIZATIONS.get().clear();
for (TxSynchronization sync : syncs) {
if (outcome == TxOutcome.COMMITTED) {
sync.afterCommit();
} else {
sync.afterRollback();
}
sync.afterCompletion(outcome);
}
}
}
@@ -0,0 +1,22 @@
package dev.relism.flash.ext.data.core;
import java.util.ArrayList;
import java.util.List;
public record Sort(List<Column> columns) {
public record Column(String column, boolean asc) {}
public static Sort by(String column) { return new Sort(List.of(new Column(column, true))); }
public static Sort desc(String column) { return new Sort(List.of(new Column(column, false))); }
public static Sort by(String col, boolean asc){ return new Sort(List.of(new Column(col, asc))); }
public Sort then(String column) { return thenBy(column, true); }
public Sort thenDesc(String column) { return thenBy(column, false); }
private Sort thenBy(String column, boolean asc) {
List<Column> next = new ArrayList<>(columns);
next.add(new Column(column, asc));
return new Sort(next);
}
}
@@ -0,0 +1,14 @@
package dev.relism.flash.ext.data.core;
public enum TransactionIsolation {
DEFAULT(-1),
READ_UNCOMMITTED(1),
READ_COMMITTED(2),
REPEATABLE_READ(4),
SERIALIZABLE(8);
private final int level;
TransactionIsolation(int level) { this.level = level; }
public int level() { return level; }
}
@@ -0,0 +1,8 @@
package dev.relism.flash.ext.data.core;
public enum TransactionPropagation {
REQUIRED,
REQUIRES_NEW,
NOT_SUPPORTED,
MANDATORY
}
@@ -0,0 +1,109 @@
package dev.relism.flash.ext.data.core;
import java.util.ArrayDeque;
import java.util.Deque;
public final class Tx {
private static final ThreadLocal<Deque<TxStatus>> STATUS_STACK =
ThreadLocal.withInitial(ArrayDeque::new);
private static volatile TxManager manager;
private Tx() {}
public static void init(TxManager txManager) {
if (manager != null) {
throw new IllegalStateException("TxManager already initialized");
}
manager = txManager;
}
public static void run(TxRunnable work) {
run(TxDefinition.DEFAULTS, work);
}
public static void run(TxDefinition definition, TxRunnable work) {
call(definition, () -> {
work.run();
return null;
});
}
public static <T> T call(TxCallable<T> work) {
return call(TxDefinition.DEFAULTS, work);
}
public static <T> T call(TxDefinition definition, TxCallable<T> work) {
TxStatus status = manager().begin(definition);
pushStatus(status);
try {
T result = work.call();
if (status.isRollbackOnly()) {
manager().rollback(status);
} else {
manager().commit(status);
}
return result;
} catch (Exception e) {
manager().rollback(status);
throw (e instanceof TxException txException) ? txException : new TxException(e);
} finally {
popStatus();
}
}
public static boolean isActive() {
return !STATUS_STACK.get().isEmpty();
}
public static void setRollbackOnly() {
currentStatus().markRollbackOnly();
}
public static <R> R resource(Class<R> type) {
return currentStatus().resource(type);
}
public static TxDefinition requiresNew() {
return TxDefinition.DEFAULTS.withPropagation(TransactionPropagation.REQUIRES_NEW);
}
public static TxDefinition readOnly() {
return TxDefinition.DEFAULTS.asReadOnly();
}
private static TxManager manager() {
if (manager == null) {
throw new IllegalStateException("No TxManager installed");
}
return manager;
}
private static TxStatus currentStatus() {
TxStatus status = STATUS_STACK.get().peek();
if (status == null) {
throw new IllegalStateException("No active transaction");
}
return status;
}
private static void pushStatus(TxStatus status) {
STATUS_STACK.get().push(status);
}
private static void popStatus() {
Deque<TxStatus> stack = STATUS_STACK.get();
if (!stack.isEmpty()) {
stack.pop();
}
}
@FunctionalInterface
public interface TxRunnable {
void run();
}
@FunctionalInterface
public interface TxCallable<T> {
T call() throws Exception;
}
}
@@ -0,0 +1,31 @@
package dev.relism.flash.ext.data.core;
public record TxDefinition(
TransactionPropagation propagation,
TransactionIsolation isolation,
boolean readOnly,
String label
) {
public static final TxDefinition DEFAULTS = new TxDefinition(
TransactionPropagation.REQUIRED,
TransactionIsolation.DEFAULT,
false,
null
);
public TxDefinition withPropagation(TransactionPropagation p) {
return new TxDefinition(p, isolation, readOnly, label);
}
public TxDefinition withIsolation(TransactionIsolation i) {
return new TxDefinition(propagation, i, readOnly, label);
}
public TxDefinition withReadOnly(boolean ro) {
return new TxDefinition(propagation, isolation, ro, label);
}
public TxDefinition asReadOnly() {
return new TxDefinition(propagation, isolation, true, label);
}
}
@@ -0,0 +1,6 @@
package dev.relism.flash.ext.data.core;
public class TxException extends RuntimeException {
public TxException(String message) { super(message); }
public TxException(Throwable cause) { super(cause); }
}
@@ -0,0 +1,7 @@
package dev.relism.flash.ext.data.core;
public interface TxManager {
TxStatus begin(TxDefinition definition);
void commit(TxStatus status);
void rollback(TxStatus status);
}
@@ -0,0 +1,6 @@
package dev.relism.flash.ext.data.core;
public enum TxOutcome {
COMMITTED,
ROLLED_BACK
}
@@ -0,0 +1,32 @@
package dev.relism.flash.ext.data.core;
import java.util.Objects;
public final class TxResourceKey {
private final String name;
private TxResourceKey(String name) {
this.name = Objects.requireNonNull(name);
}
public static TxResourceKey of(String name) {
return new TxResourceKey(name);
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof TxResourceKey that)) return false;
return name.equals(that.name);
}
@Override
public int hashCode() {
return name.hashCode();
}
@Override
public String toString() {
return name;
}
}
@@ -0,0 +1,9 @@
package dev.relism.flash.ext.data.core;
public interface TxStatus {
boolean isNewTransaction();
boolean isReadOnly();
boolean isRollbackOnly();
void markRollbackOnly();
<R> R resource(Class<R> type);
}
@@ -0,0 +1,8 @@
package dev.relism.flash.ext.data.core;
public interface TxSynchronization {
default void beforeCommit(boolean readOnly) {}
default void afterCommit() {}
default void afterRollback() {}
default void afterCompletion(TxOutcome outcome) {}
}