Java2JdbcService.java

/*
 * SPDX-FileCopyrightText: 2025 kaumei.io
 * SPDX-License-Identifier: Apache-2.0
 */
package io.kaumei.jdbc.anno.java2jdbc;

import io.kaumei.jdbc.JavaToJdbcConverter;
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 javax.lang.model.element.Element;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.type.TypeKind;
import javax.lang.model.type.TypeMirror;
import java.util.Set;

import static java.util.Objects.requireNonNull;

public class Java2JdbcService implements ProcessorSteps {
    // ----- services
    private final Context ctx;
    // ----- state
    private final Java2JdbcFactory converterFactory;
    private final ConverterService<Java2JdbcConverter> storeService;

    public Java2JdbcService(Context ctx) {
        this.ctx = requireNonNull(ctx);
        this.converterFactory = new Java2JdbcFactory(ctx);
        this.storeService = new ConverterService<>(ctx, "java2jdbc");
    }

    // ------------------------------------------------------------------------

    @Override
    public void process(ProcessorEnvironment roundEnv) {
        this.ctx.logger.info("Process Kaumei JDBC processor JavaToJdbc converter.");
        this.storeService.init(roundEnv);

        // ----- collect static converter
        this.processBasicConverter();
        this.processConfig(roundEnv);
        this.processLocal(roundEnv);

        this.processMethods();
        storeService.forEachPending(this::resolveHierarchy);
        storeService.forEachPlaceholder(this::resolvePlaceholder);
        this.ctx.logger.info("Java2JdbcService", storeService.csvStats());
    }

    // ------------------------------------------------------------------------

    private void processBasicConverter() {
        jdbcConverter("setBoolean", this.ctx.typeMirror(TypeKind.BOOLEAN));
        jdbcConverter("setByte", this.ctx.typeMirror(TypeKind.BYTE));
        staticConverter("setChar", this.ctx.typeMirror(TypeKind.CHAR));
        jdbcConverter("setShort", this.ctx.typeMirror(TypeKind.SHORT));
        jdbcConverter("setInt", this.ctx.typeMirror(TypeKind.INT));
        jdbcConverter("setLong", this.ctx.typeMirror(TypeKind.LONG));
        jdbcConverter("setFloat", this.ctx.typeMirror(TypeKind.FLOAT));
        jdbcConverter("setDouble", this.ctx.typeMirror(TypeKind.DOUBLE));
        // ----
        jdbcConverter("setBigDecimal", this.ctx.typeMirror(java.math.BigDecimal.class));
        jdbcConverter("setString", this.ctx.typeMirror(java.lang.String.class));
        jdbcConverter("setDate", this.ctx.typeMirror(java.sql.Date.class));
        jdbcConverter("setTime", this.ctx.typeMirror(java.sql.Time.class));
        jdbcConverter("setTimestamp", this.ctx.typeMirror(java.sql.Timestamp.class));
        jdbcConverter("setObject", this.ctx.typeMirror(java.sql.Struct.class));
        jdbcConverter("setRef", this.ctx.typeMirror(java.sql.Ref.class));
        jdbcConverter("setBlob", this.ctx.typeMirror(java.sql.Blob.class));
        jdbcConverter("setClob", this.ctx.typeMirror(java.sql.Clob.class));
        jdbcConverter("setArray", this.ctx.typeMirror(java.sql.Array.class));
        jdbcConverter("setURL", this.ctx.typeMirror(java.net.URL.class));
        jdbcConverter("setRowId", this.ctx.typeMirror(java.sql.RowId.class));
        jdbcConverter("setNClob", this.ctx.typeMirror(java.sql.NClob.class));
        jdbcConverter("setSQLXML", this.ctx.typeMirror(java.sql.SQLXML.class));
        jdbcConverter("setBytes", this.ctx.getArrayType(this.ctx.typeMirror(TypeKind.BYTE)));
        // ----
        staticConverter("setBoolean", this.ctx.typeMirror(Boolean.class));
        staticConverter("setByte", this.ctx.typeMirror(Byte.class));
        staticConverter("setCharacter", this.ctx.typeMirror(Character.class));
        staticConverter("setDouble", this.ctx.typeMirror(Double.class));
        staticConverter("setFloat", this.ctx.typeMirror(Float.class));
        staticConverter("setInteger", this.ctx.typeMirror(Integer.class));
        staticConverter("setLong", this.ctx.typeMirror(Long.class));
        staticConverter("setShort", this.ctx.typeMirror(Short.class));

        if (this.storeService.basic().size() != 31) { // sanity-check
            throw new ProcessorException("Invalid count of basic converter. Expected: 31, Current:" + this.storeService.basic().size()); // sanity-check
        }
    }

