EntitySpecFactory.java

package com.github.mygreen.sqlmapper.apt;

import java.sql.Date;
import java.sql.Time;
import java.sql.Timestamp;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;

import javax.annotation.processing.Generated;
import javax.annotation.processing.Messager;
import javax.lang.model.element.Modifier;

import com.github.mygreen.sqlmapper.apt.model.AptType;
import com.github.mygreen.sqlmapper.apt.model.EntityMetamodel;
import com.github.mygreen.sqlmapper.apt.model.PropertyMetamodel;
import com.github.mygreen.sqlmapper.core.util.NameUtils;
import com.github.mygreen.sqlmapper.metamodel.BooleanPath;
import com.github.mygreen.sqlmapper.metamodel.EntityPathBase;
import com.github.mygreen.sqlmapper.metamodel.EnumPath;
import com.github.mygreen.sqlmapper.metamodel.GeneralPath;
import com.github.mygreen.sqlmapper.metamodel.LocalDatePath;
import com.github.mygreen.sqlmapper.metamodel.LocalDateTimePath;
import com.github.mygreen.sqlmapper.metamodel.LocalTimePath;
import com.github.mygreen.sqlmapper.metamodel.NumberPath;
import com.github.mygreen.sqlmapper.metamodel.SqlDatePath;
import com.github.mygreen.sqlmapper.metamodel.SqlTimePath;
import com.github.mygreen.sqlmapper.metamodel.SqlTimestampPath;
import com.github.mygreen.sqlmapper.metamodel.StringPath;
import com.github.mygreen.sqlmapper.metamodel.UtilDatePath;
import com.squareup.javapoet.AnnotationSpec;
import com.squareup.javapoet.ClassName;
import com.squareup.javapoet.FieldSpec;
import com.squareup.javapoet.MethodSpec;
import com.squareup.javapoet.ParameterizedTypeName;
import com.squareup.javapoet.TypeName;
import com.squareup.javapoet.TypeSpec;
import com.squareup.javapoet.TypeVariableName;
import com.squareup.javapoet.WildcardTypeName;

import lombok.RequiredArgsConstructor;

/**
 * エンティティ用のソース生成用の{@link TypeSpec}を作成します。
 *
 * @author T.TSUCHIE
 *
 */
@RequiredArgsConstructor
public class EntitySpecFactory {

    /**
     * APT用のメッセージ出力
     */
    private final Messager messager;

    /**
     * メタモデルの生成オプション
     */
    private final MetamodelConfig metamodelConfig;

    public TypeSpec create(final EntityMetamodel entityModel) {

        // フィールド情報の作成
        List<FieldSpec> fieldSpecs = new ArrayList<>();
        for(PropertyMetamodel propertyModel : entityModel.getProperties()) {
            fieldSpecs.add(createFieldSpec(propertyModel));
        }

        if(entityModel.isEmbeddable()) {
            return createTypeSpecAsEmbeddeable(entityModel, fieldSpecs);
        } else if(entityModel.getType().isAbstract()) {
            return createTypeSpecAsAbstract(entityModel, fieldSpecs);
        } else {
            return createTypeSpecAsNormal(entityModel, fieldSpecs);
        }


    }

