/*
 * Copyright 2004-2015 the Seasar Foundation and the Others.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
 * either express or implied. See the License for the specific language
 * governing permissions and limitations under the License.
 */
package org.seasar.extension.jdbc.query;

import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Map.Entry;

import org.seasar.extension.jdbc.AutoSelect;
import org.seasar.extension.jdbc.ColumnMeta;
import org.seasar.extension.jdbc.ConditionType;
import org.seasar.extension.jdbc.DbmsDialect;
import org.seasar.extension.jdbc.EntityMapper;
import org.seasar.extension.jdbc.EntityMeta;
import org.seasar.extension.jdbc.FromClause;
import org.seasar.extension.jdbc.IterationCallback;
import org.seasar.extension.jdbc.JoinColumnMeta;
import org.seasar.extension.jdbc.JoinMeta;
import org.seasar.extension.jdbc.JoinType;
import org.seasar.extension.jdbc.OrderByClause;
import org.seasar.extension.jdbc.OrderByItem;
import org.seasar.extension.jdbc.PropertyMapper;
import org.seasar.extension.jdbc.PropertyMeta;
import org.seasar.extension.jdbc.ResultSetHandler;
import org.seasar.extension.jdbc.SelectClause;
import org.seasar.extension.jdbc.SelectForUpdateType;
import org.seasar.extension.jdbc.ValueType;
import org.seasar.extension.jdbc.Where;
import org.seasar.extension.jdbc.WhereClause;
import org.seasar.extension.jdbc.exception.BaseJoinNotFoundRuntimeException;
import org.seasar.extension.jdbc.exception.EntityColumnNotFoundRuntimeException;
import org.seasar.extension.jdbc.exception.IllegalIdPropertySizeRuntimeException;
import org.seasar.extension.jdbc.exception.JoinDuplicatedRuntimeException;
import org.seasar.extension.jdbc.exception.PropertyNotFoundRuntimeException;
import org.seasar.extension.jdbc.exception.VersionPropertyNotExistsRuntimeException;
import org.seasar.extension.jdbc.handler.BeanAutoResultSetHandler;
import org.seasar.extension.jdbc.handler.BeanIterationAutoResultSetHandler;
import org.seasar.extension.jdbc.handler.BeanListAutoResultSetHandler;
import org.seasar.extension.jdbc.handler.ObjectResultSetHandler;
import org.seasar.extension.jdbc.manager.JdbcManagerImplementor;
import org.seasar.extension.jdbc.mapper.AbstractEntityMapper;
import org.seasar.extension.jdbc.mapper.AbstractRelationshipEntityMapper;
import org.seasar.extension.jdbc.mapper.EntityMapperImpl;
import org.seasar.extension.jdbc.mapper.ManyToOneEntityMapperImpl;
import org.seasar.extension.jdbc.mapper.OneToManyEntityMapperImpl;
import org.seasar.extension.jdbc.mapper.OneToOneEntityMapperImpl;
import org.seasar.extension.jdbc.mapper.PropertyMapperImpl;
import org.seasar.extension.jdbc.util.QueryTokenizer;
import org.seasar.extension.jdbc.where.ComplexWhere;
import org.seasar.extension.jdbc.where.SimpleWhere;
import org.seasar.framework.exception.EmptyRuntimeException;
import org.seasar.framework.message.MessageFormatter;
import org.seasar.framework.util.StringUtil;
import org.seasar.framework.util.tiger.CollectionsUtil;
import org.seasar.framework.util.tiger.Pair;

/**
 * {@link AutoSelect}の実装クラスです。
 * 
 * @author higa
 * @param <T>
 *            エンティティの型です。
 * 
 */
