Java2JdbcFactory.java
/*
* SPDX-FileCopyrightText: 2025 kaumei.io
* SPDX-License-Identifier: Apache-2.0
*/
package io.kaumei.jdbc.anno.java2jdbc;
import io.kaumei.jdbc.anno.ctx.Context;
import io.kaumei.jdbc.anno.model.ConverterNameDV;
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 org.jspecify.annotations.Nullable;
import javax.lang.model.element.*;
import javax.lang.model.type.TypeKind;
import javax.lang.model.type.TypeMirror;
import java.util.List;
import static io.kaumei.jdbc.anno.ctx.JavaModelUtils.isStatic;
import static io.kaumei.jdbc.anno.ctx.JavaModelUtils.isVisible;
import static javax.lang.model.element.ElementKind.RECORD;
class Java2JdbcFactory {
private static final Msg.Message INVALID_STATIC_PARAMETER_COUNT =
JdbcMsg.JAVA_TO_JDBC_METHOD_REQUIRES_ONE_OR_THREE_PARAMETERS;
private final Context ctx;
Java2JdbcFactory(Context ctx) {
this.ctx = ctx;
}
// ------------------------------------------------------------------------
FactoryResult<Java2JdbcConverter> converterEnum(Element elem) {
if (elem.getKind() != ElementKind.ENUM) { // sanity-check
throw new IllegalArgumentException("Element must be a record."); // sanity-check
}
var storeId = StoreID.of(elem.asType());
var other = SearchKey.of(this.ctx.JAVA_String.typeMirror());
var source = SourceDV.converter(ctx, elem);
return FactoryResult.of(storeId, new ConverterEnum.Placeholder(elem.asType(), source, other));
}
// ------------------------------------------------------------------------
private FactoryResult<Java2JdbcConverter> converterObjectSimple(ExecutableElement method) {
if (isStatic(method)) { // sanity-check
throw new IllegalArgumentException("method must not be static"); // sanity-check
}
var type = method.getEnclosingElement().asType();
var messages = Msg.builder();
if (!isVisible(method)) {
messages.add(JdbcMsg.JAVA_TO_JDBC_METHOD_MUST_BE_VISIBLE);
}
// ----- check return type
var returnType = method.getReturnType();
if (returnType.getKind() == TypeKind.VOID) {
messages.add(JdbcMsg.INVALID_RETURN_TYPE);
} else if (!this.ctx.optionalFlag(method, returnType).isNonNullOrUnspecified()) {
messages.add(JdbcMsg.JAVA_TO_JDBC_RETURN_TYPE_MUST_BE_NON_NULL_OR_UNSPECIFIED);
}
// ----- check parameter
if (!method.getParameters().isEmpty()) {
messages.add(JdbcMsg.JAVA_TO_JDBC_METHOD_MUST_HAVE_NO_PARAMETERS);
}
// ----- check throws
if (!this.ctx.hasValidSqlExceptions(method)) {
messages.add(JdbcMsg.JAVA_TO_JDBC_HAS_INCOMPATIBLE_EXCEPTIONS);
}
var name = ctx.JAVA_TO_JDBC.getAnno(method, ConverterNameDV.unnamed());
if (name.isBlankName()) {
messages.add(JdbcMsg.annotationValueMustNotBeBlank("@JavaToJdbc"));
}
var storeId = StoreID.of(type, name);
var source = SourceDV.converter(ctx, method);
return messages.hasMessages()
? FactoryResult.of(storeId, source, messages.build())
: FactoryResult.of(storeId, new ConverterObjectSimple.Placeholder(type, source, method, SearchKey.of(returnType)));
}
// ------------------------------------------------------------------------
FactoryResult<Java2JdbcConverter> converterRecord(TypeElement elem) {
if (elem.getKind() != ElementKind.RECORD) { // sanity-check
throw new IllegalArgumentException("Element must be a record."); // sanity-check
}
var type = elem.asType();
var storeId = StoreID.of(type);
var source = SourceDV.converter(ctx, elem);
List<? extends RecordComponentElement> components = elem.getRecordComponents();
if (components.size() != 1) {
return FactoryResult.of(storeId, source, JdbcMsg.JAVA_TO_JDBC_RECORD_MUST_HAVE_ONE_COMPONENT);
}
var component = components.get(0);
var cType = component.asType();
if (!this.ctx.optionalFlag(elem, cType).isNonNullOrUnspecified()) {
return FactoryResult.of(storeId, source, JdbcMsg.JAVA_TO_JDBC_RECORD_COMPONENT_MUST_BE_NON_NULL_OR_UNSPECIFIED);
}
return FactoryResult.of(storeId, new ConverterRecord.Placeholder(type, source, component.getAccessor(), SearchKey.of(cType)));
}
// ------------------------------------------------------------------------
@Nullable
FactoryResult<Java2JdbcConverter> converterMethod(ExecutableElement elem) {
if (isStatic(elem)) {
return converterStatic(elem);
} else {
return converterObjectSimple(elem);
}
}
// ------------------------------------------------------------------------
@Nullable FactoryResult<Java2JdbcConverter> converterStatic(ExecutableElement method) {
if (!isStatic(method)) { // sanity-check
throw new IllegalArgumentException("method must be static"); // sanity-check
}
var messages = Msg.builder();
if (!isVisible(method)) {
messages.add(JdbcMsg.JAVA_TO_JDBC_METHOD_MUST_BE_VISIBLE);
}
var name = ctx.JAVA_TO_JDBC.getAnno(method, ConverterNameDV.unnamed());
if (name.isBlankName()) {
messages.add(JdbcMsg.annotationValueMustNotBeBlank("@JavaToJdbc"));
}
var paramSize = method.getParameters().size();
if (paramSize == 1) {
return converterStaticSimple(messages, method, name);
} else if (paramSize == 3) {
return converterStaticStatement(messages, method, name);
} else if (name.hasName()) {
var storeId = StoreID.of(ctx.JAVA_Void.typeMirror(), name);
return FactoryResult.of(storeId, SourceDV.converter(ctx, method), JdbcMsg.JAVA_TO_JDBC_NO_TYPE);
}
this.ctx.logger.warn(method, INVALID_STATIC_PARAMETER_COUNT.text());
return null;
}
// ------------------------------------------------------------------------
private FactoryResult<Java2JdbcConverter> converterStaticSimple(Msg.Builder messages,
ExecutableElement method,
ConverterNameDV name) {
if (method.getParameters().size() != 1) { // sanity-check
throw new IllegalArgumentException("Invalid parameter count: " + method.getParameters().size()); // sanity-check
}
// ----- check return type
var returnType = method.getReturnType();
if (returnType.getKind() == TypeKind.VOID) {
messages.add(JdbcMsg.INVALID_RETURN_TYPE);
} else if (!this.ctx.optionalFlag(method, returnType).isNonNullOrUnspecified()) {
messages.add(JdbcMsg.JAVA_TO_JDBC_RETURN_TYPE_MUST_BE_NON_NULL_OR_UNSPECIFIED);
}
// ----- check throws
if (!this.ctx.hasValidSqlExceptions(method)) {
messages.add(JdbcMsg.JAVA_TO_JDBC_HAS_INCOMPATIBLE_EXCEPTIONS);
}
var type = method.getParameters().get(0).asType();
var storeId = StoreID.of(method.getParameters().get(0).asType(), name);
var source = SourceDV.converter(ctx, method);
return messages.hasMessages()
? FactoryResult.of(storeId, source, messages.build())
: FactoryResult.of(storeId, new ConverterStaticSimple.Placeholder(type, source, method, SearchKey.of(method.getReturnType())));
}
// ------------------------------------------------------------------------
private FactoryResult<Java2JdbcConverter> converterStaticStatement(Msg.Builder messages,
ExecutableElement method,
ConverterNameDV name) {
if (method.getParameters().size() != 3) { // sanity-check
throw new IllegalArgumentException("Invalid parameter count: " + method.getParameters().size()); // sanity-check
}
// ----- check return type
var returnType = method.getReturnType();
if (returnType.getKind() != TypeKind.VOID) {
messages.add(JdbcMsg.INVALID_RETURN_TYPE);
}
// ----- check throws
if (!this.ctx.hasValidSqlExceptions(method)) {
messages.add(JdbcMsg.JAVA_TO_JDBC_HAS_INCOMPATIBLE_EXCEPTIONS);
}
// ----- check parameter statement
var paramStmt = method.getParameters().get(0);
if (!this.ctx.JAVA_SQL_PreparedStatement.isSameType(paramStmt.asType())) {
messages.add(JdbcMsg.JAVA_TO_JDBC_FIRST_PARAMETER_MUST_BE_PREPARED_STATEMENT);
} else if (!this.ctx.optionalFlag(method, paramStmt.asType()).isNonNullOrUnspecified()) {
messages.add(JdbcMsg.JAVA_TO_JDBC_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.JAVA_TO_JDBC_SECOND_PARAMETER_MUST_BE_INT);
}
// ----- check parameter: value
var paramValue = method.getParameters().get(2);
var paramValueType = paramValue.asType();
if (!paramValueType.getKind().isPrimitive()
&& !this.ctx.optionalFlag(method, paramValueType).isNullableOrUnspecified()) {
messages.add(JdbcMsg.JAVA_TO_JDBC_THIRD_PARAMETER_MUST_BE_NULLABLE_OR_UNSPECIFIED);
}
var storeId = StoreID.of(paramValueType, name);
var source = SourceDV.converter(ctx, method);
return messages.hasMessages()
? FactoryResult.of(storeId, source, messages.build())
: FactoryResult.of(storeId, new ConverterStaticStatement(paramValueType, source, method));
}
// ----------------------------------------------------------------
@Nullable FactoryResult<Java2JdbcConverter> tryToCreate(TypeMirror typeMirror) {
var storeId = StoreID.of(typeMirror);
var elem = this.ctx.asElementOpt(typeMirror);
if (elem == null) {
return FactoryResult.of(storeId, SourceDV.empty(), JdbcMsg.javaToJdbcElementTypeNotFound(typeMirror));
}
var source = SourceDV.converter(ctx, elem);
var messages = Msg.builder();
FactoryResult<Java2JdbcConverter> annotatedConverter = null;
for (var child : elem.getEnclosedElements()) {
if (!(child instanceof ExecutableElement executable)) {
continue;
} else if (!ctx.JAVA_TO_JDBC.hasAnno(executable)) {
continue;
}
var javaToJdbcAnno = ctx.JAVA_TO_JDBC.getAnno(executable, ConverterNameDV.unnamed());
if (javaToJdbcAnno.isBlankName()) {
messages.add(JdbcMsg.annotationValueMustNotBeBlank("@JavaToJdbc"));
} else if (javaToJdbcAnno.hasName()) {
messages.add(JdbcMsg.JAVA_TO_JDBC_ANNOTATION_NAME_UNSUPPORTED);
} else {
var converter = this.converterMethod(executable);
if (converter != null) {
if (converter.hasMessages()) {
messages.add(converter.messages());
} else if (!this.ctx.isSubtype(typeMirror, converter.type())) {
messages.add(JdbcMsg.javaToJdbcAnnotationHasWrongType(converter.type()));
} else if (annotatedConverter == null) {
annotatedConverter = converter;
} else {
messages.add(JdbcMsg.JAVA_TO_JDBC_TOO_MANY_ANNOTATIONS);
}
} else {
messages.add(INVALID_STATIC_PARAMETER_COUNT);
}
}
}
if (messages.hasMessages()) {
return FactoryResult.of(storeId, source, messages.build());
} else if (annotatedConverter != null) {
return annotatedConverter;
} else if (elem instanceof TypeElement type) {
if (type.getKind() == RECORD) {
return this.converterRecord(type);
} else if (type.getKind() == ElementKind.ENUM) {
return this.converterEnum(type);
}
}
return null;
}
}