    private FieldSpec createFieldSpec(final PropertyMetamodel propertyModel) {

        final AptType propertyType = propertyModel.getPropertyType();

        FieldSpec filedSpec;
        if(propertyModel.isCustomType() || propertyModel.isLob()) {
            // 対応できない型の場合、汎用的なGeneralPathにする
            TypeName filedTypeName = ParameterizedTypeName.get(ClassName.get(GeneralPath.class), propertyType.getTypeName());
            filedSpec = FieldSpec.builder(filedTypeName, propertyModel.getPropertyName(), Modifier.PUBLIC, Modifier.FINAL)
                    .initializer("createGeneral($S, $T.class)", propertyModel.getPropertyName(), propertyType.getTypeName())
                    .build();

        } else if(propertyModel.isEmbedded()) {
            // 埋め込み用のプロパティの場合
            // public final MPK id = new MPK(this, "id")
            String embeddedEntityMetamodelName = resolveEntityMetamodelName(propertyType.getSimpleName());
            ClassName embeddedEntityClassName = ClassName.bestGuess(embeddedEntityMetamodelName);
            filedSpec = FieldSpec.builder(embeddedEntityClassName, propertyModel.getPropertyName(), Modifier.PUBLIC, Modifier.FINAL)
                    .initializer("new $T(this, $S)", embeddedEntityClassName, propertyModel.getPropertyName())
                    .build();

        } else if(propertyType.isInheritanceOf(String.class)) {
            filedSpec = FieldSpec.builder(StringPath.class, propertyModel.getPropertyName(), Modifier.PUBLIC, Modifier.FINAL)
                    .initializer("createString($S)", propertyModel.getPropertyName())
                    .build();

        } else if (propertyType.isInheritanceOf(Number.class) || propertyType.isPrimitiveNumber()) {
            TypeName filedTypeName = ParameterizedTypeName.get(ClassName.get(NumberPath.class), propertyType.getWrapperTypeName());
            filedSpec = FieldSpec.builder(filedTypeName, propertyModel.getPropertyName(), Modifier.PUBLIC, Modifier.FINAL)
                    .initializer("createNumber($S, $T.class)", propertyModel.getPropertyName(), propertyType.getWrapperTypeName())
                    .build();

        } else if (propertyType.isInheritanceOf(Boolean.class) || propertyType.isPrimitiveBoolean()) {
            filedSpec = FieldSpec.builder(BooleanPath.class, propertyModel.getPropertyName(), Modifier.PUBLIC, Modifier.FINAL)
                    .initializer("createBoolean($S)", propertyModel.getPropertyName())
                    .build();

        } else if(propertyType.isEnum()) {
            TypeName filedTypeName = ParameterizedTypeName.get(ClassName.get(EnumPath.class), propertyType.getTypeName());
            filedSpec = FieldSpec.builder(filedTypeName, propertyModel.getPropertyName(), Modifier.PUBLIC, Modifier.FINAL)
                    .initializer("createEnum($S, $T.class)", propertyModel.getPropertyName(), propertyType.getTypeName())
                    .build();

        } else if(propertyType.isInheritanceOf(Timestamp.class)) {
            filedSpec = FieldSpec.builder(SqlTimestampPath.class, propertyModel.getPropertyName(), Modifier.PUBLIC, Modifier.FINAL)
                    .initializer("createSqlTimestamp($S)", propertyModel.getPropertyName())
                    .build();

        } else if(propertyType.isInheritanceOf(Time.class)) {
            filedSpec = FieldSpec.builder(SqlTimePath.class, propertyModel.getPropertyName(), Modifier.PUBLIC, Modifier.FINAL)
                    .initializer("createSqlTime($S)", propertyModel.getPropertyName())
                    .build();

        } else if(propertyType.isInheritanceOf(Date.class)) {
            filedSpec = FieldSpec.builder(SqlDatePath.class, propertyModel.getPropertyName(), Modifier.PUBLIC, Modifier.FINAL)
                    .initializer("createSqlDate($S)", propertyModel.getPropertyName())
                    .build();

        } else if(propertyType.isInheritanceOf(java.util.Date.class)) {
            filedSpec = FieldSpec.builder(UtilDatePath.class, propertyModel.getPropertyName(), Modifier.PUBLIC, Modifier.FINAL)
                    .initializer("createUtilDate($S)", propertyModel.getPropertyName())
                    .build();

        } else if(propertyType.isInheritanceOf(LocalDate.class)) {
            filedSpec = FieldSpec.builder(LocalDatePath.class, propertyModel.getPropertyName(), Modifier.PUBLIC, Modifier.FINAL)
                    .initializer("createLocalDate($S)", propertyModel.getPropertyName())
                    .build();

        } else if(propertyType.isInheritanceOf(LocalTime.class)) {
            filedSpec = FieldSpec.builder(LocalTimePath.class, propertyModel.getPropertyName(), Modifier.PUBLIC, Modifier.FINAL)
                    .initializer("createLocalTime($S)", propertyModel.getPropertyName())
                    .build();

        } else if(propertyType.isInheritanceOf(LocalDateTime.class)) {
            filedSpec = FieldSpec.builder(LocalDateTimePath.class, propertyModel.getPropertyName(), Modifier.PUBLIC, Modifier.FINAL)
                    .initializer("createLocalDateTime($S)", propertyModel.getPropertyName())
                    .build();

        } else {
            // 汎用的なGeneralPathにする
            TypeName filedTypeName = ParameterizedTypeName.get(ClassName.get(GeneralPath.class), propertyType.getTypeName());
            filedSpec = FieldSpec.builder(filedTypeName, propertyModel.getPropertyName(), Modifier.PUBLIC, Modifier.FINAL)
                    .initializer("createGeneral($S, $T.class)", propertyModel.getPropertyName(), propertyType.getTypeName())
                    .build();
        }

        return filedSpec;

    }

