Jdbc2JavaFactory.java

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

import io.kaumei.jdbc.anno.ctx.Context;
import io.kaumei.jdbc.anno.model.OptionalFlag;
import io.kaumei.jdbc.anno.model.SQLNameDV;
import io.kaumei.jdbc.anno.msg.JdbcMsg;
import io.kaumei.jdbc.anno.msg.Msg;
import io.kaumei.jdbc.anno.store.FactoryResult;
import io.kaumei.jdbc.anno.store.SearchKey;
import io.kaumei.jdbc.anno.store.SourceDV;
import io.kaumei.jdbc.anno.store.StoreID;

import javax.lang.model.element.ElementKind;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.type.TypeKind;
import javax.lang.model.type.TypeMirror;

import static io.kaumei.jdbc.anno.ctx.JavaModelUtils.*;

class Jdbc2JavaFactory {
    private final Context ctx;

    Jdbc2JavaFactory(Context ctx) {
        this.ctx = ctx;
    }

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

    FactoryResult<Jdbc2JavaConverter> converterStatic(ExecutableElement method) {
        var search = switch (method.getKind()) { // JaCoCo:ignore
            case METHOD -> SearchKey.jdbcToJava(ctx, method);
            case CONSTRUCTOR -> SearchKey.of(method.getEnclosingElement().asType());
            default ->
                    throw new IllegalArgumentException("Wrong method kind: " + method.getKind()); // sanity-check
        };

        var storeId = search.toStoreId();
        var type = search.type();
        var source = SourceDV.converter(ctx, method);

        var messages = Msg.builder();
        if (search.name().isBlankName()) {
            messages.add(JdbcMsg.annotationValueMustNotBeBlank("@JdbcToJava"));
        }
        if (method.getKind() == ElementKind.METHOD && !isStatic(method)) {
            messages.add(JdbcMsg.JDBC_TO_JAVA_METHOD_MUST_BE_STATIC);
        }
        if (!isVisible(method)) {
            messages.add(JdbcMsg.JDBC_TO_JAVA_METHOD_MUST_BE_VISIBLE);
        }
        if (!this.ctx.hasValidSqlExceptions(method)) {
            messages.add(JdbcMsg.JDBC_TO_JAVA_METHOD_THROWS_INCOMPATIBLE_EXCEPTIONS);
        }
        if (type.getKind() == TypeKind.VOID) {
            messages.add(JdbcMsg.JDBC_TO_JAVA_MUST_NOT_RETURN_VOID);
        }

        var paramSize = method.getParameters().size();
        if (paramSize == 0) {
            messages.add(JdbcMsg.JDBC_TO_JAVA_METHOD_REQUIRES_PARAMETER);
            return FactoryResult.of(storeId, source, messages.build());
        }

        var firstParamType = method.getParameters().get(0).asType();
        var isFirstResultSet = this.ctx.JAVA_SQL_ResultSet.isSupertypeOf(firstParamType);
        if (isFirstResultSet) {
            if (method.getKind() == ElementKind.CONSTRUCTOR && this.ctx.asElement(type).getKind() == ElementKind.RECORD) {
                // because constructor can not return null
                messages.add(JdbcMsg.JDBC_TO_JAVA_RECORD_CONSTRUCTOR_DOES_NOT_SUPPORT_RESULT_SET);
            } else if (method.getKind() == ElementKind.CONSTRUCTOR && paramSize == 2) {
                // because constructor can not return null
                messages.add(JdbcMsg.JDBC_TO_JAVA_CLASS_CONSTRUCTOR_DOES_NOT_SUPPORT_RESULT_SET_INT);
            } else if (!this.ctx.JAVA_SQL_ResultSet.isSameType(firstParamType)) {
                messages.add(JdbcMsg.JDBC_TO_JAVA_FIRST_PARAMETER_MUST_BE_RESULT_SET);
            } else if (paramSize == 1) {
                return this.converterRowResultSet(messages, method, storeId, type, source);
            } else if (paramSize == 2) {
                return this.converterColumnResultSet(messages, method, storeId, type, source);
            } else {
                messages.add(JdbcMsg.JDBC_TO_JAVA_RESULT_SET_PARAMETER_COUNT_INVALID);
            }
            return FactoryResult.of(storeId, source, messages.build());
        } else if (paramSize == 1) {
            return this.converterColumnObject(messages, method, storeId, type, source);
        }
        return this.converterRowObjects(messages, method, storeId, type, source);
    }

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

