/*
 * Copyright 2004-2013 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.util;

import java.sql.DatabaseMetaData;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;

import org.seasar.framework.exception.SQLRuntimeException;
import org.seasar.framework.util.CaseInsensitiveMap;
import org.seasar.framework.util.CaseInsensitiveSet;

/**
 * データベースメタデータ用のユーティリティです。
 * 
 * @author higa
 * @author manhole
 */
public class DatabaseMetaDataUtil {

    /**
     * インスタンスを構築します。
     */
    protected DatabaseMetaDataUtil() {
    }

    /**
     * プライマリーキーの配列を返します。
     * 
     * @param dbMetaData
     *            データベースメタデータ
     * @param tableName
     *            テーブル名
     * @return プライマリーキーの配列
     */
    public static String[] getPrimaryKeys(DatabaseMetaData dbMetaData,
            String tableName) {

        Set set = getPrimaryKeySet(dbMetaData, tableName);
        return (String[]) set.toArray(new String[set.size()]);
    }

    /**
     * プライマリーキーの {@link Set}を返します。
     * 
     * @param dbMetaData
     *            データベースメタデータ
     * @param tableName
     *            テーブル名
     * @return プライマリーキーの {@link Set}
     */
    public static Set getPrimaryKeySet(DatabaseMetaData dbMetaData,
            String tableName) {
        final String schema;
        int index = tableName.indexOf('.');
        if (index >= 0) {
            schema = tableName.substring(0, index);
            tableName = tableName.substring(index + 1);
        } else {
            schema = getUserName(dbMetaData);
        }
        final String convertedTableName = convertIdentifier(dbMetaData,
                tableName);
        Set set = new CaseInsensitiveSet();
        addPrimaryKeys(dbMetaData, convertIdentifier(dbMetaData, schema),
                convertedTableName, set);
        if (set.size() == 0) {
            addPrimaryKeys(dbMetaData, schema, tableName, set);
        }
        if (set.size() == 0 && schema != null) {
            addPrimaryKeys(dbMetaData, null, convertedTableName, set);
            if (set.size() == 0) {
                addPrimaryKeys(dbMetaData, null, tableName, set);
            }
        }
        return set;
    }

    /**
     * プライマリーキーの {@link Set}を返します。
     * 
     * @param dbMetaData
     * @param schema
     * @param tableName
     * @return
     */
    public static Set getPrimaryKeySet(DatabaseMetaData dbMetaData,
            String schema, String tableName) {
        Set set = new CaseInsensitiveSet();
        addPrimaryKeys(dbMetaData, schema, tableName, set);
        return set;
    }

    /**
     * ユーザ名を返します。
     * 
     * @param dbMetaData
     *            データベースメタデータ
     * @return ユーザ名
     */
    public static String getUserName(DatabaseMetaData dbMetaData) {
        try {
            return dbMetaData.getUserName();
        } catch (SQLException e) {
            throw new SQLRuntimeException(e);
        }
    }

    private static void addPrimaryKeys(DatabaseMetaData dbMetaData,
            String schema, String tableName, Set set) {
        try {
            ResultSet rs = dbMetaData.getPrimaryKeys(null, schema, tableName);
            while (rs.next()) {
                set.add(rs.getString(4));
            }
            rs.close();
        } catch (SQLException ex) {
            throw new SQLRuntimeException(ex);
        }
    }

    /**
     * カラム名の配列を返します。
     * 
     * @param dbMetaData
     *            データベースメタデータ
     * @param tableName
     *            テーブル名
     * @return カラム名の配列
     */
    public static String[] getColumns(DatabaseMetaData dbMetaData,
            String tableName) {

        Map map = getColumnMap(dbMetaData, tableName);
        String[] ret = new String[map.size()];
        int index = 0;
        for (Iterator i = map.values().iterator(); i.hasNext();) {
            ColumnDesc cd = (ColumnDesc) i.next();
            ret[index++] = cd.getName();
        }
        return ret;
    }

    /**
     * カラムデータの {@link Map}を返します。
     * 
     * @param dbMetaData
     *            データベースメタデータ
     * @param tableName
     *            テーブル名
     * @return カラムデータの {@link Map}
     */
    public static Map getColumnMap(DatabaseMetaData dbMetaData, String tableName) {
        return getColumnCaseInsensitiveMap(dbMetaData, tableName);
    }

    /**
     * カラムデータの {@link CaseInsensitiveMap}を返します。
     * 
     * @param dbMetaData
     *            データベースメタデータ
     * @param tableName
     *            テーブル名
     * @return カラムデータの {@link CaseInsensitiveMap}
     */
    public static CaseInsensitiveMap getColumnCaseInsensitiveMap(
            DatabaseMetaData dbMetaData, String tableName) {
        final String schema;
        int index = tableName.indexOf('.');
        if (index >= 0) {
            schema = tableName.substring(0, index);
            tableName = tableName.substring(index + 1);
        } else {
            schema = getUserName(dbMetaData);
        }
        final String convertedTableName = convertIdentifier(dbMetaData,
                tableName);
        CaseInsensitiveMap map = new CaseInsensitiveMap();
        addColumns(dbMetaData, convertIdentifier(dbMetaData, schema),
                convertedTableName, map);
        if (map.size() == 0) {
            addColumns(dbMetaData, schema, tableName, map);
        }
        if (map.size() == 0 && schema != null) {
            addColumns(dbMetaData, null, convertedTableName, map);
            if (map.size() == 0) {
                addColumns(dbMetaData, null, tableName, map);
            }
        }
        return map;
    }