    /**
     * 通常のEntityPathの定義を作成する。
     * @param entityModel エンティティモデル
     * @param fieldSpecs フィールド
     * @return クラス定義情報
     */
    private TypeSpec createTypeSpecAsNormal(final EntityMetamodel entityModel, final List<FieldSpec> fieldSpecs) {
        // エンティティのクラス
        final ClassName entityType = entityModel.getType().asClassName();

        // メタモデルのクラス名
        final String entityMetamodelName = resolveEntityMetamodelName(entityModel.getClassName());

        // 継承タイプ
        final TypeName metamodelSuperClass;
        if(entityModel.hasSuperClass()) {
            /*
             * 継承タイプ - 親が @MappedSuperclass を付与している場合
             * extends MParent<Sample>
             */
            ClassName superClass = entityModel.getSuperClassType().asClassName();
            String metamodelSuperClassName =
                    superClass.packageName()
                    + AptUtils.getPackageClassNameSeparator(entityModel)
                    + resolveEntityMetamodelName(superClass.simpleName());

            metamodelSuperClass = ParameterizedTypeName.get(ClassName.bestGuess(metamodelSuperClassName), TypeVariableName.get(entityType.simpleName()));
        } else {
            /*
             * 継承タイプ - 通常
             * extends EntityPatBase<Sample>
             */
            metamodelSuperClass = ParameterizedTypeName.get(ClassName.get(EntityPathBase.class), entityType);
        }

        /*
         * コンストラクタ
         * public MSample(Class<Sample> type, String name) {
         *    super(type, name);
         * }
         *
         */
        MethodSpec consturctor1 = MethodSpec.constructorBuilder()
                .addModifiers(Modifier.PUBLIC)
                .addParameter(ParameterizedTypeName.get(ClassName.get(Class.class), entityType), "type")
                .addParameter(String.class, "name")
                .addStatement("super(type, name)")
                .build();

        /*
         * コンストラクタ
         * public MSample(String name) {
         *    super(Sample.class, name);
         * }
         *
         */
        MethodSpec consturctor2 = MethodSpec.constructorBuilder()
                .addModifiers(Modifier.PUBLIC)
                .addParameter(String.class, "name")
                .addStatement("super($T.class, name)", entityType)
                .build();

        /*
         * エンティティの自身のインスタンスフィールド
         * public static final MSample sample = new MSample("sample");
         */
        ClassName entityClassName = ClassName.bestGuess(entityMetamodelName);
        final String entityFieldName = NameUtils.uncapitalize(entityModel.getClassName());
        FieldSpec entityFieldSpec = FieldSpec.builder(entityClassName, entityFieldName, Modifier.PUBLIC, Modifier.STATIC, Modifier.FINAL)
                .initializer("new $L($S)", entityMetamodelName, entityFieldName)
                .build();

        return TypeSpec.classBuilder(entityMetamodelName)
              .addModifiers(resolveEntityTypeModifiers(entityModel))
              .superclass(metamodelSuperClass)
              .addAnnotation(createGeneratorAnnoSpec())
              .addJavadoc("$L is SqlMapper's metamodel type for {@link $T}", entityMetamodelName, entityType)
              .addMethod(consturctor1)
              .addMethod(consturctor2)
              .addField(entityFieldSpec)
              .addFields(fieldSpecs)
              .addTypes(createStaticInnerTypeSpecs(entityModel))
              .build();

    }