    private FactoryResult<Jdbc2JavaConverter> converterColumnObject(Msg.Builder messages, ExecutableElement method,
                                                                    StoreID storeId, TypeMirror type, SourceDV source) {
        // ----- parameter type
        var param = method.getParameters().get(0);
        if (ctx.JDBC_NAME.hasAnno(param)) {
            messages.add(JdbcMsg.JDBC_TO_JAVA_NAME_MAPPING_UNSUPPORTED_FOR_ONE_PARAM);
        }
        if (!this.ctx.optionalFlag(method, param.asType()).isNonNullOrUnspecified()) {
            messages.add(JdbcMsg.JDBC_TO_JAVA_NULLABLE_PARAMETER_UNSUPPORTED);
        }
        // ----- return type
        if (!this.ctx.optionalFlag(method, method.getReturnType()).isNonNullOrUnspecified()) {
            messages.add(JdbcMsg.JDBC_TO_JAVA_RETURN_TYPE_MUST_BE_NON_NULL_OR_UNSPECIFIED);
        }

        if (messages.hasMessages()) {
            return FactoryResult.of(storeId, source, messages.build());
        }
        var jdbcType = this.ctx.erasure(param.asType());
        return FactoryResult.of(storeId, new ConverterColumnObject.Placeholder(type, source, method, SearchKey.of(jdbcType)));
    }

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

    private FactoryResult<Jdbc2JavaConverter> converterColumnResultSet(Msg.Builder messages, ExecutableElement method,
                                                                       StoreID storeId, TypeMirror type, SourceDV source) {
        // ----- check parameter statement
        var param0 = method.getParameters().get(0);
        if (!this.ctx.optionalFlag(method, param0.asType()).isNonNullOrUnspecified()) {
            messages.add(JdbcMsg.JDBC_TO_JAVA_FIRST_PARAMETER_MUST_BE_NON_NULL_OR_UNSPECIFIED);
        }
        // ----- check parameter: index
        var paramIndex = method.getParameters().get(1);
        if (paramIndex.asType().getKind() != TypeKind.INT) {
            messages.add(JdbcMsg.JDBC_TO_JAVA_SECOND_PARAMETER_MUST_BE_INT);
        }
        // ----- return type
        if (!this.ctx.optionalFlag(method, method.getReturnType()).isNullableOrUnspecified()) {
            messages.add(JdbcMsg.JDBC_TO_JAVA_RETURN_TYPE_MUST_BE_NULLABLE_OR_UNSPECIFIED);
        }

        return messages.hasMessages()
                ? FactoryResult.of(storeId, source, messages.build())
                : FactoryResult.of(storeId, new ConverterColumnResultSet(type, source, method));
    }

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

    private FactoryResult<Jdbc2JavaConverter> converterRowObjects(Msg.Builder messages, ExecutableElement method,
                                                                  StoreID storeId, TypeMirror type, SourceDV source) {
        // ----- parameter type
        var paramSize = method.getParameters().size();
        var searchKeys = new SearchKey[paramSize];
        var optionalFlags = new OptionalFlag[paramSize];
        var jdbcNames = new SQLNameDV[paramSize];

        for (int i = 0; i < paramSize; i++) {
            var paramVar = method.getParameters().get(i);
            jdbcNames[i] = this.ctx.jdbcName(paramVar);
            messages.add(jdbcNames[i].messages());
            optionalFlags[i] = this.ctx.optionalFlag(method, paramVar.asType());
            if (optionalFlags[i].isOptionalType()) {
                messages.add(JdbcMsg.jdbcToJavaParameterOptionalInvalid(paramVar.getSimpleName()));
            }
            var searchKey = SearchKey.of(ctx, paramVar);
            if (searchKey.name().isBlankName()) {
                messages.add(JdbcMsg.annotationValueMustNotBeBlank("@JdbcConverterName"));
            }
            searchKeys[i] = searchKey;
        }
        // ----- return type
        if (!this.ctx.optionalFlag(method, method.getReturnType()).isNonNullOrUnspecified()) {
            messages.add(JdbcMsg.JDBC_TO_JAVA_RETURN_TYPE_MUST_BE_NON_NULL_OR_UNSPECIFIED);
        }
        return messages.hasMessages()
                ? FactoryResult.of(storeId, source, messages.build())
                : FactoryResult.of(storeId, new ConverterRowObjects.Placeholder(type, source,
                method, optionalFlags, jdbcNames, searchKeys));
    }

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

