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) {}
}
@@ -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-hibernate</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>org.hibernate.orm</groupId>
<artifactId>hibernate-core</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,177 @@
package dev.relism.flash.ext.data.hibernate;
import dev.relism.flash.ext.data.core.*;
import org.hibernate.Session;
import org.hibernate.query.MutationQuery;
import jakarta.persistence.TypedQuery;
import java.io.Serializable;
import java.util.*;
import java.util.function.Consumer;
import java.util.stream.Collectors;
/**
* Hibernate-backed repository base.
* Never extend this directly — extend {@link Repository} from the core.
* This class is instantiated internally by flash-ext-data-hibernate.
*/
public abstract class HibernateRepository<T, ID extends Serializable>
extends Repository<T, ID> {
private final Class<T> type;
protected HibernateRepository(Class<T> type) {
this.type = type;
}
// ── Session — always safe, tx() wrapper guarantees active transaction ─────
protected Session session() {
return Tx.resource(Session.class);
}
// ── Repository abstract impl ──────────────────────────────────────────────
@Override
protected Optional<T> doFindById(ID id) {
return Optional.ofNullable(session().get(type, id));
}
@Override
protected List<T> doFindAll() {
return hql("from " + type.getSimpleName()).getResultList();
}
@Override
protected List<T> doFindAll(int page, int size) {
return hql("from " + type.getSimpleName())
.setFirstResult(page * size)
.setMaxResults(size)
.getResultList();
}
@Override
protected List<T> doFindAll(Sort sort) {
return hql("from " + type.getSimpleName() + orderClause(sort))
.getResultList();
}
@Override
protected List<T> doFindAll(int page, int size, Sort sort) {
return hql("from " + type.getSimpleName() + orderClause(sort))
.setFirstResult(page * size)
.setMaxResults(size)
.getResultList();
}
@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) {
session().persist(entity);
return entity;
}
@Override
protected T doUpdate(T entity) {
return session().merge(entity);
}
@Override
protected void doDelete(T entity) {
Session s = session();
s.remove(s.contains(entity) ? entity : s.merge(entity));
}
@Override
protected void doDeleteById(ID id) {
doFindById(id).ifPresent(this::doDelete);
}
@Override
protected boolean doExistsById(ID id) {
return doFindById(id).isPresent();
}
@Override
protected long doCount() {
return session()
.createQuery("select count(*) from " + type.getSimpleName(), Long.class)
.uniqueResultOptional()
.orElse(0L);
}
// ── Query helpers — usabili nelle sottoclassi domain ─────────────────────
protected TypedQuery<T> hql(String hql) {
return session().createQuery(hql, type);
}
protected <R> TypedQuery<R> hql(String hql, Class<R> resultType) {
return session().createQuery(hql, resultType);
}
protected Optional<T> findOne(String hql, Consumer<TypedQuery<T>> params) {
TypedQuery<T> q = hql(hql);
params.accept(q);
return q.getResultStream().findFirst();
}
protected List<T> findMany(String hql, Consumer<TypedQuery<T>> params) {
return tx(() -> {
TypedQuery<T> q = hql(hql);
params.accept(q);
return q.getResultList();
});
}
protected List<T> findMany(String hql, Consumer<TypedQuery<T>> params,
int page, int size) {
return tx(() -> {
TypedQuery<T> q = hql(hql);
params.accept(q);
return q.setFirstResult(page * size).setMaxResults(size).getResultList();
});
}
protected Page<T> findManyPaged(String hql, String countHql,
Consumer<TypedQuery<T>> params,
int page, int size) {
return tx(() -> {
long total = session()
.createQuery(countHql, Long.class)
.uniqueResultOptional()
.orElse(0L);
List<T> content = findMany(hql, params, page, size);
return new Page<>(content, page, size, total);
});
}
protected int execute(String hql, Consumer<MutationQuery> params) {
return tx(() -> {
MutationQuery q = session().createMutationQuery(hql);
params.accept(q);
return q.executeUpdate();
});
}
protected Class<T> entityType() {
return type;
}
private String orderClause(Sort sort) {
return " order by " + sort.columns().stream()
.map(c -> c.column() + (c.asc() ? " ASC" : " DESC"))
.collect(Collectors.joining(", "));
}
}
@@ -0,0 +1,114 @@
package dev.relism.flash.ext.data.hibernate;
import dev.relism.flash.ext.data.core.*;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import java.util.Objects;
public class HibernateTxManager implements TxManager {
private static final TxResourceKey HIBERNATE_STATUS_KEY = TxResourceKey.of("hibernate.tx.status");
private final SessionFactory sf;
public HibernateTxManager(SessionFactory sessionFactory) {
this.sf = Objects.requireNonNull(sessionFactory);
}
@Override
public TxStatus begin(TxDefinition definition) {
return switch (definition.propagation()) {
case REQUIRED -> ResourceRegistry.isBound(HIBERNATE_STATUS_KEY)
? joinExisting(definition)
: beginNew(definition);
case REQUIRES_NEW -> beginNew(definition);
case MANDATORY -> {
if (!ResourceRegistry.isBound(HIBERNATE_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) {
HibernateTxStatus suspended = ResourceRegistry.getOrNull(HIBERNATE_STATUS_KEY, HibernateTxStatus.class);
if (suspended != null) {
ResourceRegistry.unbind(HIBERNATE_STATUS_KEY);
}
Session s = sf.openSession();
s.beginTransaction();
if (definition.readOnly()) s.setDefaultReadOnly(true);
if (definition.isolation() != TransactionIsolation.DEFAULT) {
s.doWork(connection -> connection.setTransactionIsolation(definition.isolation().level()));
}
HibernateTxStatus status = new HibernateTxStatus(
s,
true,
definition.readOnly(),
suspended,
new HibernateTxStatus.RollbackMarker()
);
ResourceRegistry.bind(HIBERNATE_STATUS_KEY, status);
return status;
}
private TxStatus joinExisting(TxDefinition definition) {
HibernateTxStatus existing = ResourceRegistry.get(HIBERNATE_STATUS_KEY, HibernateTxStatus.class);
if (definition.readOnly() && !existing.isReadOnly()) {
throw new TxException("Cannot join read-write tx as read-only");
}
return new HibernateTxStatus(
existing.session(),
false,
definition.readOnly(),
null,
existing.rollbackMarker()
);
}
@Override
public void commit(TxStatus status) {
HibernateTxStatus s = (HibernateTxStatus) status;
if (!s.isNewTransaction()) {
return;
}
try {
if (s.isRollbackOnly() && s.session().getTransaction().isActive()) {
s.session().getTransaction().rollback();
ResourceRegistry.fireSynchronizations(TxOutcome.ROLLED_BACK);
} else {
s.session().getTransaction().commit();
ResourceRegistry.fireSynchronizations(TxOutcome.COMMITTED);
}
} finally {
cleanupAndResume(s);
}
}
@Override
public void rollback(TxStatus status) {
HibernateTxStatus s = (HibernateTxStatus) status;
if (!s.isNewTransaction()) {
s.markRollbackOnly();
return;
}
try {
if (s.session().getTransaction().isActive()) {
s.session().getTransaction().rollback();
}
ResourceRegistry.fireSynchronizations(TxOutcome.ROLLED_BACK);
} finally {
cleanupAndResume(s);
}
}
private void cleanupAndResume(HibernateTxStatus status) {
ResourceRegistry.unbind(HIBERNATE_STATUS_KEY);
status.session().close();
HibernateTxStatus suspended = status.suspended();
if (suspended != null) {
ResourceRegistry.bind(HIBERNATE_STATUS_KEY, suspended);
}
}
}
@@ -0,0 +1,44 @@
package dev.relism.flash.ext.data.hibernate;
import dev.relism.flash.ext.data.core.TxStatus;
import org.hibernate.Session;
class HibernateTxStatus implements TxStatus {
static final class RollbackMarker {
boolean rollbackOnly;
}
private final Session session;
private final boolean newTransaction;
private final boolean readOnly;
private final HibernateTxStatus suspended;
private final RollbackMarker rollbackMarker;
HibernateTxStatus(
Session session,
boolean newTransaction,
boolean readOnly,
HibernateTxStatus suspended,
RollbackMarker rollbackMarker
) {
this.session = session;
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(session);
}
Session session() { return session; }
HibernateTxStatus suspended() { return suspended; }
RollbackMarker rollbackMarker() { return rollbackMarker; }
}
@@ -0,0 +1,68 @@
package dev.relism.flash.ext.data.hibernate;
import dev.relism.flash.ext.data.core.*;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class HibernateTxManagerTest {
static SessionFactory sf;
static HibernateTxManager manager;
@BeforeAll
static void setup() {
sf = TestHelper.buildSessionFactory();
manager = new HibernateTxManager(sf);
}
@AfterAll
static void teardown() {
if (sf != null) {
sf.close();
}
}
@AfterEach
void cleanup() {
ResourceRegistry.clear();
}
@Test
void required_starts_new_when_absent() {
TxStatus s = manager.begin(TxDefinition.DEFAULTS);
assertNotNull(s.resource(Session.class));
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(Session.class), inner.resource(Session.class));
manager.rollback(outer);
}
@Test
void requires_new_uses_separate_session() {
TxStatus outer = manager.begin(TxDefinition.DEFAULTS);
TxStatus inner = manager.begin(TxDefinition.DEFAULTS.withPropagation(TransactionPropagation.REQUIRES_NEW));
assertNotSame(outer.resource(Session.class), inner.resource(Session.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);
}
}
@@ -0,0 +1,24 @@
package dev.relism.flash.ext.data.hibernate;
import org.hibernate.SessionFactory;
import org.hibernate.boot.MetadataSources;
import org.hibernate.boot.registry.StandardServiceRegistry;
import org.hibernate.boot.registry.StandardServiceRegistryBuilder;
public class TestHelper {
public static SessionFactory buildSessionFactory() {
final StandardServiceRegistry registry = new StandardServiceRegistryBuilder()
.applySetting("hibernate.connection.url", "jdbc:h2:mem:tx-hibernate;DB_CLOSE_DELAY=-1")
.applySetting("hibernate.connection.driver_class", "org.h2.Driver")
.applySetting("hibernate.dialect", "org.hibernate.dialect.H2Dialect")
.applySetting("hibernate.hbm2ddl.auto", "none")
.applySetting("hibernate.show_sql", "false")
.build();
try {
return new MetadataSources(registry).buildMetadata().buildSessionFactory();
} catch (Exception e) {
StandardServiceRegistryBuilder.destroy(registry);
throw e;
}
}
}
@@ -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();
}
};
}
}
@@ -1,11 +1,11 @@
package dev.relism.ext.jackson;
package dev.relism.flash.ext.jackson;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.json.JsonMapper;
import dev.relism.extension.FlashContext;
import dev.relism.extension.FlashExtension;
import dev.relism.routing.Middleware;
import dev.relism.flash.extension.FlashContext;
import dev.relism.flash.extension.FlashExtension;
import dev.relism.flash.routing.Middleware;
/**
* Registers JSON support into the Flash extension layer.
@@ -1,10 +1,10 @@
package dev.relism.ext.jackson;
package dev.relism.flash.ext.jackson;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import dev.relism.http.ContentType;
import dev.relism.models.Response;
import dev.relism.routing.Middleware;
import dev.relism.flash.http.ContentType;
import dev.relism.flash.models.Response;
import dev.relism.flash.routing.Middleware;
/**
* Outbound JSON marshalling middleware for class-based and lambda routes.
@@ -1,11 +1,11 @@
package dev.relism.ext.jackson;
package dev.relism.flash.ext.jackson;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import dev.relism.exceptions.HttpException;
import dev.relism.http.ContentType;
import dev.relism.models.Request;
import dev.relism.models.Response;
import dev.relism.flash.exceptions.HttpException;
import dev.relism.flash.http.ContentType;
import dev.relism.flash.models.Request;
import dev.relism.flash.models.Response;
/**
* Thread-safe JSON toolbox. Single point of access for all JSON I/O operations
@@ -1,11 +1,12 @@
package dev.relism.ext.jackson;
package dev.relism.flash.ext.jackson;
import com.fasterxml.jackson.databind.ObjectMapper;
import dev.relism.extension.FlashContext;
import dev.relism.http.ContentType;
import dev.relism.models.Request;
import dev.relism.models.RequestHandler;
import dev.relism.models.Response;
import dev.relism.flash.extension.FlashContext;
import dev.relism.flash.models.SimpleHandler;
import dev.relism.flash.http.ContentType;
import dev.relism.flash.models.Request;
import dev.relism.flash.models.RequestHandler;
import dev.relism.flash.models.Response;
import org.junit.jupiter.api.Test;
import java.nio.charset.StandardCharsets;
@@ -41,7 +42,7 @@ class JacksonExtensionTest {
}
};
RequestHandler wrapped = new RequestHandler() {
private final dev.relism.models.SimpleHandler.FunctionalHandler delegate = ext.autoJson().wrap(next);
private final SimpleHandler.FunctionalHandler delegate = ext.autoJson().wrap(next);
@Override
public Object handle(Request request, Response response) throws Exception {
@@ -1,10 +1,11 @@
package dev.relism.ext.jackson;
package dev.relism.flash.ext.jackson;
import com.fasterxml.jackson.databind.ObjectMapper;
import dev.relism.http.ContentType;
import dev.relism.models.Request;
import dev.relism.models.RequestHandler;
import dev.relism.models.Response;
import dev.relism.flash.models.SimpleHandler;
import dev.relism.flash.http.ContentType;
import dev.relism.flash.models.Request;
import dev.relism.flash.models.RequestHandler;
import dev.relism.flash.models.Response;
import org.junit.jupiter.api.Test;
import java.nio.charset.StandardCharsets;
@@ -72,7 +73,7 @@ class JacksonMiddlewareTest {
}
};
return new RequestHandler() {
private final dev.relism.models.SimpleHandler.FunctionalHandler delegate = mw.autoJson().wrap(next);
private final SimpleHandler.FunctionalHandler delegate = mw.autoJson().wrap(next);
@Override
public Object handle(Request request, Response response) throws Exception {
@@ -1,15 +1,15 @@
package dev.relism.ext.jackson;
package dev.relism.flash.ext.jackson;
import com.fasterxml.jackson.annotation.JsonView;
import com.fasterxml.jackson.databind.ObjectMapper;
import dev.relism.exceptions.HttpException;
import dev.relism.http.ContentType;
import dev.relism.http.HttpMethod;
import dev.relism.models.HeaderMap;
import dev.relism.models.Request;
import dev.relism.models.RequestLine;
import dev.relism.models.Response;
import dev.relism.routing.routers.fastpathrouter.FastPathViews;
import dev.relism.flash.exceptions.HttpException;
import dev.relism.flash.http.ContentType;
import dev.relism.flash.http.HttpMethod;
import dev.relism.flash.models.HeaderMap;
import dev.relism.flash.models.Request;
import dev.relism.flash.models.RequestLine;
import dev.relism.flash.models.Response;
import dev.relism.flash.routing.routers.fastpathrouter.FastPathViews;
import org.junit.jupiter.api.Test;
import java.nio.charset.StandardCharsets;
@@ -1,4 +1,4 @@
package dev.relism.ext.limiter;
package dev.relism.flash.ext.limiter;
import java.util.concurrent.atomic.AtomicLong;
@@ -1,4 +1,4 @@
package dev.relism.ext.limiter;
package dev.relism.flash.ext.limiter;
import java.util.concurrent.ConcurrentHashMap;
@@ -1,13 +1,15 @@
package dev.relism.ext.limiter;
package dev.relism.flash.ext.limiter;
import dev.relism.routing.Middleware;
import dev.relism.flash.exceptions.InitializationException;
import dev.relism.flash.extension.FlashContext;
import dev.relism.flash.routing.Middleware;
import java.util.concurrent.TimeUnit;
/**
* Manual rate-limit guard for lambda routes.
*
* <p>Available via {@link dev.relism.extension.FlashContext}:
* <p>Available via {@link FlashContext}:
* <pre>{@code
* Guard guard = ctx.require(Guard.class);
* }</pre>
@@ -23,7 +25,7 @@ import java.util.concurrent.TimeUnit;
* }</pre>
*
* <p>The resolver name is looked up once here (at wiring time, not on each request).
* If the name is not registered, {@link dev.relism.exceptions.InitializationException}
* If the name is not registered, {@link InitializationException}
* is thrown immediately.
*/
public final class Guard {
@@ -1,6 +1,6 @@
package dev.relism.ext.limiter;
package dev.relism.flash.ext.limiter;
import dev.relism.models.Request;
import dev.relism.flash.models.Request;
/**
* Extracts a partition key from an incoming request.
@@ -1,4 +1,7 @@
package dev.relism.ext.limiter;
package dev.relism.flash.ext.limiter;
import dev.relism.flash.exceptions.InitializationException;
import dev.relism.flash.models.RequestHandler;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
@@ -7,11 +10,11 @@ import java.lang.annotation.Target;
import java.util.concurrent.TimeUnit;
/**
* Applies a rate limit to a class-based {@link dev.relism.models.RequestHandler}.
* Applies a rate limit to a class-based {@link RequestHandler}.
*
* <p>The annotation is processed at boot time by the {@link LimiterExtension} annotation
* processor. If {@link #key()} names a resolver that was never registered,
* startup fails immediately with {@link dev.relism.exceptions.InitializationException}.
* startup fails immediately with {@link InitializationException}.
*
* <pre>{@code
* // 100 req/s per client IP fixed window
@@ -1,4 +1,4 @@
package dev.relism.ext.limiter;
package dev.relism.flash.ext.limiter;
/**
* Immutable configuration snapshot for a single rate-limit rule.
@@ -1,8 +1,8 @@
package dev.relism.ext.limiter;
package dev.relism.flash.ext.limiter;
import dev.relism.ext.limiter.strategy.FixedWindowStrategy;
import dev.relism.ext.limiter.strategy.SlidingWindowStrategy;
import dev.relism.ext.limiter.strategy.TokenBucketStrategy;
import dev.relism.flash.ext.limiter.strategy.FixedWindowStrategy;
import dev.relism.flash.ext.limiter.strategy.SlidingWindowStrategy;
import dev.relism.flash.ext.limiter.strategy.TokenBucketStrategy;
/**
* Enumeration of built-in rate-limit algorithms. Each constant is a factory
@@ -1,6 +1,6 @@
package dev.relism.ext.limiter;
package dev.relism.flash.ext.limiter;
import dev.relism.exceptions.InitializationException;
import dev.relism.flash.exceptions.InitializationException;
import java.net.InetSocketAddress;
import java.util.LinkedHashMap;
@@ -1,14 +1,16 @@
package dev.relism.ext.limiter;
package dev.relism.flash.ext.limiter;
import dev.relism.ext.openapi.OpenApiContributorRegistry;
import dev.relism.ext.openapi.OpenApiOperationContribution;
import dev.relism.ext.openapi.OpenApiResponseContribution;
import dev.relism.extension.ExtensionPhase;
import dev.relism.extension.FlashContext;
import dev.relism.extension.FlashExtension;
import dev.relism.extension.FlashRegistrar;
import dev.relism.http.HttpStatus;
import dev.relism.routing.Middleware;
import dev.relism.flash.ext.openapi.OpenApiContributor;
import dev.relism.flash.ext.openapi.OpenApiContributorRegistry;
import dev.relism.flash.ext.openapi.OpenApiOperationContribution;
import dev.relism.flash.ext.openapi.OpenApiResponseContribution;
import dev.relism.flash.extension.AnnotationProcessor;
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.http.HttpStatus;
import dev.relism.flash.routing.Middleware;
import java.nio.charset.StandardCharsets;
import java.util.List;
@@ -21,7 +23,7 @@ import java.util.Map;
* <ol>
* <li>Creates a single {@link BucketStore} shared by all rules in this extension instance.</li>
* <li>Provides a {@link Guard} in the {@link FlashContext} for manual use on lambda routes.</li>
* <li>Registers an {@link dev.relism.extension.AnnotationProcessor} for {@link Limit}:
* <li>Registers an {@link AnnotationProcessor} for {@link Limit}:
* reads the annotation once per handler class at boot, resolves the key lambda
* fail-fast, then returns a pre-compiled middleware zero map lookups at request time.</li>
* </ol>
@@ -162,9 +164,9 @@ public final class LimiterExtension implements FlashExtension {
static void register(FlashContext ctx) {
ctx.find(OpenApiContributorRegistry.class)
.ifPresent(registry -> registry.add(new dev.relism.ext.openapi.OpenApiContributor() {
.ifPresent(registry -> registry.add(new OpenApiContributor() {
@Override
public dev.relism.ext.openapi.OpenApiOperationContribution operationFor(Class<?> handlerClass) {
public OpenApiOperationContribution operationFor(Class<?> handlerClass) {
if (handlerClass.getAnnotation(Limit.class) == null) {
return OpenApiOperationContribution.builder().build();
}
@@ -1,4 +1,8 @@
package dev.relism.ext.limiter;
package dev.relism.flash.ext.limiter;
import dev.relism.flash.ext.limiter.strategy.FixedWindowStrategy;
import dev.relism.flash.ext.limiter.strategy.SlidingWindowStrategy;
import dev.relism.flash.ext.limiter.strategy.TokenBucketStrategy;
/**
* Contract for a rate-limit algorithm. Implementations must be:
@@ -10,9 +14,9 @@ package dev.relism.ext.limiter;
*
* <p>Called on every request must not allocate on the hot path.
*
* @see dev.relism.ext.limiter.strategy.FixedWindowStrategy
* @see dev.relism.ext.limiter.strategy.SlidingWindowStrategy
* @see dev.relism.ext.limiter.strategy.TokenBucketStrategy
* @see FixedWindowStrategy
* @see SlidingWindowStrategy
* @see TokenBucketStrategy
*/
public interface RateLimitStrategy {
@@ -1,8 +1,8 @@
package dev.relism.ext.limiter.strategy;
package dev.relism.flash.ext.limiter.strategy;
import dev.relism.ext.limiter.Bucket;
import dev.relism.ext.limiter.LimitConfig;
import dev.relism.ext.limiter.RateLimitStrategy;
import dev.relism.flash.ext.limiter.Bucket;
import dev.relism.flash.ext.limiter.LimitConfig;
import dev.relism.flash.ext.limiter.RateLimitStrategy;
/**
* Fixed-window rate limit: allows up to {@link LimitConfig#limit()} requests per window of
@@ -1,8 +1,8 @@
package dev.relism.ext.limiter.strategy;
package dev.relism.flash.ext.limiter.strategy;
import dev.relism.ext.limiter.Bucket;
import dev.relism.ext.limiter.LimitConfig;
import dev.relism.ext.limiter.RateLimitStrategy;
import dev.relism.flash.ext.limiter.Bucket;
import dev.relism.flash.ext.limiter.LimitConfig;
import dev.relism.flash.ext.limiter.RateLimitStrategy;
/**
* Sliding-window counter rate limit: approximates a true sliding window by interpolating
@@ -1,8 +1,8 @@
package dev.relism.ext.limiter.strategy;
package dev.relism.flash.ext.limiter.strategy;
import dev.relism.ext.limiter.Bucket;
import dev.relism.ext.limiter.LimitConfig;
import dev.relism.ext.limiter.RateLimitStrategy;
import dev.relism.flash.ext.limiter.Bucket;
import dev.relism.flash.ext.limiter.LimitConfig;
import dev.relism.flash.ext.limiter.RateLimitStrategy;
/**
* Token-bucket rate limit: tokens refill continuously at a rate of
@@ -1,14 +1,14 @@
package dev.relism.ext.limiter;
package dev.relism.flash.ext.limiter;
import dev.relism.ext.openapi.OpenApiContributor;
import dev.relism.ext.openapi.OpenApiContributorRegistry;
import dev.relism.ext.openapi.OpenApiOperationContribution;
import dev.relism.ext.openapi.OpenApiResponseContribution;
import dev.relism.extension.FlashContext;
import dev.relism.models.Request;
import dev.relism.models.RequestHandler;
import dev.relism.models.Response;
import dev.relism.routing.GET;
import dev.relism.flash.ext.openapi.OpenApiContributor;
import dev.relism.flash.ext.openapi.OpenApiContributorRegistry;
import dev.relism.flash.ext.openapi.OpenApiOperationContribution;
import dev.relism.flash.ext.openapi.OpenApiResponseContribution;
import dev.relism.flash.extension.FlashContext;
import dev.relism.flash.models.Request;
import dev.relism.flash.models.RequestHandler;
import dev.relism.flash.models.Response;
import dev.relism.flash.routing.GET;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
@@ -1,4 +1,4 @@
package dev.relism.ext.oidc;
package dev.relism.flash.ext.oidc;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
@@ -1,4 +1,4 @@
package dev.relism.ext.oidc;
package dev.relism.flash.ext.oidc;
import java.util.Map;
@@ -1,4 +1,4 @@
package dev.relism.ext.oidc;
package dev.relism.flash.ext.oidc;
/**
* OAuth2 client authentication method for the token endpoint (RFC 6749 §2.3).
@@ -1,4 +1,4 @@
package dev.relism.ext.oidc;
package dev.relism.flash.ext.oidc;
import net.minidev.json.JSONValue;
@@ -1,4 +1,4 @@
package dev.relism.ext.oidc;
package dev.relism.flash.ext.oidc;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
@@ -1,4 +1,4 @@
package dev.relism.ext.oidc;
package dev.relism.flash.ext.oidc;
import net.minidev.json.JSONValue;
@@ -1,4 +1,4 @@
package dev.relism.ext.oidc;
package dev.relism.flash.ext.oidc;
import com.nimbusds.jose.JWSAlgorithm;
import com.nimbusds.jose.jwk.source.JWKSource;
@@ -12,7 +12,7 @@ import com.nimbusds.jwt.JWTClaimsSet;
import com.nimbusds.jwt.proc.ConfigurableJWTProcessor;
import com.nimbusds.jwt.proc.DefaultJWTClaimsVerifier;
import com.nimbusds.jwt.proc.DefaultJWTProcessor;
import dev.relism.exceptions.HttpException;
import dev.relism.flash.exceptions.HttpException;
import java.io.IOException;
import java.net.URL;
@@ -1,4 +1,4 @@
package dev.relism.ext.oidc;
package dev.relism.flash.ext.oidc;
import java.util.LinkedHashSet;
import java.util.List;
@@ -1,4 +1,4 @@
package dev.relism.ext.oidc;
package dev.relism.flash.ext.oidc;
/**
* Full OIDC client configuration. Build via
@@ -1,12 +1,13 @@
package dev.relism.ext.oidc;
package dev.relism.flash.ext.oidc;
import dev.relism.ext.openapi.OpenApiContributor;
import dev.relism.ext.openapi.OpenApiContributorRegistry;
import dev.relism.ext.openapi.OpenApiOperationContribution;
import dev.relism.ext.openapi.OpenApiResponseContribution;
import dev.relism.extension.FlashContext;
import dev.relism.extension.FlashExtension;
import dev.relism.extension.FlashRegistrar;
import dev.relism.flash.ext.openapi.OpenApiContributor;
import dev.relism.flash.ext.openapi.OpenApiContributorRegistry;
import dev.relism.flash.ext.openapi.OpenApiOperationContribution;
import dev.relism.flash.ext.openapi.OpenApiResponseContribution;
import dev.relism.flash.extension.FlashContext;
import dev.relism.flash.extension.FlashExtension;
import dev.relism.flash.extension.FlashRegistrar;
import dev.relism.flash.models.Request;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
@@ -245,11 +246,11 @@ public class OidcExtension implements FlashExtension {
}
}
private String absoluteRedirectUri(dev.relism.models.Request req) {
private String absoluteRedirectUri(Request req) {
return absoluteSelf(req, config.redirectUri());
}
private String absoluteSelf(dev.relism.models.Request req, String uri) {
private String absoluteSelf(Request req, String uri) {
if (!uri.startsWith("/")) return uri;
return config.selfScheme() + "://" + req.header("Host") + uri;
}
@@ -264,7 +265,7 @@ public class OidcExtension implements FlashExtension {
/**
* Loaded lazily so that {@code flash-ext-openapi} classes are only resolved at
* runtime when {@link dev.relism.ext.openapi.OpenApiContributorRegistry} is actually on the classpath.
* runtime when {@link OpenApiContributorRegistry} is actually on the classpath.
*/
private static final class OpenApiIntegration {
static void register(FlashContext ctx,
@@ -1,8 +1,10 @@
package dev.relism.ext.oidc;
package dev.relism.flash.ext.oidc;
import dev.relism.exceptions.HttpException;
import dev.relism.models.Request;
import dev.relism.routing.Middleware;
import dev.relism.flash.exceptions.HttpException;
import dev.relism.flash.extension.FlashContext;
import dev.relism.flash.models.Response;
import dev.relism.flash.models.Request;
import dev.relism.flash.routing.Middleware;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
@@ -14,7 +16,7 @@ import java.util.Map;
import java.util.Optional;
/**
* Request-level OIDC middleware. Exposed in the {@link dev.relism.extension.FlashContext}
* Request-level OIDC middleware. Exposed in the {@link FlashContext}
* for manual use on lambda routes; injected automatically for handlers annotated with
* {@link Authenticated}, {@link RolesAllowed} or {@link ScopesAllowed}.
*
@@ -192,7 +194,7 @@ public class OidcMiddleware {
* Returns claims on success, or {@code null} if a redirect was already written to
* {@code res}. Throws {@link HttpException} 401/403 for API clients.
*/
private Map<String, Object> resolve(Request req, dev.relism.models.Response res) {
private Map<String, Object> resolve(Request req, Response res) {
// 1. Bearer token
String bearerToken = extractBearerToken(req.header("Authorization"));
if (bearerToken != null) {
@@ -258,7 +260,7 @@ public class OidcMiddleware {
);
}
private void enforcePolicy(Map<String, Object> claims, OidcAuthPolicy policy, dev.relism.models.Response res) {
private void enforcePolicy(Map<String, Object> claims, OidcAuthPolicy policy, Response res) {
checkRoles(claims, policy.requiredRoles());
checkScopes(claims, policy.requiredScopes(), policy.scopeMatch(), res);
}
@@ -270,7 +272,7 @@ public class OidcMiddleware {
}
private void checkScopes(Map<String, Object> claims, String[] required, ScopesAllowed.Match match,
dev.relism.models.Response res) {
Response res) {
if (required.length == 0) return;
if (scopesAllowed(claims, required, match)) return;
res.header("WWW-Authenticate", insufficientScopeChallenge(required));
@@ -1,4 +1,4 @@
package dev.relism.ext.oidc;
package dev.relism.flash.ext.oidc;
/**
* OIDC provider endpoints discovered from {@code {issuer}/.well-known/openid-configuration}.
@@ -1,4 +1,4 @@
package dev.relism.ext.oidc;
package dev.relism.flash.ext.oidc;
import java.time.Instant;
import java.util.Map;
@@ -1,4 +1,4 @@
package dev.relism.ext.oidc;
package dev.relism.flash.ext.oidc;
import java.util.Optional;
@@ -1,4 +1,4 @@
package dev.relism.ext.oidc;
package dev.relism.flash.ext.oidc;
import java.time.Instant;
import java.util.Optional;
@@ -1,4 +1,4 @@
package dev.relism.ext.oidc;
package dev.relism.flash.ext.oidc;
/** Parsed response from an OAuth2 token endpoint. Package-private — internal use only. */
record OidcTokenResponse(
@@ -1,4 +1,4 @@
package dev.relism.ext.oidc;
package dev.relism.flash.ext.oidc;
import java.util.List;
import java.util.Map;
@@ -1,8 +1,10 @@
package dev.relism.ext.oidc;
package dev.relism.flash.ext.oidc;
import dev.relism.flash.exceptions.HttpException;
/**
* Thrown when OIDC token validation fails (signature, claims, nonce, expiry, etc.).
* Distinct from {@link dev.relism.exceptions.HttpException}: this signals a protocol-level
* Distinct from {@link HttpException}: this signals a protocol-level
* failure, not an HTTP response callers decide the appropriate status code.
*/
public final class OidcValidationException extends RuntimeException {
@@ -1,4 +1,4 @@
package dev.relism.ext.oidc;
package dev.relism.flash.ext.oidc;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
@@ -1,4 +1,4 @@
package dev.relism.ext.oidc;
package dev.relism.flash.ext.oidc;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
@@ -1,4 +1,4 @@
package dev.relism.ext.oidc;
package dev.relism.flash.ext.oidc;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
@@ -1,4 +1,4 @@
package dev.relism.ext.oidc;
package dev.relism.flash.ext.oidc;
import net.minidev.json.JSONValue;
@@ -1,4 +1,4 @@
package dev.relism.ext.oidc;
package dev.relism.flash.ext.oidc;
import org.junit.jupiter.api.Test;
@@ -1,4 +1,4 @@
package dev.relism.ext.oidc;
package dev.relism.flash.ext.oidc;
import org.junit.jupiter.api.Test;
@@ -1,6 +1,10 @@
package dev.relism.ext.oidc;
package dev.relism.flash.ext.oidc;
import dev.relism.ext.openapi.OpenApiContributor;
import dev.relism.flash.ext.openapi.OpenApiContributorRegistry;
import dev.relism.flash.ext.openapi.OpenApiOperationContribution;
import dev.relism.flash.ext.openapi.OpenApiResponseContribution;
import dev.relism.flash.ext.openapi.OpenApiContributor;
import dev.relism.flash.extension.FlashContext;
import org.junit.jupiter.api.Test;
import java.lang.reflect.Constructor;
@@ -82,24 +86,24 @@ class OidcOpenApiInteropTest {
@Test
void securityContribution_presentForAuthenticatedHandler() throws Exception {
dev.relism.ext.openapi.OpenApiOperationContribution operation = contributor().operationFor(AuthOnly.class);
OpenApiOperationContribution operation = contributor().operationFor(AuthOnly.class);
List<Map<String, List<String>>> security = operation.security();
assertEquals(1, security.size());
assertTrue(security.getFirst().containsKey("issuer"));
}
private static OpenApiContributor contributor() throws Exception {
Class<?> clazz = Class.forName("dev.relism.ext.oidc.OidcExtension$OpenApiIntegration");
Class<?> clazz = Class.forName("dev.relism.flash.ext.oidc.OidcExtension$OpenApiIntegration");
Constructor<?> ctor = clazz.getDeclaredConstructor();
ctor.setAccessible(true);
Object instance = ctor.newInstance();
Method m = clazz.getDeclaredMethod("register", dev.relism.extension.FlashContext.class, OidcConfig.class, OidcProviderMetadata.class);
Method m = clazz.getDeclaredMethod("register", FlashContext.class, OidcConfig.class, OidcProviderMetadata.class);
m.setAccessible(true);
dev.relism.extension.FlashContext ctx = new dev.relism.extension.FlashContext();
dev.relism.ext.openapi.OpenApiContributorRegistry registry = new dev.relism.ext.openapi.OpenApiContributorRegistry();
ctx.provide(dev.relism.ext.openapi.OpenApiContributorRegistry.class, registry);
FlashContext ctx = new FlashContext();
OpenApiContributorRegistry registry = new OpenApiContributorRegistry();
ctx.provide(OpenApiContributorRegistry.class, registry);
OidcConfig config = OidcConfig.builder("https://issuer", "c", "s", "/cb").build();
OidcProviderMetadata meta = new OidcProviderMetadata("a", "t", "u", "j", "e");
@@ -109,9 +113,9 @@ class OidcOpenApiInteropTest {
}
private static Map<Integer, String> responses(Class<?> cls) throws Exception {
Map<Integer, dev.relism.ext.openapi.OpenApiResponseContribution> byCode = contributor().operationFor(cls).responses();
Map<Integer, OpenApiResponseContribution> byCode = contributor().operationFor(cls).responses();
java.util.LinkedHashMap<Integer, String> out = new java.util.LinkedHashMap<>();
for (Map.Entry<Integer, dev.relism.ext.openapi.OpenApiResponseContribution> e : byCode.entrySet()) {
for (Map.Entry<Integer, OpenApiResponseContribution> e : byCode.entrySet()) {
out.put(e.getKey(), e.getValue().description());
}
return out;
@@ -1,4 +1,4 @@
package dev.relism.ext.oidc;
package dev.relism.flash.ext.oidc;
import org.junit.jupiter.api.Test;
@@ -1,4 +1,4 @@
package dev.relism.ext.openapi;
package dev.relism.flash.ext.openapi;
import java.lang.annotation.ElementType;
import java.lang.annotation.Repeatable;
@@ -1,4 +1,4 @@
package dev.relism.ext.openapi;
package dev.relism.flash.ext.openapi;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
@@ -1,4 +1,4 @@
package dev.relism.ext.openapi;
package dev.relism.flash.ext.openapi;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
@@ -1,4 +1,4 @@
package dev.relism.ext.openapi;
package dev.relism.flash.ext.openapi;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
@@ -1,6 +1,6 @@
package dev.relism.ext.openapi;
package dev.relism.flash.ext.openapi;
import dev.relism.http.ContentType;
import dev.relism.flash.http.ContentType;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
@@ -1,14 +1,15 @@
package dev.relism.ext.openapi;
package dev.relism.flash.ext.openapi;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonProperty.Access;
import dev.relism.http.ContentType;
import dev.relism.http.HttpStatus;
import dev.relism.models.Request;
import dev.relism.models.Response;
import dev.relism.routing.Route;
import dev.relism.flash.http.HttpMethod;
import dev.relism.flash.http.ContentType;
import dev.relism.flash.http.HttpStatus;
import dev.relism.flash.models.Request;
import dev.relism.flash.models.Response;
import dev.relism.flash.routing.Route;
import java.lang.annotation.Annotation;
import java.lang.reflect.Array;
@@ -26,10 +27,8 @@ import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
@@ -566,9 +565,9 @@ public final class OpenApiBuilder {
if (meta == null) continue;
String path = readPathValue(ann);
if (path == null) continue;
dev.relism.http.HttpMethod method = meta.method();
HttpMethod method = meta.method();
return new Route() {
@Override public dev.relism.http.HttpMethod method() { return method; }
@Override public HttpMethod method() { return method; }
@Override public String path() { return path; }
@Override public Class<? extends Annotation> annotationType() { return Route.class; }
};
@@ -1,4 +1,4 @@
package dev.relism.ext.openapi;
package dev.relism.flash.ext.openapi;
import java.util.Map;
@@ -1,4 +1,4 @@
package dev.relism.ext.openapi;
package dev.relism.flash.ext.openapi;
import java.util.Collections;
import java.util.List;
@@ -1,14 +1,15 @@
package dev.relism.ext.openapi;
package dev.relism.flash.ext.openapi;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.json.JsonMapper;
import com.fasterxml.jackson.dataformat.yaml.YAMLMapper;
import dev.relism.extension.FlashContext;
import dev.relism.extension.FlashExtension;
import dev.relism.extension.FlashRegistrar;
import dev.relism.extension.RouteEvent;
import dev.relism.http.ContentType;
import dev.relism.routing.Route;
import dev.relism.flash.extension.FlashContext;
import dev.relism.flash.extension.FlashExtension;
import dev.relism.flash.extension.FlashRegistrar;
import dev.relism.flash.extension.RouteEvent;
import dev.relism.flash.http.ContentType;
import dev.relism.flash.http.HttpMethod;
import dev.relism.flash.routing.Route;
/**
* Generates and serves an OpenAPI 3.0 spec and Swagger UI under a configurable base path.
@@ -132,7 +133,7 @@ public class OpenApiExtension implements FlashExtension {
private static Route routeOf(RouteEvent event) {
return new Route() {
@Override public dev.relism.http.HttpMethod method() { return event.method(); }
@Override public HttpMethod method() { return event.method(); }
@Override public String path() { return event.path(); }
@Override public Class<? extends java.lang.annotation.Annotation> annotationType() { return Route.class; }
};
@@ -1,4 +1,4 @@
package dev.relism.ext.openapi;
package dev.relism.flash.ext.openapi;
import java.util.ArrayList;
import java.util.LinkedHashMap;
@@ -1,4 +1,4 @@
package dev.relism.ext.openapi;
package dev.relism.flash.ext.openapi;
import java.util.LinkedHashMap;
import java.util.Map;
@@ -1,4 +1,4 @@
package dev.relism.ext.openapi;
package dev.relism.flash.ext.openapi;
import java.lang.annotation.ElementType;
import java.lang.annotation.Repeatable;
@@ -1,4 +1,4 @@
package dev.relism.ext.openapi;
package dev.relism.flash.ext.openapi;
import java.util.Locale;
@@ -1,4 +1,4 @@
package dev.relism.ext.openapi;
package dev.relism.flash.ext.openapi;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
@@ -1,4 +1,4 @@
package dev.relism.ext.openapi;
package dev.relism.flash.ext.openapi;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
@@ -1,4 +1,4 @@
package dev.relism.ext.openapi;
package dev.relism.flash.ext.openapi;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
@@ -1,4 +1,4 @@
package dev.relism.ext.openapi;
package dev.relism.flash.ext.openapi;
import java.util.Locale;
@@ -1,10 +1,13 @@
package dev.relism.ext.openapi;
package dev.relism.flash.ext.openapi;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import dev.relism.http.ContentType;
import dev.relism.routing.GET;
import dev.relism.flash.models.Request;
import dev.relism.flash.models.RequestHandler;
import dev.relism.flash.models.Response;
import dev.relism.flash.http.ContentType;
import dev.relism.flash.routing.GET;
import org.junit.jupiter.api.Test;
import java.util.List;
@@ -21,9 +24,9 @@ class OpenApiBuilderTest {
@ApiOperation(summary = "Get user")
@Parameter(name = "expand", in = ParameterIn.QUERY, required = false, type = SchemaType.STRING, examples = {"roles", "permissions"})
@APIResponse(responseCode = "200", description = "User found", content = @Content(contentType = ContentType.JSON, schema = UserDto.class))
static class GetUserHandler extends dev.relism.models.RequestHandler {
static class GetUserHandler extends RequestHandler {
@Override
public Object handle(dev.relism.models.Request request, dev.relism.models.Response response) {
public Object handle(Request request, Response response) {
return null;
}
}
@@ -31,9 +34,9 @@ class OpenApiBuilderTest {
@GET("/users")
@ApiOperation(summary = "List users")
@APIResponse(responseCode = "200", content = @Content(contentType = ContentType.JSON, schema = UserDto.class, array = true))
static class ListUsersHandler extends dev.relism.models.RequestHandler {
static class ListUsersHandler extends RequestHandler {
@Override
public Object handle(dev.relism.models.Request request, dev.relism.models.Response response) {
public Object handle(Request request, Response response) {
return null;
}
}
@@ -41,9 +44,9 @@ class OpenApiBuilderTest {
@GET("/ping")
@ApiOperation(summary = "Ping")
@APIResponse(responseCode = "204", description = "No content", content = @Content(contentType = ContentType.NONE))
static class PingHandler extends dev.relism.models.RequestHandler {
static class PingHandler extends RequestHandler {
@Override
public Object handle(dev.relism.models.Request request, dev.relism.models.Response response) {
public Object handle(Request request, Response response) {
return null;
}
}
@@ -51,9 +54,9 @@ class OpenApiBuilderTest {
@GET("/secure")
@ApiOperation(summary = "Secure")
@APIResponse(responseCode = "403", description = "Custom forbidden")
static class SecureHandler extends dev.relism.models.RequestHandler {
static class SecureHandler extends RequestHandler {
@Override
public Object handle(dev.relism.models.Request request, dev.relism.models.Response response) {
public Object handle(Request request, Response response) {
return null;
}
}
@@ -61,9 +64,9 @@ class OpenApiBuilderTest {
@GET("/typed")
@ApiOperation(summary = "Typed")
@APIResponse(responseCode = "200", content = @Content)
static class TypedHandler extends dev.relism.models.RequestHandler {
static class TypedHandler extends RequestHandler {
@Override
public UserDto handle(dev.relism.models.Request request, dev.relism.models.Response response) {
public UserDto handle(Request request, Response response) {
return new UserDto();
}
}
@@ -71,9 +74,9 @@ class OpenApiBuilderTest {
@GET("/typed-list")
@ApiOperation(summary = "Typed list")
@APIResponse(responseCode = "200", content = @Content)
static class TypedListHandler extends dev.relism.models.RequestHandler {
static class TypedListHandler extends RequestHandler {
@Override
public List<UserDto> handle(dev.relism.models.Request request, dev.relism.models.Response response) {
public List<UserDto> handle(Request request, Response response) {
return List.of();
}
}
@@ -81,9 +84,9 @@ class OpenApiBuilderTest {
@GET("/typed-map")
@ApiOperation(summary = "Typed map")
@APIResponse(responseCode = "200", content = @Content)
static class TypedMapHandler extends dev.relism.models.RequestHandler {
static class TypedMapHandler extends RequestHandler {
@Override
public Map<String, UserDto> handle(dev.relism.models.Request request, dev.relism.models.Response response) {
public Map<String, UserDto> handle(Request request, Response response) {
return Map.of();
}
}
@@ -91,9 +94,9 @@ class OpenApiBuilderTest {
@GET("/merge")
@ApiOperation(summary = "Merge")
@APIResponse(responseCode = "200", description = "Manual 200")
static class MergeHandler extends dev.relism.models.RequestHandler {
static class MergeHandler extends RequestHandler {
@Override
public Object handle(dev.relism.models.Request request, dev.relism.models.Response response) {
public Object handle(Request request, Response response) {
return null;
}
}
@@ -1,16 +1,17 @@
package dev.relism.ext.openapi;
package dev.relism.flash.ext.openapi;
import com.fasterxml.jackson.databind.ObjectMapper;
import dev.relism.extension.FlashContext;
import dev.relism.extension.FlashRegistrar;
import dev.relism.extension.RouteEvent;
import dev.relism.extension.RouteListener;
import dev.relism.http.ContentType;
import dev.relism.http.HttpMethod;
import dev.relism.models.RequestHandler;
import dev.relism.models.Response;
import dev.relism.routing.GET;
import dev.relism.routing.Middleware;
import dev.relism.flash.extension.FlashContext;
import dev.relism.flash.extension.FlashRegistrar;
import dev.relism.flash.extension.RouteEvent;
import dev.relism.flash.extension.RouteListener;
import dev.relism.flash.models.Request;
import dev.relism.flash.http.ContentType;
import dev.relism.flash.http.HttpMethod;
import dev.relism.flash.models.RequestHandler;
import dev.relism.flash.models.Response;
import dev.relism.flash.routing.GET;
import dev.relism.flash.routing.Middleware;
import org.junit.jupiter.api.Test;
import java.util.ArrayList;
@@ -30,7 +31,7 @@ class OpenApiExtensionTest {
@APIResponse(responseCode = "200", description = "OK")
static class HealthHandler extends RequestHandler {
@Override
public Object handle(dev.relism.models.Request request, Response response) {
public Object handle(Request request, Response response) {
return "ok";
}
}
@@ -88,7 +89,7 @@ class OpenApiExtensionTest {
@ApiOperation(summary = "Scoped users")
static class ScopedUsersHandler extends RequestHandler {
@Override
public Object handle(dev.relism.models.Request request, Response response) {
public Object handle(Request request, Response response) {
return List.of();
}
}
@@ -1,8 +1,8 @@
package dev.relism.ext.routeviewer;
package dev.relism.flash.ext.routeviewer;
import dev.relism.ext.routeviewer.model.RouteGraph;
import dev.relism.ext.routeviewer.model.RouterNode;
import dev.relism.ext.routeviewer.model.RouteRecord;
import dev.relism.flash.ext.routeviewer.model.RouteGraph;
import dev.relism.flash.ext.routeviewer.model.RouterNode;
import dev.relism.flash.ext.routeviewer.model.RouteRecord;
import java.util.ArrayList;
import java.util.List;
@@ -1,9 +1,9 @@
package dev.relism.ext.routeviewer;
package dev.relism.flash.ext.routeviewer;
import dev.relism.ext.routeviewer.model.RouteGraph;
import dev.relism.http.ContentType;
import dev.relism.models.Request;
import dev.relism.models.Response;
import dev.relism.flash.ext.routeviewer.model.RouteGraph;
import dev.relism.flash.http.ContentType;
import dev.relism.flash.models.Request;
import dev.relism.flash.models.Response;
/**
* Serves {@code GET /routeviewer/data} the JSON payload consumed by the React SPA.
@@ -1,11 +1,11 @@
package dev.relism.ext.routeviewer;
package dev.relism.flash.ext.routeviewer;
import dev.relism.ext.routeviewer.model.RouteGraph;
import dev.relism.extension.ExtensionPhase;
import dev.relism.extension.FlashContext;
import dev.relism.extension.FlashExtension;
import dev.relism.extension.FlashRegistrar;
import dev.relism.http.ContentType;
import dev.relism.flash.ext.routeviewer.model.RouteGraph;
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.http.ContentType;
/**
* Mounts an interactive route-graph viewer at a configurable HTTP endpoint.
@@ -1,8 +1,8 @@
package dev.relism.ext.routeviewer;
package dev.relism.flash.ext.routeviewer;
import dev.relism.http.ContentType;
import dev.relism.models.Request;
import dev.relism.models.Response;
import dev.relism.flash.http.ContentType;
import dev.relism.flash.models.Request;
import dev.relism.flash.models.Response;
import java.io.IOException;
import java.io.InputStream;
@@ -1,8 +1,8 @@
package dev.relism.ext.routeviewer;
package dev.relism.flash.ext.routeviewer;
import dev.relism.http.ContentType;
import dev.relism.models.Request;
import dev.relism.models.Response;
import dev.relism.flash.http.ContentType;
import dev.relism.flash.models.Request;
import dev.relism.flash.models.Response;
import java.io.IOException;
import java.io.InputStream;
@@ -1,6 +1,7 @@
package dev.relism.ext.routeviewer.model;
package dev.relism.flash.ext.routeviewer.model;
import dev.relism.extension.RouteEvent;
import dev.relism.flash.extension.RouteListener;
import dev.relism.flash.extension.RouteEvent;
import java.util.*;
@@ -18,7 +19,7 @@ public class RouteGraph {
/** Namespace → routerType, filled on first event for each namespace. */
private final Map<String, String> routerTypes = new LinkedHashMap<>();
/** Called once per route by the {@link dev.relism.extension.RouteListener}. */
/** Called once per route by the {@link RouteListener}. */
public void add(RouteEvent event) {
routerTypes.putIfAbsent(event.namespace(), event.routerType());
byNamespace
@@ -1,7 +1,7 @@
package dev.relism.ext.routeviewer.model;
package dev.relism.flash.ext.routeviewer.model;
import dev.relism.extension.RouteEvent;
import dev.relism.routing.Middleware;
import dev.relism.flash.extension.RouteEvent;
import dev.relism.flash.routing.Middleware;
import java.lang.annotation.Annotation;
import java.util.ArrayList;
@@ -1,4 +1,4 @@
package dev.relism.ext.routeviewer.model;
package dev.relism.flash.ext.routeviewer.model;
import java.util.List;

Some files were not shown because too many files have changed in this diff Show More