    /**
     * 抽象クラスの場合の定義を作成する。
     * @param entityModel エンティティモデル
     * @param fieldSpecs フィールド
     * @return クラス定義情報
     */
    private TypeSpec createTypeSpecAsAbstract(final EntityMetamodel entityModel, final List<FieldSpec> fieldSpecs) {

        // エンティティのクラス名情報
        final ClassName entityType = entityModel.getType().asClassName();

        // メタモデルのクラス名
        final String entityMetamodelName = resolveEntityMetamodelName(entityModel.getClassName());

        // 継承タイプ
        final TypeName metamodelSuperClass;
        final TypeVariableName classTypeVariable;
        if(entityModel.hasSuperClass()) {
            /*
             * 継承タイプ - 親が @MappedSuperclass を付与している場合
             * MSample<E extends Sample> extends MParent<E>
             */
            ClassName superClass = entityModel.getSuperClassType().asClassName();
            String metamodelSuperClassName =
                    superClass.packageName()
                    + AptUtils.getPackageClassNameSeparator(entityModel)
                    + resolveEntityMetamodelName(superClass.simpleName());

           metamodelSuperClass = ParameterizedTypeName.get(ClassName.bestGuess(metamodelSuperClassName), TypeVariableName.get("E"));
           classTypeVariable = TypeVariableName.get("E", entityType);

        } else {
            /*
             * 継承タイプ - 自身が抽象クラス
             * MSample<E extends Sample> extends EntityPatBase<E>
             */
            metamodelSuperClass = ParameterizedTypeName.get(ClassName.get(EntityPathBase.class), TypeVariableName.get("E"));
            classTypeVariable = TypeVariableName.get("E", entityType);
        }


        /*
         * コンストラクタ
         * public MSample(Class<? extends E> type, String name) {
         *    super(type, name);
         * }
         */
        MethodSpec consturctor1 = MethodSpec.constructorBuilder()
                .addModifiers(Modifier.PUBLIC)
                .addParameter(ParameterizedTypeName.get(ClassName.get(Class.class), WildcardTypeName.subtypeOf(TypeVariableName.get("E"))), "type")
                .addParameter(String.class, "name")
                .addStatement("super(type, name)")
                .build();

        return TypeSpec.classBuilder(entityMetamodelName)
              .addModifiers(resolveEntityTypeModifiers(entityModel))
              .superclass(metamodelSuperClass)
              .addTypeVariable(classTypeVariable)
              .addAnnotation(createGeneratorAnnoSpec())
              .addJavadoc("$L is SqlMapper's metamodel type for {@link $T}", entityMetamodelName, entityType)
              .addMethod(consturctor1)
              .addFields(fieldSpecs)
              .addTypes(createStaticInnerTypeSpecs(entityModel))
              .build();

    }

