PropertyMetaFactory.java

package com.github.mygreen.sqlmapper.core.meta;

import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.text.DecimalFormat;
import java.util.Optional;
import java.util.UUID;

import javax.sql.DataSource;

import org.springframework.beans.factory.BeanNotOfRequiredTypeException;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.util.ReflectionUtils;
import org.springframework.util.StringUtils;

import com.github.mygreen.messageformatter.MessageFormatter;
import com.github.mygreen.sqlmapper.core.annotation.Column;
import com.github.mygreen.sqlmapper.core.annotation.Enumerated;
import com.github.mygreen.sqlmapper.core.annotation.GeneratedValue;
import com.github.mygreen.sqlmapper.core.annotation.GeneratedValue.GenerationType;
import com.github.mygreen.sqlmapper.core.annotation.Id;
import com.github.mygreen.sqlmapper.core.annotation.SequenceGenerator;
import com.github.mygreen.sqlmapper.core.annotation.TableGenerator;
import com.github.mygreen.sqlmapper.core.annotation.Temporal;
import com.github.mygreen.sqlmapper.core.annotation.Version;
import com.github.mygreen.sqlmapper.core.config.JdbcTemplateProperties;
import com.github.mygreen.sqlmapper.core.config.TableIdGeneratorProperties;
import com.github.mygreen.sqlmapper.core.dialect.Dialect;
import com.github.mygreen.sqlmapper.core.id.IdGenerationContext;
import com.github.mygreen.sqlmapper.core.id.IdGenerator;
import com.github.mygreen.sqlmapper.core.id.IdentityIdGenerator;
import com.github.mygreen.sqlmapper.core.id.SequenceIdGenerator;
import com.github.mygreen.sqlmapper.core.id.TableIdContext;
import com.github.mygreen.sqlmapper.core.id.TableIdGenerator;
import com.github.mygreen.sqlmapper.core.id.TableIdIncrementer;
import com.github.mygreen.sqlmapper.core.id.UUIDGenerator;
import com.github.mygreen.sqlmapper.core.naming.NamingRule;
import com.github.mygreen.sqlmapper.core.query.JdbcTemplateBuilder;
import com.github.mygreen.sqlmapper.core.type.ValueType;
import com.github.mygreen.sqlmapper.core.type.ValueTypeRegistry;
import com.github.mygreen.sqlmapper.core.util.ClassUtils;
import com.github.mygreen.sqlmapper.core.util.NameUtils;

import lombok.Getter;
import lombok.Setter;

/**
 * プロパティのメタ情報を作成します。
 *
 * @version 0.3
 * @author T.TSUCHIE
 *
 */
public class PropertyMetaFactory {

    @Getter
    @Setter
    @Autowired
    private NamingRule namingRule;

    @Getter
    @Setter
    @Autowired
    private MessageFormatter messageFormatter;

    @Getter
    @Setter
    @Autowired
    private ValueTypeRegistry valueTypeRegistry;

    @Getter
    @Setter
    @Autowired
    private Dialect dialect;

    @Getter
    @Setter
    @Autowired
    private DataSource dataSource;

    @Getter
    @Setter
    @Autowired
    private JdbcTemplateProperties jdbcTemplateProperties;

    @Getter
    @Setter
    @Autowired
    private TableIdGeneratorProperties tableIdGeneratorProperties;

    @Getter
    @Setter
    @Autowired
    private ApplicationContext applicationContext;