    private FactoryResult<Jdbc2JavaConverter> converterRowResultSet(Msg.Builder messages, ExecutableElement method,
                                                                    StoreID storeId, TypeMirror type, SourceDV source) {
        // ----- parameter type
        var param = method.getParameters().get(0);
        if (ctx.JDBC_NAME.hasAnno(param)) {
            messages.add(JdbcMsg.JDBC_TO_JAVA_NAME_MAPPING_UNSUPPORTED_FOR_RESULT_SET);
        }
        if (!this.ctx.optionalFlag(method, param.asType()).isNonNullOrUnspecified()) {
            messages.add(JdbcMsg.JDBC_TO_JAVA_NULLABLE_PARAMETER_UNSUPPORTED);
        }
        // ----- return type
        if (!this.ctx.optionalFlag(method, method.getReturnType()).isNonNullOrUnspecified()) {
            messages.add(JdbcMsg.JDBC_TO_JAVA_RETURN_TYPE_MUST_BE_NON_NULL_OR_UNSPECIFIED);
        }

        return messages.hasMessages()
                ? FactoryResult.of(storeId, source, messages.build())
                : FactoryResult.of(storeId, new ConverterRowResultSet(type, source, method));
    }

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

    public FactoryResult<Jdbc2JavaConverter> tryToCreate(TypeMirror typeMirror) {
        this.ctx.logger.debug("@JdbcToJava.createConverterFor", typeMirror);

        var storeId = StoreID.of(typeMirror);
        var elem = this.ctx.asElementOpt(typeMirror);
        if (elem == null) {
            return FactoryResult.of(storeId, SourceDV.empty(), JdbcMsg.jdbcToJavaElementTypeNotFound(typeMirror));
        }
        var source = SourceDV.converter(ctx, elem);
        var messages = Msg.builder();
        FactoryResult<Jdbc2JavaConverter> annotatedConverter = null;
        var skipConstructorCount = 0;
        FactoryResult<Jdbc2JavaConverter> constructorConverter = null;

        for (var child : elem.getEnclosedElements()) {
            if (!(child instanceof ExecutableElement executable)) {
                continue;
            }
            var jdbcToJavaAnno = ctx.JDBC_TO_JAVA.getAnnoOpt(executable);
            if (jdbcToJavaAnno != null) {
                if (jdbcToJavaAnno.isBlankName()) {
                    messages.add(JdbcMsg.annotationValueMustNotBeBlank("@JdbcToJava"));
                } else if (jdbcToJavaAnno.hasName()) {
                    messages.add(JdbcMsg.JDBC_TO_JAVA_ANNOTATION_NAME_UNSUPPORTED);
                } else {
                    var converter = this.converterStatic(executable);
                    this.ctx.logger.debug("converter", converter);
                    if (converter.hasMessages()) {
                        messages.add(converter.messages());
                    } else if (!this.ctx.isAssignable(converter.type(), typeMirror)) {
                        messages.add(JdbcMsg.jdbcToJavaAnnotationHasWrongType(converter.type()));
                    } else if (annotatedConverter == null) {
                        annotatedConverter = converter;
                    } else {
                        messages.add(JdbcMsg.JDBC_TO_JAVA_TOO_MANY_ANNOTATIONS);
                    }
                }
            } else if (executable.getKind() == ElementKind.CONSTRUCTOR && !executable.getParameters().isEmpty()) {
                var converter = this.converterStatic(executable);
                if (constructorConverter == null || constructorConverter.hasMessages()) {
                    constructorConverter = converter;
                } else if (!converter.hasMessages()) {
                    skipConstructorCount++;
                }
            }
        }
        // ----- first check any annotated methods
        if (messages.hasMessages()) {
            return FactoryResult.of(storeId, source, messages.build());
        } else if (annotatedConverter != null) {
            return annotatedConverter;
        }
        // ----- next check enum
        if (elem.getKind() == ElementKind.ENUM) {
            var factoryMethod = getStaticMethod(elem, "valueOf");
            return this.converterStatic(factoryMethod);
        }
        // ----- last check constructors
        if (constructorConverter == null) {
            return FactoryResult.of(storeId, source, JdbcMsg.JDBC_TO_JAVA_NO_CONSTRUCTOR_FOUND);
        } else if (skipConstructorCount > 0) {
            return FactoryResult.of(storeId, source, JdbcMsg.JDBC_TO_JAVA_TOO_MANY_CONSTRUCTORS);
        }
        return constructorConverter;
    }

}