AutoSelectImpl.java

package com.github.mygreen.sqlmapper.core.query.auto;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Stream;

import com.github.mygreen.sqlmapper.core.SqlMapperContext;
import com.github.mygreen.sqlmapper.core.dialect.Dialect;
import com.github.mygreen.sqlmapper.core.event.PostSelectEvent;
import com.github.mygreen.sqlmapper.core.meta.EntityMeta;
import com.github.mygreen.sqlmapper.core.meta.PropertyMeta;
import com.github.mygreen.sqlmapper.core.meta.PropertyValueInvoker;
import com.github.mygreen.sqlmapper.core.query.IllegalOperateException;
import com.github.mygreen.sqlmapper.core.query.JoinAssociation;
import com.github.mygreen.sqlmapper.core.query.JoinCondition;
import com.github.mygreen.sqlmapper.core.query.JoinType;
import com.github.mygreen.sqlmapper.core.query.SelectForUpdateType;
import com.github.mygreen.sqlmapper.metamodel.EntityPath;
import com.github.mygreen.sqlmapper.metamodel.OrderSpecifier;
import com.github.mygreen.sqlmapper.metamodel.Predicate;
import com.github.mygreen.sqlmapper.metamodel.PropertyPath;

import lombok.Getter;
import lombok.NonNull;

/**
 * 抽出を行うSQLを自動生成するクエリの実装です。
 *
 * @author T.TSUCHIE
 *
 * @param <T> 処理対象となるエンティティの型
 */
public class AutoSelectImpl<T> implements AutoSelect<T> {

    /**
     * SqlMapperの設定情報。
     */
    @Getter
    private final SqlMapperContext context;

    @Getter
    private final Class<T> baseClass;

    @Getter
    private final EntityPath<T> entityPath;

    @Getter
    private final EntityMeta entityMeta;

    /**
     * エンティティタイプとメタ情報のマップ
     */
    @Getter
    private final Map<Class<?>, EntityMeta> entityMetaMap = new HashMap<>();

    @Getter
    private Integer queryTimeout;

    @Getter
    private Integer fetchSize;

    @Getter
    private Integer maxRows;

    /**
     * SQLのヒントです。
     */
    @Getter
    private String hint;

    /**
     * 取得するレコード数の上限値です。
     * <p>負の値の時は無視します。
     */
    @Getter
    private int limit = -1;

    /**
     * 取得するレコード数の開始位置です。
     * <p>負の値の時は無視します。
     */
    @Getter
    private int offset = -1;

    /**
     * select句へ追加するプロパティです。
     */
    @Getter
    private final Set<PropertyPath<?>> includesProperties = new LinkedHashSet<>();

    /**
     * select句から除外するプロパティです。
     */
    @Getter
    private final Set<PropertyPath<?>> excludesProperties = new LinkedHashSet<>();

    /**
     * テーブルの結合条件の一覧です。
     */
    @SuppressWarnings("rawtypes")
    @Getter
    private final List<JoinCondition> joinConditions = new ArrayList<>();

    /**
     * エンティティの構成定義の一覧です
     */
    @SuppressWarnings("rawtypes")
    @Getter
    private final List<JoinAssociation> joinAssociations = new ArrayList<>();

    /**
     * 検索条件です。
     */
    @Getter
    private Predicate where;

    /**
     * ソート順です。
     */
    @Getter
    private List<OrderSpecifier> orders = new ArrayList<>();

    /**
     * 検索条件で指定したIDプロパティの値の配列です。
     */
    @Getter
    private Object[] idPropertyValues;

    /**
     * バージョンプロパティの値です。
     */
    @Getter
    private Object versionPropertyValue;

    /**
     * SELECT ~ FOR UPDATEのタイプです。
     */
    @Getter
    private SelectForUpdateType forUpdateType;

    /**
     * SELECT ~ FOR UPDATEでの待機時間 (秒単位) です。
     */
    @Getter
    private int forUpdateWaitSeconds = 0;