    /**
     * プロパティのメタ情報を作成します。
     * @param field フィールド
     * @param entityMeta エンティティのメタ情報。空の場合はID情報の処理をスキップします。
     * @param embeddedId 埋め込み型のIDのプロパティかどうか。
     * @return プロパティのメタ情報
     */
    public PropertyMeta create(final Field field, final Optional<EntityMeta> entityMeta, final boolean embeddedId) {

        final Class<?> declaringClass = field.getDeclaringClass();
        final PropertyMeta propertyMeta = new PropertyMeta(field.getName(), field.getType());
        propertyMeta.setEmbeddedableId(embeddedId);
        doField(propertyMeta, field);

        // フィールドに対するgetter/setterメソッドを設定します。
        for(Method method : declaringClass.getMethods()) {
            ReflectionUtils.makeAccessible(method);

            int modifiers = method.getModifiers();
            if(Modifier.isStatic(modifiers)) {
                continue;
            }

            if(ClassUtils.isSetterMethod(method)) {
                doSetterMethod(propertyMeta, method);

            } else if(ClassUtils.isGetterMethod(method) || ClassUtils.isBooleanGetterMethod(method)) {
                doGetterMethod(propertyMeta, method);
            }
        }

        // 永続化対象のプロパティはカラム情報を設定します。
        if(!propertyMeta.isEmbedded() && !propertyMeta.isTransient()) {

            doColumnMeta(propertyMeta);
            entityMeta.ifPresent(em -> doIdGenerator(propertyMeta, em));

            // プロパティに対する型変換を設定します。
            ValueType<?> valueType = valueTypeRegistry.findValueType(propertyMeta);

            // OracleなどBoolean型を純粋にサポートしていない場合は、int型に変換するタイプに変換する。
            valueType = dialect.getValueType(valueType);
            propertyMeta.setValueType(valueType);


            validateColumnProperty(declaringClass, propertyMeta);

        }

        return propertyMeta;

    }

    /**
     * プロパティのメタ情報に対する処理を実行します。
     * @param propertyMeta プロパティのメタ情報
     * @param field フィールド情報
     */
    private void doField(final PropertyMeta propertyMeta, final Field field) {

        propertyMeta.setField(field);

        final Annotation[] annos = field.getAnnotations();
        for(Annotation anno : annos) {
            if(!isSupportedAnnotation(anno)) {
                continue;
            }

            final Class<? extends Annotation> annoClass = anno.annotationType();
            propertyMeta.addAnnotation(annoClass, anno);
        }
    }

    /**
     * サポートするアノテーションか判定する。
     * <p>確実に重複するJava標準のアノテーションは除外するようにします。</p>
     *
     * @param anno 判定対象のアノテーション
     * @return tureのときサポートします。
     */
    private boolean isSupportedAnnotation(final Annotation anno) {

        final String name = anno.annotationType().getName();
        if(name.startsWith("java.lang.annotation.")) {
            return false;
        }

        return true;
    }

    /**
     * setterメソッドの情報を処理する。
     * @param propertyMeta プロパティのメタ情報
     * @param method setterメソッド
     */
    private void doSetterMethod(final PropertyMeta propertyMeta, final Method method) {

        final String methodName = method.getName();
        final String propertyName = NameUtils.uncapitalize(methodName.substring(3));

        if(!propertyMeta.getName().equals(propertyName)) {
            // プロパティ名が一致しない場合はスキップする
            return;
        }

        propertyMeta.setWriteMethod(method);

    }

    /**
     * getterメソッドの情報を処理する。
     * @param propertyMeta プロパティのメタ情報
     * @param method getterメソッド
     */
    private void doGetterMethod(final PropertyMeta propertyMeta, final Method method) {

        final String methodName = method.getName();
        final String propertyName;
        if(methodName.startsWith("get")) {
            propertyName = NameUtils.uncapitalize(methodName.substring(3));
        } else {
            // 「is」から始まる場合
            propertyName = NameUtils.uncapitalize(methodName.substring(2));
        }

        if(!propertyMeta.getName().equals(propertyName)) {
            // プロパティ名が一致しない場合はスキップする
            return;
        }

        propertyMeta.setReadMethod(method);

    }

    /**
     * カラム情報を処理する。
     * @param propertyMeta プロパティのメタ情報
     */
    private void doColumnMeta(final PropertyMeta propertyMeta) {

        final ColumnMeta columnMeta = new ColumnMeta();

        final String defaultColumnName = namingRule.propertyToColumn(propertyMeta.getName());

        Optional<Column> annoColumn = propertyMeta.getAnnotation(Column.class);
        if(annoColumn.isPresent()) {
            if(!annoColumn.get().name().isEmpty()) {
                columnMeta.setName(annoColumn.get().name());
            } else {
                columnMeta.setName(defaultColumnName);
            }

            columnMeta.setUpdatable(annoColumn.get().updatable());

        } else {
            columnMeta.setName(defaultColumnName);
        }

        propertyMeta.setColumnMeta(columnMeta);

    }

