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();
    }

}