    /**
     * {@link AutoSelectImpl}を作成します。
     * @param context SqlMapperの設定情報。
     * @param entityPath マッピングするエンティティのメタモデル。
     */
    @SuppressWarnings("unchecked")
    public AutoSelectImpl(@NonNull SqlMapperContext context, @NonNull EntityPath<T> entityPath) {
        this.context = context;
        this.entityPath = entityPath;
        this.entityMeta = context.getEntityMetaFactory().create(entityPath.getType());
        this.baseClass = (Class<T>)entityMeta.getEntityType();

        this.entityMetaMap.put(entityPath.getType(), context.getEntityMetaFactory().create(entityPath.getType()));
    }

    @Override
    public AutoSelectImpl<T> queryTimeout(int seconds) {
        this.queryTimeout = seconds;
        return this;
    }

    @Override
    public AutoSelectImpl<T> fetchSize(int fetchSize) {
        this.fetchSize = fetchSize;
        return this;
    }

    @Override
    public AutoSelectImpl<T> maxRows(int maxRows) {
        this.maxRows = maxRows;
        return this;
    }

    @Override
    public AutoSelectImpl<T> hint(String hint) {
        this.hint = hint;
        return this;
    }

    @Override
    public AutoSelectImpl<T> limit(int limit) {
        this.limit = limit;
        return this;
    }

    @Override
    public AutoSelectImpl<T> offset(int offset) {
        this.offset = offset;
        return this;
    }

    @Override
    public AutoSelectImpl<T> includes(final PropertyPath<?>... properties) {

        for(PropertyPath<?> prop : properties) {
            this.includesProperties.add(prop);
        }

        return this;

    }

    @Override
    public AutoSelectImpl<T> excludes(final PropertyPath<?>... properties) {

        for(PropertyPath<?> prop : properties) {
            this.excludesProperties.add(prop);
        }

        return this;

    }

    @Override
    public <ENTITY extends EntityPath<?>> AutoSelectImpl<T> innerJoin(@NonNull ENTITY toEntityPath,
            @NonNull JoinCondition.Conditioner<ENTITY> conditioner) {

        JoinCondition<ENTITY> condition = new JoinCondition<>(JoinType.INNER, toEntityPath, conditioner);
        validateJoinCondition(condition);

        this.entityMetaMap.put(toEntityPath.getType(), context.getEntityMetaFactory().create(toEntityPath.getType()));
        this.joinConditions.add(condition);
        return this;
    }

    @Override
    public <ENTITY extends EntityPath<?>> AutoSelectImpl<T> leftJoin(@NonNull ENTITY toEntityPath,
            @NonNull JoinCondition.Conditioner<ENTITY> conditioner) {

        JoinCondition<ENTITY> condition = new JoinCondition<>(JoinType.LEFT_OUTER, toEntityPath, conditioner);
        validateJoinCondition(condition);

        this.entityMetaMap.put(toEntityPath.getType(), context.getEntityMetaFactory().create(toEntityPath.getType()));
        this.joinConditions.add(condition);

        return this;
    }

    /**
     * 結合条件の一覧に既に同じテーブルのエンティティ情報が含まれいえるか検査します。
     * <ul>
     *   <li>比較は、結合先のエンティティ情報({@link EntityPath})既に存在するかどうか。</li>
     *   <li>結合種別({@link JoinType}) は無視します。</li>
     * </ul>
     *
     * @param condition 結合条件
     * @return 同じエンティティの組み合わせが含まれているとき {@literal true} を返します
     * @throws IllegalOperateException 既に同じ組み合わせのエンティティ(テーブル)を指定しているときにスローされます。
     */
    private void validateJoinCondition(JoinCondition<?> condition) {

        // 同じ組み合わせのエンティティが存在しかチェックします。
        for(JoinCondition<?> target : joinConditions) {
            if(target.getToEntity().equals(condition.getToEntity())) {

                // 同じ組み合わせの場合
                throw new IllegalOperateException(context.getMessageFormatter().create("query.existsSameJoinEntity")
                        .param("toEntity", condition.getToEntity().getPathMeta().getElement())
                        .format());
            }
        }

    }