    /**
     * 主キーの生成情報を処理します。
     * @param propertyMeta プロパティのメタ情報
     * @param entityMeta エンティティのメタ情報
     */
    private void doIdGenerator(final PropertyMeta propertyMeta, final EntityMeta entityMeta) {

        if(!propertyMeta.isId()) {
            return;
        }

        Optional<GeneratedValue> annoGeneratedValue = propertyMeta.getAnnotation(GeneratedValue.class);
        if(annoGeneratedValue.isEmpty()) {
            return;
        }

        final Class<?> propertyType = propertyMeta.getPropertyType();

        GenerationType generationType = annoGeneratedValue.get().strategy();
        if(generationType != GenerationType.AUTO && !dialect.supportsGenerationType(generationType)) {
            throw new InvalidEntityException(entityMeta.getEntityType(), messageFormatter.create("property.anno.attr.notSupportForDialect")
                    .paramWithClass("classType", entityMeta.getEntityType())
                    .param("property", propertyMeta.getName())
                    .paramWithAnno("anno", GeneratedValue.class)
                    .param("attrName", "strategy")
                    .param("attrValue", generationType)
                    .param("dialectName", dialect.getName())
                    .format());
        }

        // 生成戦略の補完
        if(generationType == GenerationType.AUTO) {
            generationType = dialect.getDefaultGenerationType();
        }

        final IdGenerator idGenerator;
        if(StringUtils.hasLength(annoGeneratedValue.get().generator())) {
            /*
             * 属性「generator」が指定されている場合は、「strategy」の値は無視する。
             * ただし、IDENTITYの場合はクエリ実行前に処理されてしまうので、
             * Autoに補完して間違った処理をされないようにする。
             */
            generationType = GenerationType.AUTO;

            final String generatorName = annoGeneratedValue.get().generator();
            try {
                idGenerator = applicationContext.getBean(generatorName, IdGenerator.class);

            } catch(NoSuchBeanDefinitionException  e) {
                throw new InvalidEntityException(entityMeta.getEntityType(), messageFormatter.create("property.anno.attr.noSuchBeanDefinition")
                        .paramWithClass("classType", entityMeta.getEntityType())
                        .param("property", propertyMeta.getName())
                        .paramWithAnno("anno", GeneratedValue.class)
                        .param("attrName", "generator")
                        .param("attrValue", generatorName)
                        .format(), e);
            } catch(BeanNotOfRequiredTypeException e) {
                throw new InvalidEntityException(entityMeta.getEntityType(), messageFormatter.create("property.anno.attr.beanNotOfRequiredType")
                        .paramWithClass("classType", entityMeta.getEntityType())
                        .param("property", propertyMeta.getName())
                        .paramWithAnno("anno", GeneratedValue.class)
                        .param("attrName", "generator")
                        .param("attrValue", generatorName)
                        .paramWithClass("requiredType", IdGenerator.class)
                        .format(), e);
            }
        } else if(generationType == GenerationType.IDENTITY) {
            IdentityIdGenerator identityIdGenerator = new IdentityIdGenerator(propertyType);
            idGenerator = identityIdGenerator;

        } else if(generationType == GenerationType.SEQUENCE) {
            Optional<SequenceGenerator> annoSequenceGenerator = propertyMeta.getAnnotation(SequenceGenerator.class);
            final String sequenceName;
            if(annoSequenceGenerator.isPresent() && !annoSequenceGenerator.get().sequenceName().isEmpty()) {
                sequenceName = NameUtils.tableFullName(annoSequenceGenerator.get().sequenceName(),
                        annoSequenceGenerator.get().catalog(),
                        annoSequenceGenerator.get().schema());
            } else {
                sequenceName = namingRule.sequenceNameForSequenceGenerator(entityMeta.getTableMeta().getName(), propertyMeta.getColumnMeta().getName());
            }
            SequenceIdGenerator sequenceIdGenerator = new SequenceIdGenerator(
                    dialect.getSequenceIncrementer(dataSource, sequenceName), propertyType);

            annoSequenceGenerator.map(a -> a.format()).filter(f -> !f.isEmpty()).ifPresent(f -> {
                try {
                    sequenceIdGenerator.setFormatter(new DecimalFormat(f));
                } catch(IllegalArgumentException e) {
                    throw new InvalidEntityException(entityMeta.getEntityType(), messageFormatter.create("property.anno.attr.wrongFormat")
                            .paramWithClass("classType", entityMeta.getEntityType())
                            .param("property", propertyMeta.getName())
                            .paramWithAnno("anno", TableGenerator.class)
                            .param("attrName", "format")
                            .param("attrValue", f)
                            .format(), e);
                }
            });

            idGenerator = sequenceIdGenerator;

        } else if(generationType == GenerationType.TABLE) {
            Optional<TableGenerator> annoTableGenerator = propertyMeta.getAnnotation(TableGenerator.class);

            final TableIdContext tableIdContext = new TableIdContext();
            tableIdContext.setTable(tableIdGeneratorProperties.getTable());
            tableIdContext.setSchema(tableIdGeneratorProperties.getSchema());
            tableIdContext.setCatalog(tableIdGeneratorProperties.getCatalog());
            tableIdContext.setPkColumn(tableIdGeneratorProperties.getPkColumn());
            tableIdContext.setValueColumn(tableIdGeneratorProperties.getValueColumn());
            tableIdContext.setAllocationSize(tableIdGeneratorProperties.getAllocationSize());
            tableIdContext.setInitialValue(tableIdGeneratorProperties.getInitialValue());

            annoTableGenerator.ifPresent(a -> {
                if(!a.table().isEmpty()) {
                    tableIdContext.setTable(a.table());
                }

                if(!a.schema().isEmpty()) {
                    tableIdContext.setSchema(a.schema());
                }

                if(!a.catalog().isEmpty()) {
                    tableIdContext.setCatalog(a.catalog());
                }

                if(!a.pkColumn().isEmpty()) {
                    tableIdContext.setPkColumn(a.pkColumn());
                }

                if(!a.valueColumn().isEmpty()) {
                    tableIdContext.setValueColumn(a.valueColumn());
                }

                if(a.initialValue() >= 0L) {
                    tableIdContext.setInitialValue(a.initialValue());
                }

                if(a.allocationSize() >= 1L) {
                    tableIdContext.setAllocationSize(a.allocationSize());
                }

            });


            final String sequenceName;
            if(annoTableGenerator.isPresent() && !annoTableGenerator.get().sequenceName().isEmpty()) {
                sequenceName = annoTableGenerator.get().sequenceName();
            } else {
                sequenceName = namingRule.sequenceNameForTableGenerator(entityMeta.getTableMeta().getName(), propertyMeta.getColumnMeta().getName());
            }

            TableIdGenerator tableIdGenerator = new TableIdGenerator(
                    new TableIdIncrementer(getJdbcTemplate(), tableIdContext),
                    propertyMeta.getPropertyType(), sequenceName);


            annoTableGenerator.map(a -> a.format()).filter(f -> !f.isEmpty()).ifPresent(f -> {
                try {
                    tableIdGenerator.setFormatter(new DecimalFormat(f));
                } catch(IllegalArgumentException e) {
                    throw new InvalidEntityException(entityMeta.getEntityType(), messageFormatter.create("property.anno.attr.wrongFormat")
                            .paramWithClass("classType", entityMeta.getEntityType())
                            .param("property", propertyMeta.getName())
                            .paramWithAnno("anno", TableGenerator.class)
                            .param("attrName", "format")
                            .param("attrValue", f)
                            .format(), e);
                }
            });

            idGenerator = tableIdGenerator;


        } else if(generationType == GenerationType.UUID) {
            idGenerator = new UUIDGenerator(propertyType);

        } else {
            throw new InvalidEntityException(entityMeta.getEntityType(), messageFormatter.create("property.anno.attr.notSupportValue")
                    .paramWithClass("classType", entityMeta.getClass())
                    .param("property", propertyMeta.getName())
                    .paramWithAnno("anno", GeneratedValue.class)
                    .param("attrName", "strategy")
                    .paramWithEnum("attrValue", generationType)
                    .format());
        }

        if(!idGenerator.isSupportedType(propertyType)) {
            throw new InvalidEntityException(entityMeta.getClass(), messageFormatter.create("property.anno.notSupportTypeList")
                    .paramWithClass("classType", entityMeta.getEntityType())
                    .param("property", propertyMeta.getName())
                    .paramWithAnno("anno", GeneratedValue.class)
                    .paramWithClass("actualType", propertyType)
                    .paramWithClass("expectedTypeList", idGenerator.getSupportedTypes())
                    .format());
        }

        propertyMeta.setIdGenerator(idGenerator);
        propertyMeta.setIdGeneratonType(generationType);

        // 生成対象のIDの情報
        final IdGenerationContext generationContext = new IdGenerationContext();
        generationContext.setTableMeta(entityMeta.getTableMeta());
        generationContext.setColumnMeta(propertyMeta.getColumnMeta());
        generationContext.setEntityType(entityMeta.getEntityType());
        generationContext.setPropertyType(propertyMeta.getPropertyType());
        propertyMeta.setIdGenerationContext(generationContext);

    }