    /**
     * 埋め込み用のEntityPathの定義を作成する。
     * @param entityModel エンティティモデル
     * @param fieldSpecs フィールド
     * @return クラス定義情報
     */
    private TypeSpec createTypeSpecAsEmbeddeable(final EntityMetamodel entityModel, final List<FieldSpec> fieldSpecs) {
        // エンティティのクラスタイプ
        final ClassName entityType = entityModel.getType().asClassName();

        // メタモデルのクラス名
        final String entityMetamodelName = resolveEntityMetamodelName(entityModel.getClassName());

        // 継承タイプ
        final TypeName metamodelSuperClass;
        if(entityModel.hasSuperClass()) {
            /*
             * 継承タイプ - 親が @MappedSuperclass を付与している場合
             * extends MParent<Sample>
             */
            ClassName superClass = entityModel.getSuperClassType().asClassName();
            String metamodelSuperClassName =
                    superClass.packageName()
                    + AptUtils.getPackageClassNameSeparator(entityModel)
                    + resolveEntityMetamodelName(superClass.simpleName());

            metamodelSuperClass = ParameterizedTypeName.get(ClassName.bestGuess(metamodelSuperClassName), TypeVariableName.get(entityType.simpleName()));
        } else {
            /*
             * 継承タイプ - 通常
             * extends EntityPatBase<Sample>
             */
            metamodelSuperClass = ParameterizedTypeName.get(ClassName.get(EntityPathBase.class), entityType);
        }

        /*
         * コンストラクタ
         * public MSample(Class<Sample> type, EntityPathBase<?> parent, String name) {
         *    super(type, parent, name);
         * }
         *
         */
        MethodSpec consturctor1 = MethodSpec.constructorBuilder()
                .addModifiers(Modifier.PUBLIC)
                .addParameter(ParameterizedTypeName.get(ClassName.get(Class.class), entityType), "type")
                .addParameter(ParameterizedTypeName.get(ClassName.get(EntityPathBase.class), WildcardTypeName.subtypeOf(Object.class)), "parent")
                .addParameter(String.class, "name")
                .addStatement("super(type, parent, name)")
                .build();

        /*
         * コンストラクタ
         * public MSample(EntityPathBase<?> parent, String name) {
         *    super(Sample.class, parent, name);
         * }
         *
         */
        MethodSpec consturctor2 = MethodSpec.constructorBuilder()
                .addModifiers(Modifier.PUBLIC)
                .addParameter(ParameterizedTypeName.get(ClassName.get(EntityPathBase.class), WildcardTypeName.subtypeOf(Object.class)), "parent")
                .addParameter(String.class, "name")
                .addStatement("super($T.class, parent, name)", entityType)
                .build();

        return TypeSpec.classBuilder(entityMetamodelName)
              .addModifiers(resolveEntityTypeModifiers(entityModel))
              .superclass(metamodelSuperClass)
              .addAnnotation(createGeneratorAnnoSpec())
              .addJavadoc("$L is SqlMapper's metamodel type for {@link $T}", entityMetamodelName, entityType)
              .addMethod(consturctor1)
              .addMethod(consturctor2)
              .addFields(fieldSpecs)
              .addTypes(createStaticInnerTypeSpecs(entityModel))
              .build();

    }

    /**
     * static内部クラスのメタモデルを作成します。
     * @param parentEntityModel 親のメタモデル情報
     * @return static内部のメタモデルクラス
     */
    private List<TypeSpec> createStaticInnerTypeSpecs(EntityMetamodel parentEntityModel) {

        List<TypeSpec> list = new ArrayList<>();

        for(EntityMetamodel staticInnerEntityModel : parentEntityModel.getStaticInnerEntities()) {
            list.add(create(staticInnerEntityModel));
        }

        return list;

    }

    /**
     * エンティティのメタモデルの修飾子を解決します。
     * @param entityModel エンティティのメタモデル。
     * @return 修飾子
     */
    private Modifier[] resolveEntityTypeModifiers(final EntityMetamodel entityModel) {
        List<Modifier> list = new ArrayList<>(1);
        list.add(Modifier.PUBLIC);

        if(entityModel.getType().isAbstract()) {
            list.add(Modifier.ABSTRACT);
        }

        if(entityModel.getType().isStaticInnerClass()) {
            list.add(Modifier.STATIC);
        }

        return list.toArray(new Modifier[list.size()]);
    }

    /**
     * エンティティのメタモデル名を解決します。
     *
     * @param simpleClassName パッケージ名のついていない単純なクラス名。
     * @return 接頭語 + クラス名 + 接尾語。
     */
    private String resolveEntityMetamodelName(String simpleClassName) {
        return metamodelConfig.getPrefix()
                + simpleClassName
                + metamodelConfig.getSuffix();
    }

    /**
     * クラスに付与する {@literal @Generated} アノテーション
     * @return
     */
    private AnnotationSpec createGeneratorAnnoSpec() {
        return AnnotationSpec.builder(Generated.class)
                .addMember("value", "$S", "SqlMapper - EntityMetamodelGenerator")
                .addMember("date", "$S", DateTimeFormatter.ofPattern("uuuu-MM-dd HH:mm:ss").format(LocalDateTime.now()))
                .build();
    }

}