    @Override
    public <E1, E2> AutoSelectImpl<T> associate(@NonNull EntityPath<E1> entityPath1, @NonNull EntityPath<E2> entityPath2,
            @NonNull JoinAssociation.Associator<E1, E2> associator) {

        JoinAssociation<E1, E2> association = new JoinAssociation<>(entityPath1, entityPath2, associator);

        // 同じ組み合わせのエンティティが存在しないかチェックします
        for(JoinAssociation<?, ?> target : joinAssociations) {
            if((target.getEntity1().equals(association.getEntity1()) && target.getEntity2().equals(association.getEntity2()))
                    || (target.getEntity1().equals(association.getEntity2()) && target.getEntity2().equals(association.getEntity1()))
                    ) {
                // 同じ組み合わせの場合 - エンティティ1、エンティティ2の順番は考慮しません。
                throw new IllegalOperateException(context.getMessageFormatter().create("query.existsSameAssociateEntity")
                        .param("entity1", association.getEntity1().getPathMeta().getElement())
                        .param("entity2", association.getEntity2().getPathMeta().getElement())
                        .format());
            }
        }

        this.joinAssociations.add(association);
        return this;
    }

    @Override
    public AutoSelectImpl<T> where(@NonNull Predicate where) {
        this.where = where;
        return this;
    }

    @Override
    public AutoSelectImpl<T> orderBy(OrderSpecifier... orders) {
        //TODO: 追加ではなく、上書き(直接変数に代入)する
        this.orders.addAll(Arrays.asList(orders));
        return this;
    }

    @Override
    public AutoSelectImpl<T> id(@NonNull final Object... idPropertyValues) {

        Optional<PropertyMeta> embeddedIdPropertyMeta = entityMeta.getEmbeddedIdPropertyMeta();
        if(isEmbeddedProperty(idPropertyValues, embeddedIdPropertyMeta)) {
            // 埋め込みIDのとき
            this.idPropertyValues = getEmbeddedIdValue(idPropertyValues[0], embeddedIdPropertyMeta.get().getEmbeddedablePopertyMetaList());
        } else {
            List<PropertyMeta> idPropertyMetaList = entityMeta.getIdPropertyMetaList();
            if(idPropertyMetaList.size() != idPropertyValues.length) {
                throw new IllegalOperateException(context.getMessageFormatter().create("query.noMatchIdPropertySize")
                        .paramWithClass("entityType", entityPath.getType())
                        .param("actualSize", idPropertyValues.length)
                        .param("expectedSize", idPropertyMetaList.size())
                        .format());
            }

            this.idPropertyValues = idPropertyValues;
        }

        return this;
    }

    /**
     * IDのクラスタイプが埋め込みIDであるかどうか。
     * @param idPropertyValues 判定対象の値
     * @param embeddedIdPropertyMeta 埋め込みIDのメタ情報
     * @return {@literal true}のとき埋め込みID。
     */
    private boolean isEmbeddedProperty(Object[] idPropertyValues, Optional<PropertyMeta> embeddedIdPropertyMeta) {
        if(idPropertyValues.length == 0 || embeddedIdPropertyMeta.isEmpty()) {
            return false;
        } else if(idPropertyValues.length >= 2) {
            // IDの指定件数が複数ある時点で埋め込みIDではないので対象外とする。
            return false;
        }

        Class<?> embeddedType = embeddedIdPropertyMeta.get().getPropertyType();
        return embeddedType.isAssignableFrom(idPropertyValues[0].getClass());
    }

    /**
     * 埋め込みIDの各プロパティの値を取得する。
     * @param value 埋め込みIDのインスタンス
     * @param embeddablePropertyList 埋め込みIDのプロパティのメタ情報
     * @return 埋め込みIDのプロパティの値
     */
    private Object[] getEmbeddedIdValue(Object value, Collection<PropertyMeta> embeddablePropertyList) {

        List<Object> idValueList = new ArrayList<>(embeddablePropertyList.size());
        for(PropertyMeta propertyMeta : embeddablePropertyList) {
            idValueList.add(PropertyValueInvoker.getPropertyValue(propertyMeta, value));
        }

        return idValueList.toArray(new Object[idValueList.size()]);
    }