    /**
     * カラムとなるプロパティの整合性のチェック。
     * アノテーションとクラスタイプのチェック
     *
     * @param declaringClass プロパティが定義されているクラス
     * @param propertyMeta チェック対象のプロパティ
     */
    private void validateColumnProperty(final Class<?> declaringClass, final PropertyMeta propertyMeta) {

        final Class<?> propertyType = propertyMeta.getPropertyType();

        // 主キーのタイプチェック
        if(propertyMeta.isId()
                && propertyType != String.class
                && propertyType != UUID.class
                && propertyType != Integer.class && propertyType != int.class
                && propertyType != Long.class && propertyType != long.class) {

            throw new InvalidEntityException(declaringClass, messageFormatter.create("property.anno.notSupportTypeList")
                    .paramWithClass("classType", declaringClass)
                    .param("property", propertyMeta.getName())
                    .paramWithAnno("anno", Id.class)
                    .paramWithClass("actualType", propertyType)
                    .paramWithClass("expectedTypeList", String.class, Integer.class, int.class, Long.class, long.class)
                    .format());
        }

        // 主キーでないのに値の生成用のアノテーションが付与されている場合
        if(!propertyMeta.isId() && propertyMeta.hasAnnotation(GeneratedValue.class)) {

            throw new InvalidEntityException(declaringClass, messageFormatter.create("property.anno.notIdWithGeneratedValue")
                    .paramWithClass("classType", declaringClass)
                    .param("property", propertyMeta.getName())
                    .paramWithAnno("anno", GeneratedValue.class)
                    .format());
        }


        // 列挙型のタイプチェック
        if(propertyMeta.hasAnnotation(Enumerated.class) && !propertyType.isEnum()) {

            throw new InvalidEntityException(declaringClass, messageFormatter.create("property.anno.notSupportType")
                    .paramWithClass("classType", declaringClass)
                    .param("property", propertyMeta.getName())
                    .paramWithAnno("anno", Enumerated.class)
                    .paramWithClass("actualType", propertyType)
                    .paramWithClass("expectedType", Enum.class)
                    .format());
        }

        // バージョンキーのタイプチェック
        if(propertyMeta.hasAnnotation(Version.class)
                && propertyType != Integer.class && propertyType != int.class
                && propertyType != Long.class && propertyType != long.class) {

            throw new InvalidEntityException(declaringClass, messageFormatter.create("property.anno.notSupportTypeList")
                    .paramWithClass("classType", declaringClass)
                    .param("property", propertyMeta.getName())
                    .paramWithAnno("anno", Version.class)
                    .paramWithClass("actualType", propertyType)
                    .paramWithClass("expectedTypeList", Integer.class, int.class, Long.class, long.class)
                    .format());

        }

        // 時制のタイプチェック
        if(!propertyMeta.hasAnnotation(Temporal.class)
                && propertyType == java.util.Date.class) {

            // 時制の型が不明なプロパティに対して、@Temporalが付与されていない場合
            throw new InvalidEntityException(declaringClass, messageFormatter.create("property.anno.requiredAnnoTemporal")
                    .paramWithClass("classType", declaringClass)
                    .param("property", propertyMeta.getName())
                    .paramWithAnno("anno", Temporal.class)
                    .format());
        }

    }

    /**
     * {@link TableIdGenerator}用の {@link JdbcTemplate} を取得します。
     * @return {@link JdbcTemplate}のインスタンス。
     */
    private JdbcTemplate getJdbcTemplate() {
        return JdbcTemplateBuilder.create(dataSource, jdbcTemplateProperties)
                .build();
    }

}