public class AutoSelectImpl<T> extends AbstractSelect<T, AutoSelect<T>>
        implements AutoSelect<T> {

    /**
     * 結合メタデータのリストです。
     */
    protected List<JoinMeta> joinMetaList = new ArrayList<JoinMeta>();

    /**
     * テーブルのインデックスです。
     */
    protected int tableIndex;

    /**
     * テーブル別名のマップです。
     */
    protected Map<String, String> tableAliasMap = new HashMap<String, String>();

    /**
     * エンティティメタデータのマップです。
     */
    protected Map<String, EntityMeta> entityMetaMap = new HashMap<String, EntityMeta>();

    /**
     * エンティティ名です。
     */
    protected String entityName;

    /**
     * select句へ追加するプロパティ
     */
    protected final Set<String> includesProperties = CollectionsUtil
            .newHashSet();

    /**
     * select句から除外するプロパティ
     */
    protected final Set<String> excludesProperties = CollectionsUtil
            .newHashSet();

    /**
     * select句です。
     */
    protected SelectClause selectClause = new SelectClause();

    /**
     * from句です。
     */
    protected FromClause fromClause = new FromClause();

    /**
     * where句です。
     */
    protected WhereClause whereClause = new WhereClause();

    /**
     * order by句です。
     */
    protected OrderByClause orderByClause = new OrderByClause();

    /**
     * ソート順です。
     */
    protected String orderBy = "";

    /**
     * SELECT ～ FOR UPDATEのSQL文字列です。
     */
    protected String forUpdate = "";

    /**
     * SELECT ～ FOR UPDATEのタイプです。
     */
    protected SelectForUpdateType forUpdateType;

    /**
     * SELECT ～ FOR UPDATEでロック対象となるエンティティからプロパティへのマップです。
     */
    protected Map<String, String> forUpdateTargets = CollectionsUtil
            .newLinkedHashMap();

    /**
     * SELECT ～ FOR UPDATEでの待機時間 (秒単位) です。
     */
    protected int forUpdateWaitSeconds = 0;

    /**
     * EAGERフェッチするプロパティです。
     */
    protected Set<String> eagerProperties = new LinkedHashSet<String>();

    /**
     * ヒントです。
     */
    protected String hint;

    /**
     * 値タイプのリストです。
     */
    protected List<ValueType> valueTypeList = new ArrayList<ValueType>(50);

    /**
     * selectリストのインデックスです。
     */
    protected int selectListIndex;

    /**
     * エンティティマッパーのマップです。
     */
    protected Map<String, AbstractEntityMapper> entityMapperMap = new HashMap<String, AbstractEntityMapper>();

    /**
     * where句のパラメータです。
     */
    protected List<Object> whereParams = new ArrayList<Object>();

    /**
     * Mapによるwhere句の条件指定です。
     */
    protected Map<String, ? extends Object> conditions;

    /**
     * クライテリアです。
     */
    protected String criteria;

    /**
     * クライテリア内のパラメータの配列です。
     */
    protected Object[] criteriaParams = new Object[] {};

    /**
     * クライテリア内のパラメータに対応するプロパティ名の配列です。
     */
    protected String[] criteriaPropertyNames = new String[] {};

    /**
     * IDプロパティのメタデータのリストです。
     */
    protected List<PropertyMeta> idPropertyMetaList;

    /**
     * IDプロパティの値の配列です。
     */
    protected Object[] idProperties;

    /**
     * バージョンプロパティのメタデータです。
     */
    protected PropertyMeta versionPropertyMeta;

    /**
     * バージョンプロパティの値です。
     */
    protected Object versionProperty;

    /**
     * {@link AutoSelectImpl}を作成します。
     * 
     * @param jdbcManager
     *            内部的なJDBCマネージャ
     * @param baseClass
     *            ベースクラス
     */
    public AutoSelectImpl(JdbcManagerImplementor jdbcManager, Class<T> baseClass) {
        super(jdbcManager, baseClass);
    }

    public AutoSelect<T> includes(final CharSequence... propertyNames) {
        includesProperties.addAll(Arrays.asList(toStringArray(propertyNames)));
        return this;
    }

    public AutoSelect<T> excludes(final CharSequence... propertyNames) {
        excludesProperties.addAll(Arrays.asList(toStringArray(propertyNames)));
        return this;
    }

    public AutoSelect<T> innerJoin(CharSequence name) {
        return join(name, JoinType.INNER);
    }

    public AutoSelect<T> innerJoin(CharSequence name, String condition,
            Object... params) {
        return join(name, JoinType.INNER, condition, params);
    }

    public AutoSelect<T> innerJoin(CharSequence name, Where condition) {
        return join(name, JoinType.INNER, condition);
    }

    public AutoSelect<T> innerJoin(CharSequence name, Where... conditions) {
        return join(name, JoinType.INNER, conditions);
    }

    public AutoSelect<T> innerJoin(CharSequence name, boolean fetch) {
        return join(name, JoinType.INNER, fetch);
    }

    public AutoSelect<T> innerJoin(CharSequence name, boolean fetch,
            String condition, Object... params) {
        return join(name, JoinType.INNER, fetch, condition, params);
    }

    public AutoSelect<T> innerJoin(CharSequence name, boolean fetch,
            Where condition) {
        return join(name, JoinType.INNER, fetch, condition);
    }

    public AutoSelect<T> innerJoin(CharSequence name, boolean fetch,
            Where... conditions) {
        return join(name, JoinType.INNER, fetch, conditions);
    }

    public AutoSelect<T> leftOuterJoin(CharSequence name) {
        return join(name, JoinType.LEFT_OUTER);
    }

    public AutoSelect<T> leftOuterJoin(CharSequence name, String condition,
            Object... params) {
        return join(name, JoinType.LEFT_OUTER, condition, params);
    }

    public AutoSelect<T> leftOuterJoin(CharSequence name, Where condition) {
        return join(name, JoinType.LEFT_OUTER, condition);
    }

    public AutoSelect<T> leftOuterJoin(CharSequence name, Where... conditions) {
        return join(name, JoinType.LEFT_OUTER, conditions);
    }

    public AutoSelect<T> leftOuterJoin(CharSequence name, boolean fetch) {
        return join(name, JoinType.LEFT_OUTER, fetch);
    }

    public AutoSelect<T> leftOuterJoin(CharSequence name, boolean fetch,
            String condition, Object... params) {
        return join(name, JoinType.LEFT_OUTER, fetch, condition, params);
    }

    public AutoSelect<T> leftOuterJoin(CharSequence name, boolean fetch,
            Where condition) {
        return join(name, JoinType.LEFT_OUTER, fetch, condition);
    }

    public AutoSelect<T> leftOuterJoin(CharSequence name, boolean fetch,
            Where... conditions) {
        return join(name, JoinType.LEFT_OUTER, fetch, conditions);
    }

    public AutoSelect<T> join(CharSequence name, JoinType joinType) {
        return join(name, joinType, true);
    }

    public AutoSelect<T> join(CharSequence name, JoinType joinType,
            String condition, Object... params) {
        return join(name, joinType, true, condition, params);
    }

    public AutoSelect<T> join(CharSequence name, JoinType joinType,
            Where condition) {
        return join(name, joinType, true, condition);
    }

    public AutoSelect<T> join(CharSequence name, JoinType joinType,
            Where... conditions) {
        return join(name, joinType, true, conditions);
    }

    public AutoSelect<T> join(CharSequence name, JoinType joinType,
            boolean fetch) {
        joinMetaList.add(new JoinMeta(name.toString(), joinType, fetch));
        return this;
    }

    public AutoSelect<T> join(CharSequence name, JoinType joinType,
            boolean fetch, String condition, Object... params) {
        joinMetaList.add(new JoinMeta(name.toString(), joinType, fetch,
                condition, params));
        return this;
    }

    public AutoSelect<T> join(CharSequence name, JoinType joinType,
            boolean fetch, Where condition) {
        joinMetaList.add(new JoinMeta(name.toString(), joinType, fetch,
                condition.getCriteria(), condition.getParams(), condition
                        .getPropertyNames()));
        return this;
    }

    public AutoSelect<T> join(CharSequence name, JoinType joinType,
            boolean fetch, Where... conditions) {
        if (conditions == null) {
            throw new NullPointerException("conditions");
        }
        if (conditions.length > 0) {
            final ComplexWhere where = new ComplexWhere();
            for (final Where w : conditions) {
                where.and(w);
            }
            String criteria = where.getCriteria().trim();
            if (StringUtil.isEmpty(criteria)) {
                return this;
            }
            joinMetaList.add(new JoinMeta(name.toString(), joinType, fetch,
                    criteria, where.getParams(), where.getPropertyNames()));
        }
        return this;
    }

    /**
     * 結合メタデータの数を返します。
     * 
     * @return 結合メタデータの数
     */
    protected int getJoinMetaSize() {
        return joinMetaList.size();
    }

    /**
     * 結合メタデータを返します。
     * 
     * @param index
     *            位置
     * @return 結合メタデータ
     */
    protected JoinMeta getJoinMeta(int index) {
        return joinMetaList.get(index);
    }

    @Override
    protected void prepare(String methodName) {
        prepareCallerClassAndMethodName(methodName);
        prepareTarget();
        prepareJoins();
        prepareIdVersion();
        prepareWhere();
        prepareConditions();
        prepareCriteria();
        prepareOrderBy();
        prepareForUpdate();
        prepareParams();
        prepareSql();
    }

    /**
     * 対象エンティティの準備をします。
     */
    protected void prepareTarget() {
        String tableAlias = prepareTableAlias(null);
        EntityMeta entityMeta = prepareEntityMeta(baseClass, null);
        entityName = entityMeta.getName();
        List<PropertyMapper> propertyMapperList = new ArrayList<PropertyMapper>(
                50);
        List<Integer> idIndexList = new ArrayList<Integer>();
        prepareEntity(entityMeta, null, tableAlias, propertyMapperList,
                idIndexList);
        PropertyMapper[] propertyMappers = toPropertyMapperArray(propertyMapperList);
        int[] idIndices = toIdIndexArray(idIndexList);
        entityMapperMap.put(null, new EntityMapperImpl(baseClass,
                propertyMappers, idIndices));
        final String lockHint = getLockHint(null);
        if (StringUtil.isEmpty(lockHint)) {
            fromClause.addSql(entityMeta.getTableMeta().getFullName(),
                    tableAlias);
        } else {
            fromClause.addSql(entityMeta.getTableMeta().getFullName(),
                    tableAlias, lockHint);
        }
    }

    /**
     * エンティティの準備をします。
     * 
     * @param em
     *            エンティティメタデータ
     * @param joinMeta
     *            結合メタデータ
     * @param tableAlias
     *            テーブル別名
     * @param propertyMapperList
     *            プロパティマッパーのリスト
     * @param idIndexList
     *            識別子のインデックスのリスト
     */
    protected void prepareEntity(EntityMeta em, JoinMeta joinMeta,
            String tableAlias, List<PropertyMapper> propertyMapperList,
            List<Integer> idIndexList) {
        if (count) {
            final String selectList = jdbcManager.getDialect()
                    .getCountSqlSelectList(
                    idPropertyMetaList);
            selectClause.addSql(selectList);
            valueTypeList.add(jdbcManager.getDialect().getValueType(Long.class,
                    false, null));
        } else {
            for (int i = 0; i < em.getPropertyMetaSize(); i++) {
                PropertyMeta pm = em.getPropertyMeta(i);
                if (pm.isTransient() || pm.isRelationship()) {
                    continue;
                }
                if (!isTargetProperty(pm, joinMeta)) {
                    continue;
                }
                selectClause.addSql(tableAlias, pm.getColumnMeta().getName());
                valueTypeList.add(jdbcManager.getDialect().getValueType(pm));
                propertyMapperList.add(new PropertyMapperImpl(pm.getField(),
                        selectListIndex));
                if (pm.isId()) {
                    idIndexList.add(new Integer(selectListIndex));
                }
                ++selectListIndex;
            }
        }
    }

    /**
     * select句に追加するプロパティなら{@literal true}を返します。
     * 
     * @param propertyMeta
     *            プロパティメタデータ
     * @param joinMeta
     *            結合メタデータ
     * @return select句に追加するプロパティなら{@literal true}
     */
    protected boolean isTargetProperty(final PropertyMeta propertyMeta,
            final JoinMeta joinMeta) {
        if (propertyMeta.isId()) {
            return true;
        }
        final boolean lazy = isLazy(propertyMeta, joinMeta);
        if (includesProperties.isEmpty() && excludesProperties.isEmpty()) {
            return !lazy;
        }
        String propertyName = propertyMeta.getName();
        if (joinMeta != null) {
            propertyName = joinMeta.getName() + "." + propertyName;
        }
        boolean relationship = false;
        int index = propertyName.length();
        while (index != -1) {
            propertyName = propertyName.substring(0, index);
            if (includesProperties.contains(propertyName)) {
                return !relationship || !lazy;
            }
            if (excludesProperties.contains(propertyName)) {
                return false;
            }
            relationship = true;
            index = propertyName.lastIndexOf(".");
        }
        return includesProperties.isEmpty() && !lazy;
    }

    /**
     * プロパティのフェッチタイプがLAZYなら{@literal true}を返します。
     * 
     * @param propertyMeta
     *            プロパティメタデータ
     * @param joinMeta
     *            結合メタデータ
     * @return プロパティのフェッチタイプがLAZYなら{@literal true}
     */
    protected boolean isLazy(final PropertyMeta propertyMeta,
            final JoinMeta joinMeta) {
        if (!propertyMeta.isLazy()) {
            return false;
        }
        final String propertyName = propertyMeta.getName();
        if (joinMeta == null) {
            return !eagerProperties.contains(propertyName);
        }
        final String qualifiedName = joinMeta.getName() + '.' + propertyName;
        return !eagerProperties.contains(qualifiedName);
    }

    /**
     * テーブル別名を返します。
     * 
     * @param join
     *            結合名
     * @return テーブル別名
     */
    protected String getTableAlias(String join) {
        return tableAliasMap.get(join);
    }

    /**
     * テーブル別名を作成します。
     * 
     * @return テーブル別名
     */
    protected String createTableAlias() {
        return "T" + ++tableIndex + "_";
    }

    /**
     * テーブル別名を準備します。
     * 
     * @param join
     *            結合名
     * @return テーブル別名
     */
    protected String prepareTableAlias(String join) {
        String tableAlias = createTableAlias();
        tableAliasMap.put(join, tableAlias);
        return tableAlias;
    }

    /**
     * エンティティメタデータを返します。
     * 
     * @param join
     *            結合名
     * @return エンティティメタデータ
     */
    protected EntityMeta getEntityMeta(String join) {
        return entityMetaMap.get(join);
    }

    /**
     * エンティティメタデータを準備します。
     * 
     * @param entityClass
     *            エンティティクラス
     * @param join
     *            結合名
     * @return エンティティメタデータ
     */
    protected EntityMeta prepareEntityMeta(Class<?> entityClass, String join) {
        EntityMeta entityMeta = null;
        try {
            entityMeta = jdbcManager.getEntityMetaFactory().getEntityMeta(
                    entityClass);
        } catch (RuntimeException e) {
            logger.log("ESSR0709", new Object[] { callerClass.getName(),
                    callerMethodName });
            logger.log("ESSR0711", new Object[] { baseClass.getName() });
            throw e;
        }
        entityMetaMap.put(join, entityMeta);
        return entityMeta;
    }

    /**
     * 値タイプの配列を返します。
     * 
     * @return 値タイプの配列
     */
    protected ValueType[] getValueTypes() {
        return valueTypeList.toArray(new ValueType[valueTypeList.size()]);
    }

    /**
     * プロパティマッパーの配列に変換します。
     * 
     * @param propertyMapperList
     *            プロパティマッパーのリスト
     * @return プロパティマッパーの配列
     */
    protected PropertyMapperImpl[] toPropertyMapperArray(
            List<PropertyMapper> propertyMapperList) {
        return propertyMapperList
                .toArray(new PropertyMapperImpl[propertyMapperList.size()]);
    }

    /**
     * 識別子のインデックスのリストをintの配列に変換します。
     * 
     * @param idIndexList
     * @return intの配列
     */
    protected int[] toIdIndexArray(List<Integer> idIndexList) {
        int[] idIndices = new int[idIndexList.size()];
        for (int i = 0; i < idIndices.length; i++) {
            idIndices[i] = idIndexList.get(i).intValue();
        }
        return idIndices;
    }

    /**
     * エンティティマッパーを返します。
     * 
     * @param join
     *            結合名
     * @return エンティティマッパー
     */
    protected AbstractEntityMapper getEntityMapper(String join) {
        return entityMapperMap.get(join);
    }

    /**
     * 結合の準備をします。
     * 
     * @see #prepareJoin(JoinMeta)
     */
    protected void prepareJoins() {
        for (JoinMeta joinMeta : joinMetaList) {
            prepareJoin(joinMeta);
        }
    }

    /**
     * 結合の準備をします。
     * 
     * @param joinMeta
     *            結合メタデータ
     */
    protected void prepareJoin(JoinMeta joinMeta) {
        String tableAlias = prepareTableAlias(joinMeta.getName());
        String[] names = splitBaseAndProperty(joinMeta.getName());
        EntityMeta baseEntityMeta = getBaseEntityMeta(joinMeta.getName(),
                names[0]);
        AbstractEntityMapper baseEntityMapper = getBaseEntityMapper(joinMeta
                .getName(), names[0]);
        PropertyMeta propertyMeta = getPropertyMeta(baseEntityMeta, joinMeta
                .getName(), names[1]);
        Class<?> relationshipClass = propertyMeta.getRelationshipClass();
        EntityMeta inverseEntityMeta = getInverseEntityMeta(relationshipClass,
                joinMeta.getName());
        PropertyMeta inversePropertyMeta = getInversePropertyMeta(
                inverseEntityMeta, propertyMeta);
        String baseTableAlias = getTableAlias(names[0]);
        String fkTableAlias = baseTableAlias;
        String pkTableAlias = tableAlias;
        List<JoinColumnMeta> joinColumnMetaList = propertyMeta
                .getJoinColumnMetaList();
        if (propertyMeta.getMappedBy() != null) {
            fkTableAlias = tableAlias;
            pkTableAlias = baseTableAlias;
            joinColumnMetaList = inversePropertyMeta.getJoinColumnMetaList();
        }
        if (joinMeta.isFetch()) {
            List<PropertyMapper> propertyMapperList = new ArrayList<PropertyMapper>(
                    50);
            List<Integer> idIndexList = new ArrayList<Integer>();
            if (!count) {
                prepareEntity(inverseEntityMeta, joinMeta, tableAlias,
                        propertyMapperList, idIndexList);
            }
            PropertyMapper[] propertyMappers = toPropertyMapperArray(propertyMapperList);
            int[] idIndices = toIdIndexArray(idIndexList);
            AbstractRelationshipEntityMapper remapper = createRelationshipEntityMapper(
                    relationshipClass, propertyMappers, idIndices,
                    propertyMeta, inversePropertyMeta);
            entityMapperMap.put(joinMeta.getName(), remapper);
            baseEntityMapper.addRelationshipEntityMapper(remapper);
        }
        final String lockHint = getLockHint(joinMeta.getName());
        jdbcManager.getDialect().setupJoin(fromClause, whereClause,
                joinMeta.getJoinType(),
                inverseEntityMeta.getTableMeta().getFullName(), tableAlias,
                fkTableAlias, pkTableAlias, joinColumnMetaList, lockHint,
                convertCriteria(joinMeta.getCondition()));
        if (!StringUtil.isEmpty(joinMeta.getCondition())) {
            final Object[] params = joinMeta.getConditionParams();
            final String[] propertyNames = joinMeta.getConditionPropertyNames();
            if (propertyNames == null) {
                for (Object param : params) {
                    addParam(param);
                }
            } else {
                for (int i = 0; i < params.length; ++i) {
                    prepareParams(propertyNames[i], params[i]);
                }
            }
        }
    }

    /**
     * <p>
     * 関連名をベースとプロパティに分離します。
     * </p>
     * <p>
     * <code>aaa.bbb.ccc</code>ならベースが<code>aaa.bbb</code>、プロパティが<code>ccc</code>
     * になります。
     * </p>
     * 
     * @param name
     *            関連名
     * @return ベースとプロパティの配列
     */
    protected String[] splitBaseAndProperty(String name) {
        String[] ret = new String[2];
        int index = name.lastIndexOf('.');
        if (index < 0) {
            ret[1] = name;
        } else {
            ret[0] = name.substring(0, index);
            ret[1] = name.substring(index + 1);
        }
        return ret;
    }

    /**
     * ベースのエンティティメタデータを返します。
     * 
     * @param join
     *            結合名
     * @param base
     *            ベースの結合名
     * @return ベースのエンティティメタデータ
     * @throws BaseJoinNotFoundRuntimeException
     *             ベースの結合が見つからない場合。
     */
    protected EntityMeta getBaseEntityMeta(String join, String base)
            throws BaseJoinNotFoundRuntimeException {
        EntityMeta baseEntityMeta = getEntityMeta(base);
        if (baseEntityMeta == null) {
            logger.log("ESSR0709", new Object[] { callerClass.getName(),
                    callerMethodName });
            throw new BaseJoinNotFoundRuntimeException(entityName, join, base);
        }
        return baseEntityMeta;
    }

    /**
     * ベースのエンティティメタマッパーを返します。
     * 
     * @param join
     *            結合名
     * @param baseJoin
     *            ベースの結合名
     * @return ベースのエンティティマッパー
     * @throws BaseJoinNotFoundRuntimeException
     *             ベースの結合が見つからない場合。
     */
    protected AbstractEntityMapper getBaseEntityMapper(String join,
            String baseJoin) throws BaseJoinNotFoundRuntimeException {
        AbstractEntityMapper baseEntityMapper = getEntityMapper(baseJoin);
        if (baseEntityMapper == null) {
            logger.log("ESSR0709", new Object[] { callerClass.getName(),
                    callerMethodName });
            throw new BaseJoinNotFoundRuntimeException(entityName, join,
                    baseJoin);
        }
        return baseEntityMapper;
    }

    /**
     * プロパティメタデータを返します。
     * 
     * @param baseEntityMeta
     *            ベースのエンティティメタデータ
     * @param fullPropertyName
     *            全体のプロパティ名
     * @param propertyName
     *            プロパティ名
     * @return プロパティメタデータ
     * @throws RuntimeException
     *             実行時例外が発生した場合。
     */
    protected PropertyMeta getPropertyMeta(EntityMeta baseEntityMeta,
            String fullPropertyName, String propertyName)
            throws RuntimeException {
        PropertyMeta pm = null;
        try {
            pm = baseEntityMeta.getPropertyMeta(propertyName);
        } catch (RuntimeException e) {
            logger.log("ESSR0709", new Object[] { callerClass.getName(),
                    callerMethodName });
            logger.log("ESSR0708",
                    new Object[] { entityName, fullPropertyName });
            throw e;
        }
        return pm;
    }

    /**
     * 関連の逆側のエンティティメタデータを返します。
     * 
     * @param relationshipClass
     *            関連クラス
     * @param join
     *            結合名
     * @return 関連の逆側のエンティティメタデータ
     * @throws RuntimeException
     *             実行時例外が発生した場合。
     * @throws JoinDuplicatedRuntimeException
     *             結合が重複している場合。
     * 
     */
    protected EntityMeta getInverseEntityMeta(Class<?> relationshipClass,
            String join) throws RuntimeException,
            JoinDuplicatedRuntimeException {
        EntityMeta inverseEntityMeta = null;
        try {
            inverseEntityMeta = jdbcManager.getEntityMetaFactory()
                    .getEntityMeta(relationshipClass);
        } catch (RuntimeException e) {
            logger.log("ESSR0709", new Object[] { callerClass.getName(),
                    callerMethodName });
            logger.log("ESSR0710", new Object[] { entityName, join });
            throw e;
        }
        if (entityMetaMap.put(join, inverseEntityMeta) != null) {
            logger.log("ESSR0709", new Object[] { callerClass.getName(),
                    callerMethodName });
            throw new JoinDuplicatedRuntimeException(entityName, join);
        }
        return inverseEntityMeta;
    }

    /**
     * 逆側のプロパティメタデータを返します。
     * 
     * @param inverseEntityMeta
     *            逆側のエンティティメタデータ
     * @param relationshipPropertyMeta
     *            関連のプロパティメタデータ
     * @return 逆側のプロパティメタデータ
     * 
     */
    protected PropertyMeta getInversePropertyMeta(EntityMeta inverseEntityMeta,
            PropertyMeta relationshipPropertyMeta) {
        if (relationshipPropertyMeta.getMappedBy() != null) {
            return inverseEntityMeta.getPropertyMeta(relationshipPropertyMeta
                    .getMappedBy());
        }
        return inverseEntityMeta.getMappedByPropertyMeta(
                relationshipPropertyMeta.getName(), baseClass);
    }

    /**
     * 関連エンティティマッパーを作成します。
     * 
     * @param relationshipClass
     *            関連クラス
     * @param propertyMappers
     *            プロパティマッパーの配列
     * @param idIndices
     *            識別子のインデックスの配列
     * @param propertyMeta
     *            関連のプロパティメタデータ
     * @param inversePropertyMeta
     *            逆側の関連のプロパティメタデータ
     * @return 関連エンティティマッパー
     */
    protected AbstractRelationshipEntityMapper createRelationshipEntityMapper(
            Class<?> relationshipClass, PropertyMapper[] propertyMappers,
            int[] idIndices, PropertyMeta propertyMeta,
            PropertyMeta inversePropertyMeta) {
        Field inverseField = inversePropertyMeta != null ? inversePropertyMeta
                .getField() : null;
        switch (propertyMeta.getRelationshipType()) {
        case ONE_TO_ONE:
            return new OneToOneEntityMapperImpl(relationshipClass,
                    propertyMappers, idIndices, propertyMeta.getField(),
                    inverseField);
        case ONE_TO_MANY:
            return new OneToManyEntityMapperImpl(relationshipClass,
                    propertyMappers, idIndices, propertyMeta.getField(),
                    inverseField);
        case MANY_TO_ONE:
            return new ManyToOneEntityMapperImpl(relationshipClass,
                    propertyMappers, idIndices, propertyMeta.getField(),
                    inverseField);
        }
        throw new IllegalStateException(propertyMeta.getRelationshipType()
                .toString());
    }

    /**
     * SQLに変換します。
     * 
     * @return SQL
     */
    protected String toSql() {
        String hintComment;
        if (!StringUtil.isEmpty(hint)) {
            hintComment = jdbcManager.getDialect().getHintComment(
                    convertEntityNameToTableAlias(hint));
        } else {
            hintComment = "";
        }
        StringBuilder sb = new StringBuilder(7 + hintComment.length()
                + selectClause.getLength() + fromClause.getLength()
                + whereClause.getLength() + orderByClause.getLength());
        return sb.append("select ").append(hintComment).append(
                selectClause.toSql()).append(fromClause.toSql()).append(
                whereClause.toSql()).append(orderByClause.toSql()).toString();
    }

    public AutoSelect<T> where(String criteria, Object... params) {
        if (criteria == null) {
            throw new NullPointerException("criteria");
        }
        this.criteria = criteria;
        if (params == null) {
            throw new NullPointerException("params");
        }
        for (Object o : params) {
            whereParams.add(o);
        }
        return this;
    }

    public AutoSelect<T> where(Where where) {
        if (where == null) {
            throw new NullPointerException("where");
        }
        String criteria = where.getCriteria();
        if (StringUtil.isEmpty(criteria)) {
            return this;
        }
        this.criteria = criteria;
        this.criteriaParams = where.getParams();
        this.criteriaPropertyNames = where.getPropertyNames();
        return this;
    }

    public AutoSelect<T> where(Where... wheres) {
        if (wheres == null) {
            throw new NullPointerException("whereList");
        }
        if (wheres.length > 0) {
            final ComplexWhere where = new ComplexWhere();
            for (final Where w : wheres) {
                where.and(w);
            }
            String criteria = where.getCriteria().trim();
            if (StringUtil.isEmpty(criteria)) {
                return this;
            }
            this.criteria = criteria;
            this.criteriaParams = where.getParams();
            this.criteriaPropertyNames = where.getPropertyNames();
        }
        return this;
    }

    public AutoSelect<T> where(Map<String, ? extends Object> conditions) {
        if (conditions == null) {
            throw new NullPointerException("conditions");
        }
        this.conditions = conditions;
        return this;
    }

    public AutoSelect<T> id(final Object... idProperties) {
        if (idProperties == null) {
            throw new NullPointerException("idProperties");
        }
        final EntityMeta entityMeta = jdbcManager.getEntityMetaFactory()
                .getEntityMeta(baseClass);
        idPropertyMetaList = entityMeta.getIdPropertyMetaList();
        if (idPropertyMetaList.size() != idProperties.length) {
            throw new IllegalIdPropertySizeRuntimeException(entityMeta
                    .getName(), idPropertyMetaList.size(), idProperties.length);
        }
        this.idProperties = idProperties;
        return this;
    }

    public AutoSelect<T> version(final Object versionProperty) {
        if (versionProperty == null) {
            throw new NullPointerException("versionProperty");
        }
        final EntityMeta entityMeta = jdbcManager.getEntityMetaFactory()
                .getEntityMeta(baseClass);
        if (!entityMeta.hasVersionPropertyMeta()) {
            throw new VersionPropertyNotExistsRuntimeException(entityMeta
                    .getName());
        }
        versionPropertyMeta = entityMeta.getVersionPropertyMeta();
        this.versionProperty = versionProperty;
        return this;
    }

    /**
     * where句のパラメータを準備します。
     */
    protected void prepareWhere() {
        for (final Object param : whereParams) {
            addParam(param);
        }
    }

    /**
     * where句の条件を準備します。
     */
    protected void prepareConditions() {
        if (conditions == null || conditions.size() == 0) {
            return;
        }
        final WhereClause whereTerm = new WhereClause();
        for (Map.Entry<String, ? extends Object> e : conditions.entrySet()) {
            prepareCondition(whereTerm, e.getKey(), e.getValue());
        }
        final String whereTermString = whereTerm.toSql();
        if (StringUtil.isEmpty(whereTermString)) {
            return;
        }
        whereClause.addAndSql("(");
        whereClause.addSql(whereTermString.substring(WhereClause.WHERE_KEYWORD
                .length()));
        whereClause.addSql(")");
    }

    /**
     * 条件を準備します。
     * 
     * @param whereTerm
     *            WHERE句の項
     * @param name
     *            プロパティ名
     * @param value
     *            プロパティの値
     * @param valueList
     *            値のリスト
     * @param valueClassList
     *            値のクラスのリスト
     */
    protected void prepareCondition(WhereClause whereTerm, String name,
            Object value) {
        ConditionType conditionType = ConditionType.getConditionType(name);
        String pname = conditionType.removeSuffix(name);
        String[] names = splitBaseAndProperty(pname);
        String tableAlias = getTableAlias(names[0]);
        if (tableAlias == null) {
            logger.log("ESSR0709", new Object[] { callerClass.getName(),
                    callerMethodName });
            logger.log("ESSR0716", new Object[] { name });
            throw new BaseJoinNotFoundRuntimeException(entityName, pname,
                    names[0]);
        }
        EntityMeta baseEntityMeta = getBaseEntityMeta(pname, names[0]);
        PropertyMeta propertyMeta = getPropertyMeta(baseEntityMeta, pname,
                names[1]);
        ColumnMeta columnMeta = propertyMeta.getColumnMeta();
        if (columnMeta == null) {
            logger.log("ESSR0709", new Object[] { callerClass.getName(),
                    callerMethodName });
            logger.log("ESSR0716", new Object[] { name });
            throw new EntityColumnNotFoundRuntimeException(entityName,
                    propertyMeta.getName());
        }
        String columnName = columnMeta.getName();
        List<Object> valueList = CollectionsUtil.newArrayList();
        int size = conditionType.addCondition(tableAlias, columnName, value,
                whereTerm, valueList);
        for (int i = 0; i < size; i++) {
            addParam(valueList.get(i), propertyMeta.getPropertyClass(),
                    jdbcManager.getDialect().getValueType(propertyMeta));
        }
    }

    /**
     * クライテリアの準備をします。
     */
    protected void prepareCriteria() {
        if (criteria == null) {
            return;
        }
        whereClause.addAndSql("(");
        whereClause.addSql(convertCriteria(criteria));
        whereClause.addSql(")");
    }

    /**
     * IDプロパティ及びバージョンを準備します。
     */
    protected void prepareIdVersion() {
        if (idProperties == null) {
            if (versionProperty != null) {
                logger.log("ESSR0709", new Object[] { callerClass.getName(),
                        callerMethodName });
                throw new UnsupportedOperationException(MessageFormatter
                        .getMessage("ESSR0758", null));
            }
            return;
        }
        final SimpleWhere where = new SimpleWhere();
        for (int i = 0; i < idProperties.length; ++i) {
            where.eq(idPropertyMetaList.get(i).getName(), idProperties[i]);
        }
        if (versionProperty != null) {
            where.eq(versionPropertyMeta.getName(), versionProperty);
        }
        whereClause.addSql(convertCriteria(where.getCriteria()));
        for (int i = 0; i < idProperties.length; ++i) {
            prepareParams(idPropertyMetaList.get(i).getName(), idProperties[i]);
        }
        if (versionProperty != null) {
            prepareParams(versionPropertyMeta.getName(), versionProperty);
        }
    }

    /**
     * パラメータを準備します。
     */
    protected void prepareParams() {
        for (int i = 0; i < criteriaParams.length; i++) {
            final String name = criteriaPropertyNames[i];
            final Object value = criteriaParams[i];
            prepareParams(name, value);
        }
    }

    /**
     * パラメータを準備します。
     * 
     * @param name
     *            パラメータ名
     * @param value
     *            パラメータ値
     */
    protected void prepareParams(final String name, final Object value) {
        final String[] names = splitBaseAndProperty(name);
        final EntityMeta entityMeta = getEntityMeta(names[0]);
        if (entityMeta == null) {
            logger.log("ESSR0709", new Object[] { callerClass.getName(),
                    callerMethodName });
            logger.log("ESSR0716", new Object[] { name });
            throw new BaseJoinNotFoundRuntimeException(entityName, name,
                    names[0]);
        }
        if (!entityMeta.hasPropertyMeta(names[1])) {
            logger.log("ESSR0709", new Object[] { callerClass.getName(),
                    callerMethodName });
            throw new PropertyNotFoundRuntimeException(entityName, name);
        }
        final PropertyMeta pm = entityMeta.getPropertyMeta(names[1]);
        final ValueType valueType = jdbcManager.getDialect().getValueType(pm);
        addParam(value, value.getClass(), valueType);
    }

    /**
     * SQLを準備します。
     */
    protected void prepareSql() {
        executedSql = convertLimitSql(toSql()) + forUpdate;
    }

    @Override
    protected ResultSetHandler createResultListResultSetHandler() {
        return new BeanListAutoResultSetHandler(getValueTypes(),
                getEntityMapper(), executedSql, limit);
    }

    @Override
    protected ResultSetHandler createSingleResultResultSetHandler() {
        if (count) {
            return new ObjectResultSetHandler(valueTypeList.get(0), executedSql);
        }
        return new BeanAutoResultSetHandler(getValueTypes(), getEntityMapper(),
                executedSql);
    }

    @Override
    protected ResultSetHandler createIterateResultSetHandler(
            final IterationCallback<T, ?> callback) {
        return new BeanIterationAutoResultSetHandler(getValueTypes(),
                getEntityMapper(), executedSql, limit, callback);
    }

    /**
     * エンティティマッパーを返します。
     * 
     * @return エンティティマッパー
     */
    protected EntityMapper getEntityMapper() {
        return (EntityMapper) getEntityMapper(null);
    }

    public AutoSelect<T> orderBy(String orderBy) {
        if (orderBy == null) {
            throw new NullPointerException("orderBy");
        }
        this.orderBy = orderBy;
        return this;
    }

    public AutoSelect<T> orderBy(final OrderByItem... orderByItems) {
        if (orderByItems == null) {
            throw new NullPointerException("orderByItems");
        }
        if (orderByItems.length > 0) {
            final StringBuilder buf = new StringBuilder(
                    20 * orderByItems.length);
            for (final OrderByItem item : orderByItems) {
                buf.append(item.getCriteria()).append(", ");
            }
            buf.setLength(buf.length() - 2);
            orderBy = new String(buf);
        }
        return this;
    }

    /**
     * order by句の準備をします。
     */
    protected void prepareOrderBy() {
        if (StringUtil.isEmpty(orderBy)) {
            return;
        }
        orderByClause.addSql(convertCriteria(orderBy, true));
    }

    /**
     * プロパティ名で記述されたクライテリアをカラム名に変換します。
     * 
     * @param str
     *            クライテリア
     * @return カラム名で記述されたクライテリア
     */
    protected String convertCriteria(String str) {
        return convertCriteria(str, false);
    }

    /**
     * プロパティ名で記述されたクライテリアをカラム名に変換します。
     * 
     * @param str
     *            クライテリア
     * @param convertAlias
     *            カラム名をエイリアスに置換する場合は<code>true</code>
     * @return カラム名で記述されたクライテリア
     */
    protected String convertCriteria(String str, boolean convertAlias) {
        if (StringUtil.isEmpty(str)) {
            return str;
        }
        StringBuilder sb = new StringBuilder(20 + str.length());
        QueryTokenizer tokenizer = new QueryTokenizer(str);
        for (int type = tokenizer.nextToken(); type != QueryTokenizer.TT_EOF; type = tokenizer
                .nextToken()) {
            String token = tokenizer.getToken();
            if (type == QueryTokenizer.TT_WORD) {
                String[] names = splitBaseAndProperty(token);
                String tableAlias = getTableAlias(names[0]);
                EntityMeta entityMeta = getEntityMeta(names[0]);
                if (entityMeta == null || !entityMeta.hasPropertyMeta(names[1])) {
                    sb.append(token);
                } else {
                    PropertyMeta pm = entityMeta.getPropertyMeta(names[1]);
                    String itemName = tableAlias + "."
                            + pm.getColumnMeta().getName();
                    if (convertAlias) {
                        String alias = selectClause.getColumnAlias(itemName);
                        if (!StringUtil.isEmpty(alias)) {
                            itemName = alias;
                        }
                    }
                    sb.append(itemName);
                }
            } else {
                sb.append(token);
            }
        }
        return sb.toString();
    }

    /**
     * 文字列中のエンティティ名をテーブルの別名に変換します。
     * 
     * @param str
     *            入力文字列
     * @return 入力文字列中のエンティティ名をテーブルの別名に変換した文字列
     */
    protected String convertEntityNameToTableAlias(final String str) {
        final StringBuilder buf = new StringBuilder(20 + str.length());
        final QueryTokenizer tokenizer = new QueryTokenizer(str);
        for (int type = tokenizer.nextToken(); type != QueryTokenizer.TT_EOF; type = tokenizer
                .nextToken()) {
            final String token = tokenizer.getToken();
            if (type == QueryTokenizer.TT_WORD) {
                final String tableAlias = getTableAlias(token
                        .equals(entityName) ? null : token);
                if (StringUtil.isEmpty(tableAlias)) {
                    buf.append(token);
                } else {
                    buf.append(tableAlias);
                }
            } else {
                buf.append(token);
            }
        }
        return new String(buf);
    }

    public AutoSelect<T> forUpdate() {
        final DbmsDialect dialect = getJdbcManager().getDialect();
        if (!dialect.supportsForUpdate(SelectForUpdateType.NORMAL, false)) {
            final EntityMeta entityMeta = getJdbcManager()
                    .getEntityMetaFactory().getEntityMeta(baseClass);
            throw new UnsupportedOperationException(MessageFormatter
                    .getMessage("ESSR0746", new Object[] {
                            entityMeta.getName(), dialect.getName() }));
        }

        forUpdateType = SelectForUpdateType.NORMAL;
        return this;
    }

    public AutoSelect<T> forUpdate(final CharSequence... propertyNames) {
        if (propertyNames == null) {
            throw new NullPointerException("properties");
        }
        if (propertyNames.length == 0) {
            logger.log("ESSR0709", new Object[] { callerClass.getName(),
                    callerMethodName });
            throw new EmptyRuntimeException("properties");
        }

        final DbmsDialect dialect = getJdbcManager().getDialect();
        if (!dialect.supportsForUpdate(SelectForUpdateType.NORMAL, true)) {
            final EntityMeta entityMeta = getJdbcManager()
                    .getEntityMetaFactory().getEntityMeta(baseClass);
            throw new UnsupportedOperationException(MessageFormatter
                    .getMessage("ESSR0747", new Object[] {
                            entityMeta.getName(), dialect.getName() }));
        }

        forUpdateType = SelectForUpdateType.NORMAL;
        setupForUpdateTargets(toStringArray(propertyNames));
        return this;
    }

    public AutoSelect<T> forUpdateNowait() {
        final DbmsDialect dialect = getJdbcManager().getDialect();
        if (!dialect.supportsForUpdate(SelectForUpdateType.NOWAIT, false)) {
            final EntityMeta entityMeta = getJdbcManager()
                    .getEntityMetaFactory().getEntityMeta(baseClass);
            throw new UnsupportedOperationException(MessageFormatter
                    .getMessage("ESSR0748", new Object[] {
                            entityMeta.getName(), dialect.getName() }));
        }

        forUpdateType = SelectForUpdateType.NOWAIT;
        return this;
    }

    public AutoSelect<T> forUpdateNowait(final CharSequence... propertyNames) {
        if (propertyNames == null) {
            throw new NullPointerException("properties");
        }
        if (propertyNames.length == 0) {
            throw new EmptyRuntimeException("properties");
        }

        final DbmsDialect dialect = getJdbcManager().getDialect();
        if (!dialect.supportsForUpdate(SelectForUpdateType.NOWAIT, true)) {
            final EntityMeta entityMeta = getJdbcManager()
                    .getEntityMetaFactory().getEntityMeta(baseClass);
            throw new UnsupportedOperationException(MessageFormatter
                    .getMessage("ESSR0749", new Object[] {
                            entityMeta.getName(), dialect.getName() }));
        }

        forUpdateType = SelectForUpdateType.NOWAIT;
        setupForUpdateTargets(toStringArray(propertyNames));
        return this;
    }

    public AutoSelect<T> forUpdateWait(final int seconds) {
        final DbmsDialect dialect = getJdbcManager().getDialect();
        if (!dialect.supportsForUpdate(SelectForUpdateType.WAIT, false)) {
            final EntityMeta entityMeta = getJdbcManager()
                    .getEntityMetaFactory().getEntityMeta(baseClass);
            throw new UnsupportedOperationException(MessageFormatter
                    .getMessage("ESSR0750", new Object[] {
                            entityMeta.getName(), dialect.getName() }));
        }

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

    public AutoSelect<T> forUpdateWait(final int seconds,
            final CharSequence... propertyNames) {
        if (propertyNames == null) {
            throw new NullPointerException("properties");
        }
        if (propertyNames.length == 0) {
            throw new EmptyRuntimeException("properties");
        }

        final DbmsDialect dialect = getJdbcManager().getDialect();
        if (!dialect.supportsForUpdate(SelectForUpdateType.WAIT, true)) {
            final EntityMeta entityMeta = getJdbcManager()
                    .getEntityMetaFactory().getEntityMeta(baseClass);
            throw new UnsupportedOperationException(MessageFormatter
                    .getMessage("ESSR0751", new Object[] {
                            entityMeta.getName(), dialect.getName() }));
        }

        forUpdateType = SelectForUpdateType.WAIT;
        forUpdateWaitSeconds = seconds;
        setupForUpdateTargets(toStringArray(propertyNames));
        return this;
    }

    public AutoSelect<T> eager(CharSequence... propertyNames) {
        eagerProperties.addAll(Arrays.asList(toStringArray(propertyNames)));
        return this;
    }

    public AutoSelect<T> hint(final String hint) {
        this.hint = hint;
        return this;
    }

    /**
     * FOR UPDATE句を準備します。
     */
    @SuppressWarnings("unchecked")
    protected void prepareForUpdate() {
        if (forUpdateType == null) {
            forUpdate = "";
            return;
        }
        if (limit > 0 || offset > 0) {
            throw new UnsupportedOperationException(MessageFormatter
                    .getMessage("ESSR0754", null));
        }
        final DbmsDialect dialect = getJdbcManager().getDialect();
        for (JoinMeta joinMeta : joinMetaList) {
            switch (joinMeta.getJoinType()) {
            case INNER:
                if (!dialect.supportsInnerJoinForUpdate()) {
                    logger.log("ESSR0709", new Object[] {
                            callerClass.getName(), callerMethodName });
                    final EntityMeta entityMeta = getJdbcManager()
                            .getEntityMetaFactory().getEntityMeta(baseClass);
                    throw new UnsupportedOperationException(MessageFormatter
                            .getMessage("ESSR0763", new Object[] {
                                    entityMeta.getName(), dialect.getName() }));
                }
                break;
            case LEFT_OUTER:
                if (!dialect.supportsOuterJoinForUpdate()) {
                    logger.log("ESSR0709", new Object[] {
                            callerClass.getName(), callerMethodName });
                    final EntityMeta entityMeta = getJdbcManager()
                            .getEntityMetaFactory().getEntityMeta(baseClass);
                    throw new UnsupportedOperationException(MessageFormatter
                            .getMessage("ESSR0752", new Object[] {
                                    entityMeta.getName(), dialect.getName() }));
                }
                break;
            }
        }
        final int length = forUpdateTargets.size();
        final Pair<String, String>[] aliases = new Pair[length];
        int i = 0;
        for (final Entry<String, String> entry : forUpdateTargets.entrySet()) {
            aliases[i++] = toAliasPair(entry.getKey(), entry.getValue());
        }
        forUpdate = dialect.getForUpdateString(forUpdateType,
                forUpdateWaitSeconds, aliases);
    }

    /**
     * ロックヒントを返します。
     * 
     * @param baseName
     *            ベース名
     * @return ロックヒント
     */
    protected String getLockHint(final String baseName) {
        if (forUpdateType == null) {
            return "";
        }
        if (!forUpdateTargets.isEmpty()
                && !forUpdateTargets.containsKey(baseName)) {
            return "";
        }
        return jdbcManager.getDialect().getLockHintString(forUpdateType,
                forUpdateWaitSeconds);
    }

    /**
     * SELECT ～ FOR UPDATEの対象となるプロパティを準備します
     * 
     * @param propertyNames
     *            SELECT ～ FOR UPDATEの対象となるプロパティ名の並び
     */
    protected void setupForUpdateTargets(final String[] propertyNames) {
        for (final String propertyName : propertyNames) {
            final String[] names = splitBaseAndProperty(propertyName);
            forUpdateTargets.put(names[0], names[1]);
        }
    }

    /**
     * SELECT ～ FOR UPDATEの対象となるテーブルエイリアスとカラムエイリアスのペアを返します。
     * 
     * @param baseName
     *            ベース名
     * @param propertyName
     *            プロパティ名
     * @return SELECT ～ FOR UPDATEの対象となるテーブルエイリアスとカラムエイリアスのペア
     */
    @SuppressWarnings("unchecked")
    protected Pair<String, String> toAliasPair(final String baseName,
            final String propertyName) {
        final String tableAlias = getTableAlias(baseName);
        final EntityMeta entityMeta = entityMetaMap.get(baseName);
        if (entityMeta == null) {
            logger.log("ESSR0709", new Object[] { callerClass.getName(),
                    callerMethodName });
            logger.log("ESSR0716", new Object[] { baseName });
            throw new BaseJoinNotFoundRuntimeException(entityName, baseName
                    + "." + propertyName, baseName);
        }
        final PropertyMeta propertyMeta = entityMeta
                .getPropertyMeta(propertyName);
        final ColumnMeta columnMeta = propertyMeta.getColumnMeta();
        if (columnMeta == null) {
            logger.log("ESSR0709", new Object[] { callerClass.getName(),
                    callerMethodName });
            throw new PropertyNotFoundRuntimeException(entityName,
                    baseName == null ? propertyName : baseName + "."
                            + propertyName);
        }
        final String columnAlias = columnMeta.getName();
        return Pair.pair(tableAlias, columnAlias);
    }

}
