Jdbc2JavaService.java
/*
* SPDX-FileCopyrightText: 2025 kaumei.io
* SPDX-License-Identifier: Apache-2.0
*/
package io.kaumei.jdbc.anno.jdbc2java;
import io.kaumei.jdbc.anno.ProcessorEnvironment;
import io.kaumei.jdbc.anno.ProcessorException;
import io.kaumei.jdbc.anno.ProcessorSteps;
import io.kaumei.jdbc.anno.ctx.Context;
import io.kaumei.jdbc.anno.msg.JdbcMsg;
import io.kaumei.jdbc.anno.msg.Msg;
import io.kaumei.jdbc.anno.store.*;
import io.kaumei.jdbc.core.JdbcToJavaConverter;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.type.TypeKind;
import javax.lang.model.type.TypeMirror;
import java.sql.JDBCType;
import java.util.HashMap;
import java.util.Set;
import static java.util.Objects.requireNonNull;
public class Jdbc2JavaService implements ProcessorSteps {
// ----- services
private final Context ctx;
// ----- state
private final Jdbc2JavaFactory converterFactory;
final ConverterService<Jdbc2JavaConverter> storeService;
public Jdbc2JavaService(Context ctx) {
this.ctx = requireNonNull(ctx);
this.converterFactory = new Jdbc2JavaFactory(ctx);
this.storeService = new ConverterService<>(ctx, "jdbc2java");
}
// ------------------------------------------------------------------------
@Override
public void process(ProcessorEnvironment roundEnv) {
this.ctx.logger.info("Process Kaumei JDBC processor JdbcToJava converter.");
this.storeService.init(roundEnv);
this.processBasicConverter();
this.processConfig(roundEnv);
this.processLocal(roundEnv);
this.processMethods();
storeService.forEachPending(this::resolveHierarchy);
storeService.forEachPlaceholder(this::resolvePlaceholder);
this.ctx.logger.info("Jdbc2JavaService", storeService.csvStats());
}
// ------------------------------------------------------------------------
private void processBasicConverter() {
// @formatter:off
jdbcConverter("getBoolean", this.ctx.typeMirror(TypeKind.BOOLEAN), JDBCType.BOOLEAN);
staticConverter("columnToBoolean", this.ctx.typeMirror(Boolean.class), JDBCType.BOOLEAN);
jdbcConverter("getByte", this.ctx.typeMirror(TypeKind.BYTE), JDBCType.TINYINT);
staticConverter("columnToByte", this.ctx.typeMirror(Byte.class), JDBCType.TINYINT);
staticConverter("getChar", this.ctx.typeMirror(TypeKind.CHAR), JDBCType.CHAR);
staticConverter("columnToCharacter", this.ctx.typeMirror(Character.class), JDBCType.CHAR);
jdbcConverter("getDouble", this.ctx.typeMirror(TypeKind.DOUBLE), JDBCType.DOUBLE);
staticConverter("columnToDouble", this.ctx.typeMirror(Double.class), JDBCType.DOUBLE);
jdbcConverter("getFloat", this.ctx.typeMirror(TypeKind.FLOAT), JDBCType.REAL);
staticConverter("columnToFloat", this.ctx.typeMirror(Float.class), JDBCType.REAL);
jdbcConverter("getInt", this.ctx.typeMirror(TypeKind.INT), JDBCType.INTEGER);
staticConverter("columnToInteger", this.ctx.typeMirror(Integer.class), JDBCType.INTEGER);
jdbcConverter("getLong", this.ctx.typeMirror(TypeKind.LONG), JDBCType.BIGINT);
staticConverter("columnToLong", this.ctx.typeMirror(Long.class), JDBCType.BIGINT);
jdbcConverter("getShort", this.ctx.typeMirror(TypeKind.SHORT), JDBCType.SMALLINT);
staticConverter("columnToShort", this.ctx.typeMirror(Short.class), JDBCType.SMALLINT);
// ----
jdbcConverter("getBigDecimal", this.ctx.typeMirror(java.math.BigDecimal.class), JDBCType.DECIMAL);
jdbcConverter("getString", this.ctx.typeMirror(java.lang.String.class), JDBCType.VARCHAR);
jdbcConverter("getDate", this.ctx.typeMirror(java.sql.Date.class), JDBCType.DATE);
jdbcConverter("getTime", this.ctx.typeMirror(java.sql.Time.class), JDBCType.TIME);
jdbcConverter("getTimestamp", this.ctx.typeMirror(java.sql.Timestamp.class), JDBCType.TIMESTAMP);
staticConverter("getSqlStruct", this.ctx.typeMirror(java.sql.Struct.class), JDBCType.STRUCT);
jdbcConverter("getRef", this.ctx.typeMirror(java.sql.Ref.class), JDBCType.REF);
jdbcConverter("getBlob", this.ctx.typeMirror(java.sql.Blob.class), JDBCType.BLOB);
jdbcConverter("getClob", this.ctx.typeMirror(java.sql.Clob.class), JDBCType.CLOB);
jdbcConverter("getArray", this.ctx.typeMirror(java.sql.Array.class), JDBCType.ARRAY);
jdbcConverter("getURL", this.ctx.typeMirror(java.net.URL.class), JDBCType.DATALINK);
jdbcConverter("getRowId", this.ctx.typeMirror(java.sql.RowId.class), JDBCType.ROWID);
jdbcConverter("getNClob", this.ctx.typeMirror(java.sql.NClob.class), JDBCType.NCLOB);
jdbcConverter("getSQLXML", this.ctx.typeMirror(java.sql.SQLXML.class), JDBCType.SQLXML);
jdbcConverter("getBytes", this.ctx.getArrayType(this.ctx.typeMirror(TypeKind.BYTE)), JDBCType.VARBINARY);
// @formatter:on
if (storeService.basic().size() != 31) {
throw new ProcessorException("Invalid count of basic converter. Expected: 31, Current:" + storeService.basic().size()); // sanity-check
}
}
private void jdbcConverter(String methodName, TypeMirror type, JDBCType sqlType) {
this.storeService.basic().add(StoreID.of(type), ConverterColumnNative.fromResultSet(type, methodName, sqlType));
}
private void staticConverter(String methodName, TypeMirror type, JDBCType sqlType) {
this.storeService.basic().add(StoreID.of(type), ConverterColumnNative.fromStatic(type, JdbcToJavaConverter.class, methodName, sqlType));
}
// ------------------------------------------------------------------------
private void processConfig(ProcessorEnvironment roundEnv) {
for (var executable0 : roundEnv.jdbcToJava()) {
this.ctx.logger.acceptWithDebugFlag(executable0, (executable) -> {
var converter = this.converterFactory.converterStatic(executable);
this.storeService.global().addOpt(converter);
});
}
}
// ------------------------------------------------------------------------
private void processLocal(ProcessorEnvironment roundEnv) {
for (var elem : roundEnv.jdbcInterfaces()) {
var localStore = this.storeService.local(elem);
for (var child : elem.getEnclosedElements()) {
if (child instanceof ExecutableElement executable0
&& ctx.JDBC_TO_JAVA.hasAnno(executable0)) {
this.ctx.logger.acceptWithDebugFlag(executable0, (executable) -> {
var converter = this.converterFactory.converterStatic(executable);
localStore.addOpt(converter);
});
}
}
}
}
// ------------------------------------------------------------------------
private void processMethods() {
for (var sm : ctx.sourceMethodService.values()) {
var store = this.storeService.getStoreFor(sm.method());
var search = SearchKey.of(sm.returnType().cmpOrType(), sm.jdbcConverterName());
this.storeService.process(store, search, this.converterFactory::tryToCreate);
}
}
// ------------------------------------------------------------------------
private void resolveHierarchy(Store<Jdbc2JavaConverter> store, SearchKey searchKey) {
throw new IllegalStateException("unexpected call: " + searchKey);
}
// ------------------------------------------------------------------------
private void resolvePlaceholder(Store<Jdbc2JavaConverter> store, StoreID
storeId, Converter.Placeholder placeholder) {
resolvePlaceholder(new LinkedCycle<>(), store, storeId, placeholder);
}
private SearchResult<Jdbc2JavaConverter> resolvePlaceholder(LinkedCycle<StoreID> cycle,
Store<Jdbc2JavaConverter> store,
StoreID storeId,
Converter.Placeholder placeholder0) {
if (!cycle.push(storeId)) {
return SearchResult.cycle(storeId, cycle.asList());
}
try {
var placeholder = (Jdbc2JavaConverter.Placeholder) placeholder0;
// check if we already resolved this placeholder
var result = store.get(storeId);
if (!result.hasPlaceholder()) {
return result;
}
var map = new HashMap<SearchKey, Jdbc2JavaConverter>();
for (var key : placeholder.otherPlaceholders()) {
var other = store.search(key.toStoreId(), false);
if (other.hasPlaceholder()) {
other = resolvePlaceholder(cycle, result.store(), other.storeId(), other.placeholder());
}
if (other.state() == SearchState.DYNAMIC_CYCLE) {
return store.markInvalid(storeId, other.messages());
} else if (!other.hasConverter()) {
return store.markInvalid(storeId,
JdbcMsg.INVALID_DEPENDENT_CONVERTER,
Set.of(key.toStoreId())); // we take the first invalid and stop
}
map.put(key, other.converter());
}
if (placeholder instanceof ConverterRowObjects.Placeholder row) {
var nonColumnConverters = row.invalidDependentConverters(map);
if (!nonColumnConverters.isEmpty()) {
return store.markInvalid(storeId,
Msg.merge(JdbcMsg.INVALID_DEPENDENT_CONVERTER,
JdbcMsg.JDBC_TO_JAVA_ROW_COMPONENT_REQUIRES_COLUMN_CONVERTER),
nonColumnConverters);
}
} else if (placeholder instanceof ConverterColumnObject.Placeholder columnObject) {
var nonColumnConverter = columnObject.invalidDependentConverter(map);
if (nonColumnConverter != null) {
return store.markInvalid(storeId,
Msg.merge(JdbcMsg.INVALID_DEPENDENT_CONVERTER,
JdbcMsg.JDBC_TO_JAVA_COLUMN_OBJECT_REQUIRES_COLUMN_CONVERTER),
Set.of(nonColumnConverter));
}
var invalidSqlTypeOverwrite = columnObject.invalidSqlTypeOverwrite(map);
if (invalidSqlTypeOverwrite != null) {
return store.markInvalid(storeId,
Msg.merge(JdbcMsg.INVALID_DEPENDENT_CONVERTER,
JdbcMsg.JDBC_TO_JAVA_SQL_TYPE_REQUIRES_ONE_VALUE_CONVERTER),
Set.of(invalidSqlTypeOverwrite));
}
}
var resolved = placeholder.resolve(map);
return store.markResolve(storeId, resolved);
} finally {
cycle.pop();
}
}
// ------------------------------------------------------------------------
public Msg.Result<Jdbc2JavaConverter> searchJava(ExecutableElement element, SearchKey search) {
var result = searchJava(this.storeService.getStoreFor(element), search);
if (result.hasMessages()) {
return Msg.result(result.messages());
}
if (!this.ctx.visibilityService.isVisible(element, result.value().visibility())) {
return Msg.result(JdbcMsg.JDBC_TO_JAVA_METHOD_MUST_BE_VISIBLE);
}
return Msg.result(result.value());
}
private Msg.Result<Jdbc2JavaConverter> searchJava(Store<Jdbc2JavaConverter> store, SearchKey search) {
var result = store.search(search.toStoreId(), true);
if (result.hasMessages()) {
return Msg.result(result.messages());
} else if (!ctx.isAssignable(result.converter().type(), search.type())) {
return Msg.result(JdbcMsg.INCOMPATIBLE_TYPE);
}
return Msg.result(result.converter());
}
// ------------------------------------------------------------------------
public void dump(StringBuilder out) {
this.storeService.dump(out);
}
public String csvStats() {
return this.storeService.csvStats();
}
}