    @Override
    public AutoSelectImpl<T> version(@NonNull final Object versionPropertyValue) {

        if(!entityMeta.hasVersionPropertyMeta()) {
            throw new IllegalOperateException(context.getMessageFormatter().create("query.noVersionProperty")
                    .paramWithClass("entityType", entityPath.getType())
                    .format());
        }

        this.versionPropertyValue = versionPropertyValue;

        return this;
    }

    @Override
    public AutoSelectImpl<T> forUpdate() {
        final Dialect dialect = context.getDialect();
        if(!dialect.supportsSelectForUpdate(SelectForUpdateType.NORMAL)) {
            throw new IllegalOperateException(context.getMessageFormatter().create("query.notSupportSelectForUpdate")
                    .paramWithClass("entityType", entityPath.getType())
                    .param("dialectName", dialect.getName())
                    .format());
        }

        this.forUpdateType = SelectForUpdateType.NORMAL;
        return this;
    }

    @Override
    public AutoSelectImpl<T> forUpdateNoWait() {

        final Dialect dialect = context.getDialect();
        if(!dialect.supportsSelectForUpdate(SelectForUpdateType.NOWAIT)) {
            throw new IllegalOperateException(context.getMessageFormatter().create("query.notSupportSelectForUpdateNowait")
                    .paramWithClass("entityType", entityPath.getType())
                    .param("dialectName", dialect.getName())
                    .format());
        }

        this.forUpdateType = SelectForUpdateType.NOWAIT;
        return this;
    }

    /**
     * {@literal FOR UPDATE WAIT} を追加します。
     * @param seconds  ロックを獲得できるまでの最大待機時間(秒単位)
     * @return このインスタンス自身。
     * @throws IllegalOperateException DBMSがこの操作をサポートしていない場合にスローされます。
     */
    public AutoSelectImpl<T> forUpdateWait(final int seconds) {

        final Dialect dialect = context.getDialect();
        if(!dialect.supportsSelectForUpdate(SelectForUpdateType.WAIT)) {
            throw new IllegalOperateException(context.getMessageFormatter().create("query.notSupportSelectForUpdateWait")
                    .paramWithClass("entityType", entityPath.getType())
                    .param("dialectName", dialect.getName())
                    .format());
        }

        this.forUpdateType = SelectForUpdateType.WAIT;
        this.forUpdateWaitSeconds = seconds;
        return this;
    }

    @Override
    public long getCount() {
        return new AutoSelectExecutor<>(this, true)
                .getCount();
    }

    @Override
    public T getSingleResult() {
        return  new AutoSelectExecutor<>(this, false)
                .getSingleResult(entity -> {
                    context.getApplicationEventPublisher().publishEvent(new PostSelectEvent(AutoSelectImpl.this, entityMeta, entity));
                });
    }

    @Override
    public Optional<T> getOptionalResult() {
        return new AutoSelectExecutor<>(this, false)
                .getOptionalResult(entity -> {
                    context.getApplicationEventPublisher().publishEvent(new PostSelectEvent(AutoSelectImpl.this, entityMeta, entity));
                });
    }

    @Override
    public List<T> getResultList() {
        return new AutoSelectExecutor<>(this, false)
                .getResultList(entity -> {
                    context.getApplicationEventPublisher().publishEvent(new PostSelectEvent(AutoSelectImpl.this, entityMeta, entity));
                });
    }

    @Override
    public Stream<T> getResultStream() {
        return new AutoSelectExecutor<>(this, false)
                .getResultStream(entity -> {
                    context.getApplicationEventPublisher().publishEvent(new PostSelectEvent(AutoSelectImpl.this, entityMeta, entity));
                });
    }
}