    /**
     * カラムデータの {@link CaseInsensitiveMap}を返します。
     * 
     * @param dbMetaData
     *            データベースメタデータ
     * @param schema
     *            スキーマ
     * @param tableName
     *            テーブル名
     * @return カラムデータの {@link CaseInsensitiveMap}
     */
    public static CaseInsensitiveMap getColumnMap(DatabaseMetaData dbMetaData,
            String schema, String tableName) {
        CaseInsensitiveMap map = new CaseInsensitiveMap();
        addColumns(dbMetaData, schema, tableName, map);
        return map;
    }

    private static void addColumns(DatabaseMetaData dbMetaData, String schema,
            String tableName, Map map) {
        try {
            ResultSet rs = dbMetaData.getColumns(null, schema, tableName, null);
            while (rs.next()) {
                ColumnDesc cd = new ColumnDesc();
                cd.setName(rs.getString(4));
                cd.setSqlType(rs.getInt(5));
                map.put(cd.getName(), cd);
            }
            rs.close();
        } catch (SQLException ex) {
            throw new SQLRuntimeException(ex);
        }
    }

    /**
     * 識別子を変換します。
     * 
     * @param dbMetaData
     *            データベースメタデータ
     * @param identifier
     *            識別子
     * @return 変換後の識別子
     */
    public static String convertIdentifier(DatabaseMetaData dbMetaData,
            String identifier) {

        if (identifier == null) {
            return null;
        }
        if (!supportsMixedCaseIdentifiers(dbMetaData)) {
            if (storesUpperCaseIdentifiers(dbMetaData)) {
                return identifier.toUpperCase();
            }
            return identifier.toLowerCase();
        }
        return identifier;
    }

    /**
     * 大文字小文字のミックスをサポートしているかどうか返します。
     * 
     * @param dbMetaData
     *            データベースメタデータ
     * @return 大文字小文字のミックスをサポートしているかどうか
     * @throws SQLRuntimeException
     *             SQL例外が発生した場合
     */
    public static boolean supportsMixedCaseIdentifiers(
            DatabaseMetaData dbMetaData) throws SQLRuntimeException {

        try {
            return dbMetaData.supportsMixedCaseIdentifiers();
        } catch (SQLException ex) {
            throw new SQLRuntimeException(ex);
        }
    }

    /**
     * 識別子を大文字で保存するかどうかを返します。
     * 
     * @param dbMetaData
     *            データベースメタデータ
     * @return 識別子を大文字で保存するかどうか
     * @throws SQLRuntimeException
     *             SQL例外が発生した場合
     */
    public static boolean storesUpperCaseIdentifiers(DatabaseMetaData dbMetaData)
            throws SQLRuntimeException {
        try {
            return dbMetaData.storesUpperCaseIdentifiers();
        } catch (SQLException ex) {
            throw new SQLRuntimeException(ex);
        }
    }

    /**
     * 識別子を小文字で保存するかどうかを返します。
     * 
     * @param dbMetaData
     *            データベースメタデータ
     * @return 識別子を小文字で保存するかどうか
     * @throws SQLRuntimeException
     *             SQL例外が発生した場合
     */
    public static boolean storesLowerCaseIdentifiers(DatabaseMetaData dbMetaData)
            throws SQLRuntimeException {
        try {
            return dbMetaData.storesLowerCaseIdentifiers();
        } catch (SQLException ex) {
            throw new SQLRuntimeException(ex);
        }
    }

    /**
     * スキーマをサポートしているかどうかを返します。
     * 
     * @param dbMetaData
     *            データベースメタデータ
     * @return スキーマをサポートしているかどうか
     * @throws SQLRuntimeException
     *             SQL例外が発生した場合
     */
    public static boolean supportsSchemasInTableDefinitions(
            DatabaseMetaData dbMetaData) throws SQLRuntimeException {
        try {
            return dbMetaData.supportsSchemasInTableDefinitions();
        } catch (SQLException ex) {
            throw new SQLRuntimeException(ex);
        }
    }

    /**
     * 識別子の自動作成をサポートしているかどうかを返します。
     * 
     * @param dbMetaData
     *            データベースメタデータ
     * @return 識別子の自動作成をサポートしているかどうか
     */
    public static boolean supportsGetGeneratedKeys(DatabaseMetaData dbMetaData) {
        try {
            return dbMetaData.supportsGetGeneratedKeys();
        } catch (SQLException ex) {
            throw new SQLRuntimeException(ex);
        }
    }

    /**
     * データベースのプロダクト名を返します。
     * 
     * @param dbMetaData
     *            データベースメタデータ
     * @return データベースのプロダクト名
     */
    public static String getDatabaseProductName(DatabaseMetaData dbMetaData) {
        try {
            return dbMetaData.getDatabaseProductName();
        } catch (SQLException ex) {
            throw new SQLRuntimeException(ex);
        }
    }
}