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