Store.java
/*
* SPDX-FileCopyrightText: 2025 kaumei.io
* SPDX-License-Identifier: Apache-2.0
*/
package io.kaumei.jdbc.anno.store;
import io.kaumei.jdbc.anno.ctx.Context;
import io.kaumei.jdbc.anno.msg.JdbcMsg;
import io.kaumei.jdbc.anno.msg.Msg;
import org.jspecify.annotations.Nullable;
import javax.lang.model.type.TypeKind;
import java.util.*;
import java.util.function.BiConsumer;
import static io.kaumei.jdbc.anno.utils.AnnoUtils.requireState;
public class Store<T extends Converter> {
// ----- services
protected final Context ctx;
protected final String name;
protected final @Nullable Store<T> parent;
// ----- state
private final Map<StoreID, Entry<T>> id2converter = new HashMap<>();
private final Set<SearchKey> pending = new HashSet<>();
// ------------------------------------------------------------------------
public Store(Context ctx, String name) {
this.ctx = ctx;
this.name = name;
this.parent = null;
}
public Store(Context ctx, String name, @Nullable Store<T> parent) {
this.ctx = ctx;
this.name = name;
this.parent = parent;
}
public void dump(StringBuilder out) {
out.append(this.name).append(" ----------------------------------------\n");
if (!id2converter.isEmpty()) {
var sorted = new TreeMap<>(id2converter);
for (var entry : sorted.entrySet()) {
out.append(entry.getKey());
out.append(": ");
entry.getValue().append(out);
out.append("\n");
}
}
}
public int size() {
return this.id2converter.size();
}
@Override
public String toString() {
return "Store{name=" + name + ", size=" + id2converter.size() + (this.parent == null ? "" : ", parent=" + this.parent.name) + "}";
}
// collection -------------------------------------------------------------
public void add(StoreID id, T value) {
var entry = this.id2converter.get(id);
if (entry == null) {
entry = Entry.valid(id, value);
this.id2converter.put(id, entry);
} else {
entry.markAsDuplicate(value.source());
}
}
public void addOpt(@Nullable FactoryResult<T> factoryResult) {
if (factoryResult != null) {
add(factoryResult);
}
}
public void add(FactoryResult<T> result) {
var id = result.id();
var entry = this.id2converter.get(id);
if (entry == null) {
if (result.hasMessages()) {
entry = Entry.invalid(id, result.source(), result.messages(), result.dependent());
} else if (result.hasConverter()) {
entry = Entry.valid(id, result.converter());
} else if (result.hasPlaceholder()) {
entry = Entry.valid(id, result.placeholder());
} else {
throw new IllegalStateException("Invalid: " + result); // sanity-check
}
this.id2converter.put(id, entry);
} else {
entry.markAsDuplicate(result.source());
}
}
public SearchResult<T> markInvalid(StoreID storeId, Msg.Messages messages) {
var entry = requireState(this.id2converter.get(storeId), () -> "Unknown id: " + storeId);
entry.markAsInvalid(messages, Set.of());
return entry.toSearchResult(storeId, this);
}
public SearchResult<T> markInvalid(StoreID storeId, Msg.Messages messages, Set<StoreID> dependent) {
var entry = requireState(this.id2converter.get(storeId), () -> "Unknown id: " + storeId);
entry.markAsInvalid(messages, dependent);
return entry.toSearchResult(storeId, this);
}
public SearchResult<T> markResolve(StoreID storeId, T converter) {
var entry = requireState(this.id2converter.get(storeId), () -> "Unknown id: " + storeId);
entry.resolve(converter);
return entry.toSearchResult(storeId, this);
}
public void flagPending(SearchKey key) {
pending.add(key);
}
public void forEachPending(BiConsumer<Store<T>, SearchKey> consumer) {
for (var nameType : this.pending) {
consumer.accept(this, nameType);
}
this.pending.clear();
}
public void forEachPlaceholder(PlaceholderResolver<T> resolver) {
for (var id : this.id2converter.keySet()) {
var item = this.id2converter.get(id);
if (item.placeholder != null) {
resolver.apply(this, item.id, item.placeholder);
}
}
}
public SearchResult<T> get(StoreID storeId) {
var entry = this.id2converter.get(storeId);
return entry != null
? entry.toSearchResult(storeId, this)
: SearchResult.notFound(storeId);
}
public SearchResult<T> search(StoreID storeId, boolean fullMessages) {
var found = this.searchEntryFound(storeId);
return found != null
? found.store.toSearchResult(storeId, found.entry, fullMessages)
: SearchResult.notFound(storeId);
}
public FactoryResult<T> searchHierarchy(SearchKey searchKey) {
var storeId = searchKey.toStoreId();
var found = this.searchEntryFound(storeId);
if (found != null) {
return found.entry.toFactoryResult(storeId);
} else if (searchKey.hasName() || searchKey.type().getKind() != TypeKind.DECLARED) {
return FactoryResult.of(storeId, SourceDV.empty(), JdbcMsg.NOT_FOUND);
}
return searchTypeHierarchy(searchKey);
}
private FactoryResult<T> searchTypeHierarchy(SearchKey searchKey) {
Map<SearchKey, Integer> hierarchy = ctx.collectTypeHierarchy2(searchKey.type());
int level = -1;
var candidates = new TreeMap<StoreID, Found<T>>();
for (var entry : hierarchy.entrySet()) {
var storeId = entry.getKey().toStoreId();
var found = this.searchEntryFound(storeId);
if (found != null && entry.getValue() >= level) {
if (entry.getValue() > level) {
level = entry.getValue();
candidates.clear();
}
candidates.put(storeId, found);
}
}
var storeId = searchKey.toStoreId();
if (candidates.isEmpty()) {
return FactoryResult.of(storeId, SourceDV.empty(), JdbcMsg.NOT_FOUND);
} else if (candidates.size() > 1) {
return FactoryResult.of(storeId, SourceDV.empty(), JdbcMsg.ambiguous(candidates.keySet()));
}
var found = candidates.values().iterator().next();
return found.entry.toFactoryResult(storeId);
}
private @Nullable Entry<T> searchEntry(StoreID storeId) {
var found = this.searchEntryFound(storeId);
return found == null ? null : found.entry;
}
private record Found<T extends Converter>(Store<T> store, Entry<T> entry) {
}
private @Nullable Found<T> searchEntryFound(StoreID storeId) {
var entry = this.id2converter.get(storeId);
if (entry != null) {
return new Found<>(this, entry);
} else if (this.parent != null) {
return parent.searchEntryFound(storeId);
}
return null;
}
// -------------------------
SearchResult<T> toSearchResult(StoreID storeId, Entry<T> entry, boolean fullMessages) {
if (!entry.messages.hasMessages()) {
return SearchResult.valid(storeId, entry.state.searchState, this, entry.converter, entry.placeholder);
} else if (!fullMessages) {
return SearchResult.invalid(storeId, entry.state.searchState, this, entry.messages);
}
var sb = new StringBuilder();
this.dump(sb, entry, 1);
return SearchResult.invalid(storeId, entry.state.searchState, this, JdbcMsg.converter(sb.toString()));
}
private void dump(StringBuilder sb, Entry<T> entry, int indentCount) {
var indent = " ".repeat(indentCount);
if (!sb.isEmpty()) {
sb.append('\n');
}
sb.append(indent).append("source...: ");
sb.append(entry.source.value());
sb.append('\n').append(indent).append("id.......: ").append(entry.id());
if (entry.messages.hasMessages()) {
var first = true;
for (var m : entry.messages) {
if (first) {
sb.append('\n').append(indent).append("messages.: ");
first = false;
} else {
sb.append('\n').append(indent).append(" ");
}
sb.append(m.text().replaceAll("\n", "\n" + indent + " "));
}
}
if (!entry.dependent.isEmpty()) {
for (var other : new TreeSet<>(entry.dependent)) {
var otherEntry = searchEntry(other);
if (otherEntry == null) {
sb.append('\n').append(indent).append(" converter: not found");
sb.append('\n').append(indent).append(" id.......: ").append(other);
} else if (otherEntry.messages.hasMessages()) {
// we only print dependent with messages
this.dump(sb, otherEntry, indentCount + 1);
}
}
}
}
// ------------------------------------------------------------------------
enum EntryState {
VALID(SearchState.VALID),
INVALID(SearchState.INVALID),
DUPLICATE(SearchState.INVALID);
final SearchState searchState;
EntryState(SearchState searchState) {
this.searchState = searchState;
}
}
static class Entry<T extends Converter> {
static <T extends Converter> Entry<T> valid(StoreID id, T value) {
return new Entry<>(id, EntryState.VALID, value.source(), value, null, Msg.empty());
}
static <T extends Converter> Entry<T> valid(StoreID id, Converter.Placeholder value) {
return new Entry<>(id, EntryState.VALID, value.source(), null, value, Msg.empty());
}
static <T extends Converter> Entry<T> invalid(StoreID id, SourceDV source, Msg.Messages messages, Set<StoreID> dependent) {
if (!messages.hasMessages()) { // sanity-check
throw new IllegalArgumentException(); // sanity-check
}
var entry = new Entry<T>(id, EntryState.INVALID, source, null, null, messages);
entry.dependent = dependent;
return entry;
}
// --------------------------------------------------------------------
private final StoreID id;
private final SourceDV source;
private EntryState state;
private @Nullable T converter;
private Converter.@Nullable Placeholder placeholder;
private Msg.Messages messages;
private Set<StoreID> dependent;
private Entry(StoreID id, EntryState state, SourceDV source, @Nullable T converter, Converter.@Nullable Placeholder placeholder, Msg.Messages messages) {
this.id = id;
this.state = state;
this.source = Objects.requireNonNull(source);
this.converter = converter;
this.placeholder = placeholder;
this.messages = messages;
this.dependent = Set.of();
}
SearchResult<T> toSearchResult(StoreID storeId, Store<T> store) {
if (this.messages.hasMessages()) {
return SearchResult.invalid(storeId, state.searchState, store, this.messages);
}
return SearchResult.valid(storeId, state.searchState, store, this.converter, this.placeholder);
}
FactoryResult<T> toFactoryResult(StoreID storeId) {
if (this.messages.hasMessages()) {
return FactoryResult.of(storeId, this.source, this.messages, this.dependent);
} else if (this.converter != null) {
return FactoryResult.of(storeId, this.converter);
} else if (this.placeholder != null) {
return FactoryResult.of(storeId, this.placeholder);
}
throw new IllegalStateException(this.toString()); // sanity-check
}
void markAsDuplicate(SourceDV source) {
if (state == EntryState.DUPLICATE) {
Msg.merge(this.messages, JdbcMsg.duplicateConverter(source));
} else {
this.state = EntryState.DUPLICATE;
this.placeholder = null;
this.converter = null;
this.messages = Msg.merge(JdbcMsg.duplicateConverter(this.source),
JdbcMsg.duplicateConverter(source));
}
}
void resolve(T converter) {
// sanity-check:on
if (this.converter != null || this.placeholder == null || this.messages.hasMessages() || !this.dependent.isEmpty()) {
throw new IllegalStateException(this.toString());
}
// sanity-check:off
this.converter = converter;
this.placeholder = null;
this.state = EntryState.VALID;
}
void markAsInvalid(Msg.Messages messages, Set<StoreID> dependent) {
// sanity-check:on
if (!messages.hasMessages()) {
throw new IllegalArgumentException();
} else if (state == EntryState.DUPLICATE || this.messages.hasMessages() || !this.dependent.isEmpty()) {
throw new IllegalStateException(this.toString());
}
// sanity-check:off
this.state = EntryState.INVALID;
this.placeholder = null;
this.converter = null;
this.messages = messages;
this.dependent = dependent;
}
void append(StringBuilder sb) {
sb.append(this.state);
sb.append("\n ");
sb.append("Source.....: ");
sb.append(source);
if (this.converter != null) {
sb.append("\n ");
sb.append("Converter..: ");
sb.append(this.converter);
}
if (this.placeholder != null) {
sb.append("\n ");
sb.append("Placeholder: ");
sb.append(this.placeholder);
}
if (messages.hasMessages()) {
sb.append("\n ");
sb.append("Messages...: ");
sb.append(this.messages.join(","));
}
if (!dependent.isEmpty()) {
sb.append("\n ");
sb.append("Dependent..: ");
sb.append(this.dependent);
}
}
@Override
public String toString() {
return "Entry{id=" + id + ", state=" + state //
+ (converter != null ? ", converter=" + converter : "") //
+ (placeholder != null ? ", placeholder=" + placeholder : "") //
+ (!messages.hasMessages() ? ", messages=" + messages : "") //
+ (!dependent.isEmpty() ? ", dependent=" + dependent : "") //
+ '}';
}
// --------------------------------------------------------------------
final StoreID id() {
return this.id;
}
EntryState state() {
return this.state;
}
@Nullable T converter() {
return this.state == EntryState.VALID ? this.converter : null;
}
Converter.@Nullable Placeholder placeholder() {
return this.state == EntryState.VALID ? this.placeholder : null;
}
Msg.Messages messages() {
return this.messages;
}
Set<StoreID> dependent() {
return this.dependent;
}
}
}