ConverterService.java
/*
* SPDX-FileCopyrightText: 2025 kaumei.io
* SPDX-License-Identifier: Apache-2.0
*/
package io.kaumei.jdbc.anno.store;
import io.kaumei.jdbc.anno.ProcessorEnvironment;
import io.kaumei.jdbc.anno.ctx.Context;
import io.kaumei.jdbc.anno.msg.JdbcMsg;
import org.jspecify.annotations.Nullable;
import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
import javax.lang.model.type.TypeMirror;
import java.util.*;
import java.util.function.BiConsumer;
import java.util.function.Function;
import static java.util.Objects.requireNonNull;
public class ConverterService<T extends Converter> {
// ----- services
private final Context ctx;
private final String prefix;
// ----- state
private final Store<T> basic;
private final Store<T> global;
private final Map<Element, Store<T>> local = new HashMap<>();
// ------------------------------------------------------------------------
public ConverterService(Context ctx, String prefix) {
this.ctx = ctx;
this.prefix = prefix;
this.basic = new Store<>(this.ctx, prefix + ".basic");
this.global = new Store<>(this.ctx, prefix + ".global", this.basic);
}
public void init(ProcessorEnvironment roundEnv) {
for (var element : roundEnv.jdbcInterfaces()) {
var store = new Store<>(this.ctx, prefix + ".local." + element.getQualifiedName(), this.global);
this.local.put(element, store);
}
}
public Store<T> basic() {
return this.basic;
}
public Store<T> global() {
return this.global;
}
public Store<T> local(Element element) {
return requireNonNull(local.get(element));
}
public Store<T> getStoreFor(@Nullable Element element) {
while (element != null && element.getKind() != ElementKind.PACKAGE) {
var store = local.get(element);
if (store != null) {
return store;
}
element = element.getEnclosingElement();
}
return this.global;
}
// ------------------------------------------------------------
public void forEachPending(BiConsumer<Store<T>, SearchKey> consumer) {
this.basic.forEachPending(consumer);
this.global.forEachPending(consumer);
local.values().forEach((store) -> store.forEachPending(consumer));
}
public void forEachPlaceholder(PlaceholderResolver<T> resolver) {
this.basic.forEachPlaceholder(resolver);
this.global.forEachPlaceholder(resolver);
local.values().forEach((store) -> store.forEachPlaceholder(resolver));
}
// ------------------------------------------------------------------------
private enum Result {OK, INVALID, CYCLE}
private record ResultAndCycle(Result result, List<SearchKey> cycle, @Nullable SearchKey start) {
static final ResultAndCycle RESULT_OK = new ResultAndCycle(Result.OK, List.of(), null);
static final ResultAndCycle RESULT_INVALID = new ResultAndCycle(Result.INVALID, List.of(), null);
static ResultAndCycle cycle(List<SearchKey> cycle, SearchKey start) {
return new ResultAndCycle(Result.CYCLE, cycle, start);
}
ResultAndCycle combine(ResultAndCycle other) {
if (this.result == Result.INVALID) {
return this;
}
// this in (OK,CYCLE)
return switch (other.result) {
case OK -> this;
case INVALID, CYCLE -> other;
};
}
}
public void process(Store<T> store, SearchKey searchKey, Function<TypeMirror, @Nullable FactoryResult<T>> func) {
final var cycle = new LinkedCycle<SearchKey>();
process(cycle, store, searchKey, func);
}
private ResultAndCycle process(LinkedCycle<SearchKey> cycle,
Store<T> store,
SearchKey searchKey,
Function<TypeMirror, @Nullable FactoryResult<T>> func) {
if (!cycle.push(searchKey)) {
return ResultAndCycle.cycle(cycle.asList(), searchKey);
}
try {
// ----- check if we already have a result for this search key
var result = store.search(searchKey.toStoreId(), false);
if (result.hasPlaceholder()) {
var otherResult = ResultAndCycle.RESULT_OK;
var dependent = new LinkedHashSet<StoreID>();
for (var other : result.placeholder().otherPlaceholders()) {
var current = process(cycle, store, other, func);
if (current.result == Result.INVALID) {
dependent.add(other.toStoreId());
}
otherResult = otherResult.combine(current);
}
if (otherResult.result == Result.INVALID) {
result.store().markInvalid(result.storeId(), JdbcMsg.INVALID_DEPENDENT_CONVERTER, dependent);
return ResultAndCycle.RESULT_INVALID;
} else if (otherResult.result == Result.CYCLE && cycle.hasLast(otherResult.start)) {
var currentCycle = cycle.calculateCycle(otherResult.cycle);
result.store().markInvalid(result.storeId(), JdbcMsg.cycle(currentCycle));
return ResultAndCycle.RESULT_INVALID;
}
return otherResult;
} else if (result.state() != SearchState.NOT_FOUND) {
return result.hasMessages() ? ResultAndCycle.RESULT_INVALID : ResultAndCycle.RESULT_OK;
}
// ----- try to generate dynamic converter
FactoryResult<T> factoryResult = func.apply(searchKey.type());
if (factoryResult == null) {
store.flagPending(searchKey);
return ResultAndCycle.RESULT_OK; // this is handled as ok
}
store = this.global(); // used-type converter owns global
// ----- placeholder, check dependencies
var otherResult = ResultAndCycle.RESULT_OK;
var dependent = new LinkedHashSet<StoreID>();
if (factoryResult.hasPlaceholder()) {
for (var other : factoryResult.placeholder().otherPlaceholders()) {
var current = process(cycle, store, other, func);
if (current.result == Result.INVALID) {
dependent.add(other.toStoreId());
}
otherResult = otherResult.combine(current);
}
}
if (otherResult.result == Result.INVALID) {
factoryResult = factoryResult.newWithMessages(JdbcMsg.INVALID_DEPENDENT_CONVERTER, dependent);
} else if (otherResult.result == Result.CYCLE && cycle.hasLast(otherResult.start)) {
var currentCycle = cycle.calculateCycle(otherResult.cycle);
factoryResult = factoryResult.newWithMessages(JdbcMsg.cycle(currentCycle));
otherResult = ResultAndCycle.RESULT_INVALID;
}
store.add(factoryResult);
return otherResult;
} finally {
cycle.pop();
}
}
// ------------------------------------------------------------------------
public String csvStats() {
var count = 0;
for (var repo : local.values()) {
count += repo.size();
}
// basic, global, local
return basic.size() + "," + global.size() + "," + count;
}
public void dump(StringBuilder out) {
basic.dump(out);
global.dump(out);
var t = new TreeMap<String, Store<T>>();
local.forEach((element, store) -> t.put(element.toString(), store));
for (var entry : t.entrySet()) {
var store = entry.getValue();
if (store.size() != 0) {
store.dump(out);
}
}
}
}