    private void jdbcConverter(String methodName, TypeMirror type) {
        var source = SourceDV.generic("PreparedStatement." + methodName);
        var converter = new ConverterJdbcStatement(type, source, methodName);
        this.storeService.basic().add(StoreID.of(type), converter);
    }

    private void staticConverter(String methodName, TypeMirror type) {
        var source = SourceDV.generic("JavaToJdbcConverter." + methodName);
        var converter = new ConverterStaticStatement(type, source, JavaToJdbcConverter.class.getCanonicalName(), methodName);
        this.storeService.basic().add(StoreID.of(type), converter);
    }

    // ------------------------------------------------------------------------

    private void processConfig(ProcessorEnvironment roundEnv) {
        for (var executable0 : roundEnv.javaToJdbc()) {
            this.ctx.logger.acceptWithDebugFlag(executable0, (executable) -> {
                var converter = this.converterFactory.converterMethod(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.JAVA_TO_JDBC.hasAnno(executable0)) {
                    this.ctx.logger.acceptWithDebugFlag(executable0, (executable) -> {
                        var converter = this.converterFactory.converterMethod(executable);
                        localStore.addOpt(converter);
                    });
                }
            }
        }
    }

    // ------------------------------------------------------------------------

    private void processMethods() {
        for (var sm : ctx.sourceMethodService.values()) {
            var store = this.storeService.getStoreFor(sm.method());
            sm.parameter().forEachSearchRequest(search -> this.storeService.process(store, search, this.converterFactory::tryToCreate));
        }
    }

    // ------------------------------------------------------------------------

    private void resolveHierarchy(Store<Java2JdbcConverter> store, SearchKey searchKey) {
        var result = store.searchHierarchy(searchKey);
        store.add(result);
    }

    // ------------------------------------------------------------------------

    private void resolvePlaceholder(Store<Java2JdbcConverter> store, StoreID storeId, Converter.Placeholder placeholder) {
        resolvePlaceholder(new LinkedCycle<>(), store, storeId, placeholder);
    }

    private SearchResult<Java2JdbcConverter> resolvePlaceholder(LinkedCycle<StoreID> cycle,
                                                                Store<Java2JdbcConverter> store,
                                                                StoreID storeId,
                                                                Converter.Placeholder placeholder0) {
        if (!cycle.push(storeId)) {
            return SearchResult.cycle(storeId, cycle.asList());
        }

        try {
            var placeholder = (Java2JdbcConverter.Placeholder) placeholder0;

            // check if we already resolved this placeholder
            var result = store.get(storeId);
            if (!result.hasPlaceholder()) {
                return result;
            }

            var other = store.search(placeholder.otherPlaceholder().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(placeholder.otherPlaceholder().toStoreId()));
            }

            var resolved = placeholder.resolve(other.converter());
            return store.markResolve(storeId, resolved);
        } finally {
            cycle.pop();
        }
    }

    // ------------------------------------------------------------------------

    public Msg.Result<Java2JdbcConverter> searchJava(Element element, SearchKey search) {
        var result = searchJava(this.storeService.getStoreFor(element), search);
        if (result.hasMessages()) {
            return Msg.result(result.messages());
        }
        return Msg.result(result.value());
    }

    private Msg.Result<Java2JdbcConverter> searchJava(Store<Java2JdbcConverter> store, SearchKey search) {
        var result = store.search(search.toStoreId(), true);
        if (result.hasMessages()) {
            return Msg.result(result.messages());
        } else if (!ctx.isSubtype(search.type(), result.converter